1.0.0rc4: federation WEFT_FEDERATION_TOKEN + dogfood 2026-06-06 (gate clarity, legis dev-loop, payload controls, 401 distinction)#34
Conversation
…efusing (wardline-30f3d38fa5) Dogfood friction #1: on a dirty tree `scan --format legis` failed exit 2 naming an `allow_dirty` flag that was never exposed on the CLI — presenting identically to "legis is broken." Expose `--allow-dirty` (CLI) / `allow_dirty` (MCP scan). The honest fix: a dirty tree under allow_dirty does NOT sign. The only tree_sha readable is the *committed* one, which does not describe dirty working content — signing it would be false provenance (the `_git_tree_sha` guard). Instead it falls through to the UNSIGNED dev artifact, clearly marked `dirty: true` (legis records it `unverified`). Signing stays clean-tree-only; verification stays clean-tree/CI. The loud refusal without --allow-dirty is unchanged. CLI emits a stderr warning when the artifact is dirty/unsigned; MCP reports `signed:false` + `dirty:true` in legis_artifact_status. legis ignores the unknown `dirty` top-level key on the unverified path, so ingest is unaffected; the golden clean-tree signature is byte-unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rdline-be75c6676d) Dogfood friction #2: a scan reporting summary.active:0 AND gate.tripped:true read as a bug — the agent had to run scan twice (with/without trust_suppressions) and read --help to learn the gate evaluates the unsuppressed (baselined-included) population by default. GateDecision now carries `reason` and `evaluated`. `reason` names the count and class that decided the verdict — "1 suppressed ERROR+ defect(s) (baseline/waiver/ judged) not cleared; pass --trust-suppressions (trusted checkout) or --new-since <ref> (PR)" when the trip is solely from suppressed-but-gated findings, "N active ERROR+ defect(s)" on a genuine trip (no misdirection to the suppression flags), and the mixed form when both. `evaluated` names the population: "unsuppressed (repository baseline/waiver/judged ignored)" by default, "post-suppression … honored" under --trust-suppressions. Counts come from `gate_breakdown` over the ANNOTATED findings so they match what the agent reads in `summary`. Surfaced in the MCP scan gate block, the agent_summary gate block, and on CLI stderr when the gate trips (never a silent exit 1). Both None when no --fail-on. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dline-5f662e7a4f) Dogfood friction #3: the secure gate-default (gate on the unsuppressed population) is correct, but the rollout was silent — a repo whose committed baseline used to clear --fail-on goes red with no code change, and an agent can't tell whether IT broke scan or HEAD was already red. New `baseline_migration_hint`: fires ONLY in the exact 'my repo went red with no code change' case — a committed .wardline/baseline.yaml exists, the gate trips SOLELY because baselined defects re-enter the unsuppressed population (no genuinely-active defect, no waiver/judged-only trip), and neither --trust-suppressions nor --new-since was passed. It points at both escape hatches and UPGRADING.md. Silent on a genuine active trip, a trusted/PR-scoped run, or no baseline file. Surfaced loudly on CLI stderr and as MCP `scan` gate.migration_hint (None otherwise). New UPGRADING.md documents the secure-default migration; CHANGELOG [Unreleased] gains entries for dogfood #1/#2/#3. Secure default unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…y/max_findings/include_suppressed, default explain cap (wardline-2957009961) Dogfood friction #5: the documented cost lever (`where`) did not control cost and one-shot `explain:true` was unusable on a real repo. - `where` now filters the agent_summary arrays too (it only filtered the top-level findings list before) — a filter matching 0 findings no longer returns dozens of suppressed findings inline. agent_summary build takes a display_findings view; its summary COUNTS stay whole-project. - New `summary_only:true` (counts + gate, no bodies — smallest "did the gate pass?" payload), `include_suppressed:false` (drop suppressed bodies; counts stay), `max_findings:N` (cap returned bodies). - DEFAULT explain ceiling: `explain:true` inlined provenance for EVERY active defect (56,820 chars on one line over a whole repo). Capped at 25 by default; max_findings tightens it. Findings past the cap are still returned, sans inline explanation. - New `truncation` block (findings_total/findings_returned/findings_truncated/ explanations_truncated/summary_only/include_suppressed/max_findings) so a bounded payload is never mistaken for "covered everything." CLI --format agent-summary is byte-unchanged (defaults preserve whole-project, uncapped behaviour). Docs (agents.md, legis-handoff.md --allow-dirty) + CHANGELOG updated. Full suite 2476 green; ruff/mypy/mkdocs-strict clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…wardline-be75c6676d follow-up) The gate reason counted `gate_breakdown(result.findings)` — the annotated population — so under `--new-since` a delta-scoped-out defect (converted to BASELINED by apply_delta_scope) was wrongly counted as "suppressed >= threshold", inflating the count and pointing at `--new-since` (already supplied). _gate_reason now classifies the defects that ACTUALLY gate (the unsuppressed gate population, where out-of-delta defects are BASELINED and so excluded) by their state in the emitted findings. The count is exactly what tripped the gate; the `--new-since` path no longer over-counts. The trust-suppressions branch is unchanged (gate == emitted findings there). Locked by extending the new_since differential to assert 1, not 2. Verified: legis `ScanResultsIn.scan` is typed `dict` (arbitrary mapping), so the new unsigned `dirty:true` marker rides through intake untouched — confirmed the dev artifact stays postable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ow-up) The reported one-shot blowup was 56,820 chars over 34 findings and exceeded the tool token limit; a default of 25 inlined provenances was still uncomfortably close. Lower the default ceiling to 10 — comfortably under the limit, still plenty to triage in one call — and let max_findings RAISE it when the agent explicitly accepts the larger payload (summary_only covers the common "did the gate pass?" case). New test locks that max_findings can lift the count above the default. Docs/CHANGELOG updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ped gate (wardline-be75c6676d follow-up) Dogfood re-test, #2 "Worse" half: when the gate trips solely on baselined findings summary.active is 0, so next_actions said "no active defects; rescan after edits" — telling the agent it PASSED while the gate FAILED. _next_actions_for now takes the GateDecision. With 0 active defects but a tripped gate it emits a scan action whose reason names the gate failure + the escape hatches (trust_suppressions / new_since / clear the baseline; see gate.reason / gate.migration_hint) instead of the passive "rescan after edits". The active>0 and genuinely-clean paths are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…le (wardline-53a44a3bb1) Dogfood #5: a 401 (token absent from the CLI env) was reported as "could not reach Filigree" — a wrong diagnosis that sent the agent chasing a broken-bridge / wrong- endpoint theory. The prior seam work deliberately made 401/403 SOFT (auth failure must not crash the scan loop); that is kept — only the MESSAGE changes. EmitResult now carries `status` (the HTTP status when one reached us; None when the transport itself failed) and `auth_rejected` (the 401/403 case). The CLI prints "Filigree returned 401 (auth rejected) … set WARDLINE_FILIGREE_TOKEN" vs a 5xx "server error" vs the genuine "could not reach"; the MCP scan filigree_emit block and agent_summary carry the same discriminated disabled_reason. 401/403 stays reachable=False (non-load-bearing), never exit-2. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nreachable (#5) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ince the rebrand) uv.lock still carried the pre-rebrand `clarion` optional-dependency extra; pyproject already renamed it to `loomweave` (Clarion→Loomweave). Regenerated to match — no dependency change (blake3 >=1.0, unchanged), just the extra name. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…bda branch-locality, finding-lifecycle glossary Resolves three Filigree ready-queue items, built TDD with adversarial review. PY-WL-110 weft_markers soundness gap (wardline-d62845bb18, P2) contradictory_trust.py hardcoded `wardline.decorators.*` as the only marker prefix, silently missing contradictory stacks imported from the renamed `weft_markers` shim. Now derives _MARKER_NAMES + _MARKER_MODULE_PREFIXES from BUILTIN_BOUNDARY_TYPES so the rule can't drift from the grammar. +2 tests. Lambda bindings are branch-local (wardline-36016d26f3, P3) _CURRENT_LAMBDA_BINDINGS was shared across if/else, try/except, match arms, leaking a lambda bound in one arm into siblings (over-fire). Each arm now walks an arm-local copy. NOTE: the first cut of the merge-out (clear()+full-union with the synthetic fall-through arm last) introduced a *false-negative regression* — verified empirically against HEAD: a lambda rebound in a no-else `if` / no-catch-all `match` and called after the branch resolved EXTERNAL_RAW on HEAD but INTEGRAL after the naive fix. Replaced with a delta merge (layer each arm's net add/changed bindings onto the pre-branch state in source order) that keeps the leak fix AND reproduces HEAD's after-branch bindings, so no new false negative. +3 over-fire guards, +3 no-false-negative guards. Finding-lifecycle vocabulary glossary (wardline-26e84dbd44, P3) Audited wardline's own usage: `active` is already the canonical word on every surface except the CLI summary, which printed `N new`. Relabelled to `N active` (text only; no JSON/SARIF/wire field renamed). Added the canonical glossary docs/reference/finding-lifecycle-vocabulary.md (single source of truth for new/active/suppressed/baselined/waived/judged + emitted-active vs gate population) with discipline tests + nav wiring. Cross-tool asks (Filigree first-seen "new", legis active) recorded as coordination context, not renamed. Full suite 2471 passed, ruff + mypy clean, mkdocs --strict OK. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…, MCP legis reason, strict arg validation Applies the PR #30 multi-reviewer findings (code/tests/errors/comments/types): - GateDecision.__post_init__ makes "tripped gate that reads as passed" (dogfood #2) unconstructible, not merely avoided by the factory. - Filigree 403 is now distinguished from 401 across all three render sites (CLI stderr, CLI disabled_reason, MCP) — "forbidden (token lacks access)" rather than the misleading "set WARDLINE_FILIGREE_TOKEN". - MCP dirty-unsigned legis artifact carries a loud `reason` (parity with the CLI "never gate CI on it" warning) — agent-first surfaces stay equally loud. - migration_hint threaded into the agent-summary gate block so the "see gate.migration_hint" pointer in next_actions resolves on that surface too. - Strict boolean validation for summary_only/include_suppressed/allow_dirty/ explain (reject non-bool rather than silently coercing "false"→True) + max_findings JSON schema gains `minimum: 0`. - CHANGELOG: payload-controls entry corrected to dogfood #4 (verified against the friction report: #4=payload, #5=auth); genuine-trip reason quoted verbatim. - Glossary file:line anchors tightened to the WAIVED/JUDGED assignment lines. Quality consolidation (behavior-preserving): shared severity_gates() and filigree_disabled_reason() helpers, enum-identity (`is`) unified. New tests pin 5xx rendering (CLI+MCP), the MCP legis dirty/signed projection, the mixed active+suppressed gate-reason branch, the GateDecision invariant guard, strict arg validation, and the agent-summary migration_hint. Suite 2515 passed; ruff/mypy clean; mkdocs --strict builds. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Increment the release candidate (rc1 → rc2) to carry the PR #30 review hardening (gate invariant, 403/5xx distinction, strict MCP arg validation). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ust version test - CHANGELOG: stamp the accumulated [Unreleased] work as [1.0.0rc2] - 2026-06-06 and open a fresh empty [Unreleased]; consolidate the two `### Added` blocks into one (no content change, removes a Keep-a-Changelog duplicate-section smell). - README: the quick-start scan output said "1 new" — corrected to "1 active", matching the CLI relabel shipped in this same release (and getting-started.md). - test_package: assert __version__ starts with "1.0.0" (release line) instead of the exact rc suffix, so cutting a new rc no longer breaks the test. Suite 2515 passed; ruff clean; mkdocs --strict builds. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`ruff format --check src tests` (run in CI's Lint+Format job) was red. Reformats 6 test files: two touched in this rc2 work (test_run.py, test_server_query_explain.py), test_variable_level.py (dogfood branch change), and three with pre-existing drift already on main (test_legis_intake_contract.py, test_client.py, test_sei_client_wire.py) — the gate checks the whole tree, so all six must be clean. Formatting only; no behavior change. Suite 2515 passed; ruff check + format + mypy all clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…, FP guard, doc-anchor rot) Addresses the three Important findings from the PR #32 review panel, each validated with an actual RED->GREEN cycle under debugging discipline. I-1 EmitResult contradictory states (core/filigree_emit.py): - auth_rejected is now a derived @Property (status in {401,403}), deleting the redundant axis so "auth-rejected (200)" is unrepresentable, not merely unbuilt. - __post_init__ guard mirrors GateDecision: a reachable/success result carries no error status; a soft-failure created/updated nothing. Rejects reachable+503. - Docstring corrected (status is the error status; None on transport-fail AND 2xx). - No wire change: server.py still serializes auth_rejected via the property. I-2 false-positive guard for PY-WL-110 (test_contradictory_trust.py): - Empirically: a foreign-only marker stack is filtered at the anchoring gate (provenance "fallback"), never reaching the line-81 prefix check. Added both the system-level test and the isolating test (real trust_boundary anchor + a coincidental foreign `trusted`). Mutation-proven: breaking the prefix check makes the isolating test fire a false PY-WL-110. I-3 stale file:line anchors (finding-lifecycle-vocabulary.md): - Re-derived every churned-file anchor from HEAD; corrected ~26 citations. - Added a two-way content-binding discipline test: each load-bearing anchor's token must be on the cited source line AND the doc must cite that line, so doc and code can never silently diverge again. Full suite 2520 passed; ruff/format/mypy clean; mkdocs --strict builds. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… auth Rename the Filigree bearer credential to the federation-scoped WEFT_FEDERATION_TOKEN (deconfliction plumbing across the Weft federation). The loader now prefers it (env then .env) and the operator-facing 401 auth-rejected messages point at the new name. The previous WARDLINE_FILIGREE_TOKEN is honored as a deprecated fallback (read after the new name), so existing deployments keep working unchanged. The original "use Wardline's own name to avoid coupling to a sibling FILIGREE_* name" rationale dissolves now that the name is federation-scoped rather than a sibling. - config.py: WEFT_FEDERATION_TOKEN_ENV primary + WARDLINE_FILIGREE_TOKEN_ENV deprecated fallback; _read_token helper; new-name-fully-then-old-name-fully precedence; docstring rewritten. - filigree_emit.py / cli/scan.py: 401 hints name WEFT_FEDERATION_TOKEN. - tests: config precedence + both-names + legacy fallback; end-to-end fallback test in test_install; message assertions updated. - CHANGELOG [Unreleased] entry; rc2 history lines left verbatim. Suite 2525 green, ruff/format/mypy clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Auto-regenerated skill-pack artifacts from the SessionStart hooks. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ft/wardline layout Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ep); drop waivers/baseline from operator schema; sibling port discovery prefers .weft/<sibling>/ Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…hs, sibling-endpoint keys, + store_dir/C-9c silent-fallback - waivers -> .weft/wardline/waivers.yaml (load_project_waivers); add_waiver writes member state - baseline.yaml/judged.yaml -> .weft/wardline/ via core/paths - record_bindings -> detect_siblings (detect-only, no config write) - activate_pack -> operator guidance (packs stay operator-authored) - doctor reads weft.toml, may create own subtree, reports sibling detection - MCP config-path defaults -> weft.toml; waiver_add writes member state - [wardline].store_dir operator override for state-dir relocation (paths.weft_state_dir) - C-9c: missing/malformed weft.toml -> silent defaults, never hard-fail - drop sibling-endpoint config keys (hub-pinned, pending); resolver = flag>env>published .weft/<sibling>/ port Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n/judge/fix CLIs); migrate full test suite to weft.toml + .weft/wardline layout - run.py/scan.py/judge.py/fix.py: config_path default -> weft_config_path (the missed default-path bug all 7 sweep agents flagged) - ~25 test files migrated: YAML->TOML config fixtures, baseline/judged/waivers paths, detect_siblings, activate_pack guidance, config-URL removal reframed to env/published-port - glossary line-lock anchors rebound after line shifts Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dline.yaml->weft.toml strings, root .gitignore - docs: configuration.md YAML->TOML rewrite, suppression/judge/attestation/cli/weft/agents/assurance path updates, UPGRADING + CHANGELOG breaking notes - src: help text / docstrings / comments / analyzer Location -> weft.toml; detect_siblings no-URL hint points at env var + live discovery (not the removed config key) - .gitignore: drop dead .wardline-cache/ + wardline.yaml; keep .weft/wardline committed, weft.toml tracked Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ttest_key docstring path wardline.yaml lost its .gitignore entry and got staged by add -A; it is a dead config file now (its sibling-URL keys are removed/unsupported, nothing reads it). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… doctor signal, detect coverage, docs) - store_dir: confine override under root (absolute-outside-root / ../ escape -> default), matching the writers' safe_project_file confinement; deny a malicious weft.toml a write-redirect (+tests) - doctor: distinguish absent weft.toml (ok) from present-but-unparseable (error) -> restores the operator signal C-9c silent-fallback removed - .gitignore: ignore transient sibling .weft/*/ephemeral.port (a committed one could redirect token-bearing emit to a chosen loopback port) - detect.py docstrings: point operators at the env var, not the removed [wardline.<sibling>].url key - restore lost coverage for loomweave.yaml discovery (_loomweave_url_from_config / _http_url_from_bind) - config.py: drop stale 'inert in SP0' comment; resolve_*_url param docstring reframed (shared scan-config bundle parity, not 'reserved') - waivers.add_waiver: rename misleading config_path param -> path Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…_*_url (panel: no dead apparatus) URL resolution never used pack-trust; removed from both resolve_*_url signatures and every call site (CLI scan/attest/scan-file-findings + the 3 MCP emitter/client/filer methods, their callers, and the policy-resolution helpers). config_path stays (passed positionally; reserved for the pending hub sibling-endpoint key). mypy + full suite verify no run_scan kwarg was disturbed. Glossary line-lock anchors rebound + stale state-file path prose in the vocabulary doc corrected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR advances the Wardline 1.0.0rc4 release candidate by aligning configuration/state layout with the Weft federation conventions, adopting the federation-scoped WEFT_FEDERATION_TOKEN for Filigree auth (with legacy fallback), and hardening the agent/CLI surfaces based on the 2026-06-06 dogfood report (gate clarity, payload controls, and more precise Filigree emit failure classification).
Changes:
- Adopt federation-scoped
WEFT_FEDERATION_TOKEN(preferred) with deprecated fallback toWARDLINE_FILIGREE_TOKEN, and update 401/403/5xx soft-failure reporting for Filigree emit. - Complete the Weft config/store consolidation:
weft.toml[wardline]replaceswardline.yaml, and suppression state moves under.weft/wardline/(baseline/judged/waivers) with new path helpers. - Improve agent/MCP ergonomics: gate reason/evaluated/migration hint plumbing and scan payload controls (
summary_only,max_findings,include_suppressed, truncation signaling,wherefiltering of agent_summary arrays).
Reviewed changes
Copilot reviewed 106 out of 110 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| uv.lock | Rename optional extra from clarion to loomweave for blake3 wiring. |
| UPGRADING.md | Add v1.0 migration guide for weft.toml + .weft/wardline/ state and gate-default change. |
| tests/unit/test_package.py | Loosen version assertion to pin release line rather than rc suffix. |
| tests/unit/scanner/taint/test_variable_level.py | Add branch-local lambda binding regression tests. |
| tests/unit/scanner/taint/test_provenance_clash.py | Update tests to write [wardline] config in weft.toml. |
| tests/unit/scanner/rules/test_sink_rules.py | Add rule-level regression coverage for lambda binding leakage across branches. |
| tests/unit/scanner/rules/test_contradictory_trust.py | Add tests for weft_markers boundary namespace and foreign-marker isolation. |
| tests/unit/mcp/test_tool_capabilities.py | Update sibling URL resolution tests to env/published port (no config keys). |
| tests/unit/mcp/test_server_suppression.py | Switch baseline paths to .weft/wardline/ via baseline_path(). |
| tests/unit/mcp/test_server_security.py | Update waiver add + root confinement tests for .weft/ + weft.toml. |
| tests/unit/mcp/test_server_query_explain.py | Add MCP scan payload shrinking tests (where/filtering/caps/truncation/validation). |
| tests/unit/mcp/test_server_legis_artifact.py | New tests pinning MCP legis artifact attach/signing projection behavior. |
| tests/unit/mcp/test_server_filigree_emit.py | Add emit-result status/auth classification and 401/403/5xx messaging tests. |
| tests/unit/mcp/test_server_attest.py | Update pack config test to weft.toml. |
| tests/unit/loomweave/test_sei_client_wire.py | Reformat sign_request expectations (no behavior change). |
| tests/unit/loomweave/test_client.py | Reformat sign_request expectations (no behavior change). |
| tests/unit/filigree/test_config.py | Add tests for WEFT_FEDERATION_TOKEN precedence + legacy fallback. |
| tests/unit/core/test_waivers.py | Add tests for .weft/wardline/waivers.yaml state load/add behavior. |
| tests/unit/core/test_waiver_add.py | Migrate waiver add tests from config to .weft/wardline/waivers.yaml state. |
| tests/unit/core/test_root_confinement.py | Update poisoned config fixture to weft.toml. |
| tests/unit/core/test_paths.py | New tests for canonical Weft paths + store_dir override confinement behavior. |
| tests/unit/core/test_packs.py | Update pack config tests to weft.toml TOML syntax. |
| tests/unit/core/test_legis_artifact.py | Update allow-dirty behavior to emit unsigned dirty-marked artifact. |
| tests/unit/core/test_judge_run.py | Update judged path + judge config to weft.toml; use judged_path(). |
| tests/unit/core/test_filigree_emit.py | Add tests for EmitResult status/auth derivation and invariants. |
| tests/unit/core/test_dossier_assembler.py | Update baseline write path to baseline_path(). |
| tests/unit/core/test_decorator_coverage.py | Update baseline write path to baseline_path(). |
| tests/unit/core/test_config_toml.py | New tests for weft.toml [wardline] parsing + failure modes. |
| tests/unit/core/test_cli_mcp_parity.py | Extend parity test to include gate reason/evaluated/migration_hint. |
| tests/unit/core/test_baseline_generate.py | Update baseline paths and config to weft.toml. |
| tests/unit/core/test_attest.py | Update severity config to TOML + move waivers to .weft/ state. |
| tests/unit/core/test_assure.py | Move waiver setup to .weft/wardline/waivers.yaml state. |
| tests/unit/core/test_agent_summary.py | Add tests for gate reason/evaluated/migration hint + gate-aware next_actions. |
| tests/unit/cli/test_install.py | Update install/scan URL discovery expectations + token threading to WEFT token. |
| tests/unit/cli/test_install_pack.py | Install pack becomes guidance-only; asserts no config is written. |
| tests/unit/cli/test_doctor.py | Doctor no longer writes bindings; detects only and ensures .weft/wardline/ exists. |
| tests/unit/cli/test_attest_cmd.py | Update pack config setup to weft.toml. |
| tests/unit/cli/test_assure_cmd.py | Update waiver setup to .weft/wardline/waivers.yaml state. |
| tests/docs/test_glossary_vocabulary.py | New doc-discipline tests binding glossary completeness + nav + line anchors. |
| tests/docs/init.py | Add docs test package marker. |
| tests/corpus/test_waiver_discipline.py | Read repo waivers from .weft/wardline/waivers.yaml instead of config. |
| tests/conformance/test_legis_intake_contract.py | Small formatting tweak in gate_active computation. |
| tests/cli/test_scan_summary_vocab.py | New tests pinning “active” vocabulary across CLI/MCP/agent surfaces. |
| tests/cli/init.py | Add CLI test package marker. |
| src/wardline/scanner/rules/contradictory_trust.py | Recognize marker modules by prefix set derived from builtin boundary types. |
| src/wardline/scanner/grammar.py | Docstring update: severity overrides now via weft.toml [wardline]. |
| src/wardline/scanner/context.py | Docstring update: severity overrides now via weft.toml [wardline]. |
| src/wardline/scanner/analyzer.py | Update unused-config finding location path to weft.toml. |
| src/wardline/mcp/resources.py | wardline://config resource now loads weft.toml via weft_config_path(). |
| src/wardline/loomweave/config.py | Docstring update: token not read from weft.toml. |
| src/wardline/install/pack.py | Stop writing config; emit operator guidance snippet for enabling packs. |
| src/wardline/install/doctor.py | Doctor updated for weft.toml semantics, detection-only bindings, state dir ensure. |
| src/wardline/install/detect.py | Replace binding persistence with detection-only + published port discovery. |
| src/wardline/filigree/config.py | Prefer WEFT_FEDERATION_TOKEN, fallback to WARDLINE_FILIGREE_TOKEN. |
| src/wardline/core/waivers.py | Move waivers to .weft/wardline/waivers.yaml + add load helper. |
| src/wardline/core/suppression.py | Add severity_gates + gate_breakdown helpers; tighten comparisons. |
| src/wardline/core/run.py | Weft config/state paths, secure gate reason/evaluated + migration hint plumbing. |
| src/wardline/core/paths.py | New single source of truth for weft.toml + .weft/wardline/ and sibling dirs. |
| src/wardline/core/legis.py | Clean-tree-only signing; allow-dirty emits unsigned dirty-marked artifact. |
| src/wardline/core/judged.py | Update judged state docstring to .weft/wardline/judged.yaml. |
| src/wardline/core/judge_run.py | Use weft_config_path() and .weft/wardline/judged.yaml via judged_path(). |
| src/wardline/core/filigree_emit.py | EmitResult carries status; derive auth_rejected; shared disabled_reason ladder. |
| src/wardline/core/errors.py | ConfigError docstring updated for weft.toml [wardline]. |
| src/wardline/core/discovery.py | Comment update: poisoned config is now weft.toml. |
| src/wardline/core/config_schema.py | Schema now describes weft.toml [wardline]; add store_dir; remove legacy keys. |
| src/wardline/core/baseline.py | Baseline path updated to .weft/wardline/baseline.yaml. |
| src/wardline/core/attest.py | Load config from weft.toml; read waivers from .weft/wardline/waivers.yaml. |
| src/wardline/core/attest_key.py | Docstring update: secrets not in .weft/wardline/. |
| src/wardline/core/assure.py | Waiver rollup now from .weft/wardline/waivers.yaml; doc updates. |
| src/wardline/core/agent_summary.py | Payload shrinking controls + gate-aware next_actions and migration_hint field. |
| src/wardline/cli/scan_file_findings.py | URL resolution now flag/env/published port only; update help text/call shape. |
| src/wardline/cli/main.py | Baseline command paths now under .weft/wardline/. |
| src/wardline/cli/judge.py | Judge reads config from weft.toml; judged output path updated. |
| src/wardline/cli/install.py | Install uses detect_siblings(); pack activation becomes guidance text. |
| src/wardline/cli/fix.py | Fix command reads config from weft.toml via weft_config_path(). |
| src/wardline/cli/file_finding.py | Filigree URL help/error text updated (no config key). |
| src/wardline/cli/attest.py | Loomweave URL resolution call shape updated; help text updated. |
| src/wardline/_version.py | Bump version to 1.0.0rc4. |
| README.md | Update CLI summary vocabulary from “new” to “active”. |
| mkdocs.yml | Add finding lifecycle & gate vocabulary page to nav. |
| docs/reference/cli.md | Update docs for weft.toml, .weft/wardline/ paths, and URL resolution semantics. |
| docs/index.md | Update CLI summary vocabulary from “new” to “active”. |
| docs/guides/weft.md | Document sibling URL resolution precedence and detection-only install/doctor. |
| docs/guides/suppression.md | Update suppression layer locations + vocabulary (“active”) + glossary pointer. |
| docs/guides/loomweave-taint-store.md | Update configuration references to weft.toml. |
| docs/guides/legis-handoff.md | Document --allow-dirty dev/tour flow emitting unsigned dirty-marked artifacts. |
| docs/guides/judge.md | Update judged path and config section references for weft.toml. |
| docs/guides/attestation.md | Update secret-storage note to .weft/wardline/. |
| docs/guides/assurance-posture.md | Update waiver source and config references for weft.toml + .weft/ state. |
| docs/guides/agents.md | Update install/doctor semantics + scan vocabulary + scan payload controls section. |
| docs/getting-started.md | Update CLI summary vocabulary from “new” to “active”. |
| .gitignore | Stop ignoring wardline.yaml; add ignore for .weft/*/ephemeral.port and clarify .weft/ policy. |
| .claude/skills/loomweave-workflow/SKILL.md | Document write-gated Loomweave MCP tools in skill docs. |
| .claude/skills/loomweave-workflow/.fingerprint | Update skill fingerprint. |
| .claude/skills/filigree-workflow/SKILL.md | Update error-code names from CLARION→LOOMWEAVE for registry mismatch/out-of-sync. |
| .agents/skills/loomweave-workflow/SKILL.md | Mirror Loomweave skill doc write-gating updates. |
| .agents/skills/loomweave-workflow/.fingerprint | Update skill fingerprint. |
| .agents/skills/filigree-workflow/SKILL.md | Mirror Filigree skill doc CLARION→LOOMWEAVE error-code updates. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| baselined = sum( | ||
| 1 | ||
| for f in result.findings | ||
| if f.kind is Kind.DEFECT | ||
| and f.suppressed is SuppressionState.BASELINED | ||
| and f.maturity is not Maturity.PREVIEW | ||
| and severity_gates(f.severity, fail_on) | ||
| ) | ||
| if not baselined: | ||
| return None # tripped by waived/judged only — different escape, not this hint |
| def _store_dir_override(root: Path) -> str | None: | ||
| """Read the operator's ``[wardline].store_dir`` override from weft.toml, or None. | ||
|
|
||
| Read defensively and silently (C-9c): a missing/malformed weft.toml, a non-table | ||
| ``[wardline]``, or a non-string ``store_dir`` all fall through to None so the | ||
| default location is used. This never raises — store-dir resolution must not be | ||
| load-bearing on the shared file parsing cleanly.""" | ||
| try: | ||
| parsed = tomllib.loads(weft_config_path(root).read_text(encoding="utf-8")) |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2a1246266b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if last in {"external_boundary", "trust_boundary", "trusted"} and ( | ||
| fqn.startswith("wardline.decorators.") or fqn.startswith("wardline.decorators.trust.") | ||
| ): | ||
| if last in _MARKER_NAMES and any(fqn.startswith(prefix + ".") for prefix in _MARKER_MODULE_PREFIXES): |
There was a problem hiding this comment.
Match only the real builtin marker exports
When an entity is anchored by one genuine builtin marker, this prefix check also counts any decorator whose FQN merely lives under the same prefix, such as wardline.decorators.evil.trusted or weft_markers.extra.trusted. The builtin provider only recognizes the exact public/implementation exports (P.<name> and P.trust.<name>), so this broader check can report PY-WL-110 for a foreign/non-marker decorator that happens to be nested below a builtin namespace. Reuse the same exact-export predicate here to avoid false contradictory-trust findings.
Useful? React with 👍 / 👎.
|
|
||
| cfg = config_mod.load( | ||
| _cfg(args, path) or (path / "wardline.yaml"), | ||
| _cfg(args, path) or weft_config_path(path), |
There was a problem hiding this comment.
Resolve MCP legis config from the server root
For MCP scan calls that set both a subdirectory path and an explicit config relative to the project root, the actual scan uses _cfg(args, root) earlier, but the legis attachment reloads the config with _cfg(args, path). That points at <subdir>/<config> (or silently falls back to defaults if it is absent), so the attached/signed artifact can compute rule_set_version from a different config than the findings were scanned with. Use the same project-root-relative config resolution as the scan path before building the artifact.
Useful? React with 👍 / 👎.
| for name, lam in arm.items(): | ||
| if pre.get(name) is not lam: | ||
| parent[name] = lam |
There was a problem hiding this comment.
Delete branch-local lambda bindings when every arm rebinds
When a lambda name exists before a branch and each mutually-exclusive arm rebinds it to a non-lambda, those arm maps no longer contain the name, so this merge loop never removes the stale entry from parent. After the branch, a call like cb(raw) is still resolved against the pre-branch lambda even though no path leaves cb bound to that lambda, producing new false taint findings. Track removals when all possible arms drop the binding instead of only applying added/changed entries.
Useful? React with 👍 / 👎.
| baseline = load_baseline(baseline_path(root)) | ||
| waivers = WaiverSet(load_project_waivers(root)) | ||
| judged = load_judged(judged_path(root)) |
There was a problem hiding this comment.
Honor strict defaults for state-dir resolution
With --strict-defaults, config_mod.load ignores weft.toml, but these new state-path helpers still read [wardline].store_dir from that same repo file. A checkout can therefore redirect baseline/waiver/judged loading (or point it at malformed state that makes the scan exit 2) even when the caller requested hermetic built-in defaults. The state path needs a strict-defaults path, or run_scan should bypass the override in this mode.
Useful? React with 👍 / 👎.
| except (tomllib.TOMLDecodeError, OSError, UnicodeDecodeError): | ||
| return WardlineConfig() |
There was a problem hiding this comment.
Keep explicit config parse errors loud
When a caller passes an explicit --config file that exists but is unparseable (or unreadable), this catch returns built-in defaults, silently dropping the operator's severity/exclude settings and potentially making the scan false-green. The existing missing-file guard in run_scan treats explicit config mistakes as errors; parse/read failures on that explicit path need the same loud behavior while only the implicit shared weft.toml falls back to defaults.
Useful? React with 👍 / 👎.
| if not strict_defaults: | ||
| published = _loomweave_published_url(root) | ||
| if published is not None: | ||
| return published | ||
| url = _config_for( | ||
| root, | ||
| config_path, | ||
| trust_local_packs=trust_local_packs, | ||
| trusted_packs=trusted_packs, | ||
| strict_defaults=strict_defaults, | ||
| ).loomweave_url | ||
| if url and not trust_config_urls and not _is_safe_url(url): | ||
| raise ConfigError( | ||
| f"Loading Loomweave URL {url!r} from project config is disabled by default for security. " | ||
| "Specify the URL via command-line flags, environment variables, or allow local config URLs." | ||
| ) | ||
| return url | ||
| return _loomweave_published_url(root) |
There was a problem hiding this comment.
Don't freeze published-port URLs for MCP
When wardline mcp starts while Loomweave is already serving, this new published-port rung returns a concrete URL that the CLI stores in WardlineMCPServer(..., loomweave_url=...); subsequent tool calls then treat it as an explicit flag and never reread .weft/loomweave/ephemeral.port. If the sibling restarts on a new ephemeral port during the MCP session, scans/explain calls keep using the stale port instead of the ADR-044 live publication. Keep published-port discovery inside the per-call server resolver (or preserve the source) so only real flags/env values are frozen.
Useful? React with 👍 / 👎.
…ale rc4 docs Merge-gate fixes for rc4->main. (1) [major] _read_published_port / _filigree_url_from_project could crash the scan (and `doctor` detection) on a malicious sibling ephemeral.port. str.isdigit() is a superset of what int() parses: an all-digit payload over CPython's 4300-digit cap raises ValueError, and detect.py's utf-8 read also lets Unicode "digit" chars (²³⁴) through isdigit() to raise in int() — a 3-byte file, so a length bound alone is insufficient. Both sites now guard int() with try/except (catch -> continue), restoring the helpers' fail-soft -> None contract. A planted ephemeral.port can no longer DoS every scan/detect of a repo. (2) [major] Front-page + CLI docs contradicted the rc4 headline breaking change: README configuration row repointed wardline.yaml -> weft.toml [wardline]; stale 0.2.0 --version literals bumped to 1.0.0rc4 (getting-started, cli reference x2) to match runtime. Tests: over-4300-digit + unicode-isdigit cases added to the loomweave/filigree published-port malformed params and detect_siblings; red before fix, green after. Suite 2535 green, ruff/mypy clean, mkdocs --strict builds. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… store_dir path, gate fields, ascii) Follow-up to 44c02f0; no behavior change beyond items 3 and 5. (1) [major] test gap — doctor's present-but-broken weft.toml arm (the only operator-visibility signal compensating for C-9c's silent fallback) had no test. Added one asserting a malformed weft.toml surfaces as wardline.config status=="error". (2) [minor] vacuous test — test_lambda_rebinding_in_try_survives_into_finalbody passed even without the merge-out fix (the finalbody call is linear after the try body, no branch join). Rewrote to a genuine try/except post-block join: a rebinding in the try body must survive for a call AFTER the block, where the except fall-through does not rebind cb (a clear-then-union merge letting that arm win last would drop the detection). Non-vacuity pinned by the existing branch-locality control (test_lambda_binding_is_branch_local_across_try_except: same rebind called inside the except arm stays INTEGRAL). (3) [minor] weft_state_dir returned the pre-resolve path, so store_dir = "a/../b" leaked ".." into user-printed state paths (the confinement check already ran on the .resolve()d form). Now returns the resolved value; check is byte-identical. (4) [minor] MCP gate test omitted the agent-facing reason/evaluated/migration_hint fields (assembled in server.py separately from GateDecision). Now asserted at the MCP surface (migration_hint required present-and-None: no committed baseline to migrate). (5) [minor] encoding inconsistency — detect.py read the port file utf-8/replace, config.py ascii. Aligned detect.py to ascii (correct for the ASCII-int protocol), mirroring _read_published_port exactly (ascii strict + except OSError/UnicodeDecodeError + int-guard); non-ASCII payloads now reject at decode. Suite 2536 green, ruff/mypy clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ir/config_path notes, dead apparatus, stale wardline.yaml literals) Follow-up to 58e4346, closing the remaining minor review-panel items: - Lambda FN (SAE): single-slot dict[str,ast.Lambda] misses a sink-lambda bound in a non-last branch arm. Ticketed wardline-383f83fafe (candidate-set fix is out of scope); rc4 delta-merge itself is FN-closing/FP-safe, not a regression. - store_dir dual-parse seam (Arch): config_schema.py comment notes store_dir is validated here at config.load() but consumed via a separate raw parse in core.paths that bypasses the schema; confinement (not the schema) bounds it. - config_path dead param (Arch): cli/mcp.py flag-comment for the pending hub sibling-endpoint key (weft-a2f4cf95c7); passes None vs the CLI's real path. - activate_pack dead root param: removed (+ unused Path import); caller updated. - stale wardline.yaml literals: src never writes that file, so the assertions were dead duplicates of the weft.toml guard — removed/renamed across tests; test_server_tools YAML->[wardline.autofix] TOML (now valid & read); loomweave live test dropped a decorative schema-invalid config-write. - .env precedence: no change (intended, already documented in filigree/config). Suite 2536 green, ruff/mypy clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
wardline/src/wardline/mcp/server.py
Line 213 in c65547d
When MCP argument validation is skipped because jsonschema is unavailable (or this handler is exercised directly), a JSON string like "false" is truthy here and enables trust_suppressions. In that context, repository-controlled baseline/waiver/judged entries can clear the gate even though the caller appeared to request the secure default. Use the same strict boolean parsing as _bool_arg so malformed values fail instead of inverting the security posture.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| yaml = require_yaml("loading wardline.yaml") | ||
| jsonschema = require_jsonschema("validating wardline.yaml") | ||
|
|
||
| jsonschema = require_jsonschema("validating weft.toml [wardline]") |
There was a problem hiding this comment.
Delay jsonschema import until after Wardline table check
When weft.toml exists in a base install without the scanner extra, this eager require_jsonschema runs before the TOML is parsed or the [wardline] table is checked. That means a shared weft.toml that is malformed or only contains another sibling's config raises ConfigError instead of taking the documented/default fallback, breaking zero-dependency callers such as wardline://config or doctor that only need defaults. Parse the file and return defaults for absent/non-Wardline config before importing jsonschema.
Useful? React with 👍 / 👎.
| shown_active = shown_active[: self.max_findings] | ||
| shown_suppressed = shown_suppressed[: self.max_findings] | ||
| shown_facts = shown_facts[: self.max_findings] |
There was a problem hiding this comment.
Apply max_findings as a shared cap for summary arrays
For MCP scan calls that set max_findings, the top-level findings list is capped to N, but the embedded agent_summary slices active defects, suppressed findings, and engine facts independently. A repo with all three categories can still return up to 3N finding bodies in agent_summary while truncation.findings_returned reports only the top-level count, so the payload control does not actually cap the returned bodies as documented. Use a single budget across these arrays (or derive them from the already-capped display list) so max_findings bounds the full response surface.
Useful? React with 👍 / 👎.
| default location is used. This never raises — store-dir resolution must not be | ||
| load-bearing on the shared file parsing cleanly.""" | ||
| try: | ||
| parsed = tomllib.loads(weft_config_path(root).read_text(encoding="utf-8")) |
There was a problem hiding this comment.
Honor store_dir from explicit config files
When a command is run with --config other.toml and that file sets [wardline].store_dir, the scan policy is loaded from the explicit file but suppression state is still resolved by reparsing only <root>/weft.toml here. In that setup baseline/waiver/judged records under the configured store are ignored (or new state is written to the default store), so gates and baseline updates disagree with the config the operator explicitly selected. Thread the effective config path/store setting into the state-path helpers instead of always reading the root default.
Useful? React with 👍 / 👎.
…r sweep Review of the rc4->main diff surfaced one real gap and several latent items. #1/#2 (Important): an explicit --config pointing at a present-but-malformed weft.toml silently fell back to default policy via C-9c's fail-soft — a false-green in the gate. config.load now takes `explicit`: an operator-named path raises ConfigError on a parse error / non-table [wardline]; an auto-discovered file warns (visible policy-downgrade) instead of failing silently before falling back. Threaded explicit= through run_scan, judge_run, attest, the scan/fix/judge CLIs, and the MCP legis path. Sweep (latent, no behavior change): - filigree_disabled_reason derives auth-rejection from status (drops the inconsistent reachable/auth_rejected/status triple the type already forbids). - GateDecision rejects a non-Severity fail_on; AgentSummary rejects negative max_findings — matching the EmitResult/GateDecision construction guards. - legis signed/dirty status read through one shared legis_artifact_outcome authority (was re-derived on the CLI and MCP surfaces). - dropped the dead `config` input from the MCP waiver_add schema; corrected the waivers "CLI-written" docstring to "machine-written (MCP)". - federation-token cross-tier precedence pinned (new-name-in-.env beats legacy-name-in-env). Also: re-pointed the glossary anchors + finding-lifecycle citations to the shifted source lines, and applied ruff format to two files that were already drifting against the pinned ruff (pre-existing CI-format-gate failure on rc4). Suite 2548 green; ruff check/format, mypy, mkdocs --strict all clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PR-review follow-up — config-load false-green + latent type-rigor sweep (
|
1.0.0rc4 — adopt federation-scoped
WEFT_FEDERATION_TOKENThe federation loopback token was renamed
WEFT_FEDERATION_TOKEN(deconflictionplumbing across the Weft federation). Wardline adopts it for lockstep: the original
"use Wardline's own name to avoid coupling to a sibling
FILIGREE_*name" rationaledissolves now that the name is federation-scoped rather than a sibling.
filigree/config.pyreadsWEFT_FEDERATION_TOKENfirst (env →.env), then thedeprecated fallback
WARDLINE_FILIGREE_TOKEN(env →.env). New-name-fully-then-old-name-fully precedence.WEFT_FEDERATION_TOKEN— thissupersedes the
WARDLINE_FILIGREE_TOKENtext in concern chore(deps-dev): update pymdown-extensions requirement from >=10.7 to >=10.21.2 #5 below.security"); documented under CHANGELOG
[Unreleased]. rc2 history lines left verbatim..env) keep working unchanged via thefallback and can migrate at leisure.
Suite 2525 passing, ruff + format + mypy clean. New tests cover precedence,
both-names, and the legacy fallback (config + end-to-end install path).
Closes the actionable wardline items from the 2026-06-06 Loom dogfood friction report
(label `dogfood-2026-06-06`). All five concerns from the re-test are addressed and
live-verified against a freshly-spawned MCP server.
The re-test reported #2/#3/#4 as "not addressed". Root cause: stale long-running
`wardline mcp` processes, not missing code. The install is editable
(`~/wardline/src`), so a fresh spawn already has every fix — but seven long-lived MCP
servers were frozen at their spawn-time source (one was internally inconsistent and
crashed on `GateDecision.reason`). The re-tester tested #1 via the CLI (fresh process →
worked) and #2/#3/#4 via a stale MCP server (→ looked unaddressed).
Action required after merge: partners must restart their `wardline mcp` server
(or session) to pick up the code. No restart ⇒ same "broken" output.
Fixes
Live verification (fresh server, the re-tester's exact scenarios)
Tests / quality
Full suite 2482 passing, ruff + mypy + mkdocs-strict clean. Every fix is TDD'd
(red→green); golden legis signature byte-unchanged; CLI↔MCP parity preserved; CLI
`--format agent-summary` output unchanged.
🤖 Generated with Claude Code
Supersedes #30 (head branch renamed
fix/dogfood-2026-06-06-gate-legis-payload→rc/1.0.0rc2; GitHub cannot move a PR's head branch, so this is its continuation). Adds the PR #30 multi-reviewer hardening pass and cuts release candidate1.0.0rc2.Continued from #32 (auto-closed by the rc2→rc3 branch rename). Version bumped to 1.0.0rc3.
Continues #33 (head branch renamed
rc3→rc4; GitHub closes a PR when its head branch is renamed, so this is its continuation). Adds the federationWEFT_FEDERATION_TOKENadoption and cuts release candidate1.0.0rc4.