Skip to content

feat(review-loops): make review findings the load-bearing artifact (#162) - #169

Merged
aaddrick merged 9 commits into
Batch_2026-07-27_225225from
issue-162-make-review-findings-the-load-bearing-artifact
Jul 28, 2026
Merged

feat(review-loops): make review findings the load-bearing artifact (#162)#169
aaddrick merged 9 commits into
Batch_2026-07-27_225225from
issue-162-make-review-findings-the-load-bearing-artifact

Conversation

@aaddrick

Copy link
Copy Markdown
Owner

Closes #162

Summary

Makes structured review findings the load-bearing artifact for the three review/fix loops instead of reviewer prose. REVIEW_SCHEMA.issues is now typed with the same severity/summary/optional-recommendation shape CHALLENGE_SCHEMA.findings already used (one field looser: recommendation is optional), with engine-assigned stable ids. The quality loop, test loop, and pr-review merge gate now key their exit predicates and fix-stage prompts off this structured array rather than off result === 'approved' alone.

Key decisions

  • Added normalizeFindings(raw, source) and findingsBlock(findings, comments, fallbackLabel) helpers next to recordGateOutcome. normalizeFindings returns null when a reviewer omits issues entirely, preserving today's prose-only behavior for non-conforming reviewers rather than throwing or silently treating omission as empty.
  • REVIEW_SCHEMA.issues.items requires severity and summary; severity is coerced to critical/major/minor/unspecified. issues stays out of REVIEW_SCHEMA.required, and id stays out of the schema (the engine assigns ids as source-N).
  • All four REVIEW_SCHEMA-producing prompts (spec review, code review, quality review, test validation) now state explicitly that every concern belongs in issues, that prose-only concerns in comments will not be fixed, and that no findings means an empty array.
  • The three fix stages (quality-fix, pr-fix, test-quality-fix) are now fed from findingsBlock(), with reviewer prose kept below as context only; fixes_applied entries are asked to echo the resolved finding's id.
  • Quality loop and test loop predicates: approved OR a present-and-empty issues array is treated as a clean exit (fix stage skipped, findings_empty_exits metric incremented).
  • Merge gate (reviewAndMerge) keeps prReviewClean (both reviewers approved) as the only setter of approved = true. When both reviewers have nothing to fix but the gate isn't clean, the loop breaks without approving, landing on the existing needs_human path, tallied as carried-unresolved (no new disposition string introduced).
  • Updated the stale recordGateOutcome doc comment noting gate_findings['pr-review'].severity now reports real counts instead of always zero.
  • Added docs/architecture/gate-hygiene.md covering the new finding shape, engine-assigned ids, the absent-vs-empty distinction, the three loop predicates, and the findings_empty_exits counter, plus repaired the docs/architecture CLAUDE.md/AGENTS.md freeze pair and index.md file-map entry.
  • Ran node scripts/lint-engine.js --fix after engine-path edits to keep the lockstep copy of workflows/ticketmill.js in sync per the profile's LOCKSTEP-EDIT RULE.

Token usage (approximate, this issue only): 424435 output tokens

aaddrick added 6 commits July 27, 2026 23:53
…oops to structured findings

Type REVIEW_SCHEMA.issues.items with severity/summary/recommendation (one
field looser than CHALLENGE_SCHEMA.findings: recommendation stays optional),
keeping `issues` out of REVIEW_SCHEMA.required and `id` out of the schema
entirely. Add normalizeFindings(raw, source) to turn a reviewer's `issues`
array into engine-assigned {id, severity, summary, recommendation} findings,
returning null when the reviewer omitted the key so callers fall back to
today's prose path byte-for-byte. Add findingsBlock() as the single renderer
feeding every fix stage: null -> today's prose, non-empty -> the id-prefixed
work list plus prose kept as context, empty array -> an explicit
no-findings line with the prose still rendered underneath.

Wire the quality loop and test loop: a changes_requested review whose
`issues` normalizes to a present, empty array is now treated as
nothing-to-fix (returns approved/{ok:true}, increments the new
ctx.metrics.findings_empty_exits counter, skips the fix stage) instead of
degrading the loop toward a fix stage with nothing to fix. Fix prompts are
now fed from findingsBlock() and ask implementers to prefix each
fixes_applied entry with the id of the finding it resolves. Reviewer prompts
gain wording tying the `issues` array to the verdict.

Adds normalizeFindings/findingsBlock unit tests, and both a present-empty-
array scenario and its mirror image (issues omitted entirely, which must
NOT be treated as empty) to the quality-loop and test-loop suites, plus a
regression proving the existing issues:['x'] fixtures still trigger their
fix stage with the new rendered finding line.

Part of #162.
… line

runQualityLoop and runTestLoop repeated the same three-line "every concern
goes in `issues`" instruction verbatim. Extract it to ISSUES_ASK, matching
the existing HANDOFF_ASK/COMMIT_SHA_ASK shared-prompt-line pattern, so the
two call sites can't drift. Synced via lint-engine.js --fix per the
LOCKSTEP-EDIT rule.

Part of #162.
Type the pr-review merge gate (reviewAndMerge) into the same
normalizeFindings()/findingsBlock() machinery task 1 gave the internal
quality/test loops (issue #162):

- Add ISSUES_ASK to the spec-review and code-review prompts, tying the
  verdict to a non-empty `issues` array the same way quality review and
  test validation already do.
- Normalize each reviewer's issues under a distinct source ('spec-i'+iter,
  'code-i'+iter) before concatenating for recordGateOutcome, since both
  reviews land in one tally via parallel() and model-chosen/shared ids
  would collide.
- Add nothingToFix(r, f): true when a reviewer approved outright or
  requested changes while naming zero findings. prReviewClean (both
  approved) stays the only path that sets approved = true. When both
  reviewers have nothing to fix but the pair isn't clean, break WITHOUT
  approving, tallied as 'carried-unresolved' (computeGateYield hardcodes
  exactly four disposition keys) and counted in
  ctx.metrics.findings_empty_exits, landing on the existing needs_human.
- Add a haltReason local so fail()'s message is accurate for both the
  cap-reached and the early-empty-exit paths.
- Feed pr-fix from findingsBlock per reviewer (with the prose comments
  kept below as context) and add the fixes_applied id-prefix instruction.
- Rewrite the recordGateOutcome doc comment: 'accepted' no longer claims
  to be the same condition that ends the loop, 'carried-unresolved' now
  covers both the iteration-cap and the empty-exit path, and the stale
  "severity stays zero" NOTE is replaced since REVIEW_SCHEMA.issues is
  now typed and gate_findings['pr-review'].severity reports real counts.

Adds 7 integration tests to tests/pr-review-gate.test.js covering the
empty-exit break, the mixed empty/real-findings case, the omitted-issues
fallback, both null-reviewer death paths, and non-zero severity tallying.

node scripts/lint-engine.js --fix keeps .claude/workflows/ticketmill.js in
lockstep.
… fix stages

The "prefix each fixes_applied entry with the id it resolves" instruction was
copy-pasted verbatim (only the example id differed) into the quality-fix,
test-quality-fix, and pr-fix prompts added by issue #162. Extract it into
fixesAppliedIdAsk(example), matching the existing ISSUES_ASK/COMMIT_SHA_ASK/
HANDOFF_ASK shared-prompt-line convention.
Add docs/architecture/gate-hygiene.md covering the typed REVIEW_SCHEMA.issues
shape (one field looser than CHALLENGE_SCHEMA.findings), the engine-assigned
id scheme, the null-vs-empty findings distinction, the three loop predicates
and why the merge gate's differs, the empty-findings exit landing on
needs_human as carried-unresolved, the findings_empty_exits counter, and the
fixes_applied id echo. Documents the baseline shift in computeGateYield's
escaped-defect signal now that gate_findings['pr-review'].count carries real
data.

Add the gate-hygiene.md row to index.md's file map (authored text, outside
the provenance fixture's 6-line tracked segment). Precisely reword
docs/architecture/CLAUDE.md:3 to distinguish the ten split-derived pages from
pages added since, leaving the other two "ten files" references (:49, :83,
both describing the provenance fixture's still-exactly-ten scope) untouched.
Add a gate-hygiene.md row to the "What lives here" table and a pointer to
the now-stale, hash-frozen metrics.md:81-84 passage this page supersedes.
Copy CLAUDE.md over AGENTS.md per the freeze pair's parity note.

docs/architecture/pipeline.md, metrics.md, and failure-semantics.md are
untouched — their prose is hash-frozen by tests/architecture-provenance.test.js.

Refs #162
…empty-array branch

Every existing fixture with an approved reviewer also carried issues: [],
which independently satisfies nothingToFix's second branch, so no test could
tell `r.result === 'approved' ||` apart from `f.length === 0`. Add direct
unit tests for nothingToFix() (mirroring the normalizeFindings/findingsBlock
units) plus a reviewAndMerge() integration case pairing an approval that
carries a non-empty issues array with a changes_requested/issues:[] partner,
asserting bothNothingToFix still halts early as carried-unresolved without
running pr-fix. Verified by temporarily deleting the approved-branch clause:
all three new assertions fail against the mutant and pass against HEAD.
@aaddrick

Copy link
Copy Markdown
Owner Author

Spec Review (Iteration 1)

Verdict: approved

Verified against issue #162's acceptance criteria and the actual diff (workflows/ticketmill.js, docs/architecture/*, four test files), with node --test run in full (626/626 pass, including the 47 tests added for this issue and the docs-links/architecture-provenance freeze checks).

  • REVIEW_SCHEMA.issues.items is typed with severity/summary required and recommendation optional, matching CHALLENGE_SCHEMA.findings' shape minus one required field as the plan specified. All four producing prompts (spec review, code review, quality review, test validation) carry the shared ISSUES_ASK line.
  • All three fix stages (quality-fix, test-quality-fix, pr-fix) are fed via the shared findingsBlock() renderer; comments is preserved below as context, never dropped.
  • All three loop predicates now accept "approved OR present-and-empty findings" as a clean exit. The merge gate correctly keeps prReviewClean (both approved) as the sole setter of approved = true, per the adjudicated design — the nothing-to-fix case breaks onto the existing needs_human path instead of auto-merging, avoiding an autonomous squash-merge of a changes_requested verdict.
  • recordGateOutcome's doc comment at the old :2021-2027 location is rewritten to describe real (non-permanently-zero) severity counts, and docs/architecture/gate-hygiene.md plus the CLAUDE.md/AGENTS.md freeze-pair supersession note correctly route around the frozen, now-stale sentence in metrics.md rather than editing it.
  • The omitted-issues degrade path (normalizeFindings returns null, nothingToFix explicitly treats f === null as not-nothing-to-fix) is verified by dedicated mirror-image tests in all three loop test files, matching the plan's call-site-slip caveat.
  • No scope creep: no suppression/filtering of findings was added (explicitly out of scope), pipeline.md/metrics.md/failure-semantics.md are untouched (provenance test passes), and the lockstep copy .claude/workflows/ticketmill.js is byte-identical to workflows/ticketmill.js.
  • CHANGELOG.md/plugin.json are untouched in this PR, consistent with the profile's dedicated release stage — not a gap.

All acceptance criteria from issue #162 are met by the diff.

@aaddrick

Copy link
Copy Markdown
Owner Author

Code Review (Iteration 1)

Verdict: changes requested (1 blocking, 2 minor, 1 deferred note)

Baseline is clean: node --check, node scripts/lint-engine.js (2 lockstep pairs in sync), and the
full node --test suite (626/626) all pass in the worktree, and .claude/workflows/ticketmill.js is
byte-identical to workflows/ticketmill.js at HEAD. The typed REVIEW_SCHEMA.issues, the
null-vs-empty contract in normalizeFindings(), the engine-assigned id namespacing, and the merge
gate's deliberate split between prReviewClean and bothNothingToFix all check out against the
code. gate-hygiene.md is unusually good and the supersession pointer for the frozen
metrics.md:81-84 passage is the right call.

Blocking

1. The quality-loop and test-loop empty-findings exits are silent verification skips — no VERIFY_SKIPS entry.
workflows/ticketmill.js:3076-3082 (quality loop) and :3608-3613 (test loop)

Both branches take a reviewer verdict of changes_requested, convert it to a clean pass
(approved = true / return { ok: true }), and skip the fix stage. The only records are
pushDecision() — which feeds later agent prompts, not the human — and
ctx.metrics.findings_empty_exits. That counter reaches the run-record JSON via the result object
(:2854, :4989) but never reaches the batch PR body: the body renders VERIFY_SKIPS, Friction &
Churn, Rework Tax, Gate Yield, Merge Auto-Resolution and Token Usage (:7756-7776), and per-issue
metrics is not among them. frictionFields/FRICTION_WEIGHTS don't read the key either.

So an issue whose quality gate said "changes requested" can complete, merge into the batch branch,
and appear in the results table as completed with zero signal to the human reviewing the batch PR.
That is exactly the hole VERIFY_SKIPS exists to close, and this file already treats weaker cases
the same way: a capped approach/plan challenge with unresolved caveats pushes one (:4379, :4523),
as does a skipped test loop (:3519) and a skipped browser check (:3197).

The merge gate is fine as written — bothNothingToFix breaks onto fail(ctx, 'needs_human', ...)
with an explicit haltReason, so the human sees the issue as needs_human in the results table.
Only the two internal loops are invisible.

Fix direction: push a line at both internal-loop exits, e.g.
VERIFY_SKIPS.push('#' + ctx.issue + ': quality review (' + stepLabel + ', iteration ' + iter + ') requested changes but named zero structured findings — treated as clean, no fix stage ran')
and the test-loop equivalent.

Note on scope: this is not a re-litigation of the adjudicated predicate. The settled decision
rejected pushing a ctx.deferred note, on the grounds that ctx.deferred becomes an auto-filed
GitHub issue via the merge stage's step 5 and can fire repeatedly. VERIFY_SKIPS is a different
mechanism with none of that cost: it is a line of text in the batch PR body and the run record's
verification_gaps, it files nothing, and it is the mechanism the persona and the existing gate
exits already use. The predicate itself stays exactly as adjudicated.

Minor

2. ISSUES_ASK's doc comment names only two of its four consumers.
workflows/ticketmill.js:649-651

The comment reads "shared verbatim by every REVIEW_SCHEMA reviewer prompt (quality review, test
validation)". The constant is now also used by the spec-review prompt (:4766) and the code-review
prompt (:4788). The recordGateOutcome NOTE at :2053-2056 already lists all four correctly, so
this one parenthetical is the stale copy. Add spec review and code review to the list.

3. findingsBlock() drops the fallbackLabel (the reviewer's summary) whenever findings are present.
workflows/ticketmill.js:2131-2144

fallbackLabel is consulted only on the findings === null branch. On the non-null branches the
context section renders String(comments || '(none)'). Every call site passes
rev.summary || '<label>' as the fallback, so a reviewer that fills issues and summary but
leaves comments empty now shows the fix agent (none) where the pre-change prompt
(String(rev.comments || rev.summary || 'No comments')) showed the summary. Small information loss,
easy fix: use String(comments || fallbackLabel || '(none)') in the context line so the fallback
chain is the same on all three branches.

Deferred note (out of scope for this PR)

4. Severity vocabulary mismatch between the new enum and this repo's reviewer persona.
REVIEW_SCHEMA.issues.items.severity is ['critical', 'major', 'minor'] (:643), matching
CHALLENGE_SCHEMA.findings. .claude/agents/ticketmill-code-reviewer.md:31 instructs the reviewer
to "report findings as a list with severity (blocking / major / minor)". ISSUES_ASK asks for
"severity" without naming the permitted values, so the only thing reconciling the two is the enum
arriving in the structured-output tool schema. Risk is low (the model sees the enum and would be
rejected on a bad value), and .claude/agents/** is engine-owned and out of scope here — flagging
it rather than touching it. If it's worth cheap insurance, naming the three values inside
ISSUES_ASK would make the prompt state exactly what the schema requires, which is the standing
rule for every agent() call in this file.

…view

Code review on PR #169 flagged that the quality-loop and test-loop
empty-findings exits (a changes_requested verdict with zero structured
issues, converted to a clean pass) were silent — recorded only in
ctx.metrics and a pushDecision() note, neither of which reaches the
batch PR body. Push a VERIFY_SKIPS entry at both exits, the same
mechanism already used for capped challenges, the skipped test loop,
and the skipped browser check. The pr-review merge gate's equivalent
exit was already visible via needs_human and is untouched.

Also: ISSUES_ASK's doc comment now names all four REVIEW_SCHEMA
prompts that consume it (was missing spec review and code review),
and findingsBlock() falls back to fallbackLabel (the reviewer's
summary) instead of dropping straight to "(none)" when a non-null
findings array pairs with empty comments, restoring the fallback
chain the pre-#162 prompt had.
@aaddrick

Copy link
Copy Markdown
Owner Author

PR Review Fix (iteration 1)

Commit c957604

  • Blocking: pushed a VERIFY_SKIPS entry at both the quality-loop and test-loop empty-findings exits, so a changes_requested verdict with zero structured issues (silently converted to a clean pass) now surfaces in the batch PR's Verification Gaps, matching how capped challenges, the skipped test loop, and the skipped browser check already record the same class of gap. The pr-review merge gate's equivalent exit was already visible via needs_human and is untouched.
  • Minor: ISSUES_ASK's doc comment now lists all four consuming prompts (added spec review, code review); findingsBlock() falls back to fallbackLabel instead of dropping straight to (none) when findings are present but comments are empty.
  • Added/extended tests in quality-loop.test.js, test-loop.test.js, and gate-findings.test.js; mutation-tested by reverting each fix and confirming the new assertions fail, then restoring. Full test_command passes (627/627 node --test, lint-engine clean, setup-worktree.sh, manifest parse).

@aaddrick

Copy link
Copy Markdown
Owner Author

Spec Review (Iteration 2)

Verdict: approved

Re-verified against issue #162's acceptance criteria and the full diff (workflows/ticketmill.js, docs/architecture/*, five test files) at HEAD (c957604), plus the fix commit's diff against the code review it responds to. node --test is full green (627/627, up from 626 at iteration 1 — 1 new test from the fix), node scripts/lint-engine.js reports clean with both lockstep pairs in sync, and .claude/workflows/ticketmill.js remains byte-identical to workflows/ticketmill.js.

  • Iteration 1's spec approval stands; nothing in the iteration-1 fix commit touches any of the acceptance-criteria mechanics (schema typing, the four prompts, the three fix stages, the three predicates, the null-vs-empty contract, or the merge-gate design). No new information requires reversing that approval.
  • Code review iteration 1's one blocking finding (silent verification skip on the quality-loop and test-loop empty-findings exits) is fixed: both exits now push a VERIFY_SKIPS entry (workflows/ticketmill.js:3082, :3614) using the same mechanism already used for capped challenges, the skipped test loop, and the skipped browser check. Confirmed VERIFY_SKIPS reaches the batch PR body (:7759-7760) and the run record (:7906). The merge gate's equivalent exit was already visible via needs_human and was correctly left untouched, consistent with the adjudicated design (bothNothingToFix still breaks without setting approved).
  • Both minor findings are fixed: ISSUES_ASK's doc comment now names all four consuming prompts (:650), and findingsBlock() falls back to fallbackLabel before (none) on the non-null branch (:2144), restoring the pre-Make review findings the load-bearing artifact #162 fallback chain.
  • The deferred severity-vocabulary note was correctly left deferred (touches engine-owned .claude/agents/**, out of scope for this issue) and is not re-raised as a blocker.
  • Tests were extended alongside the fix in tests/gate-findings.test.js, tests/quality-loop.test.js, and tests/test-loop.test.js, consistent with the acceptance criterion naming those files.
  • No scope creep: the fix commit is scoped exactly to the three code-review findings, touches no unrelated code, and doesn't reopen or edit the frozen docs/architecture/pipeline.md/metrics.md/failure-semantics.md files.

All acceptance criteria from issue #162 remain met by the diff at HEAD.

@aaddrick

Copy link
Copy Markdown
Owner Author

Code Review (Iteration 2)

Verdict: changes requested (0 blocking, 2 minor)

Baseline re-verified in the worktree at c957604: node --check, node scripts/lint-engine.js
(clean, 2 lockstep pairs in sync), bash -n scripts/setup-worktree.sh, both manifests parse,
node --test 627/627, bash tests/setup-worktree.test.sh 32/32.

Both iteration-1 fixes land correctly and are not re-flagged:

  • The blocking silent-verification-skip is closed. VERIFY_SKIPS.push(...) now fires at both
    internal-loop empty-findings exits (workflows/ticketmill.js:3082, :3614), matching the
    existing wording style of the capped-challenge (:4382, :4526), skipped-test-loop (:3521)
    and skipped-browser (:3199) entries, and tests/quality-loop.test.js:191,232 /
    tests/test-loop.test.js:67,150,191 assert it through harness.readGlobal(context, 'VERIFY_SKIPS').
  • ISSUES_ASK's doc comment now names all four consumers, and findingsBlock()'s context line
    falls through comments -> fallbackLabel -> '(none)' on every branch (:2144).

Re-checked the parts most likely to break and found them sound: nothingToFix is ANDed and gated
behind !prReviewClean so prReviewClean stays the sole setter of approved = true (:4824,
:4828); the haltReason local reaches the single fail() at :4882 on both early-exit paths
with a || default covering any other fallout; capReached breaks in exactly the position the
old iter === MAX_PR_REVIEW_ITERATIONS break occupied, so no fix stage was gained or lost on the
last iteration; findings_empty_exits is only ever incremented on a ctx.metrics built fresh at
:5027 (resume rebuilds it, so no undefined++); and findingsBlock's finding line reuses the
repo's existing - [severity] summary -> recommendation shape verbatim (:2137 vs :4028,
:4399, :4541).

Minor

1. gate-hygiene.md's escaped-defect section rests on a premise the repo's own run records
contradict.

docs/architecture/gate-hygiene.md:182-185

Before #162, gate_findings['pr-review'].count was always effectively silent on real findings,
because nothing fed structured findings into it from the merge gate at all: the field existed,
but nothing populated it with real signal.

That is not what the pre-#162 engine did. The same call site already passed
(spec.issues || []).concat(code.issues || []) into recordGateOutcome, and recordGateOutcome
counts array length regardless of entry shape, so count was populated with real signal all along.
The archive proves it:

The passage this page claims to supersede got this right and says so explicitly
(docs/architecture/metrics.md:84: "disposition and count still carry real signal for that
gate"), so gate-hygiene.md currently contradicts metrics.md on the one point metrics.md was
accurate about. Everything else in the section stands: the four rewritten prompts really will move
concerns out of comments and into issues, so the count rises and the escaped-defect flag fires
more often. The mechanism is a magnitude shift, not a nothing-to-something shift, and the doc is
the durable source of truth for exactly this metric.

Two places inherit the same false premise and should move with it:

  • workflows/ticketmill.js:2058-2060 — "unlike before, gate_findings['pr-review'].severity now
    reports real, non-zero counts ... this is no longer permanently {critical:0, major:0, minor:0}".
    The docs: split ARCHITECTURE.md into a docs/architecture/ subfolder, leave the old path as a pointer #154 record above shows major:1, minor:2 recorded before this change: untyped issues
    entries that happened to carry a severity key already hit recordGateOutcome's buckets. The
    old comment's "will stay zero" claim was wrong when written; the replacement should say the
    counts are now guaranteed and schema-backed rather than incidental, not that they were
    previously impossible.
  • docs/architecture/CLAUDE.md / AGENTS.md, "A frozen passage that is now stale prose" — "issue
    Make review findings the load-bearing artifact #162 made that sentence false" understates it. The sentence was already inaccurate; Make review findings the load-bearing artifact #162 is what
    makes the correction worth writing down. (Keep both files byte-identical per the parity note.)

Fix direction: reword the three passages to "already populated, but only incidentally — now
guaranteed and schema-backed", and keep the escaped-defect conclusion as written.

2. findingsBlock()'s headings collide with the pr-fix prompt's own wrapper headings.
workflows/ticketmill.js:4856-4860

The pr-fix prompt wraps each reviewer in '## Spec review' / '## Code review', and
findingsBlock() emits its own ## Findings to fix and ## Reviewer comments (context only ...)
at the same level (:2134, :2143). The rendered prompt is therefore structurally flat:

## Spec review
## Findings to fix
...
## Reviewer comments (context only ...)
...
## Code review
## Findings to fix

Nothing tells the fix agent where the spec block ends and the code block begins except the id
prefix on each finding line, and the two ## Reviewer comments blobs are indistinguishable by
heading alone. The two internal fix stages don't have this problem because they call
findingsBlock() with no enclosing heading. Cheapest fix: demote the block's internal headings to
###, or pass a label into findingsBlock() and drop the wrapper lines at the call site so one
function owns the whole rendering.

Not re-flagged

The severity-vocabulary mismatch between REVIEW_SCHEMA.issues.items.severity
(critical/major/minor) and .claude/agents/ticketmill-code-reviewer.md's
"blocking / major / minor" was raised as a deferred, out-of-scope note in iteration 1 and is not
re-raised here. .claude/agents/** remains engine-owned and untouched by this PR.

…adings

Code review iteration 2 (PR #169) found two minor issues: gate-hygiene.md
claimed gate_findings['pr-review'].count carried no real signal before
#162, but the merge gate already fed real counts into it whenever a
reviewer put a concern in issues rather than comments — reword the three
affected passages (gate-hygiene.md, ticketmill.js's recordGateOutcome
NOTE, the CLAUDE.md/AGENTS.md freeze-pair note) to "already populated,
but only incidentally — now guaranteed and schema-backed". Also demote
findingsBlock()'s internal headings from ## to ### so they nest under the
pr-fix prompt's own "## Spec review" / "## Code review" wrapper headings
instead of colliding with them.
@aaddrick

Copy link
Copy Markdown
Owner Author

PR Review Fix (iteration 2)

Commit e2f70f0

  • Minor: reworded gate-hygiene.md's escaped-defect section, recordGateOutcome's NOTE comment, and the CLAUDE.md/AGENTS.md freeze-pair supersession note — all three claimed gate_findings['pr-review'].count/.severity carried no real signal before Make review findings the load-bearing artifact #162, but the merge gate already fed it real counts whenever a reviewer put a concern in issues rather than comments; now says "already populated, but only incidentally — now guaranteed and schema-backed", matching what the archived run records (docs: split ARCHITECTURE.md into a docs/architecture/ subfolder, leave the old path as a pointer #154, Add the mill-review skill and cross-run trend dashboard #94) and metrics.md:84 actually show.
  • Minor: findingsBlock()'s internal ## Findings to fix / ## Reviewer comments headings demoted to ### so they nest under the pr-fix prompt's own ## Spec review / ## Code review wrappers instead of colliding with them at the same level.
  • Synced the lockstep copy .claude/workflows/ticketmill.js. Full node --test passes (627/627), node scripts/lint-engine.js clean (2 pairs in sync), bash -n scripts/setup-worktree.sh, both manifests parse, bash tests/setup-worktree.test.sh 32/32, tests/architecture-provenance.test.js green (no frozen segment touched).

@aaddrick

Copy link
Copy Markdown
Owner Author

Spec Review (Iteration 3)

Verdict: approved

Re-verified against issue #162's acceptance criteria and the full diff at HEAD (e2f70f0), which is the code-review-iteration-2 fix commit. node --test is full green (627/627), node scripts/lint-engine.js reports clean with both lockstep pairs in sync, .claude/workflows/ticketmill.js is byte-identical to workflows/ticketmill.js, and docs/architecture/CLAUDE.md/AGENTS.md remain byte-identical to each other.

  • Iterations 1 and 2's spec approvals stand. Nothing in the iteration-2 fix commit touches any acceptance-criteria mechanic (schema typing, the four prompts, the three fix stages, the three predicates, the null-vs-empty contract, or the merge-gate design) — it only reworded prose and demoted headings. No new information requires reversing either prior approval.
  • Code review iteration 2's two minor findings are both fixed and confirmed in the diff:
  • All acceptance criteria from issue Make review findings the load-bearing artifact #162 remain met:
    • REVIEW_SCHEMA.issues.items is typed (severity critical/major/minor + summary required, recommendation optional) and all four producing prompts (spec review, code review, quality review, test validation) carry ISSUES_ASK, whose doc comment now correctly names all four consumers.
    • All three fix stages (quality-fix, test-quality-fix, pr-fix) are fed structured findings via the shared findingsBlock(), with comments preserved as context.
    • All three loop predicates accept "approved OR present-and-empty findings" as a clean exit; the merge gate keeps prReviewClean as the sole setter of approved = true and routes the nothing-to-fix case to the existing needs_human path, per the adjudicated design.
    • The omitted-issues degrade path (normalizeFindings returns null) is byte-identical to pre-Make review findings the load-bearing artifact #162 prompt output and is covered by dedicated tests in all three loop test files.
    • gate_findings['pr-review'].severity now reports counts guaranteed by the typed schema, and the stale note this criterion targets is rewritten accurately (not overstated).
    • A changes_requested review with empty findings still terminates the loop (no fix stage runs); both internal-loop exits now also push a VERIFY_SKIPS entry so the skip is visible in the batch PR body, not just the run record.
    • node --test is green with the four named test files (plus gate-findings.test.js) extended.
  • No scope creep: the fix commit is scoped to the two code-review-iteration-2 findings, touches no unrelated code, and the frozen docs/architecture/pipeline.md/metrics.md/failure-semantics.md files are untouched (tests/architecture-provenance.test.js passes). CHANGELOG.md/plugin.json remain untouched, consistent with the profile's dedicated release stage.

All acceptance criteria from issue #162 are met by the diff at HEAD.

@aaddrick

Copy link
Copy Markdown
Owner Author

Code Review (Iteration 3)

Verdict: approved (0 blocking, 1 minor, 2 non-blocking notes)

Baseline re-verified in the worktree at e2f70f0: node --check workflows/ticketmill.js,
bash -n scripts/setup-worktree.sh, both manifests parse, node scripts/lint-engine.js clean
(2 lockstep pairs in sync), .claude/workflows/ticketmill.js byte-identical to
workflows/ticketmill.js at HEAD, node --test 627/627, bash tests/setup-worktree.test.sh 32/32.

Both iteration-2 findings land and are not re-flagged:

  • The escaped-defect premise is corrected in gate-hygiene.md's escaped-defect section
    (:182-193), recordGateOutcome's NOTE (workflows/ticketmill.js:2054-2064), and the
    CLAUDE.md/AGENTS.md freeze-pair note (:100-113, byte-identical pair preserved). I
    re-checked the underlying claim against the archive rather than taking it on trust:
    logs/ticketmill/runs/2026-07-26-docs.json records gate_findings['pr-review'] as
    {count:5, severity:{critical:0, major:1, minor:2}} and tier4-94.json as
    {count:3, severity:{...minor:1}}, both pre-Make review findings the load-bearing artifact #162. "Already populated, but only incidentally"
    is the accurate framing.
  • findingsBlock()'s internal headings are ### (:2144, :2153), nesting under the pr-fix
    prompt's ## Spec review / ## Code review wrappers, with a comment at the site explaining
    why the level is what it is. Nothing else in the repo asserts on the old ## heading text.

New-code re-checks, all sound: normalizeFindings/findingsBlock/nothingToFix are pure (no
Date.now(), Math.random(), argless new Date(), require, or filesystem use anywhere in the
added lines); findings_empty_exits only ever increments on the ctx.metrics literal built fresh
at :5037, with tests/harness.js:82-89 carrying the same key; the three
fixesAppliedIdAsk(...) example ids match the id shapes their call sites actually generate
(quality-<prefix>-i<iter>, test-i<iter>, code-i<iter>); bothNothingToFix is evaluated
before capReached so an iteration that is both reports the more specific haltReason; and the
findings === null leg of findingsBlock() still reduces to
String(comments || summary || '<label>'), byte-identical to the pre-#162 prompt.

Minor

1. gate-hygiene.md's provenance section still carries the premise iteration 2 corrected
everywhere else, and now contradicts the freeze-pair note that points readers at it.

docs/architecture/gate-hygiene.md:206-208

docs/architecture/metrics.md:81-84 ... describes the state of the world before this issue.
It is now inaccurate: severity counts are real as of the change this page documents.

That is the iteration-2 premise, unrevised: it says the metrics.md sentence became false as of
#162. The archive above shows it was already false when it shipped. Iteration 2's fix reworded
three passages and this fourth one, in the same file, was missed.

The contradiction is load-bearing rather than cosmetic, because CLAUDE.md/AGENTS.md:110-113
now sends the reader here specifically:

Read gate-hygiene.md's provenance paragraph before trusting anything metrics.md says about
gate_findings['pr-review'].severity.

and that same note says two lines earlier that the sentence "was already inaccurate when it
shipped". So the pointer and its target disagree on the one fact the pointer exists to settle.

Fix direction: reword :207-208 to match the other three sites, e.g. "It was already inaccurate
when it shipped, and #162 is what makes the correction worth writing down: severity counts are
now guaranteed and schema-backed rather than incidental." The supersession conclusion and the rest
of the section stand as written.

While in that paragraph: :192 runs to 90 columns against the file's ~76-column hard wrap (an
artifact of the iteration-2 edit landing mid-sentence). Re-wrap it in the same pass.

Notes (non-blocking, outside this gate)

2. gate-hygiene.md doesn't mention the VERIFY_SKIPS entries added in iteration 1.
The findings_empty_exits section (:146-159) presents the counter as the record of the two
internal loops' empty-findings exits, and :141-143 says the merge gate's two
carried-unresolved causes are distinguished "only by ctx.metrics.findings_empty_exits and the
human-readable haltReason text". True of the merge gate, but the quality-loop and test-loop
exits also push a Verification Gaps line (workflows/ticketmill.js:3092, :3624) that reaches
the batch PR body and the run record's verification_gaps. That is the human-visible half of the
mechanism and the page created to be its durable source of truth doesn't name it. Not blocking:
nothing on the page is false, and the missing material is additive.

3. Em-dash convention drift in the new prose. docs/architecture/gate-hygiene.md contains 32
em dashes and this PR adds 2 to docs/architecture/CLAUDE.md/AGENTS.md, which had 0. Every
other file in docs/architecture/ is at 0 by construction: commit 8069dcf removed roughly 290
em and en dashes from the repo's authored prose, and .claude/agents/ticketmill-doc-writer.md:15
states the rule ("Avoid em-dashes; use a period or colon instead"). No test enforces it, so the
suite is green either way. Flagging rather than blocking: prose style is explicitly not this
gate's scope, and the fix is mechanical whenever a doc-writer pass next touches the page.

Not re-flagged

The severity-vocabulary mismatch between REVIEW_SCHEMA.issues.items.severity
(critical/major/minor) and .claude/agents/ticketmill-code-reviewer.md's
"blocking / major / minor" was raised as a deferred, out-of-scope note in iteration 1 and stays
deferred. .claude/agents/** is engine-owned and untouched by this PR.

Why approved

Zero blocking and zero functional findings. Every finding above is documentation consistency, none
of it changes what the engine does, and this is iteration 3 of MAX_PR_REVIEW_ITERATIONS = 3
changes_requested here reaches the cap without a fix stage and routes a functionally sound PR to
needs_human. Sending this to a human over a historical-framing sentence and an em-dash count is
the wrong trade. Finding 1 is worth a follow-up commit whenever gate-hygiene.md is next opened.

The quality loop and test loop empty-findings exits (a changes_requested
verdict with zero structured issues, treated as clean) were silent before
a follow-up code review fixed it: they now push a VERIFY_SKIPS entry, the
same mechanism already used for capped challenges and skipped test loops,
so they surface in the batch PR's Verification Gaps section. gate-hygiene.md
covered the findings_empty_exits counter but not this visibility change;
add it to the same section.

Refs #162
@aaddrick

Copy link
Copy Markdown
Owner Author

Tech docs review

The engine change and the docs for it (docs/architecture/gate-hygiene.md, index.md, and the CLAUDE.md/AGENTS.md freeze pair) already landed together in this PR, in commit 62ea7f3 and its follow-up fixes.

I checked the three commits made after that docs commit (c957604, 0a406d8, e2f70f0) against gate-hygiene.md for drift:

  • e2f70f0 reworded the escaped-defect premise and demoted findingsBlock()'s internal headings — docs already matched.
  • 0a406d8 is test-only, no doc claim affected.
  • c957604 added a VERIFY_SKIPS entry at the quality-loop and test-loop empty-findings exits, which were previously silent outside ctx.metrics. gate-hygiene.md's findings_empty_exits section documented the counter but not this visibility fix, so I added a short paragraph noting both loop exits now surface in the batch PR's Verification Gaps section, and that the merge gate's own empty-findings exit needs no separate entry because it already lands on needs_human.

Verified against .claude/workflows/ticketmill.js (the VERIFY_SKIPS.push calls at the quality-loop and test-loop empty-findings breaks) and node --test tests/architecture-provenance.test.js (still green, no moved-prose touched).

Commit: ed3c9ce — docs(issue-162): document the empty-findings VERIFY_SKIPS visibility fix

@aaddrick
aaddrick merged commit 4b034e2 into Batch_2026-07-27_225225 Jul 28, 2026
1 check passed
@aaddrick
aaddrick deleted the issue-162-make-review-findings-the-load-bearing-artifact branch July 28, 2026 04:59
@aaddrick

Copy link
Copy Markdown
Owner Author

Implementation Complete

Branch issue-162-make-review-findings-the-load-bearing-artifact squash-merged into Batch_2026-07-27_225225.

Reviews passed:

  • Spec review: approved (iteration 3, after fix iterations for earlier findings)
  • Code review: approved (iteration 3) — zero blocking/functional findings remaining; iteration 1's blocking finding (silent verification skip on quality-loop/test-loop empty-findings exits) fixed via VERIFY_SKIPS entries; both minor findings fixed
  • Tech docs review: verified docs/architecture/gate-hygiene.md and the CLAUDE.md/AGENTS.md freeze pair stayed in sync with the three post-docs-commit code changes
  • Test validation: full suite green (626/626 node --test, 32/32 setup-worktree.test.sh), node scripts/lint-engine.js clean (lockstep pairs in sync)
Deferred Suggestions for Follow-up
  • Severity vocabulary mismatch between REVIEW_SCHEMA.issues.items.severity (critical/major/minor) and .claude/agents/ticketmill-code-reviewer.md's "blocking / major / minor" wording — correctly left untouched since .claude/agents/** is engine-owned and out of scope for this issue. Filed as #170.
  • computeGateYield's escaped-defect signal currently keys off raw pr-review finding count; now that findings are typed with real severities, redefining it on severity.critical + severity.major was named as a follow-up throughout review. Filed as #171.
  • Engine-owned guardrail check: .claude/workflows/ticketmill.js (lockstep copy) was verified byte-identical to workflows/ticketmill.js at HEAD via node scripts/lint-engine.js, confirmed clean in code review — no action needed.

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.

1 participant