Skip to content

0.38.0 — the record says what actually happened - #170

Merged
cfdude merged 6 commits into
mainfrom
dev
Sep 5, 2026
Merged

0.38.0 — the record says what actually happened#170
cfdude merged 6 commits into
mainfrom
dev

Conversation

@cfdude

@cfdude cfdude commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Four defects under one invariant: a write must be readable back, and a reported write must have happened. Three were reported from the field within a day of each other; the fourth was found by hitting it while fixing the others.

Grouped deliberately rather than shipped one at a time — one reviewer holds one idea across all four, and one release reaches every managed repo instead of four.

What changed

--declined-deferral silently truncated <what> at its first colon. Measured in the wild: "Set alwaysLoad:false to reclaim RAM:declined because X" recorded what: "Set alwaysLoad" — which reads as an instruction to do the thing being declined, the opposite of the record's meaning, and rendered that way in PROJECT.md. Neither split is safe for two free-text halves, so it stops guessing: a single colon behaves as before, :: is explicit, and an ambiguous value is refused with the remedy in the message. --deferral is deliberately unchanged — its left half is an epic id, which cannot contain a colon.

A deferral flag outside an archive printed updated and wrote nothing. A false statement in the project's own record. It refuses now, and names the correction path — which existed and was merely undiscoverable.

set-gate-guard wrote and nothing read. A bare invocation reports. The reader is on the toggle, not on gate-guard, which is the PreToolUse hook whose silent return is the allow signal — the reporter read that silence as a broken command.

A gate verdict was recordable only on an openspec-lane epic while set-review-mode is lane-agnostic. Every other lane's evidence became prose nothing compares, so a verdict could never read stale and integrity could not see it. The archive gate is unchanged and stays openspec-only.

--withdraw-commit — the exit from an append-only array, and the one item not on the original list. It was found the hard way: a commit was attributed, then correctly reset away, and integrity reported a dangling sha with no supported fix — hand-editing state.json is forbidden, tagging the orphan makes a false record permanent, and remove-epic destroys the disposition.

Gate 2 — two independent fresh-context reviewers (thorough)

Reviewer A returned FAIL with two Criticals. Both accepted, both fixed:

  • The new verb could walk an epic past the archive gate. Withdrawing every sha moved an epic from stale to none-attributed, and an archive that had been refused succeeded. It reads attribution-withdrawn now and is refused by name.
  • withdrawnCommits was write-only#159's own defect, shipped back in on the feature fixing it, while three readers branch on the array it empties.
  • Plus: --declined-deferral ":" recorded {what:"", reason:""} and satisfied the archive gate; the withdrawal borrowed the disposition's --reason so "the sha was reset away" became why the epic was delivered; attribute+withdraw of one sha landed in both arrays; a single withdrawal removed every duplicate and moved the Gate 2 endpoint; and the removal had none of the read-back verification the append has.

Reviewer B: no Critical, two Important. --withdraw-commit reached EPIC_FLAGS, the usage line, README and the CHANGELOG while missing from commands/epic.md's flag table — because the existing gate is one-way, deriving "documented" as the usage line union the command doc. The reverse direction is now asserted, and it reproduced B's finding mechanically before the docs were written.

Also fixed, found while in these files

  • The reconcile-gate refusal told you to bypass it with set-gate-guard off on a branch that is unconditional and which the flag does not reach — verified false, and it contradicted the command doc.
  • record-gate-review --help never said a pass requires both shas; set-gate-guard --help said "takes no flags" and never named the read form this release added.
  • Secret scanning in the pre-commit hook: core.hooksPath bypasses the git secrets hook installed at clone time, so the repo had none.

Verification

  • 1101 tests, 0 failures.
  • No migration. withdrawnCommits is additive; a state.json written by 0.37.0 loads byte-identically through render, verify-state, integrity and brief on a 158-epic file.
  • The emitted rules block is byte-identical to 0.37.0's on all three platforms — managed repos receive no change.
  • Docs merged and verified live, Real Numbers recomputed (55 releases / 1,101 tests / ~12,600 LOC).

Closes #159, #163, #165, #166.

https://claude.ai/code/session_015M7zPrTRqXqSJQu9LJypk1

core.hooksPath sends git to .githooks/ instead of .git/hooks, which bypasses
the `git secrets` hook ~/.git-templates installs at clone time. Without this
call the repo has NO secret scanning at all. Placed first in the hook so it
runs before the suite.

Claude-Session: https://claude.ai/code/session_015M7zPrTRqXqSJQu9LJypk1
#166

Four defects, one invariant: a write must be readable back, and a reported
write must have happened.

