mandrel 2.59.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.
- package/.agents/README.md +11 -9
- package/.agents/agents/acceptance-critic.md +24 -43
- package/.agents/agents/story-worker.md +18 -19
- package/.agents/docs/SDLC.md +6 -6
- package/.agents/docs/agentrc-reference.json +1 -2
- package/.agents/docs/configuration.md +29 -46
- package/.agents/docs/quality-gates.md +8 -4
- package/.agents/docs/workflows.md +1 -1
- package/.agents/instructions.md +4 -5
- package/.agents/rules/ci-remediation.md +41 -8
- package/.agents/rules/known-tooling-behavior.md +65 -15
- package/.agents/schemas/acceptance-eval-verdict.schema.json +1 -1
- package/.agents/schemas/agentrc.schema.json +6 -11
- package/.agents/schemas/story-deliver-terminal.schema.json +3 -3
- package/.agents/scripts/README.md +11 -1
- package/.agents/scripts/acceptance-eval.js +25 -27
- package/.agents/scripts/ceremony-derive.js +15 -10
- package/.agents/scripts/check-context-budget.js +148 -228
- package/.agents/scripts/check-schema-references.js +5 -3
- package/.agents/scripts/check-workflow-citations.js +33 -147
- package/.agents/scripts/coverage-capture.js +7 -4
- package/.agents/scripts/deliver-light.js +41 -100
- package/.agents/scripts/deliver-run.js +631 -0
- package/.agents/scripts/file-ci-gap.js +59 -11
- package/.agents/scripts/lib/baselines/crap-preview-incremental.js +6 -2
- package/.agents/scripts/lib/changed-files.js +30 -0
- package/.agents/scripts/lib/config/delivery-routing.js +5 -4
- package/.agents/scripts/lib/config/explain.js +1 -3
- package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +1 -1
- package/.agents/scripts/lib/config-resolver.js +1 -0
- package/.agents/scripts/lib/config-settings-schema-delivery.js +28 -21
- package/.agents/scripts/lib/coverage-capture-fullscope.js +10 -2
- package/.agents/scripts/lib/coverage-capture-incremental.js +3 -2
- package/.agents/scripts/lib/coverage-capture-usage.js +4 -1
- package/.agents/scripts/lib/doc-tiers.js +4 -2
- package/.agents/scripts/lib/feedback-loop/graduator-core.js +7 -6
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +7 -5
- package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
- package/.agents/scripts/lib/gh-exec.js +160 -0
- package/.agents/scripts/lib/observability/source-classifier.js +1 -0
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +74 -132
- package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +123 -12
- package/.agents/scripts/lib/orchestration/complexity-gate.js +180 -352
- package/.agents/scripts/lib/orchestration/light-suitability.js +71 -136
- package/.agents/scripts/lib/orchestration/plan-context.js +13 -25
- package/.agents/scripts/lib/orchestration/plan-persist/changes-repair.js +8 -6
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +76 -95
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +35 -18
- package/.agents/scripts/lib/orchestration/plan-persist/summary.js +11 -11
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +63 -29
- package/.agents/scripts/lib/orchestration/review-depth.js +14 -11
- package/.agents/scripts/lib/orchestration/run-epilogue.js +260 -182
- package/.agents/scripts/lib/orchestration/run-scoped-config.js +63 -99
- package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +3 -3
- package/.agents/scripts/lib/orchestration/single-story-close/phases/graphql-preflight.js +137 -0
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +105 -18
- package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +4 -3
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +156 -39
- package/.agents/scripts/lib/orchestration/story-init-envelope.js +71 -0
- package/.agents/scripts/lib/orchestration/task-body-validator.js +8 -17
- package/.agents/scripts/lib/orchestration/ticket-validator.js +44 -183
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +14 -25
- package/.agents/scripts/lib/story-body/body-format-lints.js +58 -12
- package/.agents/scripts/lib/story-body/story-body.js +83 -29
- package/.agents/scripts/lib/templates/decomposer-prompts.js +7 -15
- package/.agents/scripts/lib/wave-runner/live-probe.js +31 -5
- package/.agents/scripts/merge-baseline.js +4 -5
- package/.agents/scripts/plan-context.js +117 -28
- package/.agents/scripts/plan-persist.js +79 -28
- package/.agents/scripts/plan-run-epilogue.js +11 -8
- package/.agents/scripts/pr-watch-with-update.js +9 -2
- package/.agents/scripts/run-verify.js +13 -6
- package/.agents/scripts/single-story-init.js +7 -57
- package/.agents/scripts/stories-wave-tick.js +160 -26
- package/.agents/skills/core/gates-and-baselines/reference.md +0 -1
- package/.agents/skills/skills.index.json +2 -2
- package/.agents/skills/stack/qa/playwright/SKILL.md +26 -0
- package/.agents/workflows/helpers/acceptance-self-eval.md +84 -157
- package/.agents/workflows/helpers/code-review.md +4 -2
- package/.agents/workflows/helpers/deliver-digest.md +31 -24
- package/.agents/workflows/helpers/deliver-light.md +92 -101
- package/.agents/workflows/helpers/deliver-reference.md +116 -100
- package/.agents/workflows/helpers/deliver-story-reference.md +58 -124
- package/.agents/workflows/helpers/deliver-story.md +17 -18
- package/.agents/workflows/helpers/plan-reference.md +65 -54
- package/.agents/workflows/mandrel-deliver.md +47 -31
- package/.agents/workflows/mandrel-plan.md +22 -21
- package/.agents/workflows/mandrel-update.md +36 -21
- package/docs/CHANGELOG.md +35 -0
- package/lib/cli/update.js +376 -17
- package/lib/migrations/index.js +2 -0
- package/lib/migrations/steps/2.60.0-retire-audit-results-autofile.js +40 -0
- package/package.json +2 -1
- package/.agents/schemas/model-attribution.schema.json +0 -53
- package/.agents/scripts/lib/orchestration/model-attribution.js +0 -418
- package/.agents/scripts/lib/orchestration/story-plan-state.js +0 -33
- package/.agents/scripts/lib/orchestration/structured-comment-parser.js +0 -67
|
@@ -10,20 +10,20 @@
|
|
|
10
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
|
|
14
|
-
*
|
|
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 "
|
|
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
|
|
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 —
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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
80
|
import { assertNoWaveCollisions } from './wave-collision-gate.js';
|
|
85
81
|
|
|
86
|
-
/**
|
|
87
|
-
const PLAN_CHECKPOINT_SCHEMA_VERSION_V2 = 2;
|
|
88
|
-
|
|
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
|
|
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 {
|
|
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
|
|
109
|
+
export async function writePlanSummaryComment(provider, storyId, summary) {
|
|
100
110
|
if (!Number.isInteger(storyId)) {
|
|
101
|
-
throw new TypeError('
|
|
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
|
|
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
|
|
201
|
-
*
|
|
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**
|
|
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
|
|
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
|
|
423
|
-
* every created Story to `agent::ready`.
|
|
424
|
-
* `agent::ready` lands last so it can
|
|
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
|
|
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*
|
|
431
|
-
* first `agent::ready` flip is issued, so `ready` still means "fully
|
|
432
|
-
* persisted" and
|
|
433
|
-
*
|
|
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
|
-
|
|
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
|
|
|
@@ -623,18 +601,26 @@ export async function runPlanPersist({
|
|
|
623
601
|
|
|
624
602
|
// Inline Spec fold (Specs stay inline, never under docs/).
|
|
625
603
|
const seedContent = planContextEnvelope?.seed?.content ?? '';
|
|
626
|
-
const { stories: assembled } =
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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);
|
|
638
624
|
|
|
639
625
|
// Stamp the `audit::*` labels the dedup corpus is listed by. Without them a
|
|
640
626
|
// Story this path files is absent from the pool an indexed sweep matches
|
|
@@ -722,12 +708,7 @@ export async function runPlanPersist({
|
|
|
722
708
|
});
|
|
723
709
|
|
|
724
710
|
if (!dryRun) {
|
|
725
|
-
await persistStoryArtifacts({
|
|
726
|
-
provider,
|
|
727
|
-
created,
|
|
728
|
-
primary,
|
|
729
|
-
summaryBody,
|
|
730
|
-
});
|
|
711
|
+
await persistStoryArtifacts({ provider, created, summaryBody });
|
|
731
712
|
}
|
|
732
713
|
|
|
733
714
|
// Story #5139 — the container Epic is created LAST among the writes: its
|
|
@@ -39,8 +39,8 @@ import {
|
|
|
39
39
|
isExternalDependencyRef,
|
|
40
40
|
} from './external-deps.js';
|
|
41
41
|
import {
|
|
42
|
-
assertSupersedePartition,
|
|
43
42
|
normalizeSupersedes,
|
|
43
|
+
resolveSupersedePartition,
|
|
44
44
|
} from './supersede-ops.js';
|
|
45
45
|
|
|
46
46
|
/**
|
|
@@ -157,7 +157,7 @@ const PLAN_FINGERPRINT_LENGTH = 16;
|
|
|
157
157
|
*
|
|
158
158
|
* 1. A later, unrelated plan that reused a slug **and** title adopted the
|
|
159
159
|
* stale open Story — never rewriting its body or Spec, and landing this
|
|
160
|
-
* run's
|
|
160
|
+
* run's plan comments, ready-flip, and supersede comments on the wrong
|
|
161
161
|
* issue.
|
|
162
162
|
* 2. A legitimate resume after the operator edited `stories.json` adopted
|
|
163
163
|
* the pre-edit Story and kept its stale body, discarding the edit.
|
|
@@ -513,9 +513,19 @@ function assertSharedSpecAllowed(tickets, sharedSpec) {
|
|
|
513
513
|
|
|
514
514
|
/**
|
|
515
515
|
* Assemble markdown bodies for every Story: normalize → fold spec →
|
|
516
|
-
*
|
|
516
|
+
* order by dependency → resolveSupersedePartition → serialize.
|
|
517
517
|
*
|
|
518
|
-
* The
|
|
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
|
|
519
529
|
* plan never leaves Stories live against an inconsistent tracker. Story #5332
|
|
520
530
|
* retired the acceptance partition that used to run beside it: it refused
|
|
521
531
|
* only byte-identical acceptance text across siblings, and the split gate is
|
|
@@ -526,7 +536,7 @@ function assertSharedSpecAllowed(tickets, sharedSpec) {
|
|
|
526
536
|
* @param {object} [opts]
|
|
527
537
|
* @param {string|null} [opts.sharedSpec]
|
|
528
538
|
* @param {number[]} [opts.sourceTicketIds] Ids passed to `/mandrel-plan --tickets`.
|
|
529
|
-
* @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[] }}
|
|
530
540
|
*/
|
|
531
541
|
export function assemblePlanStories(tickets, opts = {}) {
|
|
532
542
|
if (!Array.isArray(tickets) || tickets.length === 0) {
|
|
@@ -537,13 +547,16 @@ export function assemblePlanStories(tickets, opts = {}) {
|
|
|
537
547
|
|
|
538
548
|
assertSharedSpecAllowed(tickets, opts.sharedSpec);
|
|
539
549
|
|
|
540
|
-
const stories =
|
|
541
|
-
(ticket) => assembleOnePlanStory(ticket, opts).story,
|
|
550
|
+
const stories = orderStoriesByDependencies(
|
|
551
|
+
tickets.map((ticket) => assembleOnePlanStory(ticket, opts).story),
|
|
542
552
|
);
|
|
543
553
|
|
|
544
|
-
|
|
554
|
+
const warnings = resolveSupersedePartition(
|
|
555
|
+
stories,
|
|
556
|
+
opts.sourceTicketIds ?? [],
|
|
557
|
+
);
|
|
545
558
|
|
|
546
|
-
return { stories };
|
|
559
|
+
return { stories, warnings };
|
|
547
560
|
}
|
|
548
561
|
|
|
549
562
|
function orderStoriesByDependencies(stories) {
|
|
@@ -901,12 +914,13 @@ async function ensurePersistLabel({
|
|
|
901
914
|
* Create Story issues via `provider.createIssue`, resumably.
|
|
902
915
|
*
|
|
903
916
|
* **Stories are born without `agent::ready`** (Story #4541). They used to
|
|
904
|
-
* carry it in the creating POST while the `story-plan-state`
|
|
917
|
+
* carry it in the creating POST while the `story-plan-state` comment was
|
|
905
918
|
* upserted afterwards, so anything that picked a Story up inside that window —
|
|
906
|
-
* or after a comment failure aborted the loop —
|
|
907
|
-
*
|
|
908
|
-
* `
|
|
909
|
-
*
|
|
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.
|
|
910
924
|
*
|
|
911
925
|
* **The loop is resumable, and adoption is content-keyed.** Each body carries
|
|
912
926
|
* a plan-fingerprint marker, and the open `type::story` backlog is indexed by
|
|
@@ -1002,7 +1016,10 @@ export async function createStoryIssues({ provider, stories, opts = {} }) {
|
|
|
1002
1016
|
const created = [];
|
|
1003
1017
|
const idBySlug = new Map();
|
|
1004
1018
|
|
|
1005
|
-
|
|
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) {
|
|
1006
1023
|
const already = byFingerprint.get(story.fingerprint);
|
|
1007
1024
|
if (!already) warnOnDivergentSameTitleStory(story, idsByTitle);
|
|
1008
1025
|
if (already) {
|
|
@@ -1087,8 +1104,8 @@ export async function createStoryIssues({ provider, stories, opts = {} }) {
|
|
|
1087
1104
|
* (Story #4541).
|
|
1088
1105
|
*
|
|
1089
1106
|
* This is what makes `agent::ready` *mean* "fully persisted": by the time it
|
|
1090
|
-
* lands, the Story's `story-plan-state`
|
|
1091
|
-
*
|
|
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.
|
|
1092
1109
|
*
|
|
1093
1110
|
* Fails closed: an un-flipped Story is invisible to `/mandrel-deliver`, which is the
|
|
1094
1111
|
* safe direction — the operator is told exactly which ids need the label.
|
|
@@ -1144,7 +1161,7 @@ export async function markStoriesReady({ provider, created }) {
|
|
|
1144
1161
|
if (failed.length > 0) {
|
|
1145
1162
|
throw new Error(
|
|
1146
1163
|
`[plan-persist] ${failed.length} Story(ies) were created with their ` +
|
|
1147
|
-
'
|
|
1164
|
+
'plan comments but could not be flipped to agent::ready:\n' +
|
|
1148
1165
|
`${failed.map((f) => ` - ${f}`).join('\n')}\n` +
|
|
1149
1166
|
'They are invisible to /mandrel-deliver until the label lands. Re-run persist ' +
|
|
1150
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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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-
|
|
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
|
-
|
|
172
|
+
`#### 📋 Plan Summary — Story #${epicId} is \`agent::ready\``,
|
|
173
173
|
'',
|
|
174
174
|
`- ${ticketCount} Story ticket(s) persisted: ${storyList}.`,
|
|
175
175
|
...reviewLines,
|
|
@@ -15,12 +15,12 @@
|
|
|
15
15
|
*
|
|
16
16
|
* Two halves, deliberately separated by the `createIssue` boundary:
|
|
17
17
|
*
|
|
18
|
-
* 1. **`
|
|
19
|
-
* runs *before* any GitHub write.
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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
|
-
*
|
|
282
|
+
* Index which Story claims each source id.
|
|
283
283
|
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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 — `
|
|
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
|
|
@@ -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})
|
|
79
|
-
* (`
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
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
|
|
89
|
-
*
|
|
90
|
-
* `
|
|
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
|
*
|