mandrel 2.58.0 → 2.60.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 (124) hide show
  1. package/.agents/README.md +17 -12
  2. package/.agents/agents/acceptance-critic.md +24 -43
  3. package/.agents/agents/story-worker.md +18 -19
  4. package/.agents/docs/SDLC.md +12 -13
  5. package/.agents/docs/agentrc-reference.json +1 -2
  6. package/.agents/docs/configuration.md +29 -46
  7. package/.agents/docs/quality-gates.md +9 -5
  8. package/.agents/docs/workflows.md +1 -1
  9. package/.agents/instructions.md +5 -7
  10. package/.agents/rules/ci-remediation.md +41 -8
  11. package/.agents/rules/known-tooling-behavior.md +65 -15
  12. package/.agents/runtime-deps.json +7 -2
  13. package/.agents/schemas/acceptance-eval-verdict.schema.json +1 -1
  14. package/.agents/schemas/agentrc.schema.json +6 -11
  15. package/.agents/schemas/crap-baseline.schema.json +1 -1
  16. package/.agents/schemas/crap-report.schema.json +1 -1
  17. package/.agents/schemas/story-deliver-terminal.schema.json +3 -3
  18. package/.agents/scripts/README.md +11 -1
  19. package/.agents/scripts/acceptance-eval.js +25 -27
  20. package/.agents/scripts/ceremony-derive.js +15 -10
  21. package/.agents/scripts/check-context-budget.js +148 -228
  22. package/.agents/scripts/check-schema-references.js +5 -3
  23. package/.agents/scripts/check-workflow-citations.js +33 -147
  24. package/.agents/scripts/coverage-capture.js +7 -4
  25. package/.agents/scripts/deliver-light.js +41 -100
  26. package/.agents/scripts/deliver-run.js +631 -0
  27. package/.agents/scripts/file-ci-gap.js +59 -11
  28. package/.agents/scripts/install-matrix-assert.js +48 -3
  29. package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +51 -33
  30. package/.agents/scripts/lib/baselines/crap-preview-incremental.js +6 -2
  31. package/.agents/scripts/lib/baselines/kinds/_crap-read.js +0 -8
  32. package/.agents/scripts/lib/baselines/kinds/crap.js +35 -18
  33. package/.agents/scripts/lib/changed-files.js +30 -0
  34. package/.agents/scripts/lib/config/delivery-routing.js +5 -4
  35. package/.agents/scripts/lib/config/explain.js +1 -3
  36. package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +1 -1
  37. package/.agents/scripts/lib/config-resolver.js +1 -0
  38. package/.agents/scripts/lib/config-settings-schema-delivery.js +28 -21
  39. package/.agents/scripts/lib/coverage-capture-fullscope.js +10 -2
  40. package/.agents/scripts/lib/coverage-capture-incremental.js +3 -2
  41. package/.agents/scripts/lib/coverage-capture-usage.js +4 -1
  42. package/.agents/scripts/lib/crap-engine.js +2 -2
  43. package/.agents/scripts/lib/crap-utils.js +21 -5
  44. package/.agents/scripts/lib/doc-tiers.js +4 -2
  45. package/.agents/scripts/lib/escomplex-ast-compat.js +39 -17
  46. package/.agents/scripts/lib/escomplex-kernel.js +298 -0
  47. package/.agents/scripts/lib/feedback-loop/graduator-core.js +7 -6
  48. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +7 -5
  49. package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
  50. package/.agents/scripts/lib/gh-exec.js +160 -0
  51. package/.agents/scripts/lib/maintainability-engine.js +3 -3
  52. package/.agents/scripts/lib/observability/source-classifier.js +1 -0
  53. package/.agents/scripts/lib/orchestration/ceremony-routing.js +74 -132
  54. package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +123 -12
  55. package/.agents/scripts/lib/orchestration/complexity-gate.js +180 -352
  56. package/.agents/scripts/lib/orchestration/light-suitability.js +71 -136
  57. package/.agents/scripts/lib/orchestration/plan-context.js +44 -50
  58. package/.agents/scripts/lib/orchestration/plan-persist/changes-repair.js +8 -6
  59. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +104 -119
  60. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +41 -25
  61. package/.agents/scripts/lib/orchestration/plan-persist/summary.js +11 -11
  62. package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +63 -29
  63. package/.agents/scripts/lib/orchestration/plan-persist/wave-collision-gate.js +107 -0
  64. package/.agents/scripts/lib/orchestration/review-depth.js +14 -11
  65. package/.agents/scripts/lib/orchestration/run-epilogue.js +260 -182
  66. package/.agents/scripts/lib/orchestration/run-scoped-config.js +63 -99
  67. package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +3 -3
  68. package/.agents/scripts/lib/orchestration/single-story-close/phases/graphql-preflight.js +137 -0
  69. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +105 -18
  70. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +4 -3
  71. package/.agents/scripts/lib/orchestration/story-follow-ups.js +156 -39
  72. package/.agents/scripts/lib/orchestration/story-init-envelope.js +71 -0
  73. package/.agents/scripts/lib/orchestration/task-body-validator.js +8 -17
  74. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +25 -209
  75. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +8 -5
  76. package/.agents/scripts/lib/orchestration/ticket-validator.js +44 -183
  77. package/.agents/scripts/lib/orchestration/ticketing/reads.js +14 -25
  78. package/.agents/scripts/lib/runtime-deps/dep-resolution.js +155 -0
  79. package/.agents/scripts/lib/runtime-deps/ensure-installed.js +44 -9
  80. package/.agents/scripts/lib/runtime-deps/parser-major.js +110 -0
  81. package/.agents/scripts/lib/runtime-deps/preflight.js +6 -25
  82. package/.agents/scripts/lib/runtime-deps/scan-imports.js +46 -1
  83. package/.agents/scripts/lib/skills/walk-skill-files.js +1 -1
  84. package/.agents/scripts/lib/story-body/body-format-lints.js +58 -12
  85. package/.agents/scripts/lib/story-body/story-body.js +83 -29
  86. package/.agents/scripts/lib/templates/decomposer-prompts.js +28 -33
  87. package/.agents/scripts/lib/wave-runner/live-probe.js +31 -5
  88. package/.agents/scripts/merge-baseline.js +4 -5
  89. package/.agents/scripts/plan-context.js +117 -28
  90. package/.agents/scripts/plan-persist.js +79 -39
  91. package/.agents/scripts/plan-run-epilogue.js +11 -8
  92. package/.agents/scripts/pr-watch-with-update.js +9 -2
  93. package/.agents/scripts/run-verify.js +13 -6
  94. package/.agents/scripts/single-story-init.js +7 -57
  95. package/.agents/scripts/stories-wave-tick.js +160 -26
  96. package/.agents/skills/core/gates-and-baselines/reference.md +0 -1
  97. package/.agents/skills/skills.index.json +2 -12
  98. package/.agents/skills/stack/qa/playwright/SKILL.md +26 -0
  99. package/.agents/workflows/audit-to-stories.md +14 -11
  100. package/.agents/workflows/helpers/acceptance-self-eval.md +84 -157
  101. package/.agents/workflows/helpers/code-review.md +4 -2
  102. package/.agents/workflows/helpers/deliver-digest.md +31 -24
  103. package/.agents/workflows/helpers/deliver-light.md +92 -101
  104. package/.agents/workflows/helpers/deliver-reference.md +116 -100
  105. package/.agents/workflows/helpers/deliver-story-reference.md +58 -124
  106. package/.agents/workflows/helpers/deliver-story.md +17 -18
  107. package/.agents/workflows/helpers/plan-reference.md +82 -60
  108. package/.agents/workflows/mandrel-deliver.md +47 -31
  109. package/.agents/workflows/mandrel-plan.md +32 -30
  110. package/.agents/workflows/mandrel-update.md +36 -21
  111. package/README.md +3 -3
  112. package/docs/CHANGELOG.md +43 -0
  113. package/lib/cli/registry.js +45 -25
  114. package/lib/cli/update.js +376 -17
  115. package/lib/migrations/index.js +2 -0
  116. package/lib/migrations/steps/2.60.0-retire-audit-results-autofile.js +40 -0
  117. package/package.json +8 -2
  118. package/.agents/schemas/model-attribution.schema.json +0 -53
  119. package/.agents/scripts/lib/orchestration/model-attribution.js +0 -418
  120. package/.agents/scripts/lib/orchestration/split-policy-validator.js +0 -188
  121. package/.agents/scripts/lib/orchestration/story-plan-state.js +0 -33
  122. package/.agents/scripts/lib/orchestration/structured-comment-parser.js +0 -67
  123. package/.agents/scripts/lib/templates/spec-author-prompts.js +0 -76
  124. package/.agents/skills/core/scope-triage/SKILL.md +0 -48