#165a — `--declined-deferral "<what>:<why not>"` split on the FIRST colon, so a
colon inside <what> silently truncated it and dumped the remainder into the
reason. Measured in the wild: "Set alwaysLoad:false to reclaim RAM:declined
because X" recorded what="Set alwaysLoad", which reads as an instruction to DO
the thing being declined and rendered that way in PROJECT.md.

Neither split is safe for two free-text halves: first-colon breaks on a colon in
<what>, last-colon breaks on a colon in the reason, and reasons are sentences so
they carry colons MORE often. So this stops guessing. `::` is the explicit
separator, a single colon behaves exactly as before, and an ambiguous value —
two or more colons with no `::` — is REFUSED with the remedy in the message. A
value with no separator at all is refused too. `--deferral` is deliberately
UNCHANGED: its left half is an epic id, which cannot contain a colon, so
first-colon is right there and the same rule would refuse a valid <section>
carrying one. Verified before writing.

#165b — supplying a deferral flag WITHOUT archiving computed the assertion,
dropped it, and printed "updated": a false statement in the project's own
record. It is NOT immutable — verified that re-archiving with
--correct-disposition overwrites it cleanly — so the correction path existed and
was merely undiscoverable. This refuses and NAMES that path.

#159 — `set-gate-guard` wrote and nothing read. A bare invocation now reports.
The reader goes on the TOGGLE, not on `gate-guard`, which is the PreToolUse
hook: it blocks with stderr + exit 2 and ALLOWS by returning silently, so its
stdout is protocol surface. Following `owners`, the output states the value AND
its limits — above all that `on` alone does not mean anything is blocked, which
is exactly the inference the reporter drew from `gateGuard: true` plus silence.

#163 — a gate verdict was recordable only on an openspec-lane epic, while
set-review-mode is lane-agnostic and its own table names "a Superpowers task
review". THE ARCHIVE GATE IS UNCHANGED and stays openspec-only: recording
evidence where a review happened must not create an obligation where none
existed. Call-site sweep (rg isOpenspecLane): archive-gate, the epic-progress
bypass render and integrity's one openspec-scoped check all keep their scope;
integrity's staleness/evidence checks read gateReview with no lane filter and so
now cover other lanes, which is what #163 asked for. Verified on a fixture that
this produces the TRUE finding it should, not a false one.

#166 — `--withdraw-commit <sha> --reason "<why>"`, the exit from an append-only
array. Found the hard way in this very session: a commit was attributed and then
correctly reset away, and integrity reported a dangling sha with no supported
way to fix it — hand-editing state.json is forbidden, tagging the orphan makes a
false record permanent, and remove-epic destroys the disposition. A reset is a
normal operation and the gate procedure requires attributing at the moment of
each commit, so an attribution can outlive its commit through no error of
process. Append-only stays: the withdrawal is recorded in a SIBLING
`withdrawnCommits` field, so the last attributed entry remains the endpoint a
recorded Gate 2 headSha is compared against. Refuses a sha the epic never
attributed, and refuses a missing reason.

conductor-09's "record-gate-review rejects a non-openspec-lane epic" is inverted
with its rationale; the write-nothing property it protected is covered by the
refusals that remain.

Closes #165, #159, #163, #166.

Claude-Session: https://claude.ai/code/session_015M7zPrTRqXqSJQu9LJypk1
…ond one lane

README, the conductor skill and three command docs for 0.38.0's surfaces:
`::` on --declined-deferral, the bare `set-gate-guard` read, `--withdraw-commit`,
and that record-gate-review now accepts any lane while the archive gate does not.

Also fixes three things this release made visible rather than introduced:

- scripts/lib/gate-guard.mjs's reconcile refusal told the reader to bypass it
  with `set-gate-guard off`. That branch is UNCONDITIONAL and the flag does not
  reach it — verified by setting it off and watching the hook still exit 2 — so
  the sentence was simply false, and contradicted commands/gate-guard.md's own
  "There is no bypass for this specific case". The tracker branch keeps its
  bypass sentence, because there the flag really does gate it.
- The single-colon `--declined-deferral` template in archive-gate.mjs's refusal
  and update-epic.mjs's usage line: still valid input, no longer the form to
  teach.
- `--withdraw-commit` was absent from the usage line, and the deferral flags
  rendered `<a value>` in --help rather than their actual shape. Placeholders
  now carry `epicId:artifact section` and `what::why not`, so the help teaches
  the separator instead of describing it elsewhere.

