mandrel 2.54.0 → 2.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/.agents/agents/story-worker.md +24 -23
  2. package/.agents/audit-checklists/accessibility.md +0 -3
  3. package/.agents/audit-checklists/mobile.md +0 -4
  4. package/.agents/docs/agentrc-reference.json +8 -2
  5. package/.agents/docs/configuration.md +5 -0
  6. package/.agents/rules/ci-remediation.md +39 -21
  7. package/.agents/schemas/agentrc.schema.json +34 -1
  8. package/.agents/schemas/lifecycle/merge.unlanded.schema.json +2 -1
  9. package/.agents/schemas/story-deliver-terminal.schema.json +1 -0
  10. package/.agents/scripts/audit-to-stories.js +374 -76
  11. package/.agents/scripts/check-audit-attribution.js +119 -62
  12. package/.agents/scripts/check-test-portability.js +512 -0
  13. package/.agents/scripts/coverage-capture.js +17 -10
  14. package/.agents/scripts/evidence-gate.js +31 -4
  15. package/.agents/scripts/file-ci-gap.js +306 -0
  16. package/.agents/scripts/generate-workflows-doc.js +65 -14
  17. package/.agents/scripts/git-cleanup.js +4 -0
  18. package/.agents/scripts/lib/ITicketingProvider.js +78 -0
  19. package/.agents/scripts/lib/audit-advisories.js +195 -0
  20. package/.agents/scripts/lib/audit-attribution.js +22 -0
  21. package/.agents/scripts/lib/audit-to-stories/audit-label-taxonomy.js +25 -1
  22. package/.agents/scripts/lib/audit-to-stories/dedupe-against-github.js +80 -29
  23. package/.agents/scripts/lib/audit-to-stories/finding-adapter.js +5 -1
  24. package/.agents/scripts/lib/audit-to-stories/issue-corpus.js +162 -0
  25. package/.agents/scripts/lib/audit-to-stories/issue-index.js +83 -0
  26. package/.agents/scripts/lib/audit-to-stories/issues-file.js +121 -0
  27. package/.agents/scripts/lib/audit-to-stories/ledger-commit.js +61 -115
  28. package/.agents/scripts/lib/audit-to-stories/ledger-pr.js +347 -0
  29. package/.agents/scripts/lib/audit-to-stories/ledger-record.js +126 -0
  30. package/.agents/scripts/lib/audit-to-stories/parse-audit-md.js +169 -44
  31. package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +11 -0
  32. package/.agents/scripts/lib/baselines/merge-envelopes.js +298 -32
  33. package/.agents/scripts/lib/bootstrap/baseline-merge-driver.js +180 -14
  34. package/.agents/scripts/lib/cli-args.js +26 -0
  35. package/.agents/scripts/lib/close-validation/gates.js +113 -7
  36. package/.agents/scripts/lib/close-validation/process.js +7 -3
  37. package/.agents/scripts/lib/close-validation/runner.js +62 -11
  38. package/.agents/scripts/lib/config/ci.js +28 -9
  39. package/.agents/scripts/lib/config-settings-schema-delivery.js +7 -0
  40. package/.agents/scripts/lib/config-settings-schema.js +52 -1
  41. package/.agents/scripts/lib/coverage-capture-fullscope.js +23 -11
  42. package/.agents/scripts/lib/coverage-capture-incremental.js +22 -16
  43. package/.agents/scripts/lib/coverage-capture-usage.js +5 -1
  44. package/.agents/scripts/lib/coverage-capture.js +77 -3
  45. package/.agents/scripts/lib/feedback-loop/graduator-core.js +53 -13
  46. package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +71 -25
  47. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +18 -25
  48. package/.agents/scripts/lib/{audit-to-stories/ledger.js → findings/audit-ledger.js} +131 -24
  49. package/.agents/scripts/lib/findings/route-finding.js +42 -2
  50. package/.agents/scripts/lib/full-suite-lock.js +232 -6
  51. package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
  52. package/.agents/scripts/lib/git/sync-from-base.js +130 -13
  53. package/.agents/scripts/lib/github/framework-repo.js +148 -2
  54. package/.agents/scripts/lib/label-constants.js +6 -1
  55. package/.agents/scripts/lib/observability/source-classifier.js +2 -0
  56. package/.agents/scripts/lib/orchestration/check-baselines/phases/compare.js +10 -2
  57. package/.agents/scripts/lib/orchestration/check-baselines/phases/refresh-ack.js +75 -15
  58. package/.agents/scripts/lib/orchestration/ci-gap-intake.js +605 -0
  59. package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +13 -8
  60. package/.agents/scripts/lib/orchestration/deliver-recover.js +82 -43
  61. package/.agents/scripts/lib/orchestration/dependency-candidates.js +8 -4
  62. package/.agents/scripts/lib/orchestration/epic-candidates.js +9 -4
  63. package/.agents/scripts/lib/orchestration/epic-container.js +66 -4
  64. package/.agents/scripts/lib/orchestration/epic-rollup.js +233 -84
  65. package/.agents/scripts/lib/orchestration/file-assumptions.js +218 -16
  66. package/.agents/scripts/lib/orchestration/git-cleanup/phases/branches.js +93 -7
  67. package/.agents/scripts/lib/orchestration/git-cleanup/phases/git-probes.js +22 -6
  68. package/.agents/scripts/lib/orchestration/git-cleanup/phases/parse-args.js +26 -5
  69. package/.agents/scripts/lib/orchestration/git-cleanup/phases/phase-drivers.js +13 -2
  70. package/.agents/scripts/lib/orchestration/git-cleanup/phases/render.js +35 -5
  71. package/.agents/scripts/lib/orchestration/merge-block-class.js +18 -3
  72. package/.agents/scripts/lib/orchestration/merge-poll.js +284 -40
  73. package/.agents/scripts/lib/orchestration/plan-persist/audit-provenance.js +197 -0
  74. package/.agents/scripts/lib/orchestration/plan-persist/epic-adoption.js +49 -2
  75. package/.agents/scripts/lib/orchestration/plan-persist/epic-ops.js +43 -7
  76. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +39 -3
  77. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +5 -0
  78. package/.agents/scripts/lib/orchestration/plan-persist/summary.js +3 -0
  79. package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +63 -0
  80. package/.agents/scripts/lib/orchestration/plan-persist/wave-serialisation.js +110 -0
  81. package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +130 -40
  82. package/.agents/scripts/lib/orchestration/resolve-stories.js +44 -1
  83. package/.agents/scripts/lib/orchestration/review-providers/native.js +31 -11
  84. package/.agents/scripts/lib/orchestration/review-providers/scoped-lint.js +27 -24
  85. package/.agents/scripts/lib/orchestration/run-epilogue.js +63 -42
  86. package/.agents/scripts/lib/orchestration/single-story-close/close-note.js +81 -0
  87. package/.agents/scripts/lib/orchestration/single-story-close/failed-terminal.js +40 -51
  88. package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +10 -2
  89. package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +101 -0
  90. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +351 -28
  91. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +27 -6
  92. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +117 -22
  93. package/.agents/scripts/lib/orchestration/story-close/baseline-upward-writeback.js +94 -12
  94. package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +6 -1
  95. package/.agents/scripts/lib/orchestration/story-follow-ups.js +32 -20
  96. package/.agents/scripts/lib/orchestration/ticket-validator.js +25 -14
  97. package/.agents/scripts/lib/orchestration/ticketing/bulk.js +30 -0
  98. package/.agents/scripts/lib/orchestration/verify-credit.js +37 -0
  99. package/.agents/scripts/lib/pinned-override-notes.js +41 -53
  100. package/.agents/scripts/lib/pinned-override-resolve.js +212 -0
  101. package/.agents/scripts/lib/qa/resolve-qa-contract.js +18 -0
  102. package/.agents/scripts/lib/single-story-sweep/sweep-lock.js +173 -9
  103. package/.agents/scripts/lib/skills/walk-skill-files.js +24 -7
  104. package/.agents/scripts/lib/test-temp.js +167 -30
  105. package/.agents/scripts/lib/validation-evidence.js +37 -0
  106. package/.agents/scripts/lib/wave-runner/footprint.js +167 -14
  107. package/.agents/scripts/lib/wave-runner/live-probe.js +7 -1
  108. package/.agents/scripts/lib/wave-runner/ready-set.js +1 -1
  109. package/.agents/scripts/merge-baseline.js +175 -21
  110. package/.agents/scripts/pr-watch-with-update.js +3 -2
  111. package/.agents/scripts/providers/github/errors.js +22 -1
  112. package/.agents/scripts/providers/github/issues.js +106 -1
  113. package/.agents/scripts/providers/github/sub-issue-add.js +18 -1
  114. package/.agents/scripts/providers/github.js +6 -0
  115. package/.agents/scripts/resolve-stories.js +44 -34
  116. package/.agents/scripts/single-story-close.js +5 -0
  117. package/.agents/scripts/stories-wave-tick.js +37 -13
  118. package/.agents/templates/docs/audit-sweep-runbook.md +41 -7
  119. package/.agents/workflows/audit-accessibility.md +16 -31
  120. package/.agents/workflows/audit-mobile.md +20 -37
  121. package/.agents/workflows/audit-to-stories.md +63 -27
  122. package/.agents/workflows/git-cleanup.md +17 -3
  123. package/.agents/workflows/helpers/audit-lens-core.md +45 -0
  124. package/.agents/workflows/helpers/deliver-digest.md +7 -6
  125. package/.agents/workflows/helpers/deliver-reference.md +35 -14
  126. package/.agents/workflows/helpers/deliver-story-reference.md +26 -8
  127. package/.agents/workflows/helpers/deliver-story.md +15 -12
  128. package/.agents/workflows/helpers/plan-reference.md +30 -0
  129. package/.agents/workflows/mandrel-plan.md +10 -13
  130. package/.agents/workflows/memory-consolidate.md +14 -9
  131. package/docs/CHANGELOG.md +37 -0
  132. package/lib/cli/registry.js +64 -21
  133. package/lib/cli/sync.js +27 -2
  134. package/package.json +7 -4
