Split out of #49, whose headline ask shipped in #52. This is the third finding in that issue's body, and it is not addressed by #52.
The gap
because the fix loop never engages for these, ruleCounts and the cross-session recurrence memory never learn about the pack's most recurrent violation class
Still true. Verified on main at 4d6bb44: runCommitGate contains zero references to loadSession, saveSession, recordViolations or bumpRecurrence. Those live only in runStopGate.
It is a direct consequence of a decision made deliberately in #52: decideGate was not reused at the commit rung, because its escalate outcome releases the turn — and at commit time releasing means letting a bad commit through. Writing the manifest and naming a fixer does not touch session state.
Why it matters
The recurrence memory is what lets a rule that keeps coming back attach a behavioural correction on a later turn, and what feeds graduationThreshold ("these rules keep recurring across sessions — consider graduating them into CLAUDE.md or a hard gate").
The classes it cannot currently see are knip, npm-peers, dependency-cruiser and stryker — which, as #49 argued, are the most labour-intensive in the pack. So the memory is blind to exactly the violations most worth graduating.
The hazard to design around
Do not simply call recordViolations from runCommitGate. recordViolations counts distinct rule-keys per turn, and the stop gate is careful to call it only when stop_hook_active is false, precisely so a fix-loop retry does not re-tally the same mistake:
const tallied = isRetry ? activeSession : recordViolations(activeSession, violations);
The commit rung has no equivalent signal. An agent that hits the gate, fixes some violations and commits again would re-tally every surviving rule on each attempt, inflating counts until everything looks recurrent — which would make graduation advice meaningless rather than merely absent.
Something like the violationDigest comparison added in #47 could serve: an unchanged manifest means the same block, not a new turn.
Scope note
This is a design question about what a "turn" means at the commit rung, not a mechanical wiring job. Worth a spec before code.
Also worth deciding: whether the commit rung should share the stop rung's session file at all, or keep its own tally. #52 already gives the commit rung a distinct manifest id (<session>-commit) so the two rungs cannot clobber each other's manifests; the same question applies to state.
Split out of #49, whose headline ask shipped in #52. This is the third finding in that issue's body, and it is not addressed by #52.
The gap
Still true. Verified on
mainat 4d6bb44:runCommitGatecontains zero references toloadSession,saveSession,recordViolationsorbumpRecurrence. Those live only inrunStopGate.It is a direct consequence of a decision made deliberately in #52:
decideGatewas not reused at the commit rung, because itsescalateoutcome releases the turn — and at commit time releasing means letting a bad commit through. Writing the manifest and naming a fixer does not touch session state.Why it matters
The recurrence memory is what lets a rule that keeps coming back attach a behavioural correction on a later turn, and what feeds
graduationThreshold("these rules keep recurring across sessions — consider graduating them into CLAUDE.md or a hard gate").The classes it cannot currently see are knip, npm-peers, dependency-cruiser and stryker — which, as #49 argued, are the most labour-intensive in the pack. So the memory is blind to exactly the violations most worth graduating.
The hazard to design around
Do not simply call
recordViolationsfromrunCommitGate.recordViolationscounts distinct rule-keys per turn, and the stop gate is careful to call it only whenstop_hook_activeis false, precisely so a fix-loop retry does not re-tally the same mistake:The commit rung has no equivalent signal. An agent that hits the gate, fixes some violations and commits again would re-tally every surviving rule on each attempt, inflating counts until everything looks recurrent — which would make graduation advice meaningless rather than merely absent.
Something like the
violationDigestcomparison added in #47 could serve: an unchanged manifest means the same block, not a new turn.Scope note
This is a design question about what a "turn" means at the commit rung, not a mechanical wiring job. Worth a spec before code.
Also worth deciding: whether the commit rung should share the stop rung's session file at all, or keep its own tally. #52 already gives the commit rung a distinct manifest id (
<session>-commit) so the two rungs cannot clobber each other's manifests; the same question applies to state.