Skip to content

fix(observability): pool the quality friction denominator by scope count - #176

Merged
aaddrick merged 6 commits into
Batch_2026-07-27_225225from
issue-165-fix-the-quality-friction-ratio-s-denominator
Jul 28, 2026
Merged

fix(observability): pool the quality friction denominator by scope count#176
aaddrick merged 6 commits into
Batch_2026-07-27_225225from
issue-165-fix-the-quality-friction-ratio-s-denominator

Conversation

@aaddrick

@aaddrick aaddrick commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Closes #165

Summary

computeFriction's quality stage term compared a run-wide aggregate (ctx.metrics.quality_iters, incremented across every call to runQualityLoop for an issue) against a single loop's cap (MAX_QUALITY_ITERATIONS = 5), via min(1, quality_iters/cap). A multi-task issue that cleared quality on iteration 2 of each of 3 tasks accumulated quality_iters=6, saturating the ratio to 1.0 — the same score as an issue whose every quality loop actually exhausted the cap. Every other capped stage in computeFriction compares one loop's iterations against that loop's cap; quality alone compared an aggregate to a per-loop cap.

Approach: pooled denominator (quality only)

Chosen over tracking the worst single scope's iteration count because pooled is a mean while worst-scope is a max — max does not fix the issue's opening complaint, it relocates it: one capped scope would pin a multi-task issue at 1.0 forever regardless of how well every other scope did.

  • workflows/ticketmill.js metrics literal gains quality_scopes: 0.
  • runQualityLoop increments ctx.metrics.quality_scopes once per invocation (if (iter === 1) ctx.metrics.quality_scopes++), below the STOP guard so a STOP'd entry touches nothing. quality_iters increments are unchanged in meaning and value.
  • computeFriction gains multiScopeField = { quality: 'quality_scopes' }. Inside the existing generic stage loop: baseCap = caps[k] > 0 ? caps[k] : 1, scopes = tracked ? Math.max(1, Number(m[multiScopeField[k]]) || 0) : null, cap = baseCap * (scopes || 1), ratio = Math.min(1, iters / cap). All seven stage drivers now carry cap and scopes (scopes is null for the six single-scope stages, a number only for quality), so contribution === Math.min(1, value / cap) and cap === baseCap * (scopes ?? 1) hold for every stage driver. Driver value stays raw quality_iters; metrics.quality_iters is unchanged.
  • tests/harness.js freshMetrics() synced with quality_scopes: 0 (hand-synced by contract; tests/run-record.test.js walks its key set).
  • .claude/workflows/ticketmill.js lockstep copy updated via node scripts/lint-engine.js --fix.
  • docs/architecture/gate-hygiene.md (pre-existing on this branch from a concurrent issue) gains a third supersession entry documenting the corrected invariant and the pooled-vs-worst-scope rationale, rather than touching docs/architecture/metrics.md, which is off-limits per the issue body and whose fixture-pinned segment covers nearly the whole file.

Key decisions

  • Pooled (mean) over worst-scope (max) denominator — see rationale above.
  • quality_scopes increments once per runQualityLoop invocation, not per iteration, and only past the STOP guard.
  • scopes is null (not 1) on the six single-scope stage drivers, keeping "no scope claim made" honest rather than implying they were counted.
  • Raw quality_iters metric value and meaning are untouched for other consumers.
  • Existing docs/architecture/metrics.md left byte-for-byte unchanged per issue constraints; the correction lives in gate-hygiene.md instead.

Acceptance criteria covered

  • Multi-task issue where every quality loop passed on iteration 1 → quality term scores 0.2 per scope (one iteration run against a five-iteration cap), not 0 — see gate-hygiene.md's "Acceptance criterion 1, in its strongest form" for why 0.2 is the sibling-consistent answer under this issue's own min(1, iters/cap) definition, not a shortfall against the literal criterion text.
  • Multi-task issue where every quality loop exhausted the cap → quality term scores 1.0.
  • Single-quality-scope issues are unaffected (scopes = 1, ratio unchanged from before).

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

aaddrick added 3 commits July 28, 2026 04:53
quality_iters is a run-wide aggregate (once per task plus once per
PR-fix round) but was compared against a single loop's cap
(MAX_QUALITY_ITERATIONS), so a multi-task issue that cleared quality
on iteration 2 of every task could saturate to 1.0 -- indistinguishable
from an issue whose every quality loop actually exhausted the cap.

Add a quality_scopes counter (incremented once per runQualityLoop
invocation, not per iteration) and pool it into computeFriction's
quality-stage cap via a new multiScopeField map, which also documents
task_review_attempts and browser_iters as multi-scope aggregates not
yet counted this way. A metrics blob with no quality_scopes field
falls back to scopes=1, so pre-existing data scores unchanged.

Corrects computeFriction's header comment, which claimed a uniform
min(1, iters/cap) ratio across all seven stages and that a first-pass
clean run scores 0 regardless of stage count -- both false given every
stage field is written with += 1 semantics, so a first-try pass has
always cost 1/cap.

Refs #165
…ts, and per-call scope counter

tests/friction.test.js: multi-scope all-capped saturates at 1.0, the
dilution case (6 iters / 3 scopes) scores 0.4 instead of saturating,
all-first-iteration (3/3) scores 0.2 with a comment recording that both
sanctioned formulas agree on 0.2 (not the acceptance criterion's
unsatisfiable literal "0"), single-scope behavior is unchanged, a
metrics blob with no quality_scopes key scores as scopes=1, appending a
first-try scope deliberately lowers the ratio (5/5 -> 6/10), a
saturating fixture proves value/cap can exceed 1 while contribution
clamps to it, scopes stays null on task-review/browser drivers, and
both driver invariants (stage: contribution === min(1, value/cap) and
cap === baseCap*(scopes ?? 1); signal: value*weight === contribution,
no cap/scopes keys) hold across a mixed fixture.

tests/compose-friction-churn.test.js: the :113 pass-through test gains
a multi-scope quality fixture so the new driver keys survive
composition unchanged.

tests/quality-loop.test.js: quality_scopes assertions added to the
existing two-calls, fully-capped-loop, and STOP-tripped scenarios.

tests/harness.test.js: a new parity test regex-reads the metrics
literal out of workflows/ticketmill.js and asserts its key set equals
Object.keys(harness.freshMetrics()), giving the hand-sync contract
teeth.

Refs #165
Extends the quality-gate section of docs/architecture/gate-hygiene.md
with quality_scopes as quality_iters' companion invocation counter, and
adds "The friction denominator: pooled, not worst-scope" covering why
pooled (mean) was chosen over worst-scope (max) — a max relocates this
issue's opening complaint rather than fixing it — the accepted
non-monotonicity cost and why dilution is safe (cap exhaustion is
carried by ctx.quality_caps and gate_findings disposition, not
quality_degrades), acceptance criterion 1's literal "scores 0" being
unmet by both sanctioned formulas (both score 0.2), the corrected
driver invariant pair, and why the other six stage drivers carry
scopes: null rather than 1.

docs/architecture/metrics.md is hash-frozen and stays byte-for-byte
unmodified; a third provenance entry supersedes metrics.md:13-14 (now
true of six stages, not seven) and notes metrics.md:15-17's "scores 0"
claim was already inaccurate independent of this change. CLAUDE.md
gains the matching frozen-passage paragraph, copied verbatim to
AGENTS.md per the freeze-pair convention. index.md and CLAUDE.md's
What-lives-here row get a refreshed gate-hygiene.md parenthetical.

Issue #165.
@aaddrick

Copy link
Copy Markdown
Owner Author

Spec Review (Iteration 1)