@@ -15,12 +15,12 @@
15
15
  *
16
16
  * Two halves, deliberately separated by the `createIssue` boundary:
17
17
  *
18
- * 1. **`assertSupersedePartition`** — a plan-time, fail-closed check that
19
- * runs *before* any GitHub write. Mirrors `assertAcceptancePartition`:
20
- * every id passed to `--tickets` must be claimed by exactly one Story,
21
- * and no Story may claim an id that was not a source ticket. A partial
22
- * supersede map is a planning error, not something to paper over at
23
- * write time.
18
+ * 1. **`resolveSupersedePartition`** — the plan-time completion pass that
19
+ * runs *before* any GitHub write. No id may be claimed by two Stories
20
+ * and no Story may claim an id that was not a source ticket — both fail
21
+ * closed. A source id nobody claimed is assigned to the primary Story
22
+ * with a warning (Story #5342): the plan is replacing it either way, so
23
+ * the only open question was bookkeeping.
24
24
  * 2. **`closeSupersededTickets`** — the bookkeeping pass that runs *after*
25
25
  * the Stories exist. It **never throws**: a throw here would leave the
26
26
  * run half-done with Stories already live. An already-closed, deleted,
@@ -279,18 +279,12 @@ function describeStoryIds(entries) {
279
279
  }
280
280
 
281
281
  /**
282
- * Fail closed on a partial supersede map.
282
+ * Index which Story claims each source id.
283
283
  *
284
- * Runs **before** `createIssue` so a mis-authored map never leaves Stories
285
- * live against an inconsistent tracker.
286
- *
287
- * @param {Array<{ slug: string, supersedes: Array<{ id: number }> }>} stories
288
- * @param {number[]} sourceTicketIds Ids passed to `/mandrel-plan --tickets`.
284
+ * @param {Array<{ slug: string, supersedes?: Array<{ id: number }> }>} list
285
+ * @returns {Map<number, string[]>} id → claiming slugs, in draft order.
289
286
  */
290
- export function assertSupersedePartition(stories, sourceTicketIds = []) {
291
- const list = Array.isArray(stories) ? stories : [];
292
- const sources = new Set(sourceTicketIds);
293
-
287
+ function indexSupersedeClaims(list) {
294
288
  /** @type {Map<number, string[]>} */
295
289
  const claims = new Map();
296
290
  for (const story of list) {
@@ -300,9 +294,47 @@ export function assertSupersedePartition(stories, sourceTicketIds = []) {
300
294
  claims.set(id, owners);
301
295
  }
302
296
  }
297
+ return claims;
298
+ }
303
299
 
304
- const errors = [];
300
+ /**
301
+ * Complete the supersede map, refusing only what the plan gets wrong.
302
+ *
303
+ * Two halves, split by who can be right (Story #5342):
304
+ *
305
+ * - **Refused.** A Story claiming an id that was never a source ticket, and
306
+ * two Stories claiming the same id. Both name an intent the run cannot
307
+ * act on — the first would comment on and close an issue nobody asked
308
+ * about, the second cannot say which Story replaced it — so they fail
309
+ * closed, **before** `createIssue`, and no Story goes live against an
310
+ * inconsistent tracker.
311
+ * - **Assigned with a warning.** A source id no Story claimed. Every id
312
+ * passed to `--tickets` is being replaced by this plan by construction;
313
+ * which Story records it is a bookkeeping detail, and the primary Story
314
+ * is the answer the operator would have given. Refusing cost a whole
315
+ * re-author round to type back a fact the run already knew.
316
+ *
317
+ * Mutates the unclaimed ids onto the primary Story's `supersedes[]`.
318
+ *
319
+ * **`stories[0]` is the primary, and it is the *only* derivation of it**
320
+ * (Story #5361): `assemblePlanStories` hands this list over already sorted by
321
+ * `orderStoriesByDependencies`, which is the same order the create loop files
322
+ * the Stories in — so the `superseded-by` comment this assignment produces
323
+ * can never name a different Story from the checkpoint and the plan summary.
324
+ * A non-empty list is the caller's contract (assembly refuses an empty
325
+ * draft before it gets here).
326
+ *
327
+ * @param {Array<{ slug: string, supersedes: Array<{ id: number, note: string|null }> }>} stories
328
+ * Dependency-ordered and non-empty.
329
+ * @param {number[]} sourceTicketIds Ids passed to `/mandrel-plan --tickets`.
330
+ * @returns {string[]} One warning per id assigned by default.
331
+ */
332
+ export function resolveSupersedePartition(stories, sourceTicketIds = []) {
333
+ const list = Array.isArray(stories) ? stories : [];
334
+ const sources = new Set(sourceTicketIds);
335
+ const claims = indexSupersedeClaims(list);
305
336
 
337
+ const errors = [];
306
338
  for (const [id, owners] of claims) {
307
339
  if (owners.length > 1) {
308
340
  errors.push(
@@ -317,23 +349,25 @@ export function assertSupersedePartition(stories, sourceTicketIds = []) {
317
349
  );
318
350
  }
319
351
  }
320
-
321
- for (const id of sources) {
322
- if (!claims.has(id)) {
323
- errors.push(
324
- `source ticket #${id} is not claimed by any Story's supersedes[] — ` +
325
- 'a partial supersede map is a planning error. Claim it, or drop it ' +
326
- 'from --tickets.',
327
- );
328
- }
329
- }
330
-
331
352
  if (errors.length > 0) {
332
353
  throw new Error(
333
354
  `[plan-persist] supersede partition failed with ${errors.length} ` +
334
355
  `error(s):\n${errors.map((e) => ` - ${e}`).join('\n')}`,
335
356
  );
336
357
  }
358
+
359
+ const primary = list[0];
360
+ const warnings = [];
361
+ for (const id of sources) {
362
+ if (claims.has(id)) continue;
363
+ primary.supersedes = [...(primary.supersedes ?? []), { id, note: null }];
364
+ warnings.push(
365
+ `source ticket #${id} was claimed by no Story's supersedes[] — ` +
366
+ `assigned to the primary Story "${primary.slug}". Author the claim ` +
367
+ 'explicitly if another Story is the one that replaces it.',
368
+ );
369
+ }
370
+ return warnings;
337
371
  }
338
372
 
339
373
  /**
@@ -567,7 +601,7 @@ export async function closeSupersededTickets({
567
601
  });
568
602
  },
569
603
  // The per-source-ticket close (Story #4952) fans out across **distinct**
570
- // tickets — `assertSupersedePartition` has already failed the run closed
604
+ // tickets — `resolveSupersedePartition` has already failed the run closed
571
605
  // if two Stories claim the same id, so no two units in flight can touch
572
606
  // the same issue. Within one unit the probe → comment → close sequence
573
607
  // stays strictly ordered: commenting on an issue the probe reported
@@ -0,0 +1,107 @@
1
+ /**
2
+ * wave-collision-gate.js — the split gate (Story #5332).
3
+ *
4
+ * Kept out of `wave-serialisation.js` because it answers a different
5
+ * question. That module *predicts* what the dispatcher will do with a draft
6
+ * and renders the prediction as a receipt; this one decides whether the draft
7
+ * may be created at all, and so owns both the one enumeration the refusal and
8
+ * the receipt share and the shape reconciliation that enumeration needs.
9
+ *
10
+ * @module lib/orchestration/plan-persist/wave-collision-gate
11
+ */
12
+
13
+ import { predictWaveSerialisation } from './wave-serialisation.js';
14
+
15
+ /**
16
+ * Expose an assembled Story's declared footprint where `storyFootprint`
17
+ * looks for it.
18
+ *
19
+ * `assemblePlanStories` returns the persisted artifact — `{ slug, title,
20
+ * body, bodyObject, acceptance, depends_on, … }` — and carries the parsed
21
+ * `changes[]` inside `bodyObject`, not at the top level. `storyFootprint`
22
+ * reads `files` / `changes` / `changeset` only, so from Story #5313 (which
23
+ * retired the body scrape that had been widening the footprint out of the
24
+ * markdown) until this Story the production call saw an empty footprint for
25
+ * every assembled Story and predicted nothing — the unit fixtures passed a
26
+ * top-level `changes` and so could not show it. The prediction is now the
27
+ * split gate, and a gate that cannot see a declaration cannot fire, so the
28
+ * shapes are reconciled here rather than by widening the dispatcher's own
29
+ * predicate: the runtime's records (`resolve-stories.js`) already carry
30
+ * `changes` at the top level and pass through untouched.
31
+ *
32
+ * @param {object} story
33
+ * @returns {object} The same record, with a top-level `changes` when one can
34
+ * be resolved from its `bodyObject`.
35
+ */
36
+ function withDeclaredFootprint(story) {
37
+ if (!story || typeof story !== 'object') return story;
38
+ const declared = [story.files, story.changes, story.changeset].some(
39
+ (shape) => Array.isArray(shape) && shape.length > 0,
40
+ );
41
+ if (declared) return story;
42
+ const fromBody = story.bodyObject?.changes;
43
+ return Array.isArray(fromBody) ? { ...story, changes: fromBody } : story;
44
+ }
45
+
46
+ /**
47
+ * Render one refused pair as a report line.
48
+ *
49
+ * @param {{ wave: number, slugs: [string, string], paths: string[], source: string }} collision
50
+ * @returns {string}
51
+ */
52
+ function formatCollision({ wave, slugs, paths, source }) {
53
+ const declared = paths.map((p) => `\`${p}\``).join(', ');
54
+ return ` - wave ${wave}: "${slugs[0]}" + "${slugs[1]}" both declare ${declared} (${source})`;
55
+ }
56
+
57
+ /**
58
+ * Compute the same-wave collisions of a draft and refuse an N>1 draft that
59
+ * has any — the split gate.
60
+ *
61
+ * ADR `20260912-5312` deleted every numeric plan-time ceiling and left the
62
+ * default-single policy enforced by prose plus `assertAcceptancePartition`,
63
+ * which refused only byte-identical acceptance text across siblings — a shape
64
+ * model output does not produce. The measured result was a plan of 18 Stories
65
+ * whose own summary comment recorded 39 shared files across 14 same-wave
66
+ * Stories: the plan refuted its own parallelism claim, after persist, with
67
+ * nothing acting on it.
68
+ *
69
+ * So the gate is the dispatcher's own predicate rather than a proxy for it. A
70
+ * pair {@link predictWaveSerialisation} names is a pair
71
+ * `stories-wave-tick.js` will refuse to co-dispatch, so the split buys no
72
+ * parallelism while still paying a delivery session per Story. Two remedies,
73
+ * both the author's to take before anything is created: merge the pair into
74
+ * the one Story it already is, or order it with `depends_on` so the members
75
+ * land in different waves.
76
+ *
77
+ * **N=1 can never trip it.** A single-Story draft has no pair to score, so
78
+ * the prediction is empty by construction.
79
+ *
80
+ * The computed collisions are **returned** so the caller hands the same value
81
+ * to the plan-summary receipt instead of recomputing it — a recomputation is
82
+ * how the refusal and the receipt would come to disagree.
83
+ *
84
+ * @param {ReturnType<typeof import('./summary.js').buildWaveTable>} waveTable
85
+ * @param {Array<object>} stories The assembled Stories, in draft order.
86
+ * @param {{ tempRoot?: string }} [options] Threaded to the predicate.
87
+ * @returns {ReturnType<typeof predictWaveSerialisation>}
88
+ * @throws {Error} When an N>1 draft has at least one colliding same-wave pair.
89
+ */
90
+ export function assertNoWaveCollisions(waveTable, stories, options = {}) {
91
+ const list = Array.isArray(stories) ? stories : [];
92
+ const collisions = predictWaveSerialisation(
93
+ waveTable,
94
+ list.map(withDeclaredFootprint),
95
+ options,
96
+ );
97
+ if (list.length <= 1 || collisions.length === 0) return collisions;
98
+ throw new Error(
99
+ `[plan-persist] ${collisions.length} same-wave collision(s) — the ` +
100
+ 'dispatcher will refuse to co-dispatch these pairs, so the split buys ' +
101
+ 'no parallelism and costs a delivery session per Story:\n' +
102
+ `${collisions.map(formatCollision).join('\n')}\n` +
103
+ 'Remedy: merge each pair into the one Story it already is (its stages ' +
104
+ 'belong in `## Slicing`), or order the pair with `depends_on` so the ' +
105
+ 'members sit in different waves.',
106
+ );
107
+ }
@@ -75,19 +75,22 @@ export const DEFAULT_DIFF_WIDTH = Object.freeze({
75
75
  * `audit-rules.json`?
76
76
  *
77
77
  * This is the **single source** of the derived level — the review depth
78
- * ({@link resolveDepth}), the acceptance-critic fresh-vs-inline routing
79
- * (`ceremony-routing.js#resolveCeremonyForRisk`), and the dispatch-side
80
- * complexity routing (`complexity-gate.js#deriveStoryShape`, Story #4722)
81
- * all consume what this returns, so no ceremony decision can disagree about
82
- * how risky a change is. Dispatch reads the **predicted** shape (the Story's
83
- * declared `changes[]` footprint) and close reads the **actual** diff — one
84
- * taxonomy, two read points, which is what keeps a lite-shaped Story whose
85
- * footprint touches a sensitive path on the full route with its fresh critic.
78
+ * ({@link resolveDepth}) and the dispatch-side complexity routing
79
+ * (`complexity-gate.js#deriveStoryShape`, Story #4722) both consume what this
80
+ * returns, so no risk decision can disagree about how risky a change is.
81
+ * Dispatch reads the **predicted** shape (the Story's declared `changes[]`
82
+ * footprint) and close reads the **actual** diff — one taxonomy, two read
83
+ * points, which is what keeps a lite-shaped Story whose footprint touches a
84
+ * sensitive path on the full route and under a deep review.
85
+ *
86
+ * The acceptance verdict owner is **not** downstream of this level: Story
87
+ * #5343 re-based `ceremony-routing.js#resolveCeremonyForRisk` on the ceremony
88
+ * profile alone, and Story #5366 removed the level from its signature.
86
89
  *
87
90
  * Returns `null` — the fail-safe "no derivable signal" level — when the change
88
- * set is empty/unknown or the manifest cannot be read. Both downstream
89
- * consumers treat `null` as the more thorough posture (`standard` depth, a
90
- * `fresh` critic), so a derivation failure never buys a change less checking.
91
+ * set is empty/unknown or the manifest cannot be read. Both consumers treat
92
+ * `null` as the more thorough posture (`standard` depth, the conservative
93
+ * `full` route), so a derivation failure never buys a change less checking.
91
94
  *
92
95
  * Total: never throws.
93
96
  *