@@ -44,6 +44,7 @@
44
44
  * [--no-auto-merge]
45
45
  * [--wait-merge | --no-wait-merge]
46
46
  * [--merge-watch-mode <sync|async>]
47
+ * [--rerun-advisory <n>]
47
48
  * [--override-review-block <reason>]
48
49
  *
49
50
  * `--override-review-block <reason>` is the one sanctioned way
@@ -225,6 +226,10 @@ runAsCli(import.meta.url, main, {
225
226
  ['--wait-merge', 'Force the in-close merge wait.'],
226
227
  ['--no-wait-merge', 'Return as soon as the PR is open; do not wait.'],
227
228
  ['--max-wait-seconds <n>', 'Per-invocation merge-wait bound.'],
229
+ [
230
+ '--rerun-advisory <n>',
231
+ 'How many times this close may re-run a failed ADVISORY (non-required) workflow run before blocking on it. Overrides `delivery.ci.rerunAdvisory` for one invocation; BOTH default to 0, so close spends no CI minutes and issues no GitHub mutation on an advisory red unless you ask. At n > 0 the failed run(s) are re-run within that allowance and the merge wait keeps polling inside its existing budget, landing or blocking on the re-run verdict.',
232
+ ],
228
233
  [
229
234
  '--merge-watch-mode <sync|async>',
230
235
  'Override delivery.mergeWatch.mode for this invocation only. `async` caps the merge wait to a short probe window and returns the resumable `pending` terminal instead of holding the foreground slot — pass it on every close of a multi-Story run. An invalid value exits non-zero before any phase runs.',
@@ -55,8 +55,8 @@
55
55
  * inFlight: number,
56
56
  * cycleError: string | null,
57
57
  * wedged: { reason, stories: [{ id, unmetBlockers }] } | null,
58
- * inFlightReservation: { available, withheld: [{ id, blockedBy, reason, source, paths }], note },
59
- * footprintGuard: { mode, withheld: [{ id, blockedBy, scope, source, paths }], advisory, note }
58
+ * inFlightReservation: { available, withheld: [{ id, blockedBy, reason, source, paths, attribution }], note },
59
+ * footprintGuard: { mode, withheld: [{ id, blockedBy, scope, source, paths, attribution }], advisory, note }
60
60
  * }
61
61
  *
62
62
  * `inFlightReservation` reports the cross-beat half of the co-dispatch guard
@@ -130,7 +130,10 @@ import { AGENT_LABELS } from './lib/label-constants.js';
130
130
  import { parseIds } from './lib/orchestration/resolve-stories.js';
131
131
  import { buildStoryAdjacency } from './lib/story-adjacency.js';
132
132
  import { expandIdList } from './lib/util/parse-id-list.js';
133
- import { OVERLAP_SOURCES } from './lib/wave-runner/footprint.js';
133
+ import {
134
+ OVERLAP_SOURCES,
135
+ renderScrapeAttribution,
136
+ } from './lib/wave-runner/footprint.js';
134
137
  import {
135
138
  createProbeContext,
136
139
  probeLiveState,
@@ -237,7 +240,10 @@ Output envelope:
237
240
  "blockedBy": 4949,
238
241
  "reason": "in-flight-earlier-beat",
239
242
  "source": "declared-overlap",
240
- "paths": ["lib/shared.js"]
243
+ "paths": ["lib/shared.js"],
244
+ "attribution": [
245
+ { "path": "lib/shared.js", "declared": true, "fields": [] }
246
+ ]
241
247
  }
242
248
  ],
243
249
  "note": "..."
@@ -250,7 +256,14 @@ Output envelope:
250
256
  "blockedBy": 4951,
251
257
  "scope": "beat",
252
258
  "source": "scraped-overlap",
253
- "paths": ["lib/other.js"]
259
+ "paths": ["lib/other.js"],
260
+ "attribution": [
261
+ {
262
+ "path": "lib/other.js",
263
+ "declared": false,
264
+ "fields": ["body:Verify"]
265
+ }
266
+ ]
254
267
  }
255
268
  ],
256
269
  "advisory": [],
@@ -268,7 +281,10 @@ footprintGuard names each Story withheld from THIS beat by a peer already
268
281
  admitted on it — the half that used to be an unreported skip — and every
269
282
  entry in either report carries the colliding paths plus a source tag
270
283
  (declared-overlap when both changes[] declarations named the path, else
271
- scraped-overlap from the text evidence). Its "mode" echoes
284
+ scraped-overlap from the text evidence) and an "attribution" list naming, per
285
+ path, the field the scrape read it from ("title", "spec", or "body:<section>"
286
+ — so a path that reached the comparison only because every Story RUNS it in
287
+ "## Verify" says so). Its "mode" echoes
272
288
  delivery.deliverRunner.footprintGuard: under "advisory" the collisions are
273
289
  detected and listed in "advisory" but never withhold, and dispatch follows the
274
290
  declared depends_on edges alone.
@@ -359,7 +375,7 @@ const RESERVATION_REASONS = Object.freeze({
359
375
  * @param {object[]|null|undefined} inFlightRecords
360
376
  * @param {Array<{id: number, blockedBy: number, source?: string, paths?: string[]}>} withheld
361
377
  * @param {Iterable<number>} [foreignHeldIds] Ids held by a foreign lease.
362
- * @returns {{ available: boolean, withheld: Array<{id: number, blockedBy: number, reason: string, source: string, paths: string[]}>, note: string|null }}
378
+ * @returns {{ available: boolean, withheld: Array<{id: number, blockedBy: number, reason: string, source: string, paths: string[], attribution: object[]}>, note: string|null }}
363
379
  */
364
380
  export function buildReservationReport(
365
381
  inFlightRecords,
@@ -390,6 +406,7 @@ export function buildReservationReport(
390
406
  : RESERVATION_REASONS.EARLIER_BEAT,
391
407
  source: w.source ?? OVERLAP_SOURCES.DECLARED,
392
408
  paths: w.paths ?? [],
409
+ attribution: w.attribution ?? [],
393
410
  }));
394
411
  return {
395
412
  available: true,
@@ -421,12 +438,16 @@ export function buildReservationReport(
421
438
  */
422
439
  export function buildFootprintGuardReport(footprintWithholds, mode) {
423
440
  const ledger = Array.isArray(footprintWithholds) ? footprintWithholds : [];
424
- const project = ({ id, blockedBy, scope, source, paths }) => ({
441
+ const project = ({ id, blockedBy, scope, source, paths, attribution }) => ({
425
442
  id,
426
443
  blockedBy,
427
444
  scope,
428
445
  source,
429
446
  paths,
447
+ // Story #5265: the per-path field attribution rides the entry itself, so
448
+ // a consumer reading the envelope never has to re-derive where a scraped
449
+ // path came from (and cannot get a different answer than the note did).
450
+ attribution: attribution ?? [],
430
451
  });
431
452
  const beat = ledger
432
453
  .filter((w) => w.scope === WITHHOLD_SCOPES.BEAT && w.enforced)
@@ -453,10 +474,11 @@ export function buildFootprintGuardReport(footprintWithholds, mode) {
453
474
  function footprintGuardNote(beat, advisory, mode) {
454
475
  const detail = (entries) =>
455
476
  entries
456
- .map(
457
- (w) =>
458
- `#${w.id} ← #${w.blockedBy} on ${w.paths.join(', ')} (${w.source})`,
459
- )
477
+ .map((w) => {
478
+ const scraped = renderScrapeAttribution(w.attribution);
479
+ const provenance = scraped ? `, scraped from ${scraped}` : '';
480
+ return `#${w.id} ← #${w.blockedBy} on ${w.paths.join(', ')} (${w.source}${provenance})`;
481
+ })
460
482
  .join('; ');
461
483
  if (beat.length > 0) {
462
484
  return (
@@ -465,7 +487,9 @@ function footprintGuardNote(beat, advisory, mode) {
465
487
  `Each is still eligible and re-admits on a later beat once its peer ` +
466
488
  `lands. A ${OVERLAP_SOURCES.SCRAPED} source means the collision came ` +
467
489
  `from path evidence in the Story text rather than from either ` +
468
- `changes[] declaration.`
490
+ `changes[] declaration — the 'scraped from' clause names the field ` +
491
+ `each such path was read out of, so a path only cited in '## Verify' ` +
492
+ `is distinguishable from an unpredicted edit target.`
469
493
  );
470
494
  }
471
495
  if (advisory.length > 0) {
@@ -51,6 +51,17 @@ to that declared tally. A mismatch — or a missing tally line — means the rep
51
51
  is not trustworthy: a finding was malformed, a severity did not resolve onto the
52
52
  canonical scale, or the lens truncated its own output.
53
53
 
54
+ The line is read **from the Executive Summary**, and a report declaring it more
55
+ than once fails as `duplicate-tally` naming both lines rather than adopting
56
+ whichever the scan reached first. Prose elsewhere in the report that quotes the
57
+ tally format is a second declaration as far as the check is concerned; move it
58
+ or reword it.
59
+
60
+ A `###` heading with no severity axis and no field bullets is read as a
61
+ **grouping header**, not as a finding — so a lens that emits `### Robust` with
62
+ `_No findings._` under it contributes zero findings and still cross-checks
63
+ against a zero tally.
64
+
54
65
  `--auto` **fails closed** on any such failure. It exits non-zero having opened
55
66
  no Issue and written no ledger, and names the offending report in
56
67
  `summary.reportFailures[]`. `--allow-missing-tally` is a `--scan` affordance
@@ -70,6 +81,10 @@ stop surprising you:
70
81
  node .agents/scripts/audit-to-stories.js --auto --dry-run
71
82
  ```
72
83
 
84
+ `--severity` is validated against the canonical scale: a typo (`--severity Hgh`)
85
+ exits non-zero naming the accepted levels rather than silently widening the run
86
+ to every finding.
87
+
73
88
  `--dry-run` performs zero GitHub writes and skips the ledger write, printing
74
89
  only the run summary. Read `totals.create` before you let the sweep file
75
90
  anything: a first full-scope run over an un-audited repository can propose more
@@ -94,11 +109,24 @@ rejected.
94
109
  `--ledger-commit` closes that loop. After the run summary has printed, and only
95
110
  when the ledger actually changed, it:
96
111
 
97
- 1. creates `chore/audit-ledger-<YYYY-MM-DD>` from the current HEAD,
98
- 2. commits **only** the ledger file, subject
112
+ 1. refuses, naming its step, if HEAD is not the base branch or there is no
113
+ `origin` — before writing anything,
114
+ 2. creates `chore/audit-ledger-<YYYY-MM-DD>-<shortsha>` from `origin/<base>`,
115
+ 3. commits **only** the ledger file, subject
99
116
  `chore(audit): reconcile audit ledger <date>`,
100
- 3. pushes the branch, and
101
- 4. opens a PR against your base branch.
117
+ 4. pushes the branch,
118
+ 5. opens a PR against your base branch, and
119
+ 6. puts the checkout back on the branch it started from.
120
+
121
+ It prints one line on stderr saying what happened: the branch and the PR URL on
122
+ success, or the skip reason otherwise.
123
+
124
+ **It is re-runnable.** The `<shortsha>` is the base commit, so a retry on the
125
+ same day does not collide with the branch a failed run left behind — it
126
+ recognises it. A ledger already committed on an **unpushed** ledger branch
127
+ resumes at the push rather than reporting `ledger-unchanged` (the ledger file is
128
+ clean: it is committed, just not pushed). Across a failed push and its retry you
129
+ get exactly one PR.
102
130
 
103
131
  **Auto-merge is never requested.** The ledger records machine-derived lifecycle
104
132
  state, so a human glance before it lands is the point — nominate that reviewer
@@ -123,8 +151,10 @@ Those bodies are **audit prose**, not delivery-ready Specs: they describe a
123
151
  symptom and a recommendation, not a scoped change with acceptance criteria a
124
152
  worker can verify against.
125
153
 
126
- Do not point `/mandrel-deliver` at a freshly-filed audit Story. Route it through
127
- `/mandrel-plan` first — the planning pass is where the finding becomes a
154
+ Do not point `/mandrel-deliver` at a freshly-filed audit Story — and as of the
155
+ label guard below, you cannot: `resolve-stories.js` refuses a Story carrying no
156
+ `agent::*` label, naming this step, unless `--allow-unlabelled` is passed. Route
157
+ it through `/mandrel-plan` first — the planning pass is where the finding becomes a
128
158
  capability slice with a `## Spec`, real `acceptance[]` items and runnable
129
159
  `verify[]` lines. Planning is deliberately not automated here: deciding what a
130
160
  finding is worth, and how far the fix should reach, is the judgement the sweep
@@ -172,6 +202,10 @@ to mint labels no taxonomy defines.
172
202
  | --- | --- | --- |
173
203
  | Non-zero exit, `summary.reportFailures[]` populated | A lens report's tally is missing or disagrees with the parse | Re-run that lens; never downgrade with `--allow-missing-tally` |
174
204
  | `ledger.unpersisted: true` in the summary | No `origin`, or HEAD off the base branch | Re-run with `--ledger-commit`, or commit the ledger by hand |
175
- | `--ledger-commit failed at step "..."` | git or `gh` failed at the named step | Fix the remote/auth and re-run; the summary above it is still valid |
205
+ | `--ledger-commit failed at step "..."` | git or `gh` failed at the named step | Fix the cause and re-run; a retry resumes rather than duplicating, and the summary above it is still valid |
206
+ | `--ledger-commit failed at step "verify-base-branch"` | HEAD is on a feature branch | Check out the base branch and re-run; nothing was committed |
207
+ | `[resolve-stories] ... carries no "agent::*" label` | An unenriched audit Story was named for delivery | Plan it (Step 5), or pass `--allow-unlabelled` deliberately |
208
+ | `[audit-to-stories] --severity "..." is not a severity` | A typo in the floor | Use one of the canonical levels |
209
+ | `duplicate-tally` in `summary.reportFailures[]` | The report declares the tally twice | Reword the prose copy; the Executive Summary line is the declaration |
176
210
  | Same findings re-proposed every cycle | The ledger is not being committed | Adopt Step 4 |
177
211
  | `totals.create` far larger than the team can absorb | Severity floor too low for a first full-scope run | Raise `--severity` and re-dry-run |
@@ -82,9 +82,9 @@ the project is configured.** Before any detection:
82
82
  - **Design tokens:** the colour tokens (`tailwind.config.*`, CSS custom
83
83
  properties, a theme object) whose literal values you need to compute contrast
84
84
  ratios statically.
85
- - **Runtime target (optional):** the `qa.environments` map (see
86
- [_Runtime verification mode_](#step-2-runtime-verification-mode-optional-corroboration))
87
- and the navigability route SSOT.
85
+ - **Runtime target (optional):** whatever the shared runtime scaffold in
86
+ [`helpers/audit-lens-core.md`](helpers/audit-lens-core.md#runtime-pass)
87
+ resolves. Note only whether one exists; the scaffold owns how it is found.
88
88
 
89
89
  Record what exists. Every finding downstream is measured against _this
90
90
  discovered surface and config_, not a generic ideal. If **no** frontend surface
@@ -135,31 +135,16 @@ Cover every static WCAG dimension:
135
135
 
136
136
  ## Step 2: Runtime verification mode (optional corroboration)
137
137
 
138
- Static detection is the default and always runs. The runtime pass is
139
- **conditional** — it runs only when a live target is configured; its absence
140
- never blocks the static report.
141
-
142
- 1. **Resolve the target from config — never a hardcoded URL.** Resolve the
143
- target through the consumer's `qa.environments.<env>.baseUrl` (via
144
- [`resolveQaEnvironment`](../scripts/lib/qa/resolve-qa-contract.js), the same
145
- resolver `/qa-run` uses): an `<env>` argument resolves by exact name or
146
- origin match; with no argument, enumerate `name → baseUrl` and let the
147
- operator pick. If **no** `qa.environments` target is configured, **skip this
148
- step** and note in the report that runtime corroboration was unavailable —
149
- do not invent a URL and do not start an arbitrary dev server.
150
- 2. **Sample routes from the navigability SSOT.** Draw the routes to exercise
151
- from the consumer's route/nav registry (`planning.navigation.navRegistry` /
152
- `routeGlobs` — the same SSOT [`/audit-navigability`](audit-navigability.md)
153
- reads), sampling a representative set (key personas' landing routes plus any
154
- route in the change-set scope) rather than a single hardcoded page.
155
- 3. **Run an accessibility engine per sampled route.** Use the
156
- `mcp__chrome-devtools__lighthouse_audit` tool's **Accessibility category**,
157
- or run **axe** via the browser tooling, against each sampled `baseUrl`-rooted
158
- route. Prefer a production-mode build.
159
- 4. **Median-of-3 or provisional.** Any runtime score or metric is subject to
160
- run-to-run variance: capture a **median-of-3** (three runs per route, report
161
- the median) before treating a number as authoritative. A single-run value is
162
- reported **provisional** and never drives a Critical/High verdict on its own.
138
+ Run the shared runtime scaffold in
139
+ [`helpers/audit-lens-core.md`](helpers/audit-lens-core.md#runtime-pass) — it
140
+ owns target resolution, route sampling, median-of-3, and the two skip reasons
141
+ (no configured target; browser tooling unavailable). This lens's probe is its
142
+ step 3:
143
+
144
+ - **Run an accessibility engine per sampled route.** Use the
145
+ `mcp__chrome-devtools__lighthouse_audit` tool's **Accessibility category**, or
146
+ run **axe** via the browser tooling, against each sampled `baseUrl`-rooted
147
+ route. Prefer a production-mode build.
163
148
 
164
149
  Corroborate static findings against the runtime results (a statically-flagged
165
150
  contrast defect confirmed by the engine graduates from provisional to
@@ -168,8 +153,8 @@ confirmed), and surface runtime-only violations the static pass could not see.
168
153
  ## Report additions
169
154
 
170
155
  Beyond the shared skeleton, the Executive Summary states the runtime mode's
171
- status (ran against `<env>` / skipped — no target configured), and the report
156
+ status (ran against `<env>`, or the scaffold's skip reason), and the report
172
157
  ends with a **Runtime Verification** section: per-route median-of-3
173
- accessibility scores when the runtime mode ran, or "_Runtime corroboration
174
- unavailable — no `qa.environments` target configured._" Drop every claimed
158
+ accessibility scores when the runtime mode ran, or that skip reason verbatim.
159
+ Drop every claimed
175
160
  violation that names no concrete element and no specific WCAG success criterion.
@@ -88,9 +88,9 @@ what they declare:
88
88
  explicit `viewport`, a Cypress `viewportWidth`/`viewportHeight`, a
89
89
  visual-regression viewport list. This is the project's own statement of which
90
90
  form factors it holds itself to.
91
- - **Runtime target (optional):** the `qa.environments` map (see
92
- [*Runtime viewport pass*](#step-3-runtime-viewport-pass-optional-corroboration))
93
- and the navigability route SSOT.
91
+ - **Runtime target (optional):** whatever the shared runtime scaffold in
92
+ [`helpers/audit-lens-core.md`](helpers/audit-lens-core.md#runtime-pass)
93
+ resolves. Note only whether one exists; the scaffold owns how it is found.
94
94
 
95
95
  Record the breakpoints, the viewport contract, and the device matrix. Every
96
96
  finding downstream is measured against *this discovered baseline*. If the
@@ -192,37 +192,20 @@ fix is a new test, name the viewport and the assertion it should make, not just
192
192
 
193
193
  ## Step 3: Runtime viewport pass (optional corroboration)
194
194
 
195
- Static detection is the default and always runs. The runtime pass is
196
- **conditional** — it runs only when a live target is configured; its absence
197
- never blocks the static report.
198
-
199
- 1. **Resolve the target from config — never a hardcoded URL.** Resolve the
200
- target through the consumer's `qa.environments.<env>.baseUrl` (via
201
- [`resolveQaEnvironment`](../scripts/lib/qa/resolve-qa-contract.js), the same
202
- resolver `/qa-run` uses): an `<env>` argument resolves by exact name or
203
- origin match; with no argument, enumerate `name → baseUrl` and let the
204
- operator pick. If **no** `qa.environments` target is configured, **skip this
205
- step** and note in the report that runtime corroboration was unavailable —
206
- do not invent a URL and do not start an arbitrary dev server.
207
- 2. **Sample routes from the navigability SSOT.** Draw the routes to exercise
208
- from the consumer's route/nav registry (`planning.navigation.navRegistry` /
209
- `routeGlobs` — the same SSOT [`/audit-navigability`](audit-navigability.md)
210
- reads), sampling a representative set (key personas' landing routes plus any
211
- route in the change-set scope) rather than a single hardcoded page.
212
- 3. **Drive two form factors per route.** Emulate a **phone** and a **tablet**
213
- viewport — `mcp__chrome-devtools__emulate` for a device profile, or
214
- `resize_page` for an explicit width/height — then, per route and viewport:
215
- take a screenshot, and evaluate the two observables static analysis cannot
216
- resolve — whether `document.scrollingElement.scrollWidth` exceeds the
217
- viewport width (horizontal overflow), and the rendered box of the
218
- interactive controls Step 1 flagged as candidates. Reload after switching
219
- form factor so load-time device gates re-run.
220
- 4. **Median-of-3 or provisional.** Any runtime measurement is subject to
221
- run-to-run variance: capture a **median-of-3** (three runs per route, report
222
- the median) before treating a number as authoritative. A single-run value is
223
- reported **provisional** and never drives a Critical/High verdict on its own.
224
- 5. **Leave the viewport as you found it.** Reset the emulation before finishing
225
- so a following lens or QA run does not inherit a phone viewport.
195
+ Run the shared runtime scaffold in
196
+ [`helpers/audit-lens-core.md`](helpers/audit-lens-core.md#runtime-pass) — it
197
+ owns target resolution, route sampling, median-of-3, and the two skip reasons
198
+ (no configured target; browser tooling unavailable). This lens's probe is its
199
+ step 3:
200
+
201
+ - **Drive two form factors per route.** Emulate a **phone** and a **tablet**
202
+ viewport — `mcp__chrome-devtools__emulate` for a device profile, or
203
+ `resize_page` for an explicit width/height — then, per route and viewport:
204
+ take a screenshot, and evaluate the two observables static analysis cannot
205
+ resolve — whether `document.scrollingElement.scrollWidth` exceeds the viewport
206
+ width (horizontal overflow), and the rendered box of the interactive controls
207
+ Step 1 flagged as candidates. Reload after switching form factor so load-time
208
+ device gates re-run.
226
209
 
227
210
  Corroborate static findings against the runtime observations (a statically
228
211
  flagged fixed width confirmed by a real horizontal overflow graduates from
@@ -233,10 +216,10 @@ that opens off-screen.
233
216
  ## Report additions
234
217
 
235
218
  Beyond the shared skeleton, the Executive Summary states the runtime mode's
236
- status (ran against `<env>` / skipped — no target configured) and names the
219
+ status (ran against `<env>`, or the scaffold's skip reason) and names the
237
220
  narrowest breakpoint the project declares, so a reader can tell what "mobile"
238
221
  meant for this run. The report ends with a **Runtime Viewport Pass** section:
239
- per-route, per-form-factor observations when the runtime mode ran, or
240
- "*Runtime corroboration unavailable — no `qa.environments` target configured.*"
222
+ per-route, per-form-factor observations when the runtime mode ran, or the
223
+ scaffold's skip reason verbatim.
241
224
  Drop every claimed finding that names no concrete element, style rule, or test
242
225
  file.
@@ -181,6 +181,14 @@ persists, via `carryProvenanceFooters`
181
181
  The carry is additive, union-preserving and idempotent, so a resumed persist
182
182
  cannot stack footers and a hand-authored fingerprint is never dropped.
183
183
 
184
+ **Persist records the ledger and the labels too.** It stamps the seed's
185
+ `audit::<dimension>` labels — the dedup corpus is listed by them, and an indexed
186
+ run answers lookups from that pool without reaching the provider, so a footer
187
+ alone leaves a plan-path Story invisible — and records each Story's
188
+ **attributed** identities. Union-only identities are not recorded: every sibling
189
+ carries every fingerprint, so an owner would be a coin flip; persist says so on
190
+ stderr. Author per-Story `provenance` to record them.
191
+
184
192
  This is deliberately not an authoring step. It used to be: the footers reached
185
193
  the seed and stopped there, leaving the authoring agent to notice HTML comments
186
194
  in a one-pager and copy them forward — a remembered step, which is to say no
@@ -243,6 +251,14 @@ every Story that has a resolvable blocker with a canonical
243
251
  GitHub `blocked_by` relations. An edge whose target was never opened (deduped,
244
252
  ledger-suppressed) drops rather than becoming a `blocked by #undefined`.
245
253
 
254
+ **This pass also records the ledger.** The `--ids` map is the only artifact
255
+ carrying the numbers just opened, so `--wire-edges` folds in the cross-run
256
+ record: each mapped group's findings are written `filed` against its Issue
257
+ (`--ledger <path>`, default `baselines/audit-ledger.json`; `--dry-run`
258
+ suppresses the write). The record runs **before** the provider loads, so a host
259
+ with no `gh` still remembers what it filed. Without it nothing ever writes
260
+ `filed` and the ledger suppresses nothing.
261
+
246
262
  **Do not skip this.** `/mandrel-deliver` has no other source for this cohort's order:
247
263
  its footprint guard ignores the shared provenance footers, so an unwired cohort
248
264
  is genuinely unordered and `/mandrel-deliver` will co-dispatch Stories the edges say
@@ -277,31 +293,45 @@ the `classifications` array:
277
293
  is skipped by default; flag in the Phase 7 summary so the operator
278
294
  can decide whether to reopen.
279
295
 
280
- `routeFinding` is handed a `searchIssues` port adapted from the
281
- project's existing GitHub provider — the actual search runs against the
282
- repo's open + closed issues for each sha in the group, and the helper's
283
- footer-confirmation step filters out false-positive search hits whose
284
- body mentions the sha in prose without the canonical marker. The
285
- workflow owns **no** parallel dedup or footer-parsing code: the
286
- fingerprint, footer round-trip, and routing all live in that one shared
287
- module.
288
-
289
- Dedup runs in **two stages** when a provider resolves: a
290
- meaning-first **semantic candidate** pass (`searchCandidates`, wired to
291
- [`lib/findings/semantic-issue-search.js`](../scripts/lib/findings/semantic-issue-search.js))
292
- runs FIRST and widens the net across open + closed issues; the exact
293
- **fingerprint / semantic-key** confirmation runs SECOND. A finding whose title
294
- was reworded but whose *location* is unchanged still confirms against the Issue
295
- that already tracks that location, because the audit filers stamp a
296
- location-based `audit-semantic-keys` footer alongside the `audit-fingerprints`
297
- footer. Filings from the
296
+ `routeFinding` reads open + closed issues through two ports, which **both run
297
+ and union their pools** — the exact `searchIssues` lookup, and the meaning-first
298
+ `searchCandidates` pass wired to
299
+ [`lib/findings/semantic-issue-search.js`](../scripts/lib/findings/semantic-issue-search.js).
300
+ Confirmation then filters that union by footer, dropping hits whose body
301
+ mentions a sha in prose without the canonical marker. A finding reworded but
302
+ unmoved still confirms, because the filers stamp a location-based
303
+ `audit-semantic-keys` footer
304
+ beside `audit-fingerprints`; so do
298
305
  [`retro-proposals-graduator`](../scripts/lib/feedback-loop/retro-proposals-graduator.js)
299
- carry the same canonical `audit-fingerprints` footer, so a sweep recognizes a
300
- graduator-filed issue and never re-files it.
306
+ filings, which a sweep therefore never re-files.
307
+
308
+ Where the corpus is pre-fetched — either off the list endpoint or from
309
+ `--issues-file` — the exact lookup is answered from that local index and the
310
+ search API is spent only on findings with no exact hit. The workflow owns **no**
311
+ parallel dedup or footer-parsing code: fingerprint, footer round-trip, and
312
+ routing all live in that one shared module.
313
+
314
+ ### When there is no `gh` CLI
315
+
316
+ **No GitHub access** (air-gapped): pass `--no-provider` to `--scan`. Every
317
+ group is classified `create` and the operator is told dedupe was skipped — a
318
+ re-run opens duplicates.
319
+
320
+ **Reachable, but not through `gh`** (a cloud sandbox: no `gh`, no direct API,
321
+ MCP fine): fetch the corpus yourself. List every issue labelled `audit::*` at
322
+ state `all` — `mcp__github__list_issues` or any other path — write the raw
323
+ result as a JSON array, and pass it:
324
+
325
+ ```bash
326
+ node .agents/scripts/audit-to-stories.js --scan --no-provider \
327
+ --issues-file temp/audits/issues.json --glob "temp/audits/audit-*-results.md"
328
+ ```
301
329
 
302
- When no provider is available (e.g. air-gapped dev environment), pass
303
- `--no-provider` to the `--scan` step — every group is classified
304
- `create` and the operator is informed that dedupe was skipped.
330
+ Real `skip-open` / `skip-reoccurring` classifications come back. The corpus is
331
+ normalised on load, so a raw list result works as-is; only `number` and `body`
332
+ are read. An unreadable file is a hard error, never a silent fall-back to an
333
+ unchecked run; an **empty** array — a valid first sweep — is reported with its
334
+ count so it cannot pass for a failed fetch.
305
335
 
306
336
  ### Cross-run ledger
307
337
 
@@ -314,8 +344,12 @@ shape). Each entry is keyed by the finding's fingerprint plus a location-based
314
344
  (`new | filed | fixed | accepted-risk | regressed`). A finding whose tracking
315
345
  Issue was closed as `not_planned` becomes `accepted-risk` and is **suppressed**
316
346
  on every later scan; a `fixed` finding that re-appears becomes `regressed`. The
317
- ledger is written by the unattended `--auto` sweep and by any `--scan --ledger`
318
- run; the plain `--scan` path leaves it untouched.
347
+ ledger is written by the unattended `--auto` sweep, by any `--scan --ledger`
348
+ run, by the Phase 5c `--wire-edges` pass, and by `plan-persist` on the Phase 5a
349
+ chained path — the two filing paths both record what they filed; the
350
+ plain `--scan` path leaves it untouched. A finding whose resolved Issue is open
351
+ is recorded `filed` and is known on re-detection; a closed Issue still outranks
352
+ that.
319
353
 
320
354
  ## Phase 7 — Summary & cleanup
321
355
 
@@ -391,8 +425,10 @@ The routine shape is **lenses full-scope → dry-run → live with a ledger PR**
391
425
  from `delivery.auditToStories.severityFloor` (default `high`, overridable with
392
426
  `--severity`), applies the two-stage dedup, reconciles the cross-run ledger,
393
427
  and prints a run-summary JSON (create / skip-open / skip-reoccurring /
394
- suppressed-by-ledger tallies, plus the re-detected open Issue numbers an
395
- operator may want a "re-detected" comment on). `--dry-run` performs zero GitHub
428
+ suppressed-by-ledger tallies, the `create`-classified group keys the `--ids`
429
+ map is built from, plus the re-detected open Issue numbers an operator may want
430
+ a "re-detected" comment on). It opens no Issues itself, so run Phase 5c after
431
+ filing or the sweep's memory stays empty. `--dry-run` performs zero GitHub
396
432
  writes and skips the ledger write, emitting only the summary.
397
433
 
398
434
  `--auto` **fails closed on any `summary.reportFailures[]` entry** (Phase 1): an
@@ -5,7 +5,7 @@ description: >-
5
5
  `git stash` entries — each step gated by operator confirmation.
6
6
  ---
7
7
 
8
- # /git-cleanup [--fast-forward-main] [--prune-remotes] [--branches] [--stashes] [--execute] [--remote] [--yes] [--drop-stashes <ref>] [--exclude <pattern>] [--json]
8
+ # /git-cleanup [--fast-forward-main] [--prune-remotes] [--branches] [--stashes] [--execute] [--remote] [--yes] [--include-content-merged] [--drop-stashes <ref>] [--exclude <pattern>] [--json]
9
9
 
10
10
  `/git-cleanup` folds the four cleanup steps operators routinely run by hand
11
11
  after a busy session into a single pipeline with per-step confirmation. It is a
@@ -36,7 +36,7 @@ flags: `node .agents/scripts/git-cleanup.js --help`.
36
36
  | --- | --- | --- |
37
37
  | **fast-forward-main** | `git fetch origin <base>` then `git merge --ff-only origin/<base>`. | Skipped silently on a dirty tree or a non-fast-forward; otherwise prompts `Fast-forward main by N commit(s)?`. Checks out `<base>` first when HEAD is elsewhere and does **not** restore the prior branch. |
38
38
  | **prune-remotes** | `git fetch --prune origin` to drop `refs/remotes/origin/*` GitHub already deleted. | Prompts before pruning. Runs as its own phase regardless of `--remote`. |
39
- | **branches** | Reaps merged local branches (squash-aware: merged-PR, git-ancestry, and content-equivalence signals), removing an attached worktree first. Also enumerates **remote-only** merged branches. | Prints the candidate list, then prompts `Reap N merged branch(es)?`. `--remote` is required **on top of** `--execute` to delete any `origin/<branch>`. `content-merged` candidates carry a weaker-signal warning. |
39
+ | **branches** | Reaps merged local branches (squash-aware: merged-PR, git-ancestry, and content-equivalence signals), removing an attached worktree first. Also enumerates **remote-only** merged branches. | Prints the candidate list, then prompts `Reap N merged branch(es)?`. `--remote` is required **on top of** `--execute` to delete any `origin/<branch>`. `content-merged` candidates carry a weaker-signal warning, and under `--yes` their **remote** ref is withheld unless `--include-content-merged` is passed (their local ref still goes — it is recoverable from the remote). |
40
40
  | **stashes** | Lists every stash and triages it. | Interactive: `drop / keep / quit` per entry (default `keep`). Under `--yes` / `--json`, drops require an explicit `--drop-stashes <ref>` allowlist (repeatable) — there is no "drop all". |
41
41
 
42
42
  ## Constraint
@@ -49,6 +49,14 @@ Two consequences the flag list alone does not carry: `--remote` deletions cannot
49
49
  be undone without re-pushing, and `--exclude '<pattern>'` is the **only** way to
50
50
  protect an in-scope merged-PR branch you want to keep.
51
51
 
52
+ That irreversibility is why `--yes` and `content-merged` do not combine on their
53
+ own. Content-equivalence says "applying this branch to the base changes nothing"
54
+ — which is true of a squash-merged branch and equally true of one whose every
55
+ change was reverted. An operator answering the prompt sees the weaker-signal
56
+ note and decides; an unattended run has nobody to decide, so it withholds the
57
+ remote delete and says so, and `--include-content-merged` is the decision made
58
+ in advance.
59
+
52
60
  Do **not** run with `--execute` if there is unmerged work that needs saving. The
53
61
  fast-forward phase skips on a dirty tree (safe), but the branches phase reaps any
54
62
  merged-PR branch in scope unless `--exclude`d.
@@ -59,9 +67,15 @@ merged-PR branch in scope unless `--exclude`d.
59
67
  # Preview all four phases (no mutation).
60
68
  node .agents/scripts/git-cleanup.js
61
69
 
62
- # Run everything non-interactively, including origin refs.
70
+ # Run everything non-interactively, including origin refs. Branches detected
71
+ # only by content-equivalence keep their origin ref — see the note below.
63
72
  node .agents/scripts/git-cleanup.js --execute --remote --yes
64
73
 
74
+ # Same, but also delete the origin refs of content-merged branches. Nobody is
75
+ # watching, so opting in is the whole confirmation this delete ever gets.
76
+ node .agents/scripts/git-cleanup.js --execute --remote --yes \
77
+ --include-content-merged
78
+
65
79
  # Only fast-forward main.
66
80
  node .agents/scripts/git-cleanup.js --fast-forward-main --execute
67
81
 
@@ -208,6 +208,51 @@ extracted and names any disagreement as a **report failure**
208
208
  report whose line is missing or wrong. A parse that silently drops findings is
209
209
  otherwise indistinguishable from a clean audit.
210
210
 
211
+ ## Runtime pass scaffold {#runtime-pass}
212
+
213
+ Some lenses corroborate their static findings against a live target. Static
214
+ detection is the default and always runs; the runtime pass is **conditional** —
215
+ it runs only when a live target and the browser tooling are both available, and
216
+ its absence never blocks the static report. The steps below are shared by every
217
+ lens that has one, so a lens documents only its own probe.
218
+
219
+ 1. **Resolve the target from config — never a hardcoded URL.** Resolve through
220
+ the consumer's `qa.environments.<env>.baseUrl` (via
221
+ [`resolveQaEnvironment`](../../scripts/lib/qa/resolve-qa-contract.js), the
222
+ same resolver `/qa-run` uses): an `<env>` argument resolves by exact name or
223
+ origin match; with no argument, enumerate `name → baseUrl` and let the
224
+ operator pick.
225
+ 2. **Sample routes from the navigability SSOT.** Draw the routes to exercise
226
+ from the consumer's route/nav registry (`planning.navigation.navRegistry` /
227
+ `routeGlobs` — the same SSOT
228
+ [`/audit-navigability`](../audit-navigability.md) reads), sampling a
229
+ representative set (key personas' landing routes plus any route in the
230
+ change-set scope) rather than a single hardcoded page.
231
+ 3. **Run the lens's own probe** against each sampled route. That step, and only
232
+ that step, is the lens's to document.
233
+ 4. **Median-of-3 or provisional.** Any runtime measurement is subject to
234
+ run-to-run variance: capture a **median-of-3** (three runs per route, report
235
+ the median) before treating a number as authoritative. A single-run value is
236
+ reported **provisional** and never drives a Critical/High verdict on its own.
237
+ 5. **Leave the environment as you found it.** Reset any emulation — viewport,
238
+ device profile, colour scheme — before finishing, so a following lens or QA
239
+ run does not inherit it.
240
+
241
+ **Skip reasons, and how to report them.** The pass is skipped, never faked, for
242
+ either of two reasons, and the Executive Summary says which:
243
+
244
+ | Condition | Reported as |
245
+ | --- | --- |
246
+ | No `qa.environments` target is configured | `skipped — no target configured` |
247
+ | The browser tooling is unavailable in this runtime | `skipped — browser tooling unavailable` |
248
+
249
+ The second is the one an unattended sweep meets: a scheduled run has no browser
250
+ MCP server attached, so a lens that treated "cannot drive" as "nothing found"
251
+ would report a clean runtime section it never ran. Do not invent a URL, do not
252
+ start an arbitrary dev server, and do not fall back to a static-only claim
253
+ dressed as a runtime one — name the skip and let the static findings stand on
254
+ their own.
255
+
211
256
  ## Execution strategy {#execution-strategy}
212
257
 
213
258
  A lens is a self-contained, read-only unit of work — exactly the shape a
@@ -107,11 +107,12 @@ Per-round mechanics: [`acceptance-self-eval.md`](acceptance-self-eval.md).
107
107
 
108
108
  ## 5. The one creditable full-suite run
109
109
 
110
- **After the self-eval loop's last fix commit, immediately before the push** —
111
- the credit is keyed on the tree, so any later commit invalidates it. Redraft
112
- rounds run scoped tests; only this final run needs credit, and a bare
113
- `npm test` / `pnpm run test` deposits **none**, so close re-runs it. Shape it
114
- by what `close-validation/gates.js` runs:
110
+ **After the self-eval loop's last fix commit, immediately after the push** —
111
+ the credit is keyed on the tree, not push state: a later commit voids it, a
112
+ push does not, and the backgrounded capture (below) ends the turn. Redraft
113
+ rounds run scoped tests; only this run needs credit, and a bare `npm test` /
114
+ `pnpm run test` deposits **none**, so close re-runs it. Shape it by what
115
+ `close-validation/gates.js` runs:
115
116
 
116
117
  ```bash
117
118
  # CRAP gate on (default) + a `test:coverage` script — writes close's stamp:
@@ -127,7 +128,7 @@ Dispatch it in the **background**: it outruns the host's sync Bash ceiling, and
127
128
  Rule 2).
128
129
 
129
130
  Read the **output**, not the exit code: capture skips — no test run, no
130
- credit — when nothing changed under the CRAP `targetDirs`. Run the scoped
131
+ credit — when nothing changed under CRAP `targetDirs`. Run the scoped
131
132
  projects for the roots you changed plus `verify[]`, not the whole suite.
132
133
 
133
134
  `verify[]` is scoped entries **plus** this one run: an entry that is itself a