`--declined-deferral "<what>:<why not>"` is MIRRORED in four places, one of them
scripts/lib/rules.mjs — the copy emitted into every managed repo's CLAUDE.md.
Changing one copy would put a user's rules block permanently out of step with
the skill, so the `::` guidance travels as prose and the mirrored template is
left alone. epic.md's flag-reference row shows `::` because that row is the
flag's own reference and is not mirrored.

conductor-13 required an exercise entry for the newly documented
--withdraw-commit: documenting a flag obliges invoking it and reading the value
back. The entry covers both halves — the array loses the sha AND the sibling
record gains it — because a check on the array alone would pass against an
implementation that erased without recording.

1092 tests, 0 failures.

Claude-Session: https://claude.ai/code/session_015M7zPrTRqXqSJQu9LJypk1
The record says what actually happened — four defects under one invariant:
a write must be readable back, and a reported write must have happened.

No migration. `withdrawnCommits` is additive and absent until a withdrawal
happens; a state.json written by 0.37.0 loads unchanged.

One deliberate behavior change for existing repos beyond the fixes: integrity's
staleness and evidence checks now reach non-openspec lanes, because they read
gateReview with no lane filter and record-gate-review no longer refuses those
lanes. Expect TRUE findings on epics that carry a verdict and attributed no
commits. The archive gate is unchanged.

Claude-Session: https://claude.ai/code/session_015M7zPrTRqXqSJQu9LJypk1
…past a gate

Two independent fresh-context reviewers (review mode: thorough). Reviewer A
returned FAIL with two Criticals; reviewer B no Critical, two Important. Every
finding accepted, none disputed.

C1 — THE ONE THAT MATTERS. The new --withdraw-commit could walk an epic through
the archive gate. gateStaleness answers `stale` when a recorded Gate 2 headSha
does not reach the attributed commits, and `none-attributed` when there is
nothing to compare; withdrawing every sha moved an epic from the first to the
second, and an archive the gate had been refusing SUCCEEDED. It now answers
`attribution-withdrawn`, the gate refuses it by name, and the verdict renders
`⚠ attribution withdrawn`. A withdrawal says a sha was WRONG — never that the
work was never done.

C1 also shipped #159's own defect back in on the feature fixing it:
`withdrawnCommits` was written and read by nothing, while three readers branch
on the array it empties. integrity now reports the withdrawal WITH its recorded
reason instead of telling an agent to "record the range that shipped" — which
invited re-attributing the very commit somebody deliberately removed.

C2 — `--declined-deferral ":"` recorded {what:"", reason:""} and SATISFIED the
archive gate: an assertion asserting nothing, which is the exact silence
archive-gate.mjs:208 exists to make impossible. `"x:"` and `"x::"` recorded a
decline with no reason while the refusal two lines above said the reason "is not
optional" — the code disagreed with its own message. Both halves must now be
non-empty.

I1 — the withdrawal borrowed --reason, which serves the DISPOSITION, so the
reason a sha was withdrawn silently became the reason the epic was delivered and
rendered that way. --withdrawal-reason is its own flag. Two records, two
reasons, two flags. (I predicted this collision when dispatching the review and
did not test it; the reviewer confirmed it.)

I2 — attributing and withdrawing the same sha in one invocation left it in BOTH
arrays and reported success. Refused.
I3 — a single withdrawal removed EVERY occurrence of a sha, deleting two entries
for one request and silently moving the Gate 2 endpoint. It removes the last
occurrence, one per request, so the endpoint moves only when the endpoint is
what you withdrew.
I4 — the removal now gets the read-back verification the append has: render()
writes after saveState(), so a removal is exactly as vulnerable to being
silently undone (#140's mechanism).

Reviewer B: `--withdraw-commit` reached EPIC_FLAGS, the usage line, README and
the CHANGELOG while missing from commands/epic.md's flag table — whose own
preamble says those flags are declared once in EPIC_FLAGS. Nothing caught it
because conductor-13's gate is ONE-WAY: it derives "documented" as the usage
line UNION the command doc, so either half discharges it. The reverse direction
— registry ⇒ documented — is now asserted for update-epic and add-epic, and it
reproduced the finding mechanically before the docs were written.

Also from B: record-gate-review --help never said a pass requires both shas;
set-gate-guard --help said "takes no flags" and never named the read form this
release added, which is ironic given #159's complaint was that there was no way
to find out whether the guard was on; the two refusals inside --declined-deferral
taught different separators; and the 0.38.0 release object did not exist.

FLAGLESS_USAGE is additive: a flagless verb absent from it renders exactly as
before.

1101 tests, 0 failures.

Claude-Session: https://claude.ai/code/session_015M7zPrTRqXqSJQu9LJypk1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

gate-guard (the read verb) prints nothing — zero bytes on stdout while the guard is on

1 participant