fix(quality-gate): tally the quality gate and bound its cap line (#163) - #172
Conversation
runQualityLoop was the only capped gate that never called
recordGateOutcome or reported gate_findings, and its cap-exhaustion exit
('degraded') was silently swallowed by both callers. Add one
recordGateOutcome(ctx, 'quality', ...) call per loop iteration (dismissed
on simplify/review agent death, accepted on approval, carried-unresolved
on the empty-findings clean exit, and carried-unresolved/re-litigated on
changes_requested depending on whether the cap was hit), plus a single
rolled-up VERIFY_SKIPS line per issue when the cap is exhausted without
approval or agent death (runQualityLoop runs once per task plus once per
PR-fix round, so the line rewrites in place instead of duplicating).
Also add a computeGateYield footnote noting quality's larger denominator
versus pr-review, and correct three stale doc comments: the
recordGateOutcome header/dismissed/bound/carried-unresolved clauses now
cover quality, and the FRICTION_WEIGHTS quality_degrades comment now
says it counts agent death inside the loop, not cap exhaustion, pointing
readers at gate_findings.quality['carried-unresolved'] instead.
Refs #163
…-op guarantees
Extend tests/quality-loop.test.js with one test per recordGateOutcome
disposition branch from task 1's map (simplify-death and review-death ->
dismissed, approved -> accepted, changes_requested-before-cap ->
re-litigated), a typed mixed-severity issues array proving non-zero
gate_findings.quality.severity (mirrors pr-review-gate.test.js:390), the
exact invariant sum(gate_findings.quality.disposition) ===
ctx.metrics.quality_iters (including across a later mid-loop death), a
fully capped loop asserting disposition {'re-litigated': 4,
'carried-unresolved': 1} / result 'degraded' / quality_degrades 0 / exactly
one VERIFY_SKIPS entry, two capped scopes on one ctx rolling up to exactly
one entry naming both, and a converging loop pushing none. Add a
carried-unresolved assertion to the existing empty-findings-exit test.
Extend tests/friction-fields.test.js to prove a capped quality loop leaves
contrarian_capped false and unresolved_count 0 -- the quality cap writes to
gate_findings, never to ctx.unresolved.
Extend tests/gate-yield.test.js to prove a 'quality' key tallies into
by_gate without perturbing escaped_defects (neither an early gate nor the
escape gate), and that the quality-denominator footnote renders only when
a quality key is present.
Extend tests/gate-findings.test.js to prove a 'quality' key tallies
independently of the pre-existing approach/plan buckets on the same ctx.
Full profile test_command (node --check, lint-engine, bash -n, manifest
JSON parse, node --test, setup-worktree.test.sh) is green: 641/641 unit
tests, 32/32 worktree-script cases.
Refs #163
… cap
Adds a "## The quality gate" section to gate-hygiene.md covering the
five-branch disposition map runQualityLoop now records through
recordGateOutcome, the sum(disposition) === quality_iters invariant, why
the empty-findings exit tallies carried-unresolved but returns approved
(computeGateYield's accepted:dismissed ratio and the approach contrarian
gate's carried-unresolved precedent), what quality_degrades does and
doesn't count, and the one-line-per-issue VERIFY_SKIPS cap roll-up.
Extends the page's provenance section to also name metrics.md:114
("rolls the three gates' gate_findings tallies"), which goes stale now
that a fourth gate (quality) routinely appears in the rollup, and can't
be corrected in place because architecture-provenance.test.js hashes
metrics.md's one tracked segment — the entire file — verbatim.
Extends the "frozen passage" section in both docs/architecture/AGENTS.md
and docs/architecture/CLAUDE.md with identical text (copied byte for
byte per the freeze-pair convention), and updates the gate-hygiene.md
file-map row in index.md, AGENTS.md, and CLAUDE.md to reflect the page's
broadened scope.
Issue #163 task 3.
Spec Review (Iteration 1)Verdict: approved Verified PR #172 against issue #163's three required changes and hard constraints, diffing against the correct base ( 1. 2. Cap exhaustion rolled up to one 3. Hard constraints:
Scope: diff against the correct base is tightly bounded to the three planned tasks — engine ( |
Code Review (Iteration 1)Verdict: changes requested — one major documentation-accuracy defect plus two minors. The engine change itself is correct: I traced every branch of Verified clean
Findings1. MAJOR — The new section says if (!fix || fix.status === 'error') { degraded = true; log(... 'fix degraded'); break }The engine's own
Fix direction: name all three deaths at 2. MINOR — The rendered line is Fix direction: prose only, no computation change — name the gates the check actually consults, e.g. 3. MINOR — " Fix direction: "hashes the file from its first heading to the end, which includes line 114" or similar, in all three copies ( Not flagged, for the record
|
…erclaims Code review for PR #172 found gate-hygiene.md describing quality_degrades as counting only two agent deaths (simplify, review) when the fix agent's death also increments it; fix all three deaths in both places that made the claim. Also corrects two provenance passages (gate-hygiene.md and its AGENTS.md/CLAUDE.md freeze pair) that described metrics.md's 329-of-332-line tracked segment as "the entire file," and reworks the escaped-defects header in computeGateYield so it names the gates the check actually consults instead of claiming every earlier gate missed the finding.
PR Review Fix (iteration 1)Commit d7b2d49
|
The "329 lines starting at its first heading, running to the end of the 332-line file" wording implied a segment that literally spans lines 5-332 on disk, which is 328 lines, not 329. The tracked segment's true length includes a trailing blank line that writeLines() strips at write time and architecture-provenance.test.js pads back in for hashing -- it never exists on disk. Say so directly: 329 tracked lines, 328 of them visible on disk (5-332), plus the one stripped trailing blank.
Spec Review (Iteration 2)Verdict: approved Re-verified PR #172 against issue #163's three required changes and hard constraints, on top of the "PR Review Fix (iteration 1)" commits ( All three requirements stand as approved at Iteration 1 — no regression from the fix commits:
Code Review (Iteration 1)'s three findings are fixed, verified directly in the tree:
Verified directly:
Scope: unchanged from Iteration 1 — engine, tests, and docs only, all within the three planned tasks plus the code-review fix-up. No scope creep found. |
Code Review (Iteration 2)Verdict: changes requested - one MINOR left over from iteration 1's MAJOR. Both documentation fixes and the escaped-defects header reword landed correctly and I re-verified each of them; the engine logic is unchanged since iteration 1 and still correct. Iteration 1 findings: re-verified
FindingMINOR - // Cap exhaustion (issue #163): the loop ran out of iterations without a
// clean review AND without an agent dying (both degrade branches above
// break out before reaching here).There are three The predicate itself is correct and I am not asking for a logic change: Fix direction: change "both degrade branches above" to "all three degrade branches above" (or "every degrade branch above"), in Verified clean
Not flagged, for the record
|
…count The cap-exhaustion comment claimed "both degrade branches above" but the loop has three (simplify, review, fix) — a leftover from before the quality_degrades undercount fix added the third branch.
PR Review Fix (iteration 2)Commit: 9a63771 Fixed the code review's one-word finding: the quality-gate cap-exhaustion comment at |
Spec Review (Iteration 3)Verdict: approved Re-verified PR #172 against issue #163's three required changes and hard constraints, on top of the "PR Review Fix (iteration 2)" commit ( All three requirements stand as approved at Iterations 1 and 2, unaffected by the latest commit:
Code Review (Iteration 2)'s one remaining finding is fixed, verified directly in the tree:
Verified directly:
Scope: unchanged from Iteration 2 — the only commit since then is a two-line comment fix in the two lockstep engine files, addressing exactly the code review's finding. No scope creep found. |
Code Review (Iteration 3)Verdict: approved — no blocking findings. Iteration 2's single MINOR is fixed, and a fresh pass over the whole diff against Iteration 2's finding: fixedMINOR ( Re-verified rather than assumedBecause this is the merge gate, I re-ran the full baseline and re-traced the parts a one-word commit could not have broken but that a stale earlier reading could have gotten wrong:
Not flagged, for the record
|
Implementation CompleteBranch
Reviews passed: spec review and code review, two fix iterations each (contrarian challenge on approach, then a code-review pass that caught a stale doc line and an off-by-one in its own follow-up wording, both corrected). Deferred Suggestions for Follow-up
|
Closes #163
Summary
The quality loop (
runQualityLoopinworkflows/ticketmill.js) was the only capped gate that never calledrecordGateOutcomeand never reportedgate_findings. It could return'degraded'on cap exhaustion (MAX_QUALITY_ITERATIONS = 5) with both callers silently ignoring that return value: no cap note, no Verification Gaps line, nogate_findingsrow, and no metric that isolated cap-exhaustion from agent-death. This PR ties the quality gate into the same observability machinery every other gate already uses.recordGateOutcome(ctx, 'quality', ...)calls insiderunQualityLoop, one per iteration, matching the branch map: simplify-agent death and review-agent death both record[]/'dismissed'; an approved review records the findings /'accepted'; the empty-findings exit records'carried-unresolved'; achanges_requestedverdict records're-litigated'on non-final iterations and'carried-unresolved'on the cap iteration (mirroring the pr-review disposition ternary). This gives the exact invariantsum(gate_findings.quality.disposition) === ctx.metrics.quality_itersper issue.VERIFY_SKIPSentry per issue via a lazily-initializedctx.quality_capsarray and a rememberedctx.quality_cap_skip_index, rewriting the same line in place across multiple quality-loop invocations (per task, per PR-fix iteration) rather than appending duplicates.ctx.unresolvedfrom the quality loop — that field feedsfrictionFields'scontrarian_capped/unresolved_count, which must stay0/falseon runs where no CONTRARIAN gate capped. The newqualitykey ingate_findingscarries this instead.quality_degradesmetric (in itsFRICTION_WEIGHTScomment and doc-clarified elsewhere) to state plainly that it counts agent death inside the loop, not quality regressing or cap exhaustion.computeGateYield, guarded onbyGate.quality, explaining that quality's denominator is naturally larger than pr-review's (one call per task plus one per PR-fix iteration, not one per PR).docs/architecture/gate-hygiene.mdwith a "## The quality gate" section covering the five-branch disposition map, the invariant, why the empty-findings exit talliescarried-unresolvedbut still returnsapproved, whatquality_degradesdoes and doesn't count, and the one-line-per-issueVERIFY_SKIPScap roll-up. Updated the provenance/stale-prose bookkeeping inAGENTS.md/CLAUDE.md/index.mdto match, including noting thatdocs/architecture/metrics.md:114goes stale on merge but can't be corrected in place (the provenance test hashes that file as a single tracked segment).Key decisions
STOP.trippedreturns beforequality_iters++, so the invariant holds unconditionally, not just on the happy path.'halted', and both callers turn'halted'intofail(), so the VERIFY_SKIPS line only asserts what's true: the loop hit its iteration cap.has_signalgets more sensitive as a side effect: quality reviews name findings on most tasks, so the Gate Yield section will now render on essentially every run where it previously could legitimately print "No gate findings recorded this run." Noted for anyone diffing run-over-run gate-yield trends across this release boundary.ctx.quality_cap_skip_index) now exists at two call sites of the same shape; a future issue should extract a shared helper instead of a third copy under a different field name.Token usage (approximate, this issue only): 283847 output tokens