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
@@ -7,23 +7,23 @@
7
7
  *
8
8
  * 1. `changes[]` repair + ticket validator + file-assumption + DAG
9
9
  * 2. Draft reachability (named soft failure, exit 3)
10
- * 3. Split-policy partition (`assertAcceptancePartition`) + spec fold
10
+ * 3. Same-wave collision refusal (`assertNoWaveCollisions`) + spec fold
11
11
  * 4. Create Story issues (`type::story` + sanitized authored labels —
12
12
  * deliberately NOT `agent::ready`), resumably via a plan fingerprint
13
- * 5. Upsert `story-plan-state` on every created Story; upsert `plan-summary`
14
- * on the primary Story
13
+ * 5. Upsert the `story-plan-state` comment on every created Story — since
14
+ * Story #5343 the one comment persist posts, carrying the plan summary
15
15
  * 6. Flip every Story to `agent::ready` — the terminal step, so `ready`
16
- * always implies "checkpoints written"
16
+ * always implies "plan comments written"
17
17
  * 7. Comment on + close the superseded `--tickets` source issues
18
18
  * (Story #4535) — bookkeeping only; never fails the run
19
19
  * 8. Temp cleanup at terminal success + a stale-plan-dir reap
20
20
  *
21
21
  * **Why `agent::ready` moved to the end (Story #4541).** Issues used to be
22
- * born `agent::ready` in the creating POST while the checkpoints were
22
+ * born `agent::ready` in the creating POST while the plan comments were
23
23
  * written afterwards. Anything picking a Story up in that window — or after
24
- * a comment failure aborted the loop — read the checkpoint as `null`
25
- * (`story-plan-state.js` degrades missing/malformed to `null`). Creating
26
- * unlabelled, writing checkpoints, then flipping closes that race.
24
+ * a comment failure aborted the loop — found a ready Story carrying none of
25
+ * the operator's delivery instructions. Creating unlabelled, writing the
26
+ * comments, then flipping closes that race.
27
27
  *
28
28
  * **No authored risk artifact (Story #4542).** Persist neither requires nor
29
29
  * accepts a risk verdict, derives no envelope from one, and computes no
@@ -75,48 +75,48 @@ import {
75
75
  createStoryIssues,
76
76
  markStoriesReady,
77
77
  } from './story-ops.js';
78
- import {
79
- buildPlanSummaryCommentBody,
80
- buildWaveTable,
81
- PLAN_SUMMARY_COMMENT_TYPE,
82
- } from './summary.js';
78
+ import { buildPlanSummaryCommentBody, buildWaveTable } from './summary.js';
83
79
  import { closeSupersededTickets } from './supersede-ops.js';
84
- import { predictWaveSerialisation } from './wave-serialisation.js';
85
-
86
- /** Checkpoint schema version written on each Story's story-plan-state. */
87
- const PLAN_CHECKPOINT_SCHEMA_VERSION_V2 = 2;
80
+ import { assertNoWaveCollisions } from './wave-collision-gate.js';
88
81
 
89
- /** Structured-comment type for the per-plan Story checkpoint. */
82
+ /** Structured-comment type for the per-plan Story comment. */
90
83
  const STORY_PLAN_STATE_TYPE = 'story-plan-state';
91
84
 
92
85
  /**
93
- * Write the `story-plan-state` checkpoint on a Story.
86
+ * Write the plan comment on a Story — the **one** comment persist posts per
87
+ * Story (Story #5343).
88
+ *
89
+ * It carries the human plan summary: the created Story set, the delivery order
90
+ * and the exact deliver command. That summary used to be a second
91
+ * `plan-summary` comment on the primary Story only, which meant the operator's
92
+ * instructions lived on a different marker from the rest — and on a different
93
+ * ticket from four Stories out of five.
94
+ *
95
+ * **No machine payload (Story #5367).** The comment used to lead with a fenced
96
+ * JSON checkpoint (a persist receipt: completion time, Story count, the
97
+ * cohort). Nothing ever read it back — the one module that parsed it had no
98
+ * production importer — so the payload and its readers are gone. The
99
+ * `story-plan-state` marker stays: it is what makes a re-persist upsert this
100
+ * comment in place instead of appending a second one.
94
101
  *
95
102
  * @param {object} provider
96
103
  * @param {number} storyId
97
- * @param {object} state
104
+ * @param {string} summary Rendered plan-summary markdown. The comment exists
105
+ * to carry it, so an empty summary is a caller bug rather than a degenerate
106
+ * comment worth posting.
107
+ * @returns {Promise<void>}
98
108
  */
99
- export async function writeCheckpointV2(provider, storyId, state) {
109
+ export async function writePlanSummaryComment(provider, storyId, summary) {
100
110
  if (!Number.isInteger(storyId)) {
101
- throw new TypeError('writeCheckpointV2 requires a numeric storyId');
111
+ throw new TypeError('writePlanSummaryComment requires a numeric storyId');
112
+ }
113
+ const body = summary?.trim();
114
+ if (!body) {
115
+ throw new TypeError(
116
+ 'writePlanSummaryComment requires a non-empty plan summary',
117
+ );
102
118
  }
103
- const body = [
104
- '### story-plan-state',
105
- '',
106
- '```json',
107
- JSON.stringify(
108
- {
109
- version: PLAN_CHECKPOINT_SCHEMA_VERSION_V2,
110
- storyId,
111
- ...state,
112
- },
113
- null,
114
- 2,
115
- ),
116
- '```',
117
- ].join('\n');
118
119
  await upsertStructuredComment(provider, storyId, STORY_PLAN_STATE_TYPE, body);
119
- return state;
120
120
  }
121
121
 
122
122
  /**
@@ -130,7 +130,7 @@ export async function writeCheckpointV2(provider, storyId, state) {
130
130
  * the helper applied are reported alongside so the operator sees what was
131
131
  * rewritten.
132
132
  *
133
- * The returned freshness counts feed the posted `plan-summary`'s freshness
133
+ * The returned freshness counts feed the posted summary's freshness
134
134
  * line: every warning is a reference the base branch disagreed with, so it
135
135
  * counts as `stale` there rather than the comment reading "clean" on a run
136
136
  * that had something to say.
@@ -197,8 +197,9 @@ function collectTextHygieneWarnings(rawStories) {
197
197
 
198
198
  /**
199
199
  * Print every warning the run collected under one heading. The dry-run is
200
- * where an operator reads these; the persist prints the same list so a
201
- * `--chain-on-clean` run loses nothing.
200
+ * where an operator reads these; the persist prints the same list so the
201
+ * chained run loses nothing on stderr, as the chain's envelope merge keeps
202
+ * it from losing anything on stdout (Story #5361).
202
203
  *
203
204
  * @param {string[]} warnings
204
205
  * @returns {void}
@@ -248,7 +249,7 @@ async function runSupersedePhase(args) {
248
249
  }
249
250
 
250
251
  /**
251
- * Render the plan-metrics line for the **posted** `plan-summary` comment,
252
+ * Render the plan-metrics line for the **posted** summary section,
252
253
  * scoped to this invocation.
253
254
  *
254
255
  * The ordering hazard this closes: the ledger record for the current run is
@@ -313,7 +314,7 @@ async function renderRunScopedPlanMetricsLine({
313
314
  * writes — and the passes that scan `body.acceptance` / `body.verify` were
314
315
  * inert on the canonical top-level authoring shape as a result. Findings the
315
316
  * raw pass already reported are dropped so the same collision is not
316
- * announced twice per run; the rest are returned for the plan-summary
317
+ * announced twice per run; the rest are returned for the summary
317
318
  * comment, which is where these findings stop being a stderr line nobody
318
319
  * keeps. Every finding is advisory (Story #5312).
319
320
  *
@@ -419,56 +420,33 @@ async function enforceReachability(reachability, config) {
419
420
  }
420
421
 
421
422
  /**
422
- * Write the per-Story checkpoint, upsert the plan-summary comment, and flip
423
- * every created Story to `agent::ready`. Terminal ordering is load-bearing:
424
- * `agent::ready` lands last so it can honestly mean "fully persisted"
425
- * (Story #4541). A dry run performs none of it.
423
+ * Write the per-Story plan comment — one comment per Story, carrying the plan
424
+ * summary (Story #5343) — and flip every created Story to `agent::ready`.
425
+ * Terminal ordering is load-bearing: `agent::ready` lands last so it can
426
+ * honestly mean "fully persisted" (Story #4541). A dry run performs none of it.
426
427
  *
427
- * **The checkpoints fan out; the phase boundary does not** (Story #4952). The
428
+ * **The comments fan out; the phase boundary does not** (Story #4952). The
428
429
  * per-Story upserts are independent of one another and run under bounded
429
430
  * concurrency, but the `await` on that whole fan-out is what keeps the
430
- * Story #4541 invariant intact: *every* checkpoint is on its ticket before the
431
- * first `agent::ready` flip is issued, so `ready` still means "fully
432
- * persisted" and a `/mandrel-deliver` that picks a Story up cannot read a null
433
- * checkpoint. Concurrency inside the phase is safe; overlapping the phases is
434
- * the race this ordering exists to close.
431
+ * Story #4541 invariant intact: *every* Story carries its plan comment before
432
+ * the first `agent::ready` flip is issued, so `ready` still means "fully
433
+ * persisted" and the operator's instructions are never missing from a ticket
434
+ * something else may already be picking up. Concurrency inside the phase is
435
+ * safe; overlapping the phases is the race this ordering exists to close.
435
436
  *
436
437
  * @param {object} args
437
438
  * @returns {Promise<void>}
438
439
  */
439
- async function persistStoryArtifacts({
440
- provider,
441
- created,
442
- primary,
443
- summaryBody,
444
- }) {
445
- const cohort = created.map((createdStory) => ({
446
- slug: createdStory.slug,
447
- id: createdStory.id,
448
- }));
440
+ async function persistStoryArtifacts({ provider, created, summaryBody }) {
449
441
  await concurrentMap(
450
442
  created,
451
- (story) =>
452
- writeCheckpointV2(provider, story.id, {
453
- persist: {
454
- completedAt: new Date().toISOString(),
455
- storyCount: created.length,
456
- primaryStoryId: primary.id,
457
- stories: cohort,
458
- },
459
- }),
460
- // The per-Story checkpoint upserts (Story #4952): each targets a
443
+ (story) => writePlanSummaryComment(provider, story.id, summaryBody),
444
+ // The per-Story comment upserts (Story #4952): each targets a
461
445
  // different issue and reads nothing another writes, so this loop was
462
446
  // serial only by construction — but see {@link persistStoryArtifacts}
463
447
  // for the phase ordering that is *not* incidental.
464
448
  { concurrency: FANOUT_CONCURRENCY },
465
449
  );
466
- await upsertStructuredComment(
467
- provider,
468
- primary.id,
469
- PLAN_SUMMARY_COMMENT_TYPE,
470
- summaryBody,
471
- );
472
450
  await markStoriesReady({ provider, created });
473
451
  }
474
452
 
@@ -543,7 +521,6 @@ function logPersistEpilogue({
543
521
  * artifacts: {
544
522
  * stories: Array<object>,
545
523
  * techSpecContent?: string|null,
546
- * planAcceptance?: string[]|null,
547
524
  * planContextEnvelope?: object|null,
548
525
  * },
549
526
  * config?: object,
@@ -569,7 +546,6 @@ export async function runPlanPersist({
569
546
  const {
570
547
  stories: rawStories = null,
571
548
  techSpecContent = null,
572
- planAcceptance = null,
573
549
  planContextEnvelope = null,
574
550
  } = artifacts ?? {};
575
551
  const {
@@ -623,21 +599,28 @@ export async function runPlanPersist({
623
599
  epicId: opts.adoptEpicId ?? null,
624
600
  });
625
601
 
626
- // Split policy + inline Spec fold (Specs stay inline, never under docs/).
602
+ // Inline Spec fold (Specs stay inline, never under docs/).
627
603
  const seedContent = planContextEnvelope?.seed?.content ?? '';
628
- const { stories: assembled } = assemblePlanStories(rawStories, {
629
- sharedSpec: techSpecContent,
630
- planAcceptance: planAcceptance ?? undefined,
631
- sourceTicketIds,
632
- // The seed this plan was authored from: an audit sweep's Single-plan seed
633
- // carries the `audit-fingerprints` / `audit-semantic-keys` footers, and
634
- // assembly copies them into the persisted Story bodies so the next sweep
635
- // recognises what it already planned (Story #4877). Since Story #5045 this
636
- // is the **fallback** — it is carried onto every Story that did not
637
- // attribute its own `provenance`, which keeps an un-attributed plan exactly
638
- // as recall-safe as it was. Empty for a `--tickets` run, a no-op there.
639
- provenanceSource: seedContent,
640
- });
604
+ const { stories: assembled, warnings: supersedeWarnings } =
605
+ assemblePlanStories(rawStories, {
606
+ sharedSpec: techSpecContent,
607
+ sourceTicketIds,
608
+ // The seed this plan was authored from: an audit sweep's Single-plan seed
609
+ // carries the `audit-fingerprints` / `audit-semantic-keys` footers, and
610
+ // assembly copies them into the persisted Story bodies so the next sweep
611
+ // recognises what it already planned (Story #4877). Since Story #5045 this
612
+ // is the **fallback** — it is carried onto every Story that did not
613
+ // attribute its own `provenance`, which keeps an un-attributed plan exactly
614
+ // as recall-safe as it was. Empty for a `--tickets` run, a no-op there.
615
+ provenanceSource: seedContent,
616
+ });
617
+
618
+ // Story #5342: the source ids assembly assigned to the primary Story by
619
+ // default. They ride the same list every other dry-run warning does, so a
620
+ // default-assigned supersede is visible in the output the operator already
621
+ // reads rather than only in the tracker afterwards.
622
+ warnings.push(...supersedeWarnings);
623
+ logWarnings(supersedeWarnings);
641
624
 
642
625
  // Stamp the `audit::*` labels the dedup corpus is listed by. Without them a
643
626
  // Story this path files is absent from the pool an indexed sweep matches
@@ -654,6 +637,31 @@ export async function runPlanPersist({
654
637
  rawFindings: validated.findings,
655
638
  });
656
639
 
640
+ // Story #5332 — the split gate, ahead of the first create. `buildWaveTable`
641
+ // needs only `{slug, title, depends_on}` and the prediction needs only the
642
+ // assembled bodies, so both can run before anything is written; they used
643
+ // to sit *after* creation, which is why the collision table could only ever
644
+ // be a receipt for a plan already live. The same computed value is handed
645
+ // to the summary rendering below rather than recomputed, so the refusal and
646
+ // the receipt can never disagree.
647
+ const waveTable = buildWaveTable(
648
+ stories.map((s) => ({
649
+ slug: s.slug,
650
+ title: s.title,
651
+ depends_on: s.depends_on,
652
+ })),
653
+ );
654
+
655
+ // Story #5265: the table says which Stories share an order; the dispatcher
656
+ // decides which of those actually run together. Run its own predicate over
657
+ // the assembled bodies — the exact artifact the tick will read back off
658
+ // GitHub — so an N>1 draft it would serialize is refused here, and the
659
+ // summary comment names the serialisation instead of promising parallelism
660
+ // the next tick refuses. One enumeration feeds both.
661
+ const waveCollisions = assertNoWaveCollisions(waveTable, stories, {
662
+ tempRoot: getPaths(config).tempRoot,
663
+ });
664
+
657
665
  const { created, planRunLabel, planRunLabelApplied } =
658
666
  await createStoryIssues({
659
667
  provider,
@@ -666,24 +674,6 @@ export async function runPlanPersist({
666
674
  recordAuditFilings({ stories, created, tickets: rawStories, dryRun });
667
675
 
668
676
  const primary = created[0];
669
- const waveTable = buildWaveTable(
670
- stories.map((s) => ({
671
- slug: s.slug,
672
- title: s.title,
673
- depends_on: s.depends_on,
674
- })),
675
- );
676
-
677
- // Story #5265: the table says which Stories share an order; the dispatcher
678
- // decides which of those actually run together, and it decides on the
679
- // evidence-widened footprint. Run its own predicate over the assembled
680
- // bodies — the exact artifact the tick will read back off GitHub — so the
681
- // comment names the serialisation instead of promising parallelism the
682
- // next tick refuses. `tempRoot` is threaded for the same reason the tick
683
- // threads it: the scrape must ignore this project's scratch root.
684
- const waveCollisions = predictWaveSerialisation(waveTable, stories, {
685
- tempRoot: getPaths(config).tempRoot,
686
- });
687
677
 
688
678
  // Story #4541: `readPlanMetrics` is declared `(epicId, config)` but was
689
679
  // called with `config` first, so the ledger path resolver received the
@@ -718,12 +708,7 @@ export async function runPlanPersist({
718
708
  });
719
709
 
720
710
  if (!dryRun) {
721
- await persistStoryArtifacts({
722
- provider,
723
- created,
724
- primary,
725
- summaryBody,
726
- });
711
+ await persistStoryArtifacts({ provider, created, summaryBody });
727
712
  }
728
713
 
729
714
  // Story #5139 — the container Epic is created LAST among the writes: its
@@ -4,7 +4,7 @@
4
4
  * Under the Story collapse (`docs/roadmap.md` § Stage 3), `/mandrel-plan` persists
5
5
  * zero-or-more Story issues directly — no Epic parent, no reconciler tree,
6
6
  * no `deliveryShape` mode matrix. Default is **one Story**; N>1 is gated by
7
- * the Stage-1 split-policy validator (`assertAcceptancePartition`).
7
+ * the supersede partition check.
8
8
  *
9
9
  * Each Story body is the single executable document: Tech Spec stays inline
10
10
  * under `## Spec`, at whatever length the work needs (Story #5312 deleted
@@ -34,14 +34,13 @@ import {
34
34
  concurrentMap,
35
35
  FANOUT_CONCURRENCY,
36
36
  } from '../../util/concurrent-map.js';
37
- import { assertAcceptancePartition } from '../split-policy-validator.js';
38
37
  import {
39
38
  externalDependencyId,
40
39
  isExternalDependencyRef,
41
40
  } from './external-deps.js';
42
41
  import {
43
- assertSupersedePartition,
44
42
  normalizeSupersedes,
43
+ resolveSupersedePartition,
45
44
  } from './supersede-ops.js';
46
45
 
47
46
  /**
@@ -158,7 +157,7 @@ const PLAN_FINGERPRINT_LENGTH = 16;
158
157
  *
159
158
  * 1. A later, unrelated plan that reused a slug **and** title adopted the
160
159
  * stale open Story — never rewriting its body or Spec, and landing this
161
- * run's checkpoints, ready-flip, and supersede comments on the wrong
160
+ * run's plan comments, ready-flip, and supersede comments on the wrong
162
161
  * issue.
163
162
  * 2. A legitimate resume after the operator edited `stories.json` adopted
164
163
  * the pre-edit Story and kept its stale body, discarding the edit.
@@ -514,17 +513,30 @@ function assertSharedSpecAllowed(tickets, sharedSpec) {
514
513
 
515
514
  /**
516
515
  * Assemble markdown bodies for every Story: normalize → fold spec →
517
- * assertAcceptancePartition → assertSupersedePartition → serialize.
516
+ * order by dependency → resolveSupersedePartition → serialize.
518
517
  *
519
- * Both partition checks run **before** any GitHub write so a mis-authored
520
- * plan never leaves Stories live against an inconsistent tracker.
518
+ * **The dependency sort runs here, once** (Story #5361). It used to run again
519
+ * inside the create loop, which meant "the primary Story" was derived twice
520
+ * from two different orderings: supersede assignment took the first *authored*
521
+ * Story, while the plan summary took the first *created*
522
+ * one. A draft whose authoring order differed from its dependency order made
523
+ * the `superseded-by` comment name a different Story from the summary. One
524
+ * sort, one ordered list threaded on to every consumer, so `stories[0]` is the
525
+ * only primary there is. The sort also refuses an unknown sibling or a cycle,
526
+ * which now fails the write-free pass rather than the first create.
527
+ *
528
+ * The partition pass runs **before** any GitHub write so a mis-authored
529
+ * plan never leaves Stories live against an inconsistent tracker. Story #5332
530
+ * retired the acceptance partition that used to run beside it: it refused
531
+ * only byte-identical acceptance text across siblings, and the split gate is
532
+ * now `assertNoWaveCollisions` in `run-plan-persist.js`, ahead of the first
533
+ * create.
521
534
  *
522
535
  * @param {object[]} tickets
523
536
  * @param {object} [opts]
524
537
  * @param {string|null} [opts.sharedSpec]
525
- * @param {string[]} [opts.planAcceptance]
526
538
  * @param {number[]} [opts.sourceTicketIds] Ids passed to `/mandrel-plan --tickets`.
527
- * @returns {{ stories: Array<{ slug: string, title: string, body: string, acceptance: string[], depends_on: string[], supersedes: Array<{ id: number, note: string|null }> }> }}
539
+ * @returns {{ stories: Array<{ slug: string, title: string, body: string, acceptance: string[], depends_on: string[], supersedes: Array<{ id: number, note: string|null }> }>, warnings: string[] }}
528
540
  */
529
541
  export function assemblePlanStories(tickets, opts = {}) {
530
542
  if (!Array.isArray(tickets) || tickets.length === 0) {
@@ -535,16 +547,16 @@ export function assemblePlanStories(tickets, opts = {}) {
535
547
 
536
548
  assertSharedSpecAllowed(tickets, opts.sharedSpec);
537
549
 
538
- const stories = tickets.map(
539
- (ticket) => assembleOnePlanStory(ticket, opts).story,
550
+ const stories = orderStoriesByDependencies(
551
+ tickets.map((ticket) => assembleOnePlanStory(ticket, opts).story),
540
552
  );
541
553
 
542
- assertAcceptancePartition(stories, {
543
- planAcceptance: opts.planAcceptance,
544
- });
545
- assertSupersedePartition(stories, opts.sourceTicketIds ?? []);
554
+ const warnings = resolveSupersedePartition(
555
+ stories,
556
+ opts.sourceTicketIds ?? [],
557
+ );
546
558
 
547
- return { stories };
559
+ return { stories, warnings };
548
560
  }
549
561
 
550
562
  function orderStoriesByDependencies(stories) {
@@ -902,12 +914,13 @@ async function ensurePersistLabel({
902
914
  * Create Story issues via `provider.createIssue`, resumably.
903
915
  *
904
916
  * **Stories are born without `agent::ready`** (Story #4541). They used to
905
- * carry it in the creating POST while the `story-plan-state` checkpoint was
917
+ * carry it in the creating POST while the `story-plan-state` comment was
906
918
  * upserted afterwards, so anything that picked a Story up inside that window —
907
- * or after a comment failure aborted the loop — read the checkpoint as `null`.
908
- * Creation now applies `type::story` plus the sanitized authored labels only;
909
- * `markStoriesReady` performs the flip as the terminal step, once every
910
- * checkpoint is on the ticket.
919
+ * or after a comment failure aborted the loop — found a ready Story carrying
920
+ * none of the operator's delivery instructions. Creation now applies
921
+ * `type::story` plus the sanitized authored labels only; `markStoriesReady`
922
+ * performs the flip as the terminal step, once every plan comment is on the
923
+ * ticket.
911
924
  *
912
925
  * **The loop is resumable, and adoption is content-keyed.** Each body carries
913
926
  * a plan-fingerprint marker, and the open `type::story` backlog is indexed by
@@ -1003,7 +1016,10 @@ export async function createStoryIssues({ provider, stories, opts = {} }) {
1003
1016
  const created = [];
1004
1017
  const idBySlug = new Map();
1005
1018
 
1006
- for (const story of orderStoriesByDependencies(list)) {
1019
+ // Already in dependency order: `assemblePlanStories` sorted once, and
1020
+ // sorting again here is what gave the run a second, disagreeing notion of
1021
+ // which Story is primary (Story #5361).
1022
+ for (const story of list) {
1007
1023
  const already = byFingerprint.get(story.fingerprint);
1008
1024
  if (!already) warnOnDivergentSameTitleStory(story, idsByTitle);
1009
1025
  if (already) {
@@ -1088,8 +1104,8 @@ export async function createStoryIssues({ provider, stories, opts = {} }) {
1088
1104
  * (Story #4541).
1089
1105
  *
1090
1106
  * This is what makes `agent::ready` *mean* "fully persisted": by the time it
1091
- * lands, the Story's `story-plan-state` checkpoint is already on the ticket, so
1092
- * a `/mandrel-deliver` that picks it up cannot read a null checkpoint.
1107
+ * lands, the Story's `story-plan-state` comment is already on the ticket, so a
1108
+ * `/mandrel-deliver` that picks it up always has the plan summary beside it.
1093
1109
  *
1094
1110
  * Fails closed: an un-flipped Story is invisible to `/mandrel-deliver`, which is the
1095
1111
  * safe direction — the operator is told exactly which ids need the label.
@@ -1145,7 +1161,7 @@ export async function markStoriesReady({ provider, created }) {
1145
1161
  if (failed.length > 0) {
1146
1162
  throw new Error(
1147
1163
  `[plan-persist] ${failed.length} Story(ies) were created with their ` +
1148
- 'checkpoints but could not be flipped to agent::ready:\n' +
1164
+ 'plan comments but could not be flipped to agent::ready:\n' +
1149
1165
  `${failed.map((f) => ` - ${f}`).join('\n')}\n` +
1150
1166
  'They are invisible to /mandrel-deliver until the label lands. Re-run persist ' +
1151
1167
  '(it resumes rather than duplicating) or add the label by hand.',
@@ -1,10 +1,14 @@
1
1
  /**
2
2
  * summary.js — plan-persist terminal summary (v2 Stage 3).
3
3
  *
4
- * Upserts a single `plan-summary` structured comment on the primary Story
5
- * at terminal success. Carries the persist receipts — including whether the
6
- * operator forced a review stop — and the dry-run `depends_on` ordering table
7
- * for the rare N>1 plan.
4
+ * Renders the persist receipts — the created Story set, whether the operator
5
+ * forced a review stop, the `depends_on` ordering table and the exact deliver
6
+ * command. Story #5343 moved that rendering onto the **`story-plan-state`**
7
+ * marker every created Story already carries: it used to be a second,
8
+ * primary-Story-only `plan-summary` comment, which cost one more write per
9
+ * plan and split the operator's reading between two markers. Story #5367
10
+ * deleted the machine checkpoint that shared the marker with it, so this
11
+ * rendering is now the whole body of the one comment persist posts.
8
12
  *
9
13
  * Story #4542 removed the risk / review-routing line: no risk level, gate
10
14
  * decision, or acceptance disposition is computed at plan time any more, so
@@ -16,11 +20,6 @@
16
20
  import { computeStoryWaves } from '../dependency-analyzer.js';
17
21
  import { renderPredictedSerialisationLines } from './wave-serialisation.js';
18
22
 
19
- /**
20
- * Structured-comment type for the persist summary.
21
- */
22
- export const PLAN_SUMMARY_COMMENT_TYPE = 'plan-summary';
23
-
24
23
  /**
25
24
  * Compute the dry-run wave assignment for a validated ticket set.
26
25
  *
@@ -114,7 +113,8 @@ function renderSharedEditorLines(conflictFindings) {
114
113
  }
115
114
 
116
115
  /**
117
- * Build the `plan-summary` structured-comment body.
116
+ * Build the body of each Story's `story-plan-state` comment (Story #5343;
117
+ * Story #5367 made it the whole body).
118
118
  *
119
119
  * @param {object} input
120
120
  * @returns {string}
@@ -169,7 +169,7 @@ export function buildPlanSummaryCommentBody({
169
169
  : '/mandrel-deliver <storyId> [<storyId> ...]';
170
170
 
171
171
  return [
172
- `### 📋 Plan Summary — Story #${epicId} is \`agent::ready\``,
172
+ `#### 📋 Plan Summary — Story #${epicId} is \`agent::ready\``,
173
173
  '',
174
174
  `- ${ticketCount} Story ticket(s) persisted: ${storyList}.`,
175
175
  ...reviewLines,