Verdict: approved

Scoped diff (against 5715792, the pre-#165 tip carrying concurrent-pipeline commits #174/#172/#169) touches exactly the files the plan called for: workflows/ticketmill.js + its .claude/workflows/ticketmill.js lockstep copy, tests/{friction,harness,harness.test,quality-loop,compose-friction-churn}.js, and docs (gate-hygiene.md, index.md, AGENTS.md/CLAUDE.md mirrors). No scope creep.

Verified against the issue:

  • Pooled formula implemented as adjudicated. quality_scopes: 0 added to the metrics literal; runQualityLoop does if (iter === 1) ctx.metrics.quality_scopes++ below the STOP.tripped guard; computeFriction gains multiScopeField = { quality: 'quality_scopes' }, cap = baseCap * (scopes || 1). quality_iters increment/meaning is untouched (per issue's explicit constraint).
  • Chosen approach and why, stated in the PR. Pooled over worst-scope, with the mean-vs-max rationale, per the issue's "state which you chose and why" requirement.
  • Lockstep: workflows/ticketmill.js and .claude/workflows/ticketmill.js are byte-identical.
  • docs/architecture/metrics.md, pipeline.md, failure-semantics.md are byte-for-byte untouched (confirmed via diff and a green tests/architecture-provenance.test.js), honoring the issue's explicit "do NOT edit" constraint. The correction instead lives in gate-hygiene.md, which already existed on this branch from a concurrent issue — the PR correctly extends it rather than recreating it or adding a duplicate index.md row.
  • Tests extended in both files the issue named (tests/friction.test.js, tests/compose-friction-churn.test.js), plus quality-loop.test.js and a new harness.test.js parity test tying freshMetrics() to the engine's metrics literal by regex. Full suite: 655/655 green.
  • Driver legibility: quality driver's value stays raw quality_iters; cap/scopes added to all seven stage drivers (scopes: null on the six not individually counted) so contribution === Math.min(1, value/cap) holds uniformly and is asserted in tests.
  • Acceptance criterion 1 ("scores 0" on an all-first-try multi-task issue) is not literally met — by design, and this is correct, not a gap. Both formulas the issue sanctions (pooled and worst-scope) compute 0.2 on that fixture, not 0, because every capped stage in computeFriction counts iterations from 1, never 0, on a clean first pass — a pre-existing property of the ratio formula, not something this PR introduces. The implementer verified this arithmetically for both sanctioned formulas and documented it prominently in gate-hygiene.md ("Acceptance criterion 1, in its strongest form"), rather than silently deviating. 0.2 is the sibling-consistent answer (matches what every other capped stage scores on a same-shape clean-first-try case) and is asserted as such in tests.
    • Minor nit: the PR body's "Acceptance criteria covered" list states this criterion as "→ quality term scores 0," which reads as literally satisfied when the delivered/tested behavior is 0.2. Recommend a one-line edit to the PR body pointing at the gate-hygiene.md explanation, so a reader of the PR description alone doesn't need to open the test file to learn the literal "0" wasn't achievable. Not blocking — the reasoning and tests are sound and consistent with each other.
  • Non-monotonicity (an issue that caps out once, then adds a clean scope, scores lower) is called out as an accepted, deliberate trade-off in both gate-hygiene.md and a dedicated test, not hidden.

No engine-owned-path violations, no untracked task artifacts, no reopening of settled approach/plan adjudications.

@aaddrick

Copy link
Copy Markdown
Owner Author

Code Review (Iteration 1)

Verdict: changes_requested — no blocking findings. Two cheap, concrete minors below that would otherwise go unowned, one deferred follow-up, one optional nit. The change itself is correct, well tested, and lockstepped.

Validation baseline (all green)

node --check workflows/ticketmill.js && node scripts/lint-engine.js && bash -n scripts/setup-worktree.sh && <both manifests parse> && node --testlint-engine: clean (workflows/ticketmill.js, 2 lockstep pairs in sync), 655/655 tests pass. workflows/ticketmill.js and .claude/workflows/ticketmill.js are byte-identical.

What I verified above that line

  • Sandbox: no Date.now() / Math.random() / argless new Date() / fs or Node API / TS syntax introduced. Object.prototype.hasOwnProperty.call at workflows/ticketmill.js:2634 is the only new builtin reach.
  • No undefined++ path. ctx.metrics is built fresh per issue at workflows/ticketmill.js:5127 and is never restored from a prior run record (I grepped every non-ctx.metrics. read site; resume is at issue granularity via resume_point). So ctx.metrics.quality_scopes++ can never hit an absent key and write NaN into a live run's metrics.
  • STOP invariant holds. workflows/ticketmill.js:3095-3097: if (STOP.tripped) return 'halted' precedes both quality_iters++ and if (iter === 1) quality_scopes++, so a STOP'd entry still touches nothing — asserted at tests/quality-loop.test.js:130-132. The iter === 1 placement (rather than a bump at function entry) is what preserves that, and it counts invocations, not iterations: tests/quality-loop.test.js:476 (one capped loop → 1) and :507 (two loops, 10 iterations total → 2).
  • Back-compat is genuine, not merely asserted. A metrics blob with no quality_scopes key falls through Math.max(1, Number(...) || 0)1, so cap === baseCap and the score is identical to pre-change. Covered at tests/friction.test.js:260-273. Non-numeric, negative, and NaN values land in that same branch.
  • The two new driver keys are safe downstream. drivers is serialized verbatim into the run record at workflows/ticketmill.js:5846-5854 (friction_churn.friction.by_issue), and no schema, agent prompt, or size cap enumerates driver keys — the only renderer is :2707, which reads name + contribution only. Composition parity is pinned at tests/compose-friction-churn.test.js:134-140.
  • Every engine line number cited in the new docs is accurate. gate-hygiene.md's :3628, :4427, :4573, :4850, :3096, :3317, :4707 all resolve to exactly the assignments/increments claimed. quality_degrades really does fire only on the agent-death path (:3186), never on cap exhaustion (:3193-3205), so point 2's "diluting the ratio is safe, the cap-out is carried elsewhere" argument checks out in code, not just in prose.
  • Engine-owned paths: only the mandated .claude/workflows/ticketmill.js lockstep copy, written by lint-engine.js --fix. No .claude/agents/**, no .claude/ticketmill.json. CHANGELOG.md / .claude-plugin/plugin.json correctly untouched (batch-level, owned by the Report-phase release stage).

Findings

1. Minor — workflows/ticketmill.js:2616-2618 (+ lockstep copy): the extension-point comment mislabels browser_iters' scope unit.

The comment reads "task_review_attempts and browser_iters are two more multi-scope aggregates (per-task task review, per-iteration browser checks)". That parenthetical names each aggregate's scope, and for browser the scope is the phase, not the iteration: browser_iters++ is per-iteration (:3317) — that is the numerator — while the multi-scope-ness comes from the two runBrowserCheck call sites, 'implement' at :4786 and 'pre-merge' at :4985. Taken literally, a per-iteration scope would make scopes === browser_iters and pin the ratio at 1 forever. gate-hygiene.md:501-504 states it correctly ("sums across the implement and pre-merge calls"), so the engine comment contradicts the doc it defers to — and it is the comment a future implementer of that explicitly named extension point reads first. Fix: (per-task task review, per-phase browser checks — implement and pre-merge), then node scripts/lint-engine.js --fix in the same commit.

2. Minor — PR body, "Acceptance criteria covered": criterion 1 is listed as covered when the shipped behavior is 0.2, not 0.

Seconding the Spec Review's nit rather than re-opening it — I agree with its substance and its severity, and I am not re-litigating the adjudicated formula. I flag it only because the recommended edit is still outstanding and currently belongs to no stage. The bullet reproduces the criterion verbatim (→ quality term scores 0) under a heading asserting coverage, while tests/friction.test.js:217 names that same fixture "the acceptance criterion's unsatisfiable literal \"0\"" and docs/architecture/gate-hygiene.md:475-490 argues at length why 0.2 is the sibling-consistent answer under both formulas the issue sanctions. The PR body is what a human reads at the merge gate; it should say 0.2 and point at that section. One-line edit, no code change.

3. Deferred (not a change request — filed here so it has a written home) — skills/mill-review/SKILL.md doesn't know quality friction is no longer comparable across engine versions.

gate-hygiene.md:515-519 correctly states "Compare quality friction only within reports generated by the same version of this engine." But mill-review is precisely the tool that builds cross-run trend lines, and its Step 4 data model carries friction_churn verbatim per run (SKILL.md:114) with no version discriminator. Its existing "Not every run's file has every block" caveat at SKILL.md:81-86 is the natural home for one sentence, and results[].metrics.quality_scopes being present-or-absent is a perfectly serviceable discriminator. Out of scope for this issue — recommend a follow-up issue rather than growing this PR.

4. Nit (optional) — tests/harness.js:216-218. makeCtx's doc comment enumerates ctx.metrics' fields and wasn't extended with quality_scopes. It is a pre-existing partial list (already omits merge_auto_resolved, merge_thrash, test_quality_fix_rounds, findings_empty_exits), so this is drift you inherited rather than drift you introduced — worth a word only because freshMetrics() a hundred lines above it now has a parity test with teeth while this prose has none.

Not flagged, deliberately

The pooled-vs-worst-scope choice, the if (iter === 1) placement, scopes: null on the other six stage drivers, and the non-monotonicity trade were all settled at approach-challenge iteration 3 and are correctly implemented here; nothing in the code contradicts the adjudication, so there is nothing to re-open. No verification skip was introduced — no check was added or bypassed, so no VERIFY_SKIPS push was owed. No incident machinery was weakened: the STOP guard, the sum(disposition) === quality_iters invariant, and the one-line-per-issue cap roll-up are all intact and still asserted.

@aaddrick

Copy link
Copy Markdown
Owner Author

PR Review Fix (iteration 1)

Commit: 8ee459c (no new code commit — code review raised no blocking findings; only the spec-review nit applied)

Fixes applied:

  • PR body's "Acceptance criteria covered" bullet now reads 0.2 (not 0) for the all-first-try multi-task case, pointing at gate-hygiene.md's "Acceptance criterion 1, in its strongest form" for why 0.2 is the sibling-consistent, non-literal-shortfall answer.

@aaddrick

Copy link
Copy Markdown
Owner Author

Spec Review (Iteration 2)

Verdict: approved

Re-verified the scoped diff against the correct merge base (origin/Batch_2026-07-27_225225; a stale local ref made the raw git diff --stat look larger at first, but git diff origin/Batch_2026-07-27_225225...HEAD confirms the PR carries exactly 3 commits — af9075d, 24eadbc, 8ee459c — touching only workflows/ticketmill.js + its .claude/workflows/ticketmill.js lockstep copy, tests/{friction,harness,harness.test,quality-loop,compose-friction-churn}.js, and docs (gate-hygiene.md, index.md, AGENTS.md/CLAUDE.md mirrors). No scope creep.

Re-checked against the issue's own text, not just the prior review's summary:

  • Pooled formula, implemented as adjudicated. quality_scopes: 0 added to the metrics literal (:5127); runQualityLoop does if (iter === 1) ctx.metrics.quality_scopes++ below the STOP.tripped guard (:3097); computeFriction gains multiScopeField = { quality: 'quality_scopes' }, cap = baseCap * (scopes || 1). quality_iters increment and meaning are untouched, as the issue required.
  • Chosen approach and why, stated in the PR — pooled over worst-scope, mean-vs-max rationale, matching the issue's "state which you chose and why."
  • Lockstep verified directly: diff workflows/ticketmill.js .claude/workflows/ticketmill.js — byte-identical.
  • docs/architecture/metrics.md, pipeline.md, failure-semantics.md are byte-for-byte untouched — confirmed via git diff --stat against the merge base (empty) and a green tests/architecture-provenance.test.js (2/2), honoring the issue's explicit "do NOT edit" constraint. The correction lives in gate-hygiene.md instead, including two new provenance-supersession entries for metrics.md:13-14 and :15-17.
  • Tests extended in both files the issue named (tests/friction.test.js +197 lines, tests/compose-friction-churn.test.js +12 lines), plus quality-loop.test.js and harness.test.js. Ran the full suite myself: 655/655 green.
  • Acceptance criteria, checked one at a time against live test fixtures, not just descriptions:
    • Multi-task issue, every quality loop capped (quality_iters:15, quality_scopes:3) → cap:15, contribution:1 (friction.test.js). Met.
    • Multi-task issue, every quality loop clean on iteration 1 (quality_iters:3, quality_scopes:3) → 0.2, not the literal 0 the criterion states. As iteration 1 already found and I'm not reopening: both formulas the issue sanctions (pooled and worst-scope) compute 0.2 on this fixture, because every capped stage counts iterations from 1, never 0, on a clean pass — a pre-existing property of min(1, iters/cap), not something this PR introduces. Documented at length in gate-hygiene.md ("Acceptance criterion 1, in its strongest form") and asserted in tests. No new information changes this determination from iteration 1; staying consistent.
    • Single quality scope → unaffected (friction.test.js: quality_iters:5,scopes:1 → 1.0; 2,1 → 0.4; back-compat with no quality_scopes key at all → falls back to scopes:1, identical to pre-change).
    • quality_iters unchanged in meaning/value — confirmed, only a new sibling counter was added.
    • Driver value stays raw quality_iters (the numerator itself), so it visibly changes whenever the numerator does — satisfies "the rendered value changes with it."
    • node --test green, both named files extended. Met.
  • PR-body nit from iteration 1 is fixed: "Acceptance criteria covered" now states 0.2, not 0, and points at the gate-hygiene.md explanation.

Not my lane, noted for completeness only: the code review's Minor 1 (an engine comment at :2616-2618 mislabeling browser_iters' scope unit as "per-iteration" instead of "per-phase") is still unfixed in the current diff. It's a comment-accuracy issue, not an acceptance-criterion or scope-creep issue, so it doesn't change this verdict — flagging only so it isn't lost between review lanes.

No engine-owned-path violations, no untracked task artifacts, no reopening of settled approach/plan adjudications. Consistent with the iteration-1 spec-review approval; nothing here reverses it.

@aaddrick

Copy link
Copy Markdown
Owner Author

Code Review (Iteration 2)

Verdict: changes_requested — one finding, comment-only, ~2 lines plus a lint-engine --fix. No behavioral defect; the arithmetic, the counter placement, the lockstep, and the tests are all correct and I re-verified them below.

Validation baseline (all green)

node --check workflows/ticketmill.js → ok. node scripts/lint-engine.jsclean (workflows/ticketmill.js, 2 lockstep pairs in sync). bash -n scripts/setup-worktree.sh → ok. Both manifests parse. node --test655/655 pass.

Finding

1. Major (comment-only) — workflows/ticketmill.js:2569-2571 and :2616-2618 (+ lockstep copy): the two comments this PR writes into computeFriction both misstate which stages are multi-scope, and the first contradicts the doc and the code comment shipped alongside it in the same PR.

(a) New in this diff, not previously flagged — the module header at :2569-2571:

// running below a cap is normal, not friction. Six of the seven stages run
// at most once per issue, so their denominator is that single loop's cap;
// quality alone can run once per task plus once per PR-fix round, so its

Both halves are false, and verifiably so from this repo:

  • task_review_attempts++ (:4707) sits in the review-attempt loop nested inside the per-task loop at :4658 (for (let ti = 0; ti < tasks.length; ti++)). Task-review runs once per task, exactly the way quality does — so "quality alone can run once per task" is wrong.
  • browser_iters++ (:3317) is summed across two runBrowserCheck call sites, 'implement' at :4786 and 'pre-merge' at :4985, so browser is multi-scope too.
  • That leaves four genuinely single-scope stages (approach, plan, test, pr-review), not six.

This is not a nit about phrasing. The claim "six of the seven stages run at most once per issue, so their denominator is that single loop's cap" asserts that the bug this PR exists to fix is now absent everywhere except quality, when in fact two of the other six carry the same aggregate-numerator-vs-single-loop-cap defect and are only left alone because they lack scope counters. A maintainer who reads the header and stops there concludes the invariant is repaired; it isn't.

The contradiction is internal to this PR, which is what makes it cheap to see and cheap to fix:

  • docs/architecture/gate-hygiene.md, section "Why scopes is null, not 1, on the other six stage drivers" (added in 8ee459c), says it correctly: "task_review_attempts and browser_iters are multi-scope aggregates in exactly the same sense quality_iters is … The remaining four stages (approach, plan, test, pr-review) are genuinely single-scope."
  • workflows/ticketmill.js:2616-2617, 45 lines below the header in the same commit, also says it correctly: "task_review_attempts and browser_iters are two more multi-scope aggregates … not yet counted this way."

So the header is the only one of the three artifacts that gets it wrong.

(b) Carried from Iteration 1, finding 1 — still present verbatim at :2616-2618: the extension-point comment's parenthetical (per-task task review, per-iteration browser checks). Browser's scope is the phase (implement / pre-merge), not the iteration — browser_iters per-iteration is the numerator. Read literally by whoever implements the extension point this comment explicitly advertises, scopes === browser_iters pins the browser ratio at 1 forever. gate-hygiene.md states it correctly ("sums across the implement and pre-merge calls"), so the engine comment contradicts the doc it defers to.

I am re-raising (b) rather than treating it as settled because it was never dispositioned. The PR Review Fix (iteration 1) comment closed it with "code review raised no blocking findings — only the spec-review nit applied." That reads the Iteration 1 verdict as if changes_requested with no blocking findings meant nothing was requested; the two minors in that review were requested changes. No reviewer or fix stage has said "no change needed" here, so the finding is unowned rather than resolved — and (a) above lands in the same three lines of comment anyway, so both are one edit.

Fix direction: reword :2570 to "Four of the seven stages run at most once per issue … quality is the one whose pooled denominator is implemented; task-review and browser are multi-scope too and not yet counted (see multiScopeField below)", and :2617 to (per-task task review, per-phase browser checks — implement and pre-merge). Then node scripts/lint-engine.js --fix in the same commit so .claude/workflows/ticketmill.js stays byte-identical.

What I verified above the finding line

  • Sandbox clean. No Date.now(), Math.random(), argless new Date(), fs/Node API, or TS syntax introduced. Object.prototype.hasOwnProperty.call at :2634 is the only new builtin reach.
  • The pooled arithmetic is right and the fallbacks are total. scopes = tracked ? Math.max(1, Number(m[field]) || 0) : null maps absent / NaN / negative / non-numeric quality_scopes all to 1, so cap === baseCap and a pre-change metrics blob scores exactly as it did before (tests/friction.test.js:260-273). cap = baseCap * (scopes || 1) can never be 0baseCap is already guarded and scopes is Math.max(1, …) — so no divide-by-zero was introduced.
  • The counter counts invocations, not iterations, and the STOP invariant survives. :3095-3097: if (STOP.tripped) return 'halted' precedes both increments, so a STOP'd entry still touches neither (tests/quality-loop.test.js:130-132). if (iter === 1) is what makes it per-call: one capped 5-iteration loop → 1 (:476), two loops / 10 iterations → 2 (:507).
  • No undefined++ and no resume hole. I re-grepped every non-ctx.metrics. read of .metrics: it is built fresh per issue at :5127 and only ever flows outward (:2925 fail(), :5092 the success record, :5717 metricsMissing). Nothing merges a prior run's metrics back into ctx, so quality_scopes can never be absent on a live increment.
  • quality_iters is untouched in meaning and value, as the issue required — :3096 is unchanged and the sum(gate_findings.quality.disposition) === quality_iters invariant still holds (tests/quality-loop.test.js:425-452).
  • The two new driver keys are safe downstream. I re-grepped every drivers consumer: the only renderer is :2707 (name + contribution), :2675 serializes the array verbatim into the run record, and no schema, agent prompt, or size cap enumerates driver keys. Composition parity is pinned at tests/compose-friction-churn.test.js:134-140, and the scopes/cap shape split is pinned by the invariant test at tests/friction.test.js:335-378 reading the MAX_* constants live rather than hardcoding them.
  • Docs stay honest. metrics.md, pipeline.md, failure-semantics.md byte-for-byte untouched (the issue forbids editing metrics.md); the third supersession entry in AGENTS.md/CLAUDE.md is mirrored identically in both files (diff → no output); index.md's file-map row updated to match. architecture-provenance.test.js green.
  • Engine-owned paths: only the mandated .claude/workflows/ticketmill.js lockstep copy, written by lint-engine.js --fix. No .claude/agents/**, no .claude/ticketmill.json. CHANGELOG.md / .claude-plugin/plugin.json correctly untouched (batch-level, Report-phase release stage).
  • No verification skip introduced, so no VERIFY_SKIPS push was owed; no incident machinery weakened (STOP guard, disposition tally, one-line-per-issue cap roll-up all intact and still asserted).

Not re-flagged

  • The pooled-vs-worst-scope choice, the if (iter === 1) placement, scopes: null on the other six stage drivers, and the accepted non-monotonicity trade were settled at approach-challenge iteration 3 and are correctly implemented. Nothing in the code contradicts the adjudication.
  • Acceptance criterion 1's literal 0: dispositioned by the Spec Review, and the PR body edit in iteration 1 landed — the bullet now reads 0.2 and points at gate-hygiene.md. Closed.
  • Iteration 1's finding 3 (skills/mill-review/SKILL.md has no engine-version discriminator for cross-run quality-friction comparison) stays a follow-up issue, not a change request on this PR. Still recommended; still out of scope here.
  • Iteration 1's nit 4 (tests/harness.js:216-218 makeCtx doc comment omits quality_scopes) — optional, pre-existing drift you inherited, and freshMetrics() now has a parity test with teeth (tests/harness.test.js:71-93). Fold it into the finding-1 commit if convenient; not required.

The module header claimed six of the seven capped stages run at most
once per issue, and the multiScopeField comment called browser's scope
unit "per-iteration". Both are wrong: task-review and browser are
multi-scope aggregates in exactly the same sense quality is (only
approach/plan/test/pr-review are genuinely single-scope), and browser's
scope is the phase (implement/pre-merge), not the iteration -- iteration
is the numerator. Reword both comments to match gate-hygiene.md and the
code they sit beside.
@aaddrick

Copy link
Copy Markdown
Owner Author

PR Review Fix (iteration 2)

Commit: 09c5113

Fixed the code review's finding: reworded the module header (:2569) and the multiScopeField comment (:2617) in workflows/ticketmill.js. Both now correctly say four (not six) stages are single-scope, name task-review and browser as the other two multi-scope aggregates, and describe browser's scope as per-phase (implement/pre-merge) rather than per-iteration — matching gate-hygiene.md and the rest of the code. Synced the lockstep copy via node scripts/lint-engine.js --fix; 655/655 tests green.

…er edit

commit 09c5113 added 3 net lines to computeFriction's module header
comment, shifting the seven `= iter`/`++` statements gate-hygiene.md
cites as evidence for "iteration counting starts at 1, not 0" by +3
each. No functional change.
@aaddrick

Copy link
Copy Markdown
Owner Author

Spec Review (Iteration 3)

Verdict: approved

Re-verified the two commits added since iteration 2's approval:

  • 09c5113 (the fix for iteration 2's code-review finding: reworded the computeFriction module header and multiScopeField comment so task-review and browser are correctly described as multi-scope aggregates, leaving only approach/plan/test/pr-review as genuinely single-scope) — matches gate-hygiene.md's own wording, confirmed by direct comparison.
  • 49e531e (a pure doc-citation resync in gate-hygiene.md: 09c5113 added 3 net lines to the header, shifting the seven = iter/++ line numbers the "iteration counting starts at 1, not 0" argument cites). I checked all seven new citations (:3631, :4430, :4576, :4853, :3099, :3320, :4710) against the current workflows/ticketmill.js — every one resolves to exactly the statement claimed. No functional change, confirmed by diff (doc-only).

Re-confirmed the invariants iteration 1/2 already established, nothing regressed:

  • Pooled formula intact. quality_scopes: 0 in the metrics literal, if (iter === 1) ctx.metrics.quality_scopes++ below the STOP.tripped guard, multiScopeField = { quality: 'quality_scopes' }, cap = baseCap * (scopes || 1). quality_iters increment/meaning untouched.
  • Lockstep: diff workflows/ticketmill.js .claude/workflows/ticketmill.js — byte-identical.
  • Hash-frozen docs untouched: metrics.md/pipeline.md/failure-semantics.md absent from git diff --stat against the merge base; tests/architecture-provenance.test.js — 2/2 green.
  • Full suite: node --test — 655/655 green.
  • Scope: git diff origin/Batch_2026-07-27_225225...HEAD --stat touches exactly the plan's 11 files (engine + lockstep, 5 test files, 4 doc files). No creep.
  • Acceptance criteria: cap-exhaustion → 1.0 (met), single-scope unaffected (met, back-compat verified), quality_iters unchanged (met), driver legibility via cap/scopes (met). Criterion 1's literal "scores 0" text remains mathematically unreachable under the issue's own specified formula for any real multi-task first-try pass (min(1, 3/15) = 0.2, not 0, since iteration counts start at 1) — settled as correct-not-a-gap at iterations 1 and 2 with rationale in gate-hygiene.md; no new information surfaces here, so per the consistency instruction this is not reopened.

No scope creep, no engine-owned-path violations beyond the required lockstep copy, no regressions from the last two commits.

@aaddrick

Copy link
Copy Markdown
Owner Author

Code Review (Iteration 3)

Verdict: changes_requested — three text-only findings, together one commit's worth of editing (two comments, one line citation). No behavioral defect. Finding 1 is iteration 2's accepted finding applied to two of its three sites: a third comment inside the same function still carries the corrected-away claim, and now contradicts the comment 21 lines above it.

Validation baseline (all green)

node --check workflows/ticketmill.js → ok. node scripts/lint-engine.jsclean (workflows/ticketmill.js, 2 lockstep pairs in sync); diff workflows/ticketmill.js .claude/workflows/ticketmill.js → byte-identical. bash -n scripts/setup-worktree.sh → ok. Both manifests parse. node --test655/655 pass.

Findings

1. Major (comment-only) — workflows/ticketmill.js:2638 (+ lockstep copy): the loop-body comment still says "the six single-scope stages", the claim iteration 2 corrected 21 lines above it.

// scopes stays null (not 1) for the six single-scope stages: a number
// here is a claim "this many invocations were pooled", and only
// quality currently has a counter backing that claim.

Commit 09c5113 fixed this claim at :2569 (now "Four of the seven stages … task-review and browser are multi-scope aggregates too") and at :2617 (now "task_review_attempts and browser_iters are two more multi-scope aggregates … per-phase browser checks"). The third site was missed, so computeFriction now asserts both things within 21 lines: :2617 "two more multi-scope aggregates", :2638 "the six single-scope stages".

This is the one place where the four/two split actually changes what the code means. docs/architecture/gate-hygiene.md:505-513 spells it out — task_review_attempts and browser_iters are multi-scope in exactly the sense quality_iters is, and only approach/plan/test/pr-review are genuinely single-scope — so scopes: null means "no scope count applies" for four stages and "not yet counted" for two. The comment flattens both into "single-scope", which is the reading that leads a future implementer of the extension point advertised at :2617 to skip the very two stages it names.

Not a re-flag: iteration 2's finding named :2569-2571 and :2616-2618, and both are fixed. This is a third site of the same claim. I grepped for further instances (single-scope, six of the seven, six stages) across the worktree — no other engine hits.

Fix: reword :2638 to "for the other six stage drivers" (or "for the six stages without scope counters"), then node scripts/lint-engine.js --fix in the same commit.

2. Minor — tests/friction.test.js:318: the test name makes the same claim, and it renders in TAP output.

computeFriction: scopes stays null (not a number) for single-scope stages — asserted on a task-review driver and a browser driver

The two drivers this test asserts on are precisely the two the same PR documents as not single-scope. Fixture and assertions are correct; only the name is wrong. Fix: "…for stage drivers with no scope counter — asserted on a task-review driver and a browser driver", or similar.

3. Minor — tests/harness.js:81: the anchor this PR introduced is already stale by 3 lines.

// ticketmill.js:5127) — kept in sync by hand … — the metrics literal is at :5130 now, shifted by 09c5113's +3-line header edit. 49e531e resynced gate-hygiene.md's seven citations for that same shift (I verified all seven land on the statements claimed) but missed this one, so the anchor currently points at revisit_risk. The parity test at tests/harness.test.js:78-93 regex-reads the literal and does not depend on the number, so nothing is broken. Fix: update to :5130, or drop the number and cite processIssue()'s ctx init by name — this anchor has now rotted twice inside one PR.

What I verified above the finding line

  • Sandbox clean. No Date.now(), Math.random(), argless new Date(), fs/Node API, or TS syntax. Object.prototype.hasOwnProperty.call at :2634 remains the only new builtin reach.
  • Pooled arithmetic and its fallbacks are total. scopes = tracked ? Math.max(1, Number(m[field]) || 0) : null maps absent / NaN / negative / non-numeric to 1; cap = baseCap * (scopes || 1) can never be 0 (both factors floored at 1), so no divide-by-zero was introduced and a pre-change metrics blob scores exactly as before (tests/friction.test.js:260-273).
  • Counter semantics and the STOP invariant. :3098-3100: if (STOP.tripped) return 'halted' precedes both increments, and if (iter === 1) makes the new one per-invocation. On live data quality_scopes >= 1 whenever quality_iters >= 1, and it can never exceed the number of loops actually entered. Pinned at tests/quality-loop.test.js:130-132, :476, :507.
  • quality_iters untouched in meaning and value, as the issue required; the sum(gate_findings.quality.disposition) === quality_iters invariant still holds.
  • The two new driver keys are safe downstream. drivers is sorted at :2677, pushed at :2678, rendered at :2710 (name + contribution only), and serialized verbatim into the run record at :5849; buildRunRecord writes untruncated (:5787-5792), and no size cap, schema, or agent prompt enumerates driver keys. Composition parity pinned at tests/compose-friction-churn.test.js:134-140.
  • Doc citations re-verified individually. gate-hygiene.md:577-579's :3631, :4430, :4576, :4853, :3099, :3320, :4710 each resolve to the exact = iter assignment or ++ increment claimed. AGENTS.md and CLAUDE.md are byte-identical (diff → no output); index.md's file-map row matches the new supersession scope.
  • Engine-owned paths: only the mandated .claude/workflows/ticketmill.js lockstep copy, written by lint-engine.js --fix. No .claude/agents/**, no .claude/ticketmill.json. CHANGELOG.md / .claude-plugin/plugin.json correctly untouched (batch-level, Report-phase release stage).
  • metrics.md, pipeline.md, failure-semantics.md byte-for-byte untouched; tests/architecture-provenance.test.js green.
  • No verification skip introduced, so no VERIFY_SKIPS push was owed; no incident machinery weakened (STOP guard, disposition tally, one-line-per-issue cap roll-up all intact and still asserted).

Dispositioned, not re-flagged

  • The pooled-vs-worst-scope choice, the if (iter === 1) placement, scopes: null on the other six drivers, and the accepted non-monotonicity trade were settled at approach-challenge iteration 3 and are correctly implemented.
  • Acceptance criterion 1's literal 0: closed at iteration 1 — the PR body now reads 0.2 and points at gate-hygiene.md.
  • Iteration 1's finding 3 (skills/mill-review/SKILL.md has no engine-version discriminator for cross-run quality-friction comparison): still a recommended follow-up issue, explicitly not a change request on this PR.
  • Iteration 1's nit 4 (tests/harness.js:216-218 makeCtx doc comment omits quality_scopes): no change required — inherited drift, and freshMetrics() now has a parity test with teeth. Fold in only if convenient.
  • tests/compose-friction-churn.test.js:8's workflows/ticketmill.js:5301/5470 citation is stale, but it was already stale on the base branch (both line numbers pointed elsewhere there too). Inherited, out of scope here, noted so it is not mistaken for PR-introduced drift.

Findings 1-3 are the last text-accuracy items I have; I searched exhaustively for further instances rather than leaving room for a fourth pass. With those three edits (plus lint-engine.js --fix) this is an approve.

@aaddrick

Copy link
Copy Markdown
Owner Author

Spec Review (Iteration 1)

Verdict: approved

Verified the scoped diff (git diff origin/Batch_2026-07-27_225225...HEAD, 5 commits: af9075d, 24eadbc, 8ee459c, 09c5113, 49e531e) against issue #165's requirements directly, not by re-quoting prior review comments already on this PR.

Files touched match the plan exactly: workflows/ticketmill.js + its .claude/workflows/ticketmill.js lockstep copy, five test files (tests/{friction,harness,harness.test,quality-loop,compose-friction-churn}.js), and docs (gate-hygiene.md, index.md, AGENTS.md/CLAUDE.md mirrors). No scope creep.

Acceptance criteria, checked against the current diff and tests:

  • Multi-task issue, every quality loop passed on iteration 1 → scores 0. Not literally met — the shipped/tested value is 0.2 (tests/friction.test.js:217). This is a real, documented deviation, not an oversight: every capped stage in computeFriction counts iterations starting at 1 (approach_iters = iter, plan_iters = iter, test_iters = iter, pr_review_iters = iter, all pre-existing), so a clean first pass has always cost 1/cap, never 0, for any stage. Both formulas the issue itself sanctions (pooled and worst-scope) compute 0.2 on this fixture — verified arithmetically in the issue's own decision-chain comments and pinned in gate-hygiene.md's "Acceptance criterion 1, in its strongest form" section. 0.2 is the sibling-consistent answer, not a shortfall unique to this implementation.
  • Multi-task issue, every quality loop exhausted the cap → scores 1.0. Met — tests/friction.test.js:188 (quality_iters:15, quality_scopes:3cap:15, contribution:1).
  • Single quality scope → scores exactly what it scores today. Met — tests/friction.test.js:246 and the back-compat case at :262 (a metrics blob with no quality_scopes key falls back to scopes:1 via Math.max(1, Number(...)||0), byte-identical to pre-change scoring).
  • quality_iters unchanged in meaning and value. Met — the increment at workflows/ticketmill.js:3099 is untouched; only a new sibling counter (quality_scopes) was added, incremented once per runQualityLoop invocation (if (iter === 1) ctx.metrics.quality_scopes++) below the STOP.tripped guard.
  • Driver value is interpretable and moves with the numerator. Met — value stays raw quality_iters; cap/scopes are added to all seven stage drivers so contribution === Math.min(1, value/cap) holds uniformly, pinned by a saturating fixture (tests/friction.test.js:303, quality_iters:12value/cap:2.4 while contribution clamps to 1).
  • node --test stays green, both named files extended. Verified myself: 655/655 pass. tests/friction.test.js (+197 lines) and tests/compose-friction-churn.test.js (+12 lines) both extended as required.

Dependency note honored — diff touches only computeFriction and the metrics init, nothing else in the concurrent set.

Documentation target. docs/architecture/gate-hygiene.md already existed on this branch (from concurrent issue #163) — the PR correctly appends a section rather than recreating it or duplicating the index.md file-map row, matching the issue's "create it if absent" instruction. docs/architecture/metrics.md, pipeline.md, and failure-semantics.md are byte-for-byte untouched (confirmed via git diff --stat against the merge base and a green tests/architecture-provenance.test.js, 2/2), honoring the issue's explicit "do NOT edit" constraint. CHANGELOG.md is untouched in this PR — consistent with this pipeline's established pattern of deferring CHANGELOG/version bumps to the batch-level Report/release stage rather than per-issue PRs, not a miss against this issue.

Lockstep verified directly: diff workflows/ticketmill.js .claude/workflows/ticketmill.js → byte-identical; node scripts/lint-engine.js → clean.

No engine-owned-path violations beyond the required lockstep copy, no untracked task artifacts.

Noted, not blocking (not my lane): the PR's own commit history shows an unresolved code-review finding (three comment-accuracy nits — e.g. workflows/ticketmill.js:2638 still saying "the six single-scope stages" after a prior fix corrected two sibling comments to "four") that has not yet been fixed as of the current HEAD. These are comment-text-only, do not affect any acceptance criterion, functionality, or test outcome, and are squarely code-review's domain rather than spec's — flagging only so it isn't lost, not as a reason to withhold spec approval.

@aaddrick

Copy link
Copy Markdown
Owner Author

Code Review (Iteration 1)

Verdict: changes_requested — no blocking findings, no behavioral defect. Three comment/label-only minors, all in one round of edits. The arithmetic, the counter placement, the lockstep, and the tests are correct and I re-verified them below. Finding 1 is a surviving third instance of the exact claim a prior review round already adjudicated as false and 09c5113 fixed in the other two places.

Validation baseline (all green)

node --check workflows/ticketmill.js → ok. bash -n scripts/setup-worktree.sh → ok. Both manifests parse. node scripts/lint-engine.jsclean (workflows/ticketmill.js, 2 lockstep pairs in sync). node --test "tests/*.test.js"655/655 pass. diff workflows/ticketmill.js .claude/workflows/ticketmill.js → identical. diff docs/architecture/CLAUDE.md docs/architecture/AGENTS.md → identical.

Verified above that line

  • Sandbox clean. No Date.now() / Math.random() / argless new Date() / fs or Node API / TS syntax introduced. New builtin reach is Object.prototype.hasOwnProperty.call (:2636) and Math.max/Number — all pure.
  • Counter placement. workflows/ticketmill.js:3099-3100: quality_scopes++ sits below the same if (STOP.tripped) return 'halted' guard as quality_iters++, gated on iter === 1. A STOP'd entry touches neither; a fully capped loop counts 1 scope, 5 iters; two calls count 2 scopes, 10 iters. Asserted at tests/quality-loop.test.js:130-132, :476, :509.
  • No undefined++ / NaN path. ctx.metrics is built fresh per issue at :5130 and never restored from a prior run record, so the increment can never hit an absent key.
  • Backward compatibility. A metrics blob predating this change (no quality_scopes) falls through Math.max(1, Number(undefined) || 0)1, so cap === baseCap and historical rows score byte-identically. Covered at tests/friction.test.js:265-276. No .claude/ticketmill.json profile shape change, so no re-onboarding break.
  • Driver-shape blast radius. cap/scopes are additive keys on stage drivers only; computeFriction's markdown render reads name/contribution only, composeFrictionChurn passes drivers through by reference (asserted tests/compose-friction-churn.test.js:136), buildRunRecord carries the whole friction_churn verbatim, and skills/mill-review/SKILL.md explicitly never recomputes. Nothing downstream enumerates driver keys.
  • freshMetrics() sync has teeth now. tests/harness.test.js:71-92 regex-reads the engine's single-line metrics: { … }, literal and diffs its key set against harness.freshMetrics(). tests/run-record.test.js:71 walks the same key set, so quality_scopes reaches the run record.
  • Frozen-passage discipline. docs/architecture/metrics.md, pipeline.md, failure-semantics.md byte-for-byte untouched; tests/architecture-provenance.test.js green. The correction lands as a third supersession entry in gate-hygiene.md with the matching paragraph mirrored into the CLAUDE.md/AGENTS.md freeze pair. The seven = iter/++ citations at gate-hygiene.md:576-579 (:3099, :3320, :3631, :4430, :4576, :4710, :4853) all resolve correctly on HEAD after the 49e531e resync — I checked each one.
  • No verification skips, no weakened machinery. No new code path skips a check; nothing touches the stub-task guard, settled-decisions ledger, handoff notes, comment markers, claim label-safety, browser lock, or degrade windows. No agentType introduced. No MAX_* constant, stage order, or pipeline-diagram-affecting change, so no .d2 re-render is owed.

Findings

1. Minor — workflows/ticketmill.js:2638 (+ the .claude/ lockstep copy): "the six single-scope stages" is the same false claim iteration 2 rejected, surviving in a third location.

// scopes stays null (not 1) for the six single-scope stages: a number

09c5113 corrected the module header (:2569-2575) and the multiScopeField comment (:2617-2621) to say four stages are single-scope and that task-review/browser are multi-scope aggregates not yet counted. This third comment, written in the same diff, still calls all six of them single-scope. As it stands the file contradicts itself six lines apart, and contradicts gate-hygiene.md's item 5, which spells out that task_review_attempts and browser_iters are multi-scope and that giving them scopes: 1 "would be a false claim."

Fix direction: reword to something scope-agnostic that matches what the code actually keys on — e.g. "for the six stages with no scope counter" — then node scripts/lint-engine.js --fix to resync the lockstep copy.

2. Minor — tests/friction.test.js:311: the test title repeats the same mislabel.

computeFriction: scopes stays null (not a number) for single-scope stages — asserted on a task-review driver and a browser driver

The two drivers this test asserts on are exactly the two the PR documents as multi-scope-but-uncounted. The assertions are right; only the title is wrong. Fix direction: retitle to "for stages with no scope counter."

3. Minor — tests/harness.js:81: stale line citation, off by 3.

The comment cites workflows/ticketmill.js:5127 for the metrics literal it hand-syncs against; the literal is at :5130 on HEAD. 09c5113 added 3 net lines to computeFriction's header, and 49e531e resynced gate-hygiene.md's citations for exactly that shift but missed this one. Low impact — the new parity test catches real drift regardless — but it is the same resync the previous commit set out to do. Fix direction: 51275130.

No change required (recorded so it is not re-flagged)

  • Acceptance criterion 1 scoring 0.2, not the literal 0. Both formulas the issue sanctions produce 0.2 on the all-first-iteration fixture; gate-hygiene.md's item 3 and tests/friction.test.js:217-229 both document why. Correct disposition, not a shortfall.
  • Accepted non-monotonicity (5/5 = 1.0 → 6/10 = 0.6 on appending a clean scope). Documented as a deliberate trade in gate-hygiene.md item 2, asserted as intentional at tests/friction.test.js:279-300, and cap-out visibility is carried by ctx.quality_caps' VERIFY_SKIPS line and the carried-unresolved disposition, neither of which dilutes. No change wanted.
  • Cross-version comparability of the quality term. Historical records score unchanged (no quality_scopes → scopes 1), so a mill-review trend line will show a one-time step for multi-scope issues at this engine version. gate-hygiene.md states the caveat; propagating it into skills/mill-review/SKILL.md is a reasonable follow-up, deliberately not requested here.
  • Pooling task_review_attempts / browser_iters the same way. Named as a natural follow-up in both the code comment and gate-hygiene.md item 5. Out of scope.
  • metrics.md:15-17's pre-existing "scores 0 across every one of them" inaccuracy. Predates this change, sits inside the hash-frozen segment, correctly flagged rather than edited.
  • Per-issue CHANGELOG / plugin.json bump. Batch-level, owned by the gated Report-phase release stage. Not a per-issue PR concern.

Code review (iteration 1) found a third surviving instance of a claim
09c5113 already corrected in two other places: task-review and browser
are multi-scope aggregates without a counter yet, not single-scope
stages. Reword the computeFriction comment, the matching test title,
and resync harness.js's stale :5127 line citation to :5130.
@aaddrick

Copy link
Copy Markdown
Owner Author

PR Review Fix (iteration 1)

Commit: 24b2d17

Fixed all three code-review minors: reworded the computeFriction comment at workflows/ticketmill.js:2638 (and its .claude/ lockstep copy) from "the six single-scope stages" to "the six stages with no scope counter," retitled the matching test in tests/friction.test.js, and resynced tests/harness.js's stale :5127 line citation to :5130. node --test stays 655/655, lint-engine reports the lockstep copy clean.

@aaddrick

Copy link
Copy Markdown
Owner Author

Spec Review (Iteration 2)

Verdict: approved

Re-verified the fix commit added since this run's iteration-1 approval: 24b2d17 ("PR Review Fix (iteration 1)"), which addressed the three code-review minors raised at that point.

Scope unchanged. git diff origin/Batch_2026-07-27_225225...HEAD --stat still touches exactly the plan's 11 files: workflows/ticketmill.js + its .claude/workflows/ticketmill.js lockstep copy, five test files (tests/{friction,harness,harness.test,quality-loop,compose-friction-churn}.js), and docs (gate-hygiene.md, index.md, AGENTS.md/CLAUDE.md mirrors). No scope creep introduced by the fix commit.

Fix commit verified directly against the findings it claims to close:

  • workflows/ticketmill.js:2638 (+ lockstep copy) now reads "the six stages with no scope counter" — confirmed by direct read, no longer contradicts the corrected header/multiScopeField comments 45-70 lines above it. diff workflows/ticketmill.js .claude/workflows/ticketmill.js — byte-identical.
  • tests/friction.test.js:318 test title now reads "for stages with no scope counter" — matches the fixture's actual drivers (task-review, browser), no longer mislabels them single-scope.
  • tests/harness.js:81 citation now reads :5130 — confirmed against the actual engine file, where the metrics: { … } literal is on line 5130.

Acceptance criteria, unchanged from iteration 1's determination, re-confirmed against live tests:

  • Multi-task issue, every quality loop capped → contribution: 1. Met.
  • Multi-task issue, every quality loop clean on iteration 1 → 0.2, not the literal 0 the issue's text states. Dispositioned as correct-not-a-gap across three prior iterations of this same PR (both issue-sanctioned formulas produce 0.2 on this fixture because iteration counts start at 1, never 0, for every capped stage — a pre-existing property of min(1, iters/cap), not introduced here). No new information since; staying consistent per the standing instruction.
  • Single quality scope → unaffected, back-compat verified (tests/friction.test.js:265-276).
  • quality_iters unchanged in meaning/value. Met.
  • Driver value stays raw quality_iters, visibly moves with the numerator. Met.
  • node --test green: 655/655 pass, both named files (tests/friction.test.js, tests/compose-friction-churn.test.js) extended.

Frozen docs untouched. metrics.md, pipeline.md, failure-semantics.md absent from the merge-base diff; tests/architecture-provenance.test.js — 2/2 green.

Lockstep and manifests. node scripts/lint-engine.js → clean (2 lockstep pairs in sync). Both .claude-plugin/plugin.json and marketplace.json parse.

No engine-owned-path violations, no untracked task artifacts, no reopening of settled approach/plan adjudications. Consistent with this run's own iteration-1 approval; nothing here reverses it.

@aaddrick

Copy link
Copy Markdown
Owner Author

Code Review (Iteration 2)

Verdict: approved — all three iteration-1 minors are fixed, nothing new introduced, and I found no blocking, major, or minor findings on the current head. Reviewed 24b2d170367e028e74d5376bc48e22d3a4737b68 (matches origin/issue-165-fix-the-quality-friction-ratio-s-denominator) against origin/Batch_2026-07-27_225225.

Validation baseline (all green)

node --check workflows/ticketmill.js → ok. bash -n scripts/setup-worktree.sh → ok. Both manifests parse. node scripts/lint-engine.jsclean (workflows/ticketmill.js, 2 lockstep pairs in sync). node --test "tests/*.test.js"655/655 pass. diff workflows/ticketmill.js .claude/workflows/ticketmill.js → identical. diff docs/architecture/CLAUDE.md docs/architecture/AGENTS.md → identical.

Iteration-1 findings, re-verified as fixed

  1. workflows/ticketmill.js:2638 "six single-scope stages" mislabel — fixed in 24b2d17, now reads "for the six stages with no scope counter," and the .claude/ lockstep copy carries the identical edit (resynced via lint-engine.js --fix, not hand-edited). I swept the whole tree for survivors: grep -rn "single-scope" returns exactly three hits, all correct — gate-hygiene.md:487 and :511 (both about the four genuinely single-scope stages) and tests/friction.test.js:283 ("worst-single-scope", the rejected alternative). No fourth instance.
  2. tests/friction.test.js:318 test title — retitled to "for stages with no scope counter." Assertions unchanged and still correct (task-review and browser drivers, scopes === null).
  3. tests/harness.js:81 stale :5127 citation — now :5130, and sed -n '5130p' lands on the metrics: { … }, literal. I re-resolved every line citation the docs make after this edit, since 24b2d17 was a 2-for-2 line swap and could have shifted them: :3099 quality_iters++, :3320 browser_iters++, :3631 test_iters = iter, :4430 approach_iters = iter, :4576 plan_iters = iter, :4710 task_review_attempts++, :4853 pr_review_iters = iter. All seven still resolve. metrics.md:13-14 cited by the new CLAUDE.md/AGENTS.md paragraph and by gate-hygiene.md's supersession section is in fact the "Seven capped pipeline stages…" sentence.

Re-verified above the baseline (unchanged conclusions from iteration 1, re-checked on this head, not copied forward)

  • Sandbox clean. No Date.now(), Math.random(), argless new Date(), fs/Node API, or TS syntax in the diff. New builtin reach is Object.prototype.hasOwnProperty.call, Math.max, Math.min, Number — all pure and resume-safe.
  • Counter placement. quality_scopes++ sits below the if (STOP.tripped) return 'halted' guard, gated on iter === 1, so a STOP'd entry increments neither counter and a capped loop counts 1 scope / 5 iters. Both call sites (:4770 per task, :4981 per PR-fix round) are covered by tests/quality-loop.test.js:130-132, :476, :509.
  • No undefined++. ctx.metrics is constructed fresh per issue at :5130 and never rehydrated from a run record, so the new key always exists at increment time.
  • Backward compatibility. A pre-change metrics blob with no quality_scopes falls through Math.max(1, Number(undefined) || 0)1, so cap === baseCap and historical rows score byte-identically (tests/friction.test.js:265-276). No .claude/ticketmill.json profile-shape change, so no re-onboarding break for target repos running the copied engine.
  • Driver-shape blast radius. cap/scopes are additive keys on stage drivers; the markdown render reads name/contribution only, composeFrictionChurn passes drivers through by reference (now pinned by a deepStrictEqual against computeFriction's direct output at tests/compose-friction-churn.test.js:136), buildRunRecord carries friction_churn verbatim, and skills/mill-review/SKILL.md never recomputes. Nothing downstream enumerates driver keys.
  • Harness parity has teeth. tests/harness.test.js:71-92 regex-reads the engine's single-line metrics literal and diffs its key set against freshMetrics(), with an assertion message that names the exact repair. A future field added on either side alone fails loudly.
  • Frozen-passage discipline. metrics.md, pipeline.md, failure-semantics.md byte-for-byte untouched; tests/architecture-provenance.test.js green. The correction lands as a supersession entry in gate-hygiene.md with the matching paragraph mirrored into the CLAUDE.md/AGENTS.md freeze pair and the index.md row updated in step.
  • No verification skips, no weakened machinery, no agentType. No new code path skips a check without a VERIFY_SKIPS push; the stub-task guard, settled-decisions ledger, handoff notes, comment markers, claim label-safety, browser lock, and degrade windows are all untouched. No MAX_* constant, stage order, or pipeline-shape change, so no .d2 re-render is owed.

Carried forward from iteration 1, still not change requests

  • skills/mill-review/SKILL.md engine-version discriminator for cross-run quality-friction comparison — follow-up issue, not a change on this PR. The caveat is stated in gate-hygiene.md.
  • tests/harness.js's makeCtx doc comment omitting quality_scopesno change required (inherited drift, and freshMetrics() now has a parity test).
  • tests/compose-friction-churn.test.js:8's stale :5301/:5470 citation — inherited from the base branch, out of scope here.
  • Pooling task_review_attempts / browser_iters the same way — named as a follow-up in both the code comment and gate-hygiene.md item 5.
  • Acceptance criterion 1 scoring 0.2 rather than the literal 0, and the accepted non-monotonicity trade — both adjudicated and documented; correct dispositions.
  • Per-issue CHANGELOG / plugin.json bump — batch-level, owned by the Report-phase release stage.

Ready to merge.

@aaddrick
aaddrick merged commit fed5261 into Batch_2026-07-27_225225 Jul 28, 2026
1 check passed
@aaddrick
aaddrick deleted the issue-165-fix-the-quality-friction-ratio-s-denominator branch July 28, 2026 13:26
@aaddrick

Copy link
Copy Markdown
Owner Author

Implementation Complete

Branch issue-165-fix-the-quality-friction-ratio-s-denominator squash-merged into Batch_2026-07-27_225225.

  • Spec review: passed
  • Code review: passed

No deferred follow-up suggestions were collected during implementation.

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