Copilot-native agent, skill, and panel surface - #362
Merged
Conversation
AGENTS.md was 1833 lines / 122,662 bytes and is injected into every agent
session on every turn by every harness, so its size is a fixed cost paid
before any work begins. It reached that size by accretion: every change
appended to it.
Split it into a ~35KB index that carries the rules, with the rationale and
reference depth one link away under docs/contributing/. The governing rule
for the split is that a prohibition never moves, only its rationale does, so
"Don'ts" is intact and the critical-subsystems index keeps a row per
subsystem naming where it lives and the risk. That table was 27,495 bytes in
30 lines; one cell was 4,367 characters. It is now 23 readable sections.
Two structural defects fixed on the way through: "## Development workflow"
was an empty heading whose subsections were mis-nested under
"## Changelog & Releases", which is why a section nominally about release
notes measured 18KB; and the changelog process was documented twice, ~800
lines apart, with separately maintained overlapping content.
Three gates needed changing so this is a restructure and not a coverage
regression:
* scan_process_markers has no default arm, so docs/contributing/* would
have been silently unclassified and exempt by accident. It is now listed
explicitly in the exempt arm, as a decision.
* The retired-surface scan in policy_docs.rs read only AGENTS.md. Moving
that prose would have left the test passing while scanning nothing, so
it now scans docs/contributing/ with the same patterns.
* A byte budget and a link-resolution check were added, because without a
ratchet the file simply regrows, and a router with rotted links is worse
than the monolith it replaced.
Verified no content was lost: every subsystem's prose is present, and no
link target was dropped. Two migration guides that only AGENTS.md linked
are now indexed in docs/README.md, where consumer docs belong.
Copilot becomes the canonical definition surface for the ADR, panel, and delivery process. Everything here is additive: `.opencode/` is untouched, so the program running against the existing process is unaffected. Thirteen agents, each pinning its own model. The three role agents carry the concurrent-slice rules, the fix-round scoping rule, and the merge ordering that the delivery tooling forces. The ten panel seats are read-only by construction rather than by instruction, because a reviewer with a shell both escapes the evidence staging and puts ten lanes on the shared Nix store and the heavy-gate semaphore. Each seat's checklist is anchored to this repo's actual invariants rather than to generic review advice, which is the point of running ten independent reviewers instead of one. Binding lives in the committed skill tables. That indirection is not a preference: on Copilot CLI 1.0.75, agent frontmatter honours `model` and rejects every spelling of effort and context tier, repo-scope settings filter through an allowlist that excludes `subagents`, and a subagent does not inherit the session's reasoning effort. So an unpinned panel lane runs at the model default while a record would attest the policy level. `reasoningEffort` in frontmatter is the sharp edge: it is accepted with no warning and does nothing, which is exactly the shape that produces a confident false attestation. Three layers defend that. Frontmatter `model` makes a hand-invoked agent fail to the right model rather than inheriting the caller's; `check-bindings.mjs` catches a mispinned or illegal effort before a run; and the record helper takes the observed effort as input and fails closed instead of defaulting to the policy string.
… ( copilotw3 ) spec-kit 0.14.4 now serves both integrations. The Copilot side uses skills mode, so `.github/skills/speckit-*/SKILL.md` invoked as `/speckit-specify`; the opencode side keeps its dotted commands under `.opencode/commands/` byte-identical, because a program is running against them. The import is deliberately by hand rather than by `specify init`. That was trialled on a scratch copy first and does three unacceptable things during the overlap: - it REPLACES `installed_integrations` rather than appending, silently dropping the opencode install the running program uses; - it rewrites shared `.specify/scripts/` and `.specify/templates/`, reintroducing banned dash codepoints into tracked files, which fails `make check-tier0`; - it rewrites a hardcoded command separator in shared error strings, which then misdirects users of the other integration. So the shared trees are untouched, `integration.json` is hand-merged to list both, and the skill text is de-dashed on import. `check-bindings.mjs` grows a coexistence guard that fails if either integration disappears from that array, which is the shape an accidental re-init takes. `integration` and `default_integration` stay `opencode` for the overlap; they select only a cosmetic invoke separator and the standing rule is that the old path wins. Those three hazards are recorded in the friction log rather than only in this message, since the same manual import is required on every spec-kit upgrade. docs/contributing/copilot-agents.md carries the narrative: the two processes and why the ADR runs separately, the authoring and execution sequences, the qualified wave-identifier scheme, the measured binding substrate with its three defensive layers against a silent effort downgrade, and the cutover condition. AGENTS.md gets a router row only; the byte ratchet would reject a section, which is the intended forcing function.
…namespace ( copilotw3 ) The delivery state layout is `<state-root>/<wave>/<candidate-id>/...`, in which the program is not a path component. That is harmless while ADR046 is the only program, but the moment a second one exists its `W1` and ADR-046's `W1` name the same state directory, and a bare `W1` in a panel record or a commit tag stops being unambiguous. The canonical form becomes one lowercase token fusing program and wave, with no separator: `adr046w1`, `spec001w1`. Fusing them rather than adding a `<program>/<wave>` path component makes uniqueness intrinsic to the identifier, so it survives being copied into an artifact reference, a commit subject, a panel record, or a checkpoint, none of which have a path structure to lean on. It also needs no state-layout change, which is what keeps the in-flight program safe. Backwards compatibility is absolute, and is asserted rather than asserted-in-prose: - `--program ADR046 --wave W1` keeps working, is not deprecated, and is not on a timer. `the_legacy_wave_namespace_is_unchanged` pins the accept and reject sets verbatim. - `a_legacy_wave_addresses_its_existing_state_directory_unchanged` proves every `W0`..`W8` still resolves to its existing directory byte for byte. Re-addressing a wave would invalidate the candidate digests binding its existing snapshots, seals, and panel records, so this is the test that matters most. - `both_wave_forms_yield_the_same_ordinal` pins the ordering that enforces "wave N+1 waits for wave N to merge" across both forms. - `two_programs_with_the_same_ordinal_do_not_collide` demonstrates the collision the scheme exists to prevent. The closed-set guarantee is kept for the legacy namespace and stated honestly for the new one: a qualified token is a bounded lowercase ASCII alphanumeric string, so it still cannot express a separator, a traversal, an absolute path, a control character, whitespace, uppercase, or an unbounded length. The parser matches on a byte slice pattern rather than slicing at a byte offset, because a byte-offset split of arbitrary operator input can land inside a multi-byte character and panic; multi-byte rejection cases are pinned. A qualified wave whose embedded program disagrees with `--program` is rejected as the inconsistency it is, rather than letting either side silently win and write state under an address nobody asked for. Spec correction: an earlier draft added a `wave_forms` field to the `help` stage's state object to document both forms. That field is reachable from the pinned wire fingerprint, whose gate requires a `DELIVERY_SCHEMA_VERSION` bump for any field change, and `ensure_schema` matches the version exactly - so the bump would make every existing ADR-046 artifact unreadable. Documenting a string is not worth breaking the running program, so the field was dropped and the same information lives in the rejection message and in docs/contributing/copilot-agents.md.
Interactive work does not need this. Per-lane binding is carried by the skill dispatch tables, so an ordinary toad or `copilot` session already binds every role correctly, which is the whole point of putting the tables in committed files. The launcher exists only for an unattended run with no terminal: it pins the session binding, the continuation and credit ceilings, and the log destination, so a multi-day run cannot quietly drift or bill without bound. Three guards, all negative-tested rather than assumed: - A dirty worktree is refused. An unattended run that starts on top of somebody else's uncommitted work cannot tell its own diff from theirs, and the panel round would then review both. - A protected branch is refused with the worktree command to use instead. - `check-bindings.mjs` runs before the session starts, because a mispinned effort is a silent downgrade that produces a plausible-looking panel record rather than an error. Flag names verified against the installed CLI rather than taken from documentation: `--mode autopilot`, `--no-ask-user`, `--max-autopilot-continues`, `--max-ai-credits`, `--log-dir`, `--resume=<id>` all exist as used. `bind-subagents.mjs` from the plan is deliberately not shipped. Its only purpose was to write per-agent pins into the operator's `~/.copilot/settings.json`, and dispatch parameters were measured sufficient with no settings block in either scope. Shipping a tool whose sole function is to modify operator settings, when nothing depends on it, adds a maintenance surface against an explicit requirement that the setup not touch that file.
…( copilotw6 ) ADR 0048 records the decision and, more importantly, the measured substrate, because several published claims about it are wrong and a future reader needs to know which parts were observed rather than assumed. Specifically: agent frontmatter is the only working declarative model pin; repo-scope settings honour neither `model` nor `subagents`, whose allowlist was read out of the binary; no frontmatter spelling of effort applies, and `reasoningEffort` is accepted with no warning while doing nothing, which is the shape that produces a confident false attestation; `--agent` is ignored over ACP; and a subagent does not inherit session effort. That last pair is why the record spends space on the silent downgrade. An unpinned panel lane runs at the model default while its record would attest `high`, producing a plausible-looking artifact rather than an error on the gate that seals a wave. The ADR names all three defending layers so none is removed later as redundant. panel-review.md now names the Copilot files as the reference implementation and records `.opencode/opencode.json` as the frozen legacy binding retained for the in-flight program, with the explicit rule that the legacy surface wins any disagreement until the cutover. Validation: `make check-tier0` PASS (2463 files), `make test-adr-index-coverage` PASS (46 ADRs indexed), `make test-changelog` PASS (74 fragments), policy_docs 12 passed, policy_lints 3 passed, policy_dash_gate 8 passed, check-bindings 13 agents against 13 rows consistent.
Two gaps, both in the same gate. The shellcheck sweep used `-maxdepth 1` under `scripts/`, so the new `scripts/copilot/autopilot.sh` and the panel skill's `stage-diffs.sh` were never linted. The sweep now also descends `scripts/copilot/` and `.github/skills/`, taking the script count from 17 to 19. `check-bindings.mjs` now runs as part of the same gate rather than by hand. Folding it into an existing target instead of adding a linter is deliberate: the repository's standing rule is against new tooling, and the check reads committed files only, so it costs nothing. It belongs in a gate rather than in a habit because the failure it catches is a mispinned dispatch table, and a panel lane dispatched without an explicit reasoning effort silently runs at the model default while its record would attest the policy level. That is a false attestation on the wave-sealing gate, not a warning. Both arms fail closed on a missing interpreter or a missing script rather than skipping.
… H6 H7 H8 H9 H10 H11 H12 H13 H14 H15 H16 H17 H18 ) Round one of the panel on this branch returned 2 of 10 sign-offs with 18 HIGH findings. This is the scoped fix round; it addresses those findings and nothing else. It is one commit rather than eighteen because the findings collapse into five statements, and splitting a single statement across a docs file, an agent prompt and a validator would make the diff harder to review rather than easier. The AGENTS.md split lost three prohibitions, not just their rationale. Four seats independently found that the ownership-marker conventions, the cgroup delegation forbidden surfaces, and the screenshot-redaction rule were moved wholesale into the non-auto-loaded contributing tree. The governing principle was that a prohibition never moves and only its rationale does, so this restores all three as compact imperative Don'ts while the reasoning stays relocated. Restoring the full prose would defeat the exercise and breach the byte ratchet; restating the rule costs a line each. The same split truncated table cells naively. Eight Where cells were cut mid-brace-expansion, so rows named paths that do not exist, and six risk sentences ended mid-word. Both are repaired; no ellipsis truncation remains. check-bindings.mjs was fail-open in four places. A missing model.rs, an unparseable policy constant, and an unknown model in either an agent frontmatter or a skill row all warned and continued. A validator that warns on the input it cannot read is not a validator, and the specific consequence here is a false attestation, so all four now fail closed. Each was negative-tested by breaking it deliberately. make-records.mjs and stage-diffs.sh wrote records non-atomically and accepted unbounded reviewer free text. Records now write-then-rename, and summaries and individual findings carry a character ceiling so a record stays a verdict rather than becoming a transcript. The panel skill claimed a reviewer could report its own binding. It cannot: in this very round five of ten seats named a model other than the one the harness dispatched. observed.json is now documented as harness-sourced only, which is the sole trustworthy input. make-records.mjs now has real coverage, since it produces the artifacts that seal a wave and was previously exercised only when a round happened to run. Twenty-one cases in scripts/copilot/test-make-records.mjs, wired into the existing test-lint arm rather than a new gate, and mutation-tested by disabling the effort guard to confirm the suite fails. The memory registers' category and disposition vocabularies are now enforced by check-bindings.mjs. This immediately caught a row this branch had itself written with disposition "filed-guard", which does not group with "filed" and would have silently broken the three-wave escalation count. Also: the networking and nixos seat prompts gained the invariants their reviewers found missing, the qualified commit-tag grammar is documented alongside the legacy forms, and autopilot prunes its run logs. Validation: make test-lint, make check-tier0, make test-policy, make test-fixture-contracts, cargo test -p xtask (399), policy_docs (12). AGENTS.md 37996 bytes against the 40000 ratchet.
… H6 ) Six findings from the second panel round, batched because five of them are the same statement about the same failure class: a helper that writes an artifact must not be able to leave a partial or stale one behind, and a constant that is mirrored out of Rust must not be able to drift silently. kernel H1, H2: make-records.mjs and stage-diffs.sh both wrote through a fixed temporary name, so two concurrent rounds collided, and neither removed its temporary on the error path. Both now carry the pid in the temporary name and clean up on failure; stage-diffs.sh returns non-zero from stage() rather than continuing with a half-written diff. observability H1: the autopilot log pruner globbed and word-split the directory listing, so a log directory whose name contained whitespace was removed by prefix. Pruning is now null-delimited end to end. Verified against 26 directories including one with an embedded newline: exactly five kept, the newline-named directory intact, no partial-path removal. observability H2: the fold-target wave pattern accepted any token ending in a digit, so a malformed target reached the register unflagged. It now carries the documented grammar. rust H1: six constants mirrored from packages/xtask/src/delivery/ were free to drift from their Rust sources, which would make a record the helper considers valid and the sealer rejects. check-bindings.mjs now reads both sides and compares them, including a check that the reviewer free-text ceilings stay no looser than MAX_STRING_BYTES. All five mirrors negative-tested. networking H1: the Net VM row in the critical-subsystems index had a truncated Where cell naming a path that does not exist, and a risk sentence cut mid-word. Repaired, and every remaining table cell in the file was checked programmatically for balance and truncation. Also corrects the qualified wave tokens in the three memory registers. Nine rows carried a hyphenated program component, which the grammar does not admit; this is the same defect the docs seat raised against the commit subjects, which was fixed branch-wide but not in the registers. check-bindings.mjs now enforces the grammar on the register wave column, so it cannot recur.
Three findings from two seats, batched because two of them are the same statement about the same file and splitting them would produce three commits that each move one line. H1 and H2 are the software seat's, both in the panel round's staging script. `stage()` removed its temporary when the *generating command* failed but not when the *rename* failed, so a full disk or a permission error on the final `mv` exited under `set -e` with the temporary still sitting next to its destination. That is the identical residue defect the previous round closed for `address.json` and for the record helper, left behind in the one path neither touched. The rename now cleans up and returns non-zero like every other failure route. The same patch had duplicated the comment block above `stage()` rather than replacing it, leaving three stale lines that described an older design; those are gone. H3 is the rust seat's. The record helper mirrors seven values out of the Rust delivery crate, and the previous round pinned six of them. The seventh is the seat roster, which is an array rather than a scalar and so needed its own comparison instead of fitting the existing table. An unguarded roster is the worst of the seven to lose: a short one writes a record set missing a seat and the gate rejects the wave for an absence nobody can explain, and a long one attests a seat the roster does not contain. It is now compared element-wise and in order, because the two agree on order today and a reordering is itself drift worth surfacing. Each fix was negative-tested rather than reasoned about. The roster guard was checked against a dropped seat, a reordered roster, and a roster the parser cannot read, and it fails closed on all three. The rename cleanup was checked by shimming `mv` to fail: the fixed script exits non-zero and leaves nothing behind, and reverting the fix under the same shim leaves the temporary, which is what makes the check load-bearing rather than decorative.
The roster comparison in check-bindings.mjs is enforced by parsing make-records.mjs with a regex. That shape can stop matching without anything else changing, and a guard that no longer matches fails open in silence rather than announcing itself. It sits on the attestation path, where the consequence of failing open is an incomplete record set discovered at seal time, so it earns a test that proves it still fires. test-check-bindings.mjs builds a fixture holding exactly the inputs the gate reads, mutates the helper roster, and asserts both a nonzero exit and a substring from the roster diagnostic. Exit status alone would pass if the gate failed for an unrelated reason, which is precisely how a dead guard hides. The baseline case is load bearing for the same reason: without it a fixture broken by something else would satisfy every negative case vacuously. Verified by mutation. With the roster block deleted from the gate, four of five cases fail and the baseline still passes, so the failures are attributable to the guard rather than to the fixture. Scoped to the finding. The scalar constant mirrors share a loop and are not parsed by their own regex, so extending the harness to them is recorded in deferred-work rather than done here.
…H1 H2 ) Two findings against the harness added last round, both of which say the same thing about it that it says about the gate: a check that quietly stops checking is worse than no check. Batched deliberately, and the reason is a dependency rather than convenience. H2 derives the roster out of the fixture, which is only sound if the fixture is a faithful copy of what the gate reads; that faithfulness is exactly what H1 establishes. Landing H2 alone would derive from a fixture not yet known to mirror the gate, and landing H1 alone would require a commit that still carries the hardcoded roster H2 exists to delete, so the split would manufacture an intermediate state containing a defect the panel has already sustained. Reviewing them together is reviewing one statement: the harness must track its own inputs, both the files it copies and the roster it perturbs. H1, from the test seat. The fixture listed only the gate's required reads. `check-bindings.mjs` also reads `.github/copilot/settings.json` behind an `existsSync` guard, and an optional read left out of the fixture does not announce itself the way a required one does: the gate simply skips the block and the baseline still passes. The file does not exist today, which is precisely why omitting it was a trap rather than a harmless simplification. Optional inputs are now listed separately and copied when present. Demonstrated. With a `.github/copilot/settings.json` carrying "subagents" placed in the repo, the real gate fails, the fixed harness fails its baseline in agreement with it, and the previous revision reports five of five passing against a repository whose gate is red. H2, from the rust seat, which rejected the justification rather than only the code. The harness hardcoded the roster and the evidence claimed a stale copy would be caught by the baseline case. That is false: the baseline mutates nothing, so it never evaluates the array at all. The roster is now read out of the fixture, which removes the third copy instead of documenting it. Demonstrated by reordering the roster consistently in both model.rs and make-records.mjs. The derived harness passes five of five, genuinely perturbing the roster the repository uses. The hardcoded revision passes its baseline, passes the dropped-seat and extra-seat cases vacuously against a roster no longer in use, and fails the reordered case with a message describing a defect that is not the one present. Also assert that a mutation changes the file, so a rewrite that produced the original text names itself instead of being reported as the guard failing to fire.
…w6fu6 H1 ) The test seat found that `.specify/integration.json` and `.specify/memory` sat in REQUIRED_INPUTS while the gate reads both behind an `existsSync` guard that skips rather than fails. Sustained. Two consequences, and the seat named the sharper one: an unconditional copy of a path the repo is allowed to drop throws ENOENT, which is the same defect the previous commit fixed for `settings.json`. Demonstrated by moving `.specify/integration.json` aside. The corrected classification passes five of five; the previous one dies with an ENOENT stack trace before running a single case. The comment is rewritten to state the rule the split actually encodes. It previously claimed every required entry announces itself through a baseline failure, which was false for the two entries above, and a comment that replaces one false claim with a subtler one is worse than the original. Classification is now taken from measurement rather than from reading a call site, because reading one call site is what produced the error. A probe that omits each declared input in turn and records whether the gate hard-fails reports six required and three optional, matching this list exactly. That probe also overturned an intermediate revision of this commit: `.github/skills` looks optional, since the directory scan is skip-guarded like the other two, but the required record helper lives inside that tree, so omitting the directory hard-fails after all. It stays required, and the reasoning that nearly moved it is now a comment so the next reader does not repeat it.
The docs seat found that the comment above `rosterFromFixture` narrated how the function came to exist, naming a superseded revision and the seat that rejected it. Sustained. That is commit-message content: a reader of the source needs to know why the roster is derived, not which review round produced the decision, and the repo's own rule is to comment what needs clarification rather than to log how it got there. The reasoning itself is kept, because it is the part that stops the next author reintroducing the defect. It now explains why a written-down roster would be a third copy and why this suite could not detect its drift, stated as a property of the code rather than as a history of it.
…fu6 H1 ) The classification corrected in this round was verified by a probe that omits each declared fixture input in turn and records whether the gate hard-fails. It overturned a wrong intermediate answer, so it earned its place, but it was run by hand and is not committed: the round's findings did not ask for it and adding a second harness would widen a fix round well past what the panel raised. That leaves the classification asserted by a comment, which is the weaker guarantee the register exists to track rather than to hide.
…opilotw6fu7 H1 ) The two input lists in the roster-guard harness stated a rule about the gate in a comment, and a comment cannot notice when the gate stops obeying it. Both directions of drift are defects: a required read that becomes skippable makes every negative case vacuous, because the fixture still builds with the path present; an optional read listed as required throws ENOENT the day the repo legitimately drops that path, before a single case runs. Omit exactly one input, run the gate, and check the exit status the classification predicts. The baseline case already establishes that a complete fixture exits 0, so a nonzero exit is attributable to the omission. An optional path the repo does not ship is reported as a skip rather than counted, since there is nothing to omit and a silent pass would overstate the coverage. Demonstrated against both failures it prevents. Filing .specify/integration.json as required: FAIL classification: omitting required .specify/integration.json fails the gate: expected a nonzero exit, got 0 Filing .github/agents as optional: FAIL classification: omitting optional .github/agents still passes: expected exit 0, got 1 The register row that recorded this as deferred is closed in the same commit, because the row exists only to describe work that was not done.
…opilotw6fu7 H1 ) Recording the H1 fix in deferred-work.md was rejected by the gate, and the rejection was the gate's own defect rather than a bad row. Two of them, in series, both of the fail-open kind this branch keeps finding. The disposition was read as the last cell of the row. The three registers do not share a column shape: deferred-work.md carries a trailing Ref column, so that read took the ref and validated nothing. Every disposition in that register had been unchecked since the guard was written. Locate the column from the header instead, and fail closed when a header names no Disposition column at all. That change then rejected a real friction-log row, because the row splits on "|" and the statement quotes a shell pipeline as "\|". The escape is one extra cell to a naive split, shifting every column after it. Split on unescaped pipes and unescape the cell text. Both are pinned by cases rather than by a hand-run, since a comment is not a test. Each is discriminating: the trailing-Ref case gives the appended row a ref that is itself a legal wave token, so the old last-cell read accepts it, FAIL a bogus disposition is rejected even behind a trailing Ref column: expected exit 1, got 0 and the escaped-pipe case gives the row a Recurrence value that is a legal disposition, so a shifted read lands on it. The intermediate state, header lookup with a naive split, misreads the real register the same way: error: .specify/memory/friction-log.md:25: disposition "1" is not in the closed set This is coupled to the finding rather than separate from it: the register row that records H1 as resolved cannot be written until the gate reads the column it claims to read.
…w6fu8 H1 H2 H4 ) Three seats reached the same conclusion independently: the header lookup added last round still failed open, in two ways, and the invariant it introduced had no coverage. These are one change rather than three, because they are all the same question, and splitting them would ship an intermediate that reads a row correctly by one measure and wrongly by another. The body names each so the batching is reviewable. The fallback was the defect. When no header had been seen the parser fell back to the last cell, which is the exact behaviour this guard was written to replace: a data row above any header, or a register with no header at all, was validated against its trailing Ref column, and a ref that happens to be a legal wave token passes. There is now no fallback. An unaddressable row is rejected, as is a row shorter than the header's disposition index. The index also resets at every line that is not a row, so one table cannot lend its shape to the next. The split was wrong in both directions. A lookbehind for one backslash treats `\|` correctly but refuses to split after `\\`, so a cell ending in a literal backslash fuses with its neighbour and the row falls short of the disposition index, where it was skipped in silence. Walking the row handles both: `\|` is a literal pipe, `\\` is a literal backslash and does not protect what follows. Both are pinned by discriminating cases, against the gate as it stood at 57a2d58: FAIL a bogus disposition is rejected in a row whose cell ends in a backslash: expected exit 1, got 0 FAIL a row that precedes any header row is rejected rather than guessed at: expected exit 1, got 0 The third case covers the header-with-no-Disposition-column invariant, which was asserted last round and never exercised.
…e ( copilotw6fu8 H3 ) The classification loop omits one input at a time to measure how the gate reacts to its absence. Listing the gate script itself among those inputs measured nothing: node exits nonzero because the script is missing, which it would do just as reliably if every check inside it had been deleted. A case that cannot fail is worse than no case, because the tally counts it. The gate is now copied by name, outside the two classified lists, so the loop runs only over paths whose absence the gate itself has to handle. The case count falls by one and the coverage does not change.
…w6fu9 H1 ) The row splitter dropped its last cell unconditionally, on the assumption that every row ends in a pipe. Markdown does not require one, so a row written without it lost a real column. The width test that followed then skipped any row left with fewer than four cells, and skipping is the wrong verb: it bypassed the wave check and the category check as well as the disposition check, so a short row was not partially validated but wholly unvalidated. Only an empty leading or trailing cell is dropped now, which is the artefact the outer pipes actually produce. The header records its width alongside its disposition index, and a data row of any other width is rejected rather than skipped. That rejection closes a second way through, in the opposite direction, which the finding did not name but which the same test now covers. An unescaped pipe inside a cell adds a column, so the header's index lands one cell to the left. In this register that is the Recurrence column, whose vocabulary overlaps the disposition vocabulary, so a row could present a legal value there while its real disposition went unread. Both directions are the same defect, a row whose columns do not line up with the header it is validated against, and both are covered because a fix for one that left the other open would not be a fix. Against the gate as it stood at eacd5a6: FAIL a row narrower than its header is rejected rather than skipped: expected exit 1, got 0 FAIL a row wider than its header is rejected rather than read off by one: expected exit 1, got 0
…tw6fu10 H1 H2 H3 ) Three ways for a register to exit this gate with an invalid value unread. They are one commit because they are one question, whether a row was actually looked at, and each is a hole in the same enumeration rather than three separate features. A register written without leading pipes is a valid Markdown table that this parser cannot see: every line fails the leading-pipe test, no header is ever found, no row is ever reached, and the gate reports success over a file it did not read. The absence of a header is now itself the failure, which is the only honest report available when nothing was validated. A data row whose first cell reads Wave was taken for a header. It was skipped unvalidated, and worse, it redefined the column shape for every row beneath it. Only the first header of a table defines it now; a later Wave-leading row is data and is validated as such. An empty cell short-circuited its own check. Wave, category and disposition were each guarded by a truthiness test that treated a blank as nothing to verify, so a row could omit all three and pass. They are mandatory, and a blank one is now named as missing. The Ref column stays optional and unvalidated, which is deliberate: two committed rows legitimately leave it empty. Against the gate as it stood at 13f2016, with the second case discriminating on the reason rather than merely the status: FAIL a register with no leading pipes is rejected rather than wholly skipped: expected exit 1, got 0 FAIL a data row that looks like a header is validated, not treated as one: exited 1 as expected but the output does not mention "is not a legal wave token", so it failed for another reason FAIL an empty disposition is rejected rather than skipped: expected exit 1, got 0 FAIL an empty category is rejected rather than skipped: expected exit 1, got 0 FAIL an empty wave is rejected rather than skipped: expected exit 1, got 0
…10 H4 H5 ) Both halves of this surface reported only their successes, and the defect found two rounds ago was a row that was silently skipped. A count of what passed cannot distinguish a check that ran from one that quietly stopped running, which is precisely how that row went unnoticed for eight rounds. The gate now names the number of register rows it validated beside the agents and binding rows. A register that is missing, unreadable, or shaped so that no row is reached used to leave the success line unchanged; the count moves instead. The harness now prints how many cases it ran out of how many it holds, and how many it skipped. The skip is legitimate, since an optional input absent from the repo has nothing to classify, but it belongs in the tally rather than scrolled off above it.
… copilotw6fu10 H6 ) The width failure offered two remedies, both about escaping, and omitted the simplest and most likely one: the author left a column out. An operator reading it would hunt for a stray pipe in a row that has no pipe problem at all. The omitted column is named first, and the escape hatch is now spelled rather than described, so the message shows what to type instead of alluding to it.
…otw6fu10 H7 ) Several comments in this file had accumulated into an account of the defects found in it: what a previous version did, why that was wrong, which reading it let through. That is useful to a reviewer following the rounds and useless to the next person to open the file, who needs to know what the code guarantees now, not what it once failed to. Each affected comment is rewritten to state the invariant. The escape handling says what an escape means rather than how a naive split misread one; the register loop says that every row is addressed against the header above it or refused, rather than recounting the fallback that used to guess; the policy reader says it fails closed rather than describing the version that warned. The comments that explain a non-obvious reason are kept, because they still answer a question the code cannot: why the mirrored constants are pinned here, and why a follow-up round is a legal origin but not a legal fold target. Comments only. No behaviour changes.
…opilotw6fu11 H1 H2 ) Two seats found the same shape from different ends, and the fix is one statement, so they are closed together. The security seat found that a row which lost its leading pipe reads as prose: it is skipped before any check runs, and because some earlier table in the file had already set the sawHeader flag, the gate still exited 0. The observability seat found that a register file which is absent entirely is skipped the same way, and that the success line reports only a bare row count, so nothing in the output distinguishes a missing register from a small one. Both are the gate claiming to have validated content it never opened. Every pipe in these registers belongs to a table, so a line carrying an unescaped pipe without a leading one is now refused as a malformed row rather than passed over as prose; the escape rule matches the one splitRow already uses. An absent register is refused outright, because all three are the memory this process runs on and an absent one is an unrecorded gap rather than an empty register. That reclassifies .specify/memory from an optional input to a required one, which the harness measures rather than asserts, so the classification case for it now expects a hard failure. Both changes are demonstrated against the previous gate: the pipeless row exits 0 there, and so does the omitted register directory. Splitting these would land a commit that closes one half of a single invariant, so it is offered as a batch and the panel is invited to reject it.
…otw6fu11 H3 ) Three columns became mandatory last round, and two of the three refusals named the defect without naming the remedy: an empty wave said only that the row named no wave, and an empty category explained why the omission mattered but not what the taxonomy was. The empty-disposition refusal already listed its vocabulary, as do all three of the invalid-value refusals, so an author could reach the useful message only by first guessing a wrong value. Both now carry the same vocabulary their invalid-value siblings do.
…lotw6fu11 H4 H5 ) Both findings are the harness scoring a case green on evidence weaker than the claim it makes, so they are closed together. The category taxonomy is closed so the three-wave escalation rule can count a recurrence, and a near-miss spelling groups with nothing. The gate has always refused one, but no case exercised that path, and the previous round's evidence wrongly claimed the new empty-category case covered it. It does not: an empty cell and a misspelt one take different branches. A case for the misspelt one is added. It passes against the previous gate too, so it is coverage for an unexercised path rather than a regression demonstration. The classification cases assert that omitting a required input hard-fails the gate, but checked only the exit status. Any hard failure scored them green, including one with nothing to do with the omission, which is the same status-only weakness found earlier in this wave. Each required input now records the diagnostic that ties the failure to the path left out, and a required input added without one is refused rather than falling back to the weaker check.
… copilotw6fu12 H4 ) The classification comment said a nonzero exit was attributable to the omission and not to unrelated breakage, and then the next sentence explained why each case also has to name its expected diagnostic. Both cannot be true. The expectText assertion exists precisely because the gate has many failures and they all produce the same status, so the status alone establishes only that a complete fixture would have passed. Say that instead. The baseline rules out a fixture that was broken before the omission; it does not identify which failure fired.
…otw6fu12 H1 ) Two register diagnostics named the defect and stopped there. A header with no Disposition column and a row above any header both leave the author knowing the gate is unhappy and not knowing which edit settles it, which is the same gap closed for the empty cells a round ago. Name the edit: add the column and a cell per row, or move the row below the header. The register shapes differ from each other, so an author who has just copied a row between registers is the likely reader here.
…otw6fu12 H5 H6 ) Both findings are the same defect at the same call site: the gate decided a register was readable and then read it wrong. Splitting them would put the two lines of the read in separate commits. A file written with lone CR line endings parsed as a single line. Its first cell was a valid header, so the header check passed and every data row beneath it went unchecked while the gate exited 0. That is the exact fail-open the last two rounds closed elsewhere, reached by a file encoding rather than by a malformed table. Split on all three line endings. A register path that is a directory passed the existence check and then threw EISDIR out of readFileSync, so the gate crashed instead of reporting which register was wrong. Require a regular file and say so. A broken symlink already failed cleanly and still does. Both cases fail against the previous gate.
…w6fu13 H1 H2 ) Round 12 gave two messages a remedy. Round 13 asked whether the rest of them have one, naming six that do not, and the answer was no: most of the gate's failures named the defect and stopped there. An author who trips one has to read the validator to learn what edit fixes it, which is the same defect the round-12 finding described, only wider. Every fail() now ends in an imperative naming the edit. The lost-row message also stops saying the line must be "clear of the table", which does not say what to do, and says to separate it from the table with a blank line, which is exactly the mechanism the parser implements. Both findings come from the product seat and are one pass over one class of message, so they are batched. Reject the batching if the rewording deserves its own reading: the messages are independent and split cleanly.
…w6fu13 H3 H5 H6 ) Three shapes made the gate report success over content it had skipped. A fence left open at end of file swallowed every line after it, including a whole table, and the run passed. A fence opened between two rows of a table swallowed the rows beneath it. A register emptied of every row but its header passed with the count simply going down, so deleting the register's contents was indistinguishable from never having written them. Each is now refused. The row count is per register, so a header with nothing under it is a failure rather than a smaller number. Measured against the previous commit: the unterminated fence and the empty register both exited 0 there. The fence opened inside a table did exit 1, but on the unrelated complaint that the row below it precedes a header, because the fence had reset the table state. The assertion on the message text is what distinguishes those two, and it is why the case is worth having. The findings come from the test and observability seats. They are batched because they are one statement -- the gate does not pass over rows it did not read -- and splitting them would put three halves of that statement in three commits. Reject the batching and they separate cleanly.
…ilotw6fu13 H4 ) The predicate has two arms and a boundary, and one case covered all three. It opened no table, so both arms could fire and neither was distinguished, and nothing sat at the pipes > 1 edge. Two cases now separate them. The first opens a blank line before the row, so the table is closed and only the first arm can reject it. The second puts a single trailing pipe below a closed table, which is one pipe short of the first arm and has no table for the second, and expects the gate to pass. Measured by mutating the gate rather than by reading it. Dropping the first arm leaves the pre-existing case green and fails the new one, which is the gap this closes. Widening the boundary to pipes > 0 fails the second case with the lost-row message on an ordinary sentence.
…w6fu13 H7 ) Two comments claimed a file fused by lone CRs would read as a single line whose first cell was a valid header, and pass with every row unread. That is wrong, and the docs seat is right to say so. These registers open with a markdown title, so a fused file begins with a hash and never reaches the header check at all: it is refused as a lost row, or leaves sawHeader false. The exit status was never 0. The defect the split fixes is real -- not one row in such a file is read as a row -- but it is caught today only incidentally, by two checks aimed at something else. A register written without a leading title would fuse into a line whose first cell is a valid header, and that one would have passed until the preceding commit began refusing a header with no rows under it. The evidence supplied to round 13 carried the same wrong claim; the correction is recorded here rather than only in the next round's notes.
…otw6fu14 H1 ) The product seat re-read every fail() in the file, not only the ones the last commit touched, and found four still naming a defect without naming an edit: the unreadable policy constants, a panel agent granted a writing tool, a repo-scope settings key that governs nothing, and a register with no header row. Each now ends in the edit. The settings one says where the binding does belong, since removing the key without pinning it at dispatch would leave the lane unbound.
The security seat found the zero-rows message promised a remedy that did
not work: it told an author to record why the register was empty, and no
prose an author could add would satisfy the check. Product independently
asked for the check to be removed, on the grounds that an empty register
is a valid zero state.
Security offered two ways out and this takes the second: implement the
escape hatch the message claimed. A line reading exactly
<!-- d2b-register: intentionally empty -->
makes emptiness something an author stated rather than something the gate
cannot tell apart from a truncation. The marker is refused once the
register has rows again, because one left behind would licence the next
truncation silently.
The other half of both findings is rebutted rather than implemented, and
the panel is asked to judge the rebuttal. Removing the check would restore
the state observability raised in round 13: a register emptied by accident
passes, and the row count simply goes down with nothing to compare it
against. It is not a lifecycle action here either. The memory skill
disposes rows in place -- folded, filed, resolved, wontfix -- and its first
triage rule is to count a category's rows across three waves, which needs
the history to still be there. Nixos reached the same conclusion this
round from the register shapes. So a register at zero rows means the
history was lost, unless the author says otherwise, which is now
expressible.
Security also found the unterminated-fence message did not say where the
fence opened. It now names the line.
Two findings from two seats are batched because the second is one line
inside the same block of end-of-register checks the first rewrites.
Reject the batching and they split cleanly.
… copilotw6fu15 H1 H2 H3 H4 H5 H6 ) The rust seat found the harness spelling the intentionally-empty marker as a literal in two cases rather than reading the constant, which is a drift hazard: a reworded marker would leave those cases asserting a string the gate no longer matches, and they would keep passing while testing nothing. The harness now declares the marker once and every case uses it. The test seat rejected the previous round's relaxed assertion. It was weakened from an exact line number to a substring because the fixture register is shorter than the repository's, and the integrator verified the real number by hand instead. A hand check is not repeated, so an off-by-one would go unnoticed - which is the whole defect the line number was added to fix. The case now writes a register of known length, so the expected line is a property of the case rather than of however long the register happens to be, and the exact value is pinned. The test seat also named four uncovered properties of the marker, all of which the security seat had relied on when it signed off. Each is now exercised: recognised around surrounding whitespace, ignored inside a fence, unable to excuse a register with no header at all, and harmless when repeated. Batched because the six findings are one change to one file: introducing the shared constant is what the new cases are written against, and splitting them would produce five commits that cannot be read apart. Rejecting the batching is reasonable if the panel would rather see the constant land alone. Each case was shown to discriminate by mutating the gate and confirming only that case fails: reporting i instead of i + 1 fails the line-number case with "opened on line 6"; dropping the trim fails the whitespace case; honouring the marker before the fence guard fails the fenced case; letting the marker short-circuit the header check fails the no-header case; and treating a repeat as an error fails the repetition case. 44 cases, 43 run, 1 skipped, all pass.
…pilotw6fu15 H7 H8 ) The docs seat found the intentionally-empty marker documented nowhere but the validator that enforces it. A convention that exists only inside the code checking it is not discoverable: an author reaching an empty register learns the marker exists by tripping the failure, and nothing tells them the marker must be removed once rows return. The d2b-memory skill owns the registers, so the rule now sits beside the disposition vocabulary it follows from, and states the three things the diagnostics cannot: that a register does not shrink on its own because rows are dispositioned in place, that the marker excuses an absent row and never an absent table, and that it must be removed when the first row returns. The changelog fragment described the gate without mentioning that it reads the registers at all, so neither the parse requirement nor the marker reached a reader of the release notes. Both are consumer-facing: they constrain how anyone maintaining a register has to write it. Batched because the two findings are one omission described twice - the marker was missing from both places a reader would look, and the wording has to agree between them. make test-lint and make check-tier0 both pass. The gate reports 13 agents, 13 binding rows and 14 register rows; the harness reports 44 cases, 43 run, 1 skipped, all passing.
…w6fu16 ) Sixteen panel rounds on one validator and its harness produced observations that belong in the register rather than in a commit message nobody will search. Six rows, recorded after the round-16 gate closed unanimously, so this commit carries register data and no reviewed content. Two are about the panel harness itself and both are recurring. A lane dispatched through the generic subagent type keeps its shell even though the seat's own agent file grants only view, grep and glob, and one lane used it to write probe files into the repository root; the tool restriction binds only when the agent is invoked as itself. Separately, a seat returns an empty response and casts no vote unless its prompt explicitly forbids it, which happened three times and is invisible unless the tally is counted rather than assumed. Two are about how findings arrive. A prompt that mandates auditing a whole class of diagnostic produced a finding naming four sites that already complied, because the seat read each message's first clause and stopped. And two seats asked to delete a check that two other seats had just defended, which no amount of weighing the seats could settle; it took evidence from a third file. One is a real gap in the tooling: a coverage case cannot discriminate against the commit before it, because the behaviour it covers already existed. Proving such a case is not vacuous needs a deliberate mutation of the gate and a restore afterwards, done by hand five times in this branch, and no target automates it. The last is the disk incident. Copying the packages tree to build an out-of-tree probe pulls the cargo target with it and had consumed 34 GB before it was stopped; the working fixture is 600 KB. The gate reports 13 agents, 13 binding rows and 20 register rows. The harness and make check-tier0 both pass.
v3 advanced by three commits after this branch opened: the ADR-046 Wave 4
merge and two CI lane changes. Only AGENTS.md overlapped, because this
branch rewrote it into a router while v3 edited the prose that moved.
Resolved by keeping the router and folding every v3 edit into the file
that now owns the corresponding prose, so no rule from either side is
lost:
- the debugging build profile and the eval-only fixture-contract lane
description land in docs/contributing/gates-and-lints.md
- the named rust-test-cache subtree lands in docs/contributing/workflow.md
- the api-surface census and the three rows that moved from advisory to
the enforcing fixture-contract lane land in
docs/contributing/critical-subsystems.md
v3's CI Rust-partition paragraph and its one advisory-to-enforcing
correction merged into AGENTS.md cleanly and were kept there.
AGENTS.md is 38414 bytes against its 40000-byte ratchet. All pinned
anchors survive: the nine dash codepoint labels, "single repair owner",
the daemon-only section header, the ADR 0015 cross-reference, and the
d2bd / d2b-priv-broker.socket / SpawnRunner mentions.
…opilotw6fu17 H1 M1 ) Multi-finding: this closes two round-17 defects that are one statement - the router stopped describing the tree accurately at the merge, in an index cell and in two row labels. Splitting them would produce two one-line commits making the same point about the same table. H1 (docs seat) and M1 (product seat), the same defect found twice: the critical-subsystems index row for the capability mint surface still named only `packages/d2b-bus/tests/public_mint_surface.rs`. v3 moved that surface to a rustdoc-JSON census and updated its own copy of the cell; the resolution folded that into docs/contributing/critical-subsystems.md but left the index row pointing at the old shape, so the router and the doc it routes to disagreed. The row now names the census crate and the golden tree alongside the source-leg test. H1 (product seat): this branch folded two new rules into files whose router rows did not advertise them. The debugging build profile went into gates-and-lints.md, reachable only from a row about heavy lanes; the rust-test-cache path went into workflow.md, reachable only from a row about worktrees and PRs. Neither is findable in one hop from a row whose label describes something else. Both labels now say what their target actually covers. Also records, as deferred work rather than fixing it here: v3 flipped the fixture-contract lane to enforcing in AGENTS.md but left 16 statements across 12 files in docs/reference/ describing that coverage as advisory. Those files are byte-identical to origin/v3, so the drift is inherited, not introduced by this branch, and sweeping it belongs in its own change.
…rule ( copilotw6fu19 ) The rule that any content change invalidates every prior sign-off is non-terminating for a round's own record. The closing round's friction can only be written once it has closed, writing it reopens the round, and the round that reopens has the same problem, so the register can never record the round that closed the phase. The carve-out is deliberately mechanical rather than a judgement call: appended rows, in .specify/memory/ only, describing a round that has already closed, in a commit touching nothing else. Bundling anything with it reopens the phase, which keeps the exemption from becoming a way to slip a change past a closed gate. Found by running the process on itself; recorded in the friction log as the round that surfaced it.
Five friction rows and one deferred item, all describing rounds that have closed. Memory-only, per the carve-out in the preceding commit.
…H3 ) Three seats found the carve-out defective on independent grounds and all three are right. The termination worry it answered was unfounded. Recording round N reopens the phase, round N+1 reviews only that bookkeeping commit, and reviewing an accurate record generates no new friction, so there is nothing to record and the loop closes. That is the same fixed-point argument fix rounds already rely on, so no exception was ever needed. It was also an escape hatch. The registers are not history alone; a deferred-work row records a commitment, so an appended row can defer a fix the panel never agreed to, or mark a requirement wontfix, and under the carve-out that would land unreviewed. Two of its four conditions were not mechanical either: whether a row describes a round that has already closed is unverifiable, and nothing enforced that the commit touched nothing else. And it had been added to one of the four places that state the rule, leaving AGENTS.md, docs/contributing/panel-review.md and the autopilot skill contradicting it. Closes the round-20 findings from product, security and docs.
The termination worry and the rejected exemption, both from the round that just closed them.
…otw6fu21 H1 H2 H3 ) The fu19 friction row said the skill now carves out memory-only appends. That was true when written and false once the carve-out was reverted, so a cold reader would have taken a withdrawn change for an active rule. Raised independently by docs and observability. The row now records that the carve-out was written and reverted, dispositioned wontfix rather than resolved, because the friction was real and the fix was rejected. The fu20 rows recorded that three seats rejected the exemption without recording why, which is the part that prevents a recurrence. Three lessons are now stated: a register row is a commitment and not only a history, an exemption condition that cannot be checked mechanically does not bound the exemption, and the invalidation rule is stated in four files so amending one silently contradicts three. The last is left open because it is a standing hazard rather than something this branch closed. The deferred row used the Ref column for prose where every other row uses a wave token, and needed a paragraph to read, which the register header says makes it a task rather than an entry. Shortened and the column cleared. Closes the round-21 findings from docs and observability.
Round 22 returned 10/10 on the register corrections. Two rows record what this phase learned since the last fold. The first is a near miss worth writing down. The agent tool's default working directory is a sibling worktree rather than this one, and putting a command in the background detaches the leading cd from everything after it. A diff staged for panel review was therefore computed in the wrong checkout, and because that checkout is detached on one of this branch's own commits the result was a real diff of the wrong range rather than an error. It was caught by reading the diff, not by any check. The second records that two seats independently raising the same finding has been correct on all four occasions it happened here.
…pilotw6fu23 H1 ) The test seat found that copilotw6fu21 was categorised build when the hazard it describes is that validation evidence was silently invalidated. Nothing failed to build; a diff staged for review was computed in the wrong checkout and looked plausible. Evidence integrity is the test seat's surface, and it owns the categorisation. Recategorised to test. One row added for friction the same round surfaced. The panel contract requires each round to stage two ranges, a delta and a full branch diff, and a seat read the full one while reporting on the delta. The two-range requirement is correct and is not being changed, but a directory holding two diffs is itself a source of misattribution, and that is worth recording rather than treating as a one-off reviewer error. It is batched into this commit rather than appended after the round so it is reviewed once. Closes the round-23 finding from test. The software seat's round-23 finding was withdrawn after the staged files were measured; it had described full.diff while reporting on delta.diff.
…lotw6fu24 H1 ) The row was recategorized last round from build to test, to answer a finding that the hazard is invalidated evidence rather than a build problem. That answered the objection but picked the wrong replacement: the artifact the incident corrupted was a diff staged for panel review, not a validation run, and the sibling row appended in the same commit describes the same hazard class under signoff. Two seats reached that conclusion independently, one of them the seat whose finding produced the cell. Also records the underlying friction, which is that the closed category vocabulary is validated per row and never across rows, so a fix scoped to one finding can leave two adjacent rows disagreeing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…pilotw6fu25 ) The file-issue operation named two labels that do not exist in this repository. gh issue create fails outright on an unknown label, so the documented command could not have worked on first use. It now creates both idempotently with --force before filing. The panel could not have caught this: its lanes hold only view, grep and glob by construction, so no seat can query repository state. That blind spot is the cost of the read-only design and is recorded rather than left implicit. Also records the spec-kit authoring track as unproven end to end. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…sed ( copilotw6fu26 H1 H2 H3 H4 H5 ) Round 26 raised the file-issue block from three seats independently, and the findings converged on one section, so they are closed in one commit. The tag enumerates all five: the unstated scope expansion, the closed set not enforced at substitution, the unsafe quoting, the error suppression that masked an authorisation denial, and the instruction-versus-mechanism objection that replaced it. The block now queries the repository's labels once and creates only the names that are absent, under `set -euo pipefail`: - The closed category set is stated at the point of substitution, because the value reaches a shell. Anything outside `signoff`, `build`, `test`, `merge`, `codegen`, `disk` is rejected rather than passed through. - Substituted positions are single-quoted, with a sentence saying the quotes belong to the command and not the placeholder. The opposite recommendation was raised in the same round on readability grounds; the safety property won and the readability concern was answered textually instead. - `--force` is not used. It updates an existing label's color and description, so on a repository owning its own `test` or `build` label it would silently overwrite that label. Skipping creation when the name is present leaves it untouched. - `2>/dev/null || true` is not used either. It was the first attempt at idempotency and it masked exactly the authorisation denial an operator needs to see, resurfacing it as a misleading "label not found" from `gh issue create` two commands later. - Replacing that with an instruction to classify the failure was also rejected, because an instruction is something an agent can skip. Querying first removes the failure rather than classifying it, so nothing depends on matching gh's error wording. - `--color` is given explicitly. It is optional, and gh picks a random color when it is omitted. Three properties were executed rather than argued, with gh stood in for so the repository was not mutated: the negative grep inside `if !` does not trip `set -e` and the script completes; a denied create aborts before `gh issue create` is reached; a failing `gh label list` aborts the same way, which is what makes the unguarded first call the authorisation probe. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…pilotw6fu27 H1 H2 H3 H4 H5 ) Round 27 found that the previous commit's prose claimed a safety property the snippet did not actually have. Two seats reached the same defect independently, which in this panel's experience is near-certain signal, and one of them carried it further than the other. The claim was that single-quoting the substituted category stops a value which escaped the closed-set check from being expanded by the shell. But `--title` was still double-quoted, so the property held only for `--label`. The larger exposure was the one neither the prose nor the first finding named: the title also carries the row's one-line statement, which unlike the category is free text rather than a closed set of six words. Under double quotes a statement holding `$PWD` would silently expand and publish a host path into a public issue title, and one holding an unbound name would trip `set -u` and echo that name into the transcript as the wrong error. The `--body-file` path was unquoted entirely, so a path containing a space would word-split. Every substituted position is now single-quoted, and the constraint says so for all of them rather than for the category alone. Two presentation findings are also closed. The statement that the operation mutates repository settings now appears before the code block rather than in a bullet after it, because a privilege boundary has to be visible before the mutation rather than after. The bullets are recast as prohibitions naming the defect each one prevents, and the paragraph of drafting history is gone: in a file that is loaded into an agent's context, the rule is what prevents the regression and the history is not. That follows the same principle this branch applies to AGENTS.md - a prohibition stays where the reader is, its rationale does not - and it is why the constraints are phrased as "do not" rather than as explanations. Correcting the record on the parent commit: its trailing tag enumerated three findings where five were closed. The convention is that the tag names every finding a commit closes. Those five were the scope-expansion statement, the closed-set enforcement at substitution, the unsafe quoting, the error suppression that masked an authorisation denial, and the instruction-versus- mechanism objection that replaced it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Three rows, all from the file-issue exchange. The first is the most useful thing this phase has produced about the panel itself. Two seats found the quoting defect independently while six missed it, and when the six were asked why rather than simply re-reviewed, five named the same cause: the surrounding prose asserted the safety property, so it read as already established and none of them audited the actual quote characters. That is a failure mode of confident documentation, not of the reviewers, and it is worth knowing that a claim in prose measurably lowers scrutiny of the code it describes. The second records that satisfying a finding is not the same as fixing its class. The category was quoted correctly at the argument the finding named and left double-quoted at a second and unquoted at a third. The third records a conflict that resolved on evidence rather than by adjudication. Two seats gave opposite recommendations on whether a reviewed commit's message could be rewritten. It could, because a message-only rewrite leaves an identical tree object, and the invalidation rule is triggered by a content change; showing the two tree hashes matching settled it without either seat having to concede a judgement call. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…6fu29 ) The panel kept returning scattered, low-value findings, and the cause was in these files rather than in the reviewers. The rule for what blocks a round was written once in the panel contract and then restated in each seat's own prompt. It diverged. Measured across the ten agents: two carried it in full, one substituted its own test, four carried a partial variant each excluding a different thing, and three carried no threshold at all beyond "confine findings to defects in the delta". A seat with no stated bar treats anything it notices as blocking, and because signoff is true iff recommendations is empty, each of those cost a full extra round across all ten seats. The next round then reviewed a larger diff, which offered more to find. That is the receding gate the contributing doc already describes, arrived at by ten reviewers correctly applying ten different thresholds. Replace all ten per-seat paragraphs with one shared section, and enforce byte-identity in check-bindings.mjs so it cannot drift again. Prose alone is what produced the divergence, so a prose fix alone would reproduce it. The shared bar also carries two rules that came out of observed misses on this branch and belong to every seat rather than to one: report the defect class rather than the single instance, and treat a claim that a property holds as something to check rather than as established. It fixes the severity gap too: the commit convention cites a severity letter taken from the reviewer JSON, but only two seats mentioned severity at all and none defined the vocabulary. Seat-specific content is untouched. The exclusions that were previously spelled per seat are preserved in the shared text, so no seat loses a qualifier it had.
…opilotw6fu30 C1 C2 C3 H1 H2 ) Five findings from one round, landed together because they are one statement: the commit that introduced the shared finding bar asked seats for a structured finding without checking what consumes it, and pinned the bar's start without pinning its end. C1 (rust). The bar asks each seat for an object. `make-records.mjs` stringified a recommendation only to length-check it and then pushed the original value into the record, and `PanelRecord.recommendations` in packages/xtask/src/delivery/panel.rs is `Vec<String>`. An object therefore passed every check in the helper and failed at the seal, which is the one gate these records exist to feed. Rendering now happens once, before the length check, the verdict digest, and the record, so `output_sha256` binds exactly what is stored. C2 (security) and C3 (test) are the same defect found independently, and H2 (kernel) is its third face. The extent ran from the bar heading to whatever `## ` came next, so a seat could inject a section between the bar and `## Output`, or embed a heading inside the bar, and the compared slice would still match every other seat byte for byte while that seat read a different threshold. Matching a bare substring had the same shape. The start is now a unique heading on its own line and the end is pinned to `## Output`, with anything between them rejected. H1 (docs). The prose describing how the bar had diverged miscounted: three seats carried a partial variant and four carried none, not four and three. The delta was recounted seat by seat rather than the correction taken on assertion, which is the standard this commit's own bar sets. Kernel also proposed treating end-of-file as the bar's extent. Not taken: software and security both showed the closed failure is correct, and C2 shows a permissive boundary is precisely the fail-open being closed. Kernel's CRLF concern is dissolved rather than argued, since the new anchors match `\r?\n`. Every new check has a negative control: an injected section, a duplicate heading, a missing `## Output`, and reverting the record fix each fail with the intended message, and the tree returns green.
v3 grew AGENTS.md by 105 lines while this branch was slimming it into a router backed by docs/contributing/, so the two edits conflicted across the whole middle of the file. Resolved by keeping the router and porting v3's genuinely new content to where the router points, rather than by taking either side wholesale: - the three-way Rust gate split and the API census shard rationale go to docs/contributing/gates-and-lints.md, with the partition names and a link left in AGENTS.md since that is the command a contributor runs; - the realized flake check and its targeted store cache go to the same file, next to the existing note that the video binary contract is the one realized check; - the mold and Cranelift measurements, the d2b-resource-api seal cache, and `make clean` go to the disk hygiene contract in docs/contributing/workflow.md. Taking v3's side would have restored a 1934-line AGENTS.md and blown the 40,000-byte context budget that policy_docs.rs asserts. The resolved file is 38,812 bytes. No v3 content is dropped; every added block was placed rather than merged away.
Operator preference for the development binding. Both the implementer and the integrator move together, because the integrator runs fix rounds against the implementer's output and splitting them would make a fix round reason at a different depth than the change it is fixing. Scoped deliberately to the Copilot surface this branch introduces: - the architect stays on claude-opus-5 at xhigh, so the planning and reviewing bindings are unchanged; - the ten panel seats stay on gemini-3.1-pro-preview at high, which is not a preference but a seal requirement - delivery/panel.rs rejects a record on any other binding, and the panel model is deliberately not the coding model; - .opencode/opencode.json and the ADR-046 specs keep gpt-5.6-sol. Those bind the concurrent program running on the old process, and one of them is a frozen spec statement about that program rather than a knob. ADR 0048's gpt-5.6-sol mentions are left alone for the same reason: they record what a substrate probe observed, and rewriting an observation to match a later preference would destroy the evidence the ADR exists to carry. Frontmatter and the dispatch row must agree or check-bindings.mjs fails, so both moved; the validator already knew gpt-5.6-luna supports max and long_context, so no capability entry changed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Copilot-native agent, skill, and panel surface
Makes GitHub Copilot the canonical agent definition surface for this repo, adds
spec-kit for Copilot alongside the existing opencode install, and gives the
ten-role panel a per-seat agent with its own researched prompt and a pinned
model binding.
Everything is additive. The ADR-046 program running on the opencode path is
untouched:
.opencode/is byte-identical, the--program ADR046 --wave W1CLIform keeps working and is not deprecated, and no existing state directory,
snapshot, seal, or panel record is moved or re-addressed.
What lands
Thirteen agents under
.github/agents/. Three role agents (d2b-architecton
claude-opus-5,d2b-implementerandd2b-integratorongpt-5.6-luna) andten panel seats on
gemini-3.1-pro-preview. Panel agents declaretools: [view, grep, glob], so they are mechanically read-only rather thanprompt-restricted, and they cannot take a heavy-gate slot or touch the shared
Nix store.
Six skills under
.github/skills/, plus the spec-kit command set installedin skills mode:
d2b-panel-round,d2b-wave-delivery,d2b-memory,d2b-autopilot,d2b-adr, and thespeckit-*set.Qualified wave tokens.
validate_wavenow accepts a single qualified tokensuch as
adr046w1orspec001w1in addition to the legacy closed setW0..W8. The program becomes intrinsic to the token, which removes a latentstate-path collision: the delivery state layout is
<root>/<wave>/<candidate>/...with no program component, so a second program'sW1would have named ADR-046's directory. The closed-set security property ispreserved rather than loosened, and a qualified token whose embedded program
disagrees with
--programis rejected.AGENTS.md becomes a router. It went from 122,662 bytes to roughly 38,100,
with no rule deleted: prohibitions stay, rationale moves to a new
docs/contributing/tree. A byte ratchet inpolicy_docs.rskeeps it fromregrowing, and
policy_docs.rswas extended so its retired-surface negativescan follows the prose into the new tree rather than silently losing coverage.
docs/contributing/*was added explicitly to the exempt arm ofscan_process_markers, which previously had no default arm and would have letthe moved prose pass by accident.
Delivery memory.
.specify/memory/gainsfriction-log.md,deferred-work.md, andengineering-debt.md, with enforced wave-token,category, and disposition vocabularies checked by the binding gate.
Two committed checks.
scripts/copilot/check-bindings.mjsruns insidemake test-lintand asserts every agent has a binding row, that each effort islegal for its model, that panel rows match the xtask policy constants, and that
no inert effort-like key appears in agent frontmatter.
scripts/copilot/test-check-bindings.mjsis its negative coverage: a fixtureharness that mutates the seat roster and asserts the guard actually fires.
Why the binding is where it is
Measured against Copilot CLI 1.0.75 rather than taken from documentation, and
three published claims did not survive contact:
reasoningEffortin agent frontmatter is accepted with no warning and iscompletely inert.
effortLevelandcontextTierare warned and ignored. Nospelling of effort works in frontmatter.
.github/copilot/settings.jsonhonours neithermodelnorsubagents; its allowlist is ten keys and excludes both.--agentis ignored over ACP.Effort therefore has to be passed at dispatch, and the binding table lives in
committed skill markdown. An agent that omits
modelinherits the parentsession's model, so each agent also pins
modelin frontmatter: that convertsthe worst hand-invocation outcome from "wrong model, attested as Gemini" into
"right model, wrong effort".
check-bindings.mjsenforces the two agree.This matters because
packages/xtask/src/delivery/panel.rsattestsreasoning_effort: "high", and an unpinned lane silently runs at the modeldefault. That is a false attestation on the binding gate, so the record helper
takes observed effort as an input and fails closed rather than defaulting to the
policy string.
No file under
~/.copilot/is modified by this change.Validation
Run on the tip of this branch.
make check(D2B_ENABLE_FIXTURE_BUILD=1)test-performance-budgets)D2B_ENABLE_FIXTURE_BUILD=1 make test-fixture-contractsmake check-tier0make test-lintnode scripts/copilot/check-bindings.mjsnode scripts/copilot/test-check-bindings.mjstest-performance-budgetsis advisory by the classification intests/layer1-jobs.jsonand is not cited as validation evidence.Review
Signed off unanimously, ten of ten, by the panel this change introduces, using
its own
d2b-panel-roundskill and its own ten seat agents.Thirty-one rounds were run. Findings were fixed in the round that raised them,
and each fix was demonstrated against the failure it claims to prevent rather
than only re-run green. Where a coverage case could not discriminate against the
commit before it, the gate was deliberately mutated to confirm the case is not
vacuous, and the mutation and its observed failure are recorded in that round's
evidence.
The root cause behind the round count
Thirty-one rounds is more than this change should have needed, and the reason
turned out to be a defect in the panel surface itself rather than in the work
under review. It was found by measuring the seat prompts rather than by
reasoning about them.
The bar a finding must clear is stated once in the panel contract, and was then
restated in each seat's own agent file - where it drifted. Grepping all ten
seats found the bar written ten different ways: two carried it in full, one
substituted its own wording, three carried it partially, and four stated no
threshold at all. Because
signoffistrueiffrecommendationsis[], aseat with no threshold has no way to record a minor observation except by
blocking the round, and each block costs a full round across all ten seats.
The contract did say this, but only in
AGENTS.md, and as a SHOULD on whoeverwrites the prompt. That is the actual failure: a dispatched lane reads its own
agent file, not
AGENTS.md, so a rule living only there is absent from thereview it governs, and nothing detected the divergence.
A prose fix would have reproduced the defect, since prose is what produced it.
The bar is now one byte-identical block in all ten seat files, and
check-bindings.mjscompares those ten slices and fails on any difference. Theextraction is anchored closed - the block must start at a unique line-start
heading and end at
## Output, and a duplicate or intervening heading isrejected - because a permissive boundary would let an injected heading yield a
slice identical to every other seat while that seat silently read extra
instructions. Both the deletion and the mutation negative controls were run and
both fire.
The round that verified the fix returned five clean sign-offs from seats that
each answered their assigned substantive question. It also returned five
findings, three of them critical, all against the fix itself: the structured
finding shape it introduced did not match
Vec<String>inpackages/xtask/src/delivery/panel.rsand would have failed at sealing, and thebar's extent was the fail-open described above, which
security,testandkernelreached independently. Those are fixed, and the record now renders eachrecommendation once, before the length check and the digest, so
output_sha256binds exactly what is stored.
Findings that were rebutted, and findings that were sustained
Where the integrator disputed a finding, the rebuttal and its evidence were put
back to the seat that raised it, which was explicitly told that withdrawing an
incorrect finding was permitted and that sustaining a correct one was not
discouraged. Three seats withdrew after being shown a measurement rather than an
argument:
docs/reference/. A per-filegit diff --quiet origin/v3showed all twelve byte-identical tov3, so thedrift is inherited rather than introduced here.
tests/test-lint.shruns both binding scripts as hard failures.sha256sumandgit diff --staton the authoritative range showed it hadread the full-branch diff while reporting on the delta.
Three findings were sustained after the same treatment, and each was a real
defect. The pattern that made this work is that the seat was given the
measurement and the standing that it could keep its finding; every withdrawal
was on evidence, and no seat withdrew because it was pushed.
Four defects were raised independently by two seats in the same round. All four
were real. That is now recorded in the friction register as a signal worth
trusting rather than as a coincidence.
Once, a seat proposed a refinement to a cell that another seat's earlier
sustained finding had produced. Rather than the integrator picking between two
reviewers, the competing argument was put back to the seat that owned the
original finding, with the bearing facts and explicit standing to sustain its
own position. It agreed with the refinement, which made the change unanimous
rather than adjudicated.
Some findings closed without a commit. A finding that asks for evidence rather
than for a change is answered by running the gate, appending the result to the
round's evidence file, and returning it to the same seat. The tree never moves,
so the other sign-offs in that round stay valid.
The panel overruled the integrator, and was right
The integrator proposed carving an exception into the panel skill, exempting
memory-only appends from the rule that any content change invalidates every
prior sign-off in that phase. The stated motivation was that recording a round
reopens the phase, which appears to be a loop that cannot close.
The panel rejected it seven of ten, on three independent grounds:
reopens the phase; round N+1 reviews only the bookkeeping; an accurate record
generates no new friction; the loop closes. A later round was run as a direct
test of exactly this and the prediction held.
A row can defer work, close a finding, or record a rule, so an unreviewed
append is an unreviewed change to what the branch claims.
files. An exemption written into one of them leaves the other three
contradicting it.
A fourth objection is the one worth keeping: an exemption whose condition is not
mechanically checkable bounds nothing, because the next author decides for
themselves whether their append qualifies.
The carve-out was reverted in full and the reasoning recorded, so the next
person to find this loop uncomfortable can read why it is load-bearing instead
of rediscovering the argument.
Deferrals are recorded in
.specify/memory/deferred-work.md, and the frictionthis branch's own sign-off surfaced is recorded in
.specify/memory/friction-log.md, rather than left implicit. Between them theynow carry 43 rows, every one produced by running this process on itself.
Merging
v3v3advanced while this branch was in review, and both times the commit editedAGENTS.md, the single file this branch rewrites into a router. That is amove-versus-edit conflict, which a diff3 hunk cannot disambiguate on its own:
"the other side deleted this" and "the other side rewrote this" look identical.
Both merges were resolved the same way: extract
git diff <base>..<v3 tip> -- AGENTS.mdfirst, keep this branch's side of every hunk, and then hand-fold eachof v3's substantive edits into whichever file now owns that prose. Merged rather
than rebased, so the trailing tags and the commit SHAs cited in the review
evidence stay valid.
The first merge folded six edits:
--profile debuggingfor full DWARFdocs/contributing/gates-and-lints.mddocs/contributing/gates-and-lints.md.scratch/rust-test-cache/as one CI-restored cache surfacedocs/contributing/workflow.mdpackages/d2b-api-surface/docs/contributing/critical-subsystems.mdand theAGENTS.mdindex rowdocs/contributing/critical-subsystems.mdtest-rustrollupdocs/contributing/gates-and-lints.mdThe second merge brought in
0a643484, which cuts the PR gate critical path.It grew
AGENTS.mdby a further 105 lines, so taking v3's side wholesale wouldhave restored a 1934-line file and blown the 40,000-byte context budget that
policy_docs.rsasserts. Its additions were placed rather than merged away:docs/contributing/gates-and-lints.md, with the partition names inAGENTS.mdsince that is the command a contributor runsdocs/contributing/gates-and-lints.mddocs/contributing/workflow.mdd2b-resource-apiseal cache kept outside the restored surfacedocs/contributing/workflow.mdmake cleanand its knobsdocs/contributing/workflow.mdThe resolved file is 38,812 bytes, inside the budget. No v3 content is dropped.
The fold was verified by grepping a distinctive phrase from every v3 addition
across both
AGENTS.mdanddocs/contributing/. The panel'sobservabilityseat named the limit of that check precisely: it proves presence, not
completeness, and no gate in this repo detects a rule silently dropped from
prose. That is why the first merge went through three further panel rounds
rather than being treated as mechanical.
Those rounds found two defects the greps could not. The
AGENTS.mdindex rowfor the capability mint surface still named only the pre-merge path, and two
router labels left v3's folded rules unreachable in one hop. Each was raised
independently by two seats, and both are fixed.
One finding was answered with measurement rather than argument. A seat reported
stale statements under
docs/reference/;git diff --quiet origin/v3per fileshowed all twelve files byte-identical to
v3, so the drift is inherited, notintroduced here. It is recorded in
.specify/memory/deferred-work.mdratherthan fixed on this branch, which is what that register exists for. The seat
re-ran the check itself and withdrew the finding.