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.
- package/.agents/README.md +17 -12
- package/.agents/agents/acceptance-critic.md +24 -43
- package/.agents/agents/story-worker.md +18 -19
- package/.agents/docs/SDLC.md +12 -13
- package/.agents/docs/agentrc-reference.json +1 -2
- package/.agents/docs/configuration.md +29 -46
- package/.agents/docs/quality-gates.md +9 -5
- package/.agents/docs/workflows.md +1 -1
- package/.agents/instructions.md +5 -7
- package/.agents/rules/ci-remediation.md +41 -8
- package/.agents/rules/known-tooling-behavior.md +65 -15
- package/.agents/runtime-deps.json +7 -2
- package/.agents/schemas/acceptance-eval-verdict.schema.json +1 -1
- package/.agents/schemas/agentrc.schema.json +6 -11
- package/.agents/schemas/crap-baseline.schema.json +1 -1
- package/.agents/schemas/crap-report.schema.json +1 -1
- 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/install-matrix-assert.js +48 -3
- package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +51 -33
- package/.agents/scripts/lib/baselines/crap-preview-incremental.js +6 -2
- package/.agents/scripts/lib/baselines/kinds/_crap-read.js +0 -8
- package/.agents/scripts/lib/baselines/kinds/crap.js +35 -18
- 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/crap-engine.js +2 -2
- package/.agents/scripts/lib/crap-utils.js +21 -5
- package/.agents/scripts/lib/doc-tiers.js +4 -2
- package/.agents/scripts/lib/escomplex-ast-compat.js +39 -17
- package/.agents/scripts/lib/escomplex-kernel.js +298 -0
- 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/maintainability-engine.js +3 -3
- 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 +44 -50
- package/.agents/scripts/lib/orchestration/plan-persist/changes-repair.js +8 -6
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +104 -119
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +41 -25
- 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/plan-persist/wave-collision-gate.js +107 -0
- 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-conflicts.js +25 -209
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +8 -5
- package/.agents/scripts/lib/orchestration/ticket-validator.js +44 -183
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +14 -25
- package/.agents/scripts/lib/runtime-deps/dep-resolution.js +155 -0
- package/.agents/scripts/lib/runtime-deps/ensure-installed.js +44 -9
- package/.agents/scripts/lib/runtime-deps/parser-major.js +110 -0
- package/.agents/scripts/lib/runtime-deps/preflight.js +6 -25
- package/.agents/scripts/lib/runtime-deps/scan-imports.js +46 -1
- package/.agents/scripts/lib/skills/walk-skill-files.js +1 -1
- 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 +28 -33
- 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 -39
- 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 -12
- package/.agents/skills/stack/qa/playwright/SKILL.md +26 -0
- package/.agents/workflows/audit-to-stories.md +14 -11
- 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 +82 -60
- package/.agents/workflows/mandrel-deliver.md +47 -31
- package/.agents/workflows/mandrel-plan.md +32 -30
- package/.agents/workflows/mandrel-update.md +36 -21
- package/README.md +3 -3
- package/docs/CHANGELOG.md +43 -0
- package/lib/cli/registry.js +45 -25
- 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 +8 -2
- package/.agents/schemas/model-attribution.schema.json +0 -53
- package/.agents/scripts/lib/orchestration/model-attribution.js +0 -418
- package/.agents/scripts/lib/orchestration/split-policy-validator.js +0 -188
- package/.agents/scripts/lib/orchestration/story-plan-state.js +0 -33
- package/.agents/scripts/lib/orchestration/structured-comment-parser.js +0 -67
- package/.agents/scripts/lib/templates/spec-author-prompts.js +0 -76
- package/.agents/skills/core/scope-triage/SKILL.md +0 -48
|
@@ -101,22 +101,37 @@ import { classifyStory, storyIdOf } from './ready-set.js';
|
|
|
101
101
|
* Ids outside the probed set are ignored: they are not part of this run and
|
|
102
102
|
* must not consume its cap.
|
|
103
103
|
*
|
|
104
|
+
* The second arm is also reported on its own, as `unlabelled`. A claimed id
|
|
105
|
+
* that still reads `agent::ready` is in flight either because its init is
|
|
106
|
+
* still running or because the spawn that claimed it never started one, and
|
|
107
|
+
* those two are indistinguishable here — but only the second pins the id in
|
|
108
|
+
* the caller's ledger forever. Live state cannot tell them apart; the operator
|
|
109
|
+
* can, so the fact is surfaced rather than acted on (Story #5363).
|
|
110
|
+
*
|
|
104
111
|
* @param {Array<{id?: number, number?: number, labels?: string[], state?: string}>} storyRecords
|
|
105
112
|
* @param {Iterable<number>} [dispatched] Ids the host has spawned.
|
|
106
|
-
* @returns {Set<number>} In-flight Story
|
|
113
|
+
* @returns {{inFlight: Set<number>, unlabelled: Set<number>}} In-flight Story
|
|
114
|
+
* ids, and the subset of them claimed by the caller that live state still
|
|
115
|
+
* reports as `agent::ready`.
|
|
107
116
|
*/
|
|
108
117
|
function deriveInFlightIds(storyRecords, dispatched = []) {
|
|
109
118
|
const claimed = new Set(dispatched);
|
|
110
|
-
|
|
119
|
+
// Bucketed by live class rather than tested twice. The admission rule is
|
|
120
|
+
// unchanged; it is only that the `ready` bucket IS the claimed-but-unlabelled
|
|
121
|
+
// set, since `ready` is the one class the rule admits on the caller's claim.
|
|
122
|
+
const byClass = { executing: new Set(), ready: new Set() };
|
|
111
123
|
for (const rec of storyRecords) {
|
|
112
124
|
const id = storyIdOf(rec);
|
|
113
125
|
if (id === null) continue;
|
|
114
126
|
const cls = classifyStory(rec);
|
|
115
127
|
if (cls === 'executing' || (claimed.has(id) && cls === 'ready')) {
|
|
116
|
-
|
|
128
|
+
byClass[cls].add(id);
|
|
117
129
|
}
|
|
118
130
|
}
|
|
119
|
-
return
|
|
131
|
+
return {
|
|
132
|
+
inFlight: new Set([...byClass.executing, ...byClass.ready]),
|
|
133
|
+
unlabelled: byClass.ready,
|
|
134
|
+
};
|
|
120
135
|
}
|
|
121
136
|
|
|
122
137
|
/**
|
|
@@ -251,6 +266,7 @@ export function createProbeContext({
|
|
|
251
266
|
* doneIds: Set<number>,
|
|
252
267
|
* inFlight: number,
|
|
253
268
|
* blockedIds: number[],
|
|
269
|
+
* stalledDispatch: number[],
|
|
254
270
|
* foreignHeld: Array<{id: number, holder: string}>
|
|
255
271
|
* }>}
|
|
256
272
|
*/
|
|
@@ -294,7 +310,10 @@ export async function probeLiveState({
|
|
|
294
310
|
// They are consumed in-process by `planReadySet` and never serialized into
|
|
295
311
|
// the beat envelope, which stays a list of ids.
|
|
296
312
|
const bodyById = new Map(stories.map((s) => [s.id, s.body ?? '']));
|
|
297
|
-
const inFlightIds = deriveInFlightIds(
|
|
313
|
+
const { inFlight: inFlightIds, unlabelled } = deriveInFlightIds(
|
|
314
|
+
stories,
|
|
315
|
+
dispatched,
|
|
316
|
+
);
|
|
298
317
|
// A Story another operator's lease holds occupies a (global) dispatch slot
|
|
299
318
|
// just like an in-flight one: fold it into the in-flight set so it is both
|
|
300
319
|
// withheld (via the projected label) and excluded from a false wedge, but
|
|
@@ -322,6 +341,13 @@ export async function probeLiveState({
|
|
|
322
341
|
doneIds: new Set(envelope.done),
|
|
323
342
|
inFlight: inFlightIds.size,
|
|
324
343
|
blockedIds: deriveBlockedIds(stories),
|
|
344
|
+
// Claimed by the caller, still labelled `agent::ready`: a live init window
|
|
345
|
+
// or a spawn that never reached one. Reported, never released here. A
|
|
346
|
+
// foreign lease is its own withhold reason and outranks the claim, so it
|
|
347
|
+
// is subtracted — one Story must not carry two recoveries.
|
|
348
|
+
stalledDispatch: [...unlabelled]
|
|
349
|
+
.filter((id) => !foreignHeld.has(id))
|
|
350
|
+
.sort((a, b) => a - b),
|
|
325
351
|
foreignHeld: [...foreignHeld].map(([id, holder]) => ({ id, holder })),
|
|
326
352
|
};
|
|
327
353
|
}
|
|
@@ -36,9 +36,8 @@
|
|
|
36
36
|
*
|
|
37
37
|
* ## Not every `baselines/*.json` is an envelope
|
|
38
38
|
*
|
|
39
|
-
* That glob also matches arch-cycles, cyclomatic, dead-exports, audit-ledger
|
|
40
|
-
* context-budget
|
|
41
|
-
* row identity. Anything whose `$schema` is not a known per-kind envelope is
|
|
39
|
+
* That glob also matches arch-cycles, cyclomatic, dead-exports, audit-ledger
|
|
40
|
+
* and context-budget — files with their own shapes and no row identity. Anything whose `$schema` is not a known per-kind envelope is
|
|
42
41
|
* handed straight back to `git merge-file`, so registering the driver cannot
|
|
43
42
|
* change their behaviour.
|
|
44
43
|
*/
|
|
@@ -164,8 +163,8 @@ function delegateToGit(basePath, oursPath, theirsPath) {
|
|
|
164
163
|
* `git merge-file` even though `.gitattributes` routes them here, so the
|
|
165
164
|
* attribute promised a row merge the driver never performed.
|
|
166
165
|
*
|
|
167
|
-
* Anything else — arch-cycles, audit-ledger, context-budget
|
|
168
|
-
*
|
|
166
|
+
* Anything else — arch-cycles, audit-ledger, context-budget — resolves `null`
|
|
167
|
+
* and is handed back to git unchanged.
|
|
169
168
|
*
|
|
170
169
|
* @param {unknown} ours
|
|
171
170
|
* @param {unknown} theirs
|
|
@@ -24,21 +24,22 @@
|
|
|
24
24
|
* scratch (Story #4741). Envelope carries `amends`.
|
|
25
25
|
*
|
|
26
26
|
* Flags:
|
|
27
|
-
* --out <path>
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
27
|
+
* --out <path> Override where the envelope is written (parent dirs
|
|
28
|
+
* created). **Optional since Story #5342** — with no
|
|
29
|
+
* `--out` the envelope lands at
|
|
30
|
+
* `<tempRoot>/plan-<slug>/plan-context.json`, the plan
|
|
31
|
+
* directory `/mandrel-plan` would have named by hand, and
|
|
32
|
+
* `stories.template.json` lands beside it. That is where
|
|
33
|
+
* `plan-persist.js` auto-discovers the `--tickets` source
|
|
34
|
+
* ids from (Story #4554); without a captured envelope
|
|
35
|
+
* persist cannot know a `--tickets` run happened, and
|
|
36
|
+
* superseding degrades to the `--source-tickets` flag.
|
|
37
|
+
* --pretty Pretty-print the written JSON envelope.
|
|
37
38
|
*
|
|
38
39
|
* stdout is reserved for a single JSON payload (Story #2278 discipline) —
|
|
39
|
-
* the
|
|
40
|
-
* `routeAllOutputToStderr()` runs before any
|
|
41
|
-
* is unconditionally parseable by `JSON.parse`.
|
|
40
|
+
* the compact digest naming the written artifacts (Story #4708
|
|
41
|
+
* script-output contract): `routeAllOutputToStderr()` runs before any
|
|
42
|
+
* pipeline code so the stream is unconditionally parseable by `JSON.parse`.
|
|
42
43
|
*
|
|
43
44
|
* Exit codes:
|
|
44
45
|
* 0 — envelope emitted.
|
|
@@ -54,6 +55,8 @@ import path from 'node:path';
|
|
|
54
55
|
import { parseArgs } from 'node:util';
|
|
55
56
|
import { runAsCli } from './lib/cli-utils.js';
|
|
56
57
|
import {
|
|
58
|
+
getPaths,
|
|
59
|
+
PROJECT_ROOT,
|
|
57
60
|
resolveConfig,
|
|
58
61
|
validateOrchestrationConfig,
|
|
59
62
|
} from './lib/config-resolver.js';
|
|
@@ -66,6 +69,77 @@ import {
|
|
|
66
69
|
import { recordPlanInvocation } from './lib/orchestration/plan-metrics.js';
|
|
67
70
|
import { createProvider } from './lib/provider-factory.js';
|
|
68
71
|
|
|
72
|
+
/** Longest slug segment a default plan directory carries. */
|
|
73
|
+
const PLAN_SLUG_MAX_LENGTH = 48;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Reduce free text to the hyphen-case segment a plan directory is named by.
|
|
77
|
+
*
|
|
78
|
+
* Deliberately lossy: the slug is a human-readable handle on a temp
|
|
79
|
+
* directory, not an identity — two runs from the same seed land in the same
|
|
80
|
+
* directory and the second overwrites the first, which is the idempotent
|
|
81
|
+
* behaviour the operator already got from typing the same `--out` twice.
|
|
82
|
+
*
|
|
83
|
+
* @param {string} raw
|
|
84
|
+
* @returns {string} A non-empty hyphen-case slug (`plan` when nothing survives).
|
|
85
|
+
*/
|
|
86
|
+
export function slugifyPlanLabel(raw) {
|
|
87
|
+
const slug = String(raw ?? '')
|
|
88
|
+
.toLowerCase()
|
|
89
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
90
|
+
.replace(/^-+|-+$/g, '')
|
|
91
|
+
.slice(0, PLAN_SLUG_MAX_LENGTH)
|
|
92
|
+
.replace(/-+$/, '');
|
|
93
|
+
return slug === '' ? 'plan' : slug;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Resolve where the envelope is written when the operator passed no `--out`
|
|
98
|
+
* (Story #5342).
|
|
99
|
+
*
|
|
100
|
+
* `--out` was mandatory in practice and optional in the CLI: persist
|
|
101
|
+
* auto-discovers the envelope and the `stories.template.json` beside it from
|
|
102
|
+
* the plan directory, so a run without it silently lost superseding and the
|
|
103
|
+
* authoring skeleton. The path it always pointed at is derivable — the
|
|
104
|
+
* configured `tempRoot`, a `plan-<slug>` directory named for what is being
|
|
105
|
+
* planned — so the CLI derives it rather than asking.
|
|
106
|
+
*
|
|
107
|
+
* Exported for tests: this is the join where a missing flag stops costing
|
|
108
|
+
* the plan its source ids.
|
|
109
|
+
*
|
|
110
|
+
* @param {object} args
|
|
111
|
+
* @param {string} args.mode One of `seed` | `seed-file` | `tickets` | `amends`.
|
|
112
|
+
* @param {string} [args.seedText]
|
|
113
|
+
* @param {string} [args.seedFilePath]
|
|
114
|
+
* @param {number[]} [args.ticketIds]
|
|
115
|
+
* @param {number} [args.amendsId]
|
|
116
|
+
* @param {object} [args.config] Resolved config (for `project.paths.tempRoot`).
|
|
117
|
+
* @param {string} [args.cwd]
|
|
118
|
+
* @returns {string} Absolute path to the envelope file.
|
|
119
|
+
*/
|
|
120
|
+
export function resolveDefaultOutPath({
|
|
121
|
+
mode,
|
|
122
|
+
seedText,
|
|
123
|
+
seedFilePath,
|
|
124
|
+
ticketIds,
|
|
125
|
+
amendsId,
|
|
126
|
+
config,
|
|
127
|
+
cwd = PROJECT_ROOT,
|
|
128
|
+
}) {
|
|
129
|
+
const byMode = {
|
|
130
|
+
amends: () => `amends-${amendsId}`,
|
|
131
|
+
tickets: () => `tickets-${(ticketIds ?? []).join('-')}`,
|
|
132
|
+
'seed-file': () => path.parse(String(seedFilePath ?? '')).name,
|
|
133
|
+
};
|
|
134
|
+
const label = (byMode[mode] ?? (() => String(seedText ?? '')))();
|
|
135
|
+
return path.resolve(
|
|
136
|
+
cwd,
|
|
137
|
+
getPaths(config).tempRoot,
|
|
138
|
+
`plan-${slugifyPlanLabel(label)}`,
|
|
139
|
+
'plan-context.json',
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
69
143
|
/**
|
|
70
144
|
* Parse a comma-/space-separated ticket id list into positive integers.
|
|
71
145
|
*
|
|
@@ -109,8 +183,10 @@ export function parseAmendsId(raw) {
|
|
|
109
183
|
}
|
|
110
184
|
|
|
111
185
|
/**
|
|
112
|
-
* Build the envelope
|
|
113
|
-
*
|
|
186
|
+
* Build the envelope, write it (plus the `stories.template.json` skeleton)
|
|
187
|
+
* to `outPath` — or to the derived default when none was passed
|
|
188
|
+
* (Story #5342) — and print the compact digest on stdout. Exported for
|
|
189
|
+
* tests.
|
|
114
190
|
*
|
|
115
191
|
* @param {object} args
|
|
116
192
|
* @returns {Promise<object>} the emitted envelope.
|
|
@@ -145,14 +221,26 @@ export async function emitPlanContext({
|
|
|
145
221
|
const json = pretty
|
|
146
222
|
? JSON.stringify(envelope, null, 2)
|
|
147
223
|
: JSON.stringify(envelope);
|
|
148
|
-
|
|
224
|
+
const resolvedOut =
|
|
225
|
+
outPath ??
|
|
226
|
+
resolveDefaultOutPath({
|
|
227
|
+
mode,
|
|
228
|
+
seedText,
|
|
229
|
+
seedFilePath,
|
|
230
|
+
ticketIds,
|
|
231
|
+
amendsId,
|
|
232
|
+
config,
|
|
233
|
+
cwd: cwd ?? undefined,
|
|
234
|
+
});
|
|
235
|
+
{
|
|
149
236
|
// Script-output contract (Story #4708, AC-5): the full envelope is a
|
|
150
237
|
// ~40KB artifact that would ride resident in the transcript for every
|
|
151
|
-
// later turn.
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
await
|
|
155
|
-
|
|
238
|
+
// later turn. It is always captured to disk (Story #5342 derives the
|
|
239
|
+
// path when `--out` is absent), so stdout carries a compact digest
|
|
240
|
+
// naming the artifacts instead of the payload itself.
|
|
241
|
+
await writeEnvelopeFile(resolvedOut, json);
|
|
242
|
+
await writeStoriesTemplateFile(resolvedOut, envelope);
|
|
243
|
+
const resolved = path.resolve(resolvedOut);
|
|
156
244
|
const digest = {
|
|
157
245
|
digest: 'plan-context',
|
|
158
246
|
mode: envelope.mode,
|
|
@@ -182,8 +270,6 @@ export async function emitPlanContext({
|
|
|
182
270
|
amends: envelope.amends ? { id: envelope.amends.id } : null,
|
|
183
271
|
};
|
|
184
272
|
stdout.write(`${JSON.stringify(digest)}\n`);
|
|
185
|
-
} else {
|
|
186
|
-
stdout.write(`${json}\n`);
|
|
187
273
|
}
|
|
188
274
|
return envelope;
|
|
189
275
|
}
|
|
@@ -216,8 +302,8 @@ async function writeEnvelopeFile(outPath, json) {
|
|
|
216
302
|
* Emit the ready-to-fill Story authoring template next to the captured
|
|
217
303
|
* envelope (Story #4707 — one-shot authoring). The planner copies it to
|
|
218
304
|
* `stories.json` and fills the placeholders; no step of the authoring path
|
|
219
|
-
* requires reading `story-body.js` source. Written
|
|
220
|
-
*
|
|
305
|
+
* requires reading `story-body.js` source. Written on every run (Story
|
|
306
|
+
* #5342), and throwing on failure for the same reason the envelope write
|
|
221
307
|
* does: a silently missing template re-opens the format-discovery loop it
|
|
222
308
|
* exists to close. The envelope's advisory `complexitySignals` are threaded
|
|
223
309
|
* through so the skeleton's `changes[]` arrive pre-resolved to
|
|
@@ -346,14 +432,17 @@ runAsCli(import.meta.url, main, {
|
|
|
346
432
|
invocation:
|
|
347
433
|
'node .agents/scripts/plan-context.js (--seed "<text>" | --seed-file <path> | --tickets <ids> | --amends <id>) [--out <path>] [--pretty]',
|
|
348
434
|
summary:
|
|
349
|
-
'Build the /mandrel-plan authoring-context envelope on stdout. Exactly one entry form must be supplied.',
|
|
435
|
+
'Build the /mandrel-plan authoring-context envelope. Writes it (and stories.template.json) under <tempRoot>/plan-<slug>/ and prints the digest on stdout. Exactly one entry form must be supplied.',
|
|
350
436
|
flags: [
|
|
351
437
|
['--seed "<text>"', 'Inline seed prose.'],
|
|
352
438
|
['--seed-file <path>', 'Seed document to read.'],
|
|
353
439
|
['--tickets <ids>', 'Comma-separated existing ticket ids to re-plan.'],
|
|
354
440
|
['--amends <id>', 'Amend the Spec of an existing Story.'],
|
|
355
|
-
[
|
|
356
|
-
|
|
441
|
+
[
|
|
442
|
+
'--out <path>',
|
|
443
|
+
'Override the derived <tempRoot>/plan-<slug>/plan-context.json path.',
|
|
444
|
+
],
|
|
445
|
+
['--pretty', 'Pretty-print the written JSON envelope.'],
|
|
357
446
|
],
|
|
358
447
|
},
|
|
359
448
|
});
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* changes[] repair → ticket validator / DAG → reachability →
|
|
11
11
|
* split-policy partition → fold Spec into each Story body →
|
|
12
12
|
* createIssue(s) with type::story, resumably by plan fingerprint (NOT
|
|
13
|
-
* agent::ready) → story-plan-state
|
|
14
|
-
*
|
|
13
|
+
* agent::ready) → one `story-plan-state` comment (the plan summary) on
|
|
14
|
+
* every Story → flip every Story to agent::ready →
|
|
15
15
|
* comment + close superseded source tickets → temp cleanup + stale reap.
|
|
16
16
|
*
|
|
17
17
|
* Story #4542 retired the authored risk verdict: persist neither requires nor
|
|
@@ -29,7 +29,6 @@
|
|
|
29
29
|
* --plan-context <file> Optional explicit path to the `plan-context.js`
|
|
30
30
|
* envelope. Its `sourceTickets[]` is what makes
|
|
31
31
|
* `--tickets` superseding work without a flag
|
|
32
|
-
* --plan-acceptance <file> Optional JSON string[] for partition coverage
|
|
33
32
|
* --source-tickets <ids> Explicit OVERRIDE of the envelope-derived source
|
|
34
33
|
* ids, for hand-driven runs. Each id must be
|
|
35
34
|
* claimed by exactly one Story's `supersedes[]`;
|
|
@@ -37,20 +36,16 @@
|
|
|
37
36
|
* --no-close-superseded Keep the source tickets open (no comment, no
|
|
38
37
|
* close) — for a genuinely partial supersede
|
|
39
38
|
* --dry-run Assemble + validate without GitHub writes
|
|
40
|
-
* --chain-on-clean Fast path (Story #4741; any plan since Story
|
|
41
|
-
* #5312): run the write-free dry-run first, and
|
|
42
|
-
* when it passes clean chain straight into the
|
|
43
|
-
* real persist in the SAME invocation, collapsing
|
|
44
|
-
* the two operator round-trips into one. A
|
|
45
|
-
* dry-run failure stops before any createIssue.
|
|
46
|
-
* Ignored when `--dry-run` is also set
|
|
47
39
|
* --force-review Operator-forced review stop before persist lands
|
|
48
40
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
41
|
+
* **Persist is one command (Story #5342).** Without `--dry-run` the CLI runs
|
|
42
|
+
* the write-free dry-run first — the
|
|
43
|
+
* changes[] repair, the validator, DAG, reachability, split/supersede
|
|
44
|
+
* partition, Spec fold — and, when the gate list comes back clean, chains
|
|
45
|
+
* straight into the real persist in the SAME invocation. A dry-run failure
|
|
46
|
+
* stops before any `createIssue`, and the run lists every warning (a
|
|
47
|
+
* footprint probe that disagrees with the base branch, an empty `verify[]`,
|
|
48
|
+
* an open question in a body) either way. `--dry-run` still creates nothing.
|
|
54
49
|
*
|
|
55
50
|
* stdout is reserved for the JSON result (Story #2278 discipline, extended to
|
|
56
51
|
* this CLI by Story #4541): `routeAllOutputToStderr()` runs before any
|
|
@@ -83,12 +78,11 @@ import {
|
|
|
83
78
|
} from './lib/orchestration/plan-persist/plan-context-source.js';
|
|
84
79
|
import {
|
|
85
80
|
runPlanPersist,
|
|
86
|
-
|
|
81
|
+
writePlanSummaryComment,
|
|
87
82
|
} from './lib/orchestration/plan-persist/run-plan-persist.js';
|
|
88
83
|
import {
|
|
89
84
|
buildPlanSummaryCommentBody,
|
|
90
85
|
buildWaveTable,
|
|
91
|
-
PLAN_SUMMARY_COMMENT_TYPE,
|
|
92
86
|
} from './lib/orchestration/plan-persist/summary.js';
|
|
93
87
|
import { resolveSourceTicketIds } from './lib/orchestration/plan-persist/supersede-ops.js';
|
|
94
88
|
import { createProvider } from './lib/provider-factory.js';
|
|
@@ -96,9 +90,8 @@ import { createProvider } from './lib/provider-factory.js';
|
|
|
96
90
|
export {
|
|
97
91
|
buildPlanSummaryCommentBody,
|
|
98
92
|
buildWaveTable,
|
|
99
|
-
PLAN_SUMMARY_COMMENT_TYPE,
|
|
100
93
|
runPlanPersist,
|
|
101
|
-
|
|
94
|
+
writePlanSummaryComment,
|
|
102
95
|
};
|
|
103
96
|
|
|
104
97
|
const CLI_OPTIONS = {
|
|
@@ -106,12 +99,10 @@ const CLI_OPTIONS = {
|
|
|
106
99
|
'tech-spec': { type: 'string' },
|
|
107
100
|
'plan-dir': { type: 'string' },
|
|
108
101
|
'plan-context': { type: 'string' },
|
|
109
|
-
'plan-acceptance': { type: 'string' },
|
|
110
102
|
'source-tickets': { type: 'string' },
|
|
111
103
|
'close-superseded': { type: 'boolean', default: true },
|
|
112
104
|
'no-close-superseded': { type: 'boolean', default: false },
|
|
113
105
|
'dry-run': { type: 'boolean', default: false },
|
|
114
|
-
'chain-on-clean': { type: 'boolean', default: false },
|
|
115
106
|
'force-review': { type: 'boolean', default: false },
|
|
116
107
|
'epic-title': { type: 'string' },
|
|
117
108
|
'epic-goal': { type: 'string' },
|
|
@@ -121,9 +112,8 @@ const CLI_OPTIONS = {
|
|
|
121
112
|
const USAGE =
|
|
122
113
|
'Usage: plan-persist.js --stories <file> ' +
|
|
123
114
|
'[--tech-spec <file>] [--plan-dir <dir>] [--plan-context <file>] ' +
|
|
124
|
-
'[--plan-acceptance <file>] ' +
|
|
125
115
|
'[--source-tickets <ids>] [--no-close-superseded] ' +
|
|
126
|
-
'[--dry-run] [--
|
|
116
|
+
'[--dry-run] [--force-review] ' +
|
|
127
117
|
'[--epic-title <text> --epic-goal <text> | --epic <id>]';
|
|
128
118
|
|
|
129
119
|
async function readOptional(filePath, { required }) {
|
|
@@ -159,9 +149,6 @@ export function resolveInputPaths(values) {
|
|
|
159
149
|
techSpecPath: values['tech-spec']
|
|
160
150
|
? path.resolve(values['tech-spec'])
|
|
161
151
|
: null,
|
|
162
|
-
planAcceptancePath: values['plan-acceptance']
|
|
163
|
-
? path.resolve(values['plan-acceptance'])
|
|
164
|
-
: null,
|
|
165
152
|
planDir,
|
|
166
153
|
planContextPath: resolvePlanContextPath(values['plan-context'], planDir),
|
|
167
154
|
};
|
|
@@ -172,9 +159,6 @@ async function loadArtifacts(paths) {
|
|
|
172
159
|
const techSpecContent = paths.techSpecPath
|
|
173
160
|
? await readOptional(paths.techSpecPath, { required: true })
|
|
174
161
|
: null;
|
|
175
|
-
const planAcceptance = paths.planAcceptancePath
|
|
176
|
-
? await readJsonFile(paths.planAcceptancePath, 'plan-acceptance')
|
|
177
|
-
: null;
|
|
178
162
|
const planContextEnvelope = await loadPlanContextEnvelope(
|
|
179
163
|
paths.planContextPath,
|
|
180
164
|
);
|
|
@@ -182,7 +166,6 @@ async function loadArtifacts(paths) {
|
|
|
182
166
|
return {
|
|
183
167
|
stories,
|
|
184
168
|
techSpecContent,
|
|
185
|
-
planAcceptance,
|
|
186
169
|
planContextEnvelope,
|
|
187
170
|
};
|
|
188
171
|
}
|
|
@@ -330,8 +313,9 @@ async function runPersistInvocation({
|
|
|
330
313
|
}
|
|
331
314
|
|
|
332
315
|
/**
|
|
333
|
-
*
|
|
334
|
-
* chain a clean dry-run into
|
|
316
|
+
* The default persist path (Story #4741 AC-1/AC-3; widened to any plan by
|
|
317
|
+
* Story #5312; made the default by Story #5342): chain a clean dry-run into
|
|
318
|
+
* the real persist in ONE operator invocation.
|
|
335
319
|
*
|
|
336
320
|
* Two passes over the **same** loaded artifacts:
|
|
337
321
|
*
|
|
@@ -344,6 +328,14 @@ async function runPersistInvocation({
|
|
|
344
328
|
* to gate this step went with the plan-side lite claim: a clean dry-run
|
|
345
329
|
* is the review the chain exists to fold.
|
|
346
330
|
*
|
|
331
|
+
* The caller reads the **second** pass's envelope, so the first pass's
|
|
332
|
+
* evidence has to be carried onto it (Story #5361). The repair pass mutates
|
|
333
|
+
* the loaded tickets in place, which is what makes replaying the identical
|
|
334
|
+
* artifacts possible at all — and it is also why pass 2 recomputes an empty
|
|
335
|
+
* `repairs[]`: by then there is nothing left to repair. The evidence is
|
|
336
|
+
* preserved rather than re-derived, because re-running the repair pass would
|
|
337
|
+
* report repairs the persisting pass did not make.
|
|
338
|
+
*
|
|
347
339
|
* Exported for tests — this is where the round-trip collapse lives, so a
|
|
348
340
|
* regression here silently re-opens the second operator round-trip (or
|
|
349
341
|
* worse, persists a plan the dry-run never gated).
|
|
@@ -376,6 +368,14 @@ export async function runPersistChain({
|
|
|
376
368
|
metricsSince,
|
|
377
369
|
dryRun: false,
|
|
378
370
|
});
|
|
371
|
+
persistResult.repairs = mergeEvidence(
|
|
372
|
+
dryResult.repairs,
|
|
373
|
+
persistResult.repairs,
|
|
374
|
+
);
|
|
375
|
+
persistResult.warnings = mergeEvidence(
|
|
376
|
+
dryResult.warnings,
|
|
377
|
+
persistResult.warnings,
|
|
378
|
+
);
|
|
379
379
|
persistResult.chain = {
|
|
380
380
|
attempted: true,
|
|
381
381
|
persisted: true,
|
|
@@ -384,6 +384,51 @@ export async function runPersistChain({
|
|
|
384
384
|
return persistResult;
|
|
385
385
|
}
|
|
386
386
|
|
|
387
|
+
/**
|
|
388
|
+
* Union two evidence lists, dry-run first, dropping an entry the second pass
|
|
389
|
+
* reported identically. Order is the operator's reading order; the dedupe is
|
|
390
|
+
* by rendered content because a repair is a plain record and a warning is a
|
|
391
|
+
* string, so two passes that noticed the same thing noticed it byte-for-byte.
|
|
392
|
+
*
|
|
393
|
+
* @param {unknown} first
|
|
394
|
+
* @param {unknown} second
|
|
395
|
+
* @returns {unknown[]}
|
|
396
|
+
*/
|
|
397
|
+
function mergeEvidence(first, second) {
|
|
398
|
+
const merged = [];
|
|
399
|
+
const seen = new Set();
|
|
400
|
+
for (const entry of [
|
|
401
|
+
...(Array.isArray(first) ? first : []),
|
|
402
|
+
...(Array.isArray(second) ? second : []),
|
|
403
|
+
]) {
|
|
404
|
+
const key = JSON.stringify(entry);
|
|
405
|
+
if (seen.has(key)) continue;
|
|
406
|
+
seen.add(key);
|
|
407
|
+
merged.push(entry);
|
|
408
|
+
}
|
|
409
|
+
return merged;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Decide whether this invocation persists after its gates, or only validates.
|
|
414
|
+
*
|
|
415
|
+
* Story #5342: chaining is the default, not a flag. Every invocation that is
|
|
416
|
+
* not an explicit `--dry-run` runs the gate list and then persists what it
|
|
417
|
+
* passed, so the two operator round-trips collapse without anyone having to
|
|
418
|
+
* remember an opt-in. Story #5361 removed the no-op alias Story #5342 had
|
|
419
|
+
* kept for existing call-sites, rather than accepting and ignoring it: a flag
|
|
420
|
+
* that cannot change an outcome is a shim, and `parseArgs` refuses an unknown
|
|
421
|
+
* option, so passing it now fails loudly instead of reading as honoured.
|
|
422
|
+
*
|
|
423
|
+
* Exported for tests: this one predicate is what makes the CLI one command.
|
|
424
|
+
*
|
|
425
|
+
* @param {object} values Parsed `parseArgs` values.
|
|
426
|
+
* @returns {boolean} `true` to run the gates and then persist.
|
|
427
|
+
*/
|
|
428
|
+
export function shouldChainPersist(values) {
|
|
429
|
+
return values?.['dry-run'] !== true;
|
|
430
|
+
}
|
|
431
|
+
|
|
387
432
|
/**
|
|
388
433
|
* Attach the plan-metrics roll-up for **this** invocation.
|
|
389
434
|
*
|
|
@@ -450,10 +495,7 @@ async function main() {
|
|
|
450
495
|
const paths = resolveInputPaths(values);
|
|
451
496
|
const artifacts = await loadArtifacts(paths);
|
|
452
497
|
|
|
453
|
-
|
|
454
|
-
// (Story #4741). `--dry-run` always wins — an explicit dry-run never writes.
|
|
455
|
-
const useChain =
|
|
456
|
-
values['chain-on-clean'] === true && values['dry-run'] !== true;
|
|
498
|
+
const useChain = shouldChainPersist(values);
|
|
457
499
|
|
|
458
500
|
let result;
|
|
459
501
|
try {
|
|
@@ -492,7 +534,7 @@ runAsCli(import.meta.url, main, {
|
|
|
492
534
|
invocation:
|
|
493
535
|
'node .agents/scripts/plan-persist.js --stories <file> [--tech-spec <file>] [--dry-run] [options]',
|
|
494
536
|
summary:
|
|
495
|
-
'Validate an authored plan and persist it as GitHub Stories. Prints the result envelope as JSON on stdout.',
|
|
537
|
+
'Validate an authored plan and persist it as GitHub Stories in one invocation (pass --dry-run to validate only). Prints the result envelope as JSON on stdout.',
|
|
496
538
|
flags: [
|
|
497
539
|
['--stories <file>', 'Authored stories.json (required).'],
|
|
498
540
|
['--tech-spec <file>', 'Optional companion techspec.md.'],
|
|
@@ -501,10 +543,8 @@ runAsCli(import.meta.url, main, {
|
|
|
501
543
|
'--plan-context <file>',
|
|
502
544
|
'The plan-context envelope this draft was authored against.',
|
|
503
545
|
],
|
|
504
|
-
['--plan-acceptance <file>', 'Acceptance artifact to attach.'],
|
|
505
546
|
['--source-tickets <ids>', 'Ticket ids this plan supersedes.'],
|
|
506
547
|
['--dry-run', 'Validate and report; create nothing.'],
|
|
507
|
-
['--chain-on-clean', 'Persist immediately when the dry run is clean.'],
|
|
508
548
|
['--no-close-superseded', 'Leave superseded source tickets open.'],
|
|
509
549
|
[
|
|
510
550
|
'--force-review',
|
|
@@ -25,6 +25,8 @@ import { expandIdList } from './lib/util/parse-id-list.js';
|
|
|
25
25
|
const CLI_OPTIONS = {
|
|
26
26
|
stories: { type: 'string' },
|
|
27
27
|
cwd: { type: 'string' },
|
|
28
|
+
/** Story #5343 — opt-in; see `run-epilogue.js` RUN_EPILOGUE_STEP_KINDS. */
|
|
29
|
+
'audit-roster': { type: 'boolean', default: false },
|
|
28
30
|
};
|
|
29
31
|
|
|
30
32
|
/**
|
|
@@ -51,15 +53,10 @@ export async function main(argv = process.argv.slice(2), deps = {}) {
|
|
|
51
53
|
options: CLI_OPTIONS,
|
|
52
54
|
strict: false,
|
|
53
55
|
});
|
|
54
|
-
|
|
55
|
-
typeof values.stories === 'string' && values.stories.trim().length > 0;
|
|
56
|
-
if (!hasStoriesFlag) {
|
|
56
|
+
if (typeof values.stories !== 'string' || !values.stories.trim()) {
|
|
57
57
|
throw new Error('Usage: node plan-run-epilogue.js --stories 1,2,3');
|
|
58
58
|
}
|
|
59
|
-
const cwd =
|
|
60
|
-
typeof values.cwd === 'string' && values.cwd.trim()
|
|
61
|
-
? values.cwd.trim()
|
|
62
|
-
: process.cwd();
|
|
59
|
+
const cwd = values.cwd?.trim() || process.cwd();
|
|
63
60
|
const config = resolveConfigImpl({ cwd });
|
|
64
61
|
const provider = createProviderImpl(config);
|
|
65
62
|
|
|
@@ -87,6 +84,7 @@ export async function main(argv = process.argv.slice(2), deps = {}) {
|
|
|
87
84
|
provider,
|
|
88
85
|
config,
|
|
89
86
|
cwd,
|
|
87
|
+
auditRoster: values['audit-roster'] === true,
|
|
90
88
|
});
|
|
91
89
|
warnOnUnresolvedBase(result, logger);
|
|
92
90
|
warnOnEmptyRollup(result, logger);
|
|
@@ -99,6 +97,7 @@ export async function main(argv = process.argv.slice(2), deps = {}) {
|
|
|
99
97
|
|
|
100
98
|
/**
|
|
101
99
|
* Surface an unresolvable combined landed diff as a loud operator warning.
|
|
100
|
+
* Only reachable under `--audit-roster`: a default run enumerates no roster.
|
|
102
101
|
*
|
|
103
102
|
* The roster's changed-file set is the input the host walks its audit lenses
|
|
104
103
|
* against; a silent absence would read as "nothing changed" and the lens walk
|
|
@@ -167,7 +166,7 @@ function warnOnEmptyRollup(result, logger = Logger) {
|
|
|
167
166
|
await runAsCli(import.meta.url, main, {
|
|
168
167
|
usage: {
|
|
169
168
|
invocation:
|
|
170
|
-
'node .agents/scripts/plan-run-epilogue.js --stories <id,id,...> [--cwd <path>]',
|
|
169
|
+
'node .agents/scripts/plan-run-epilogue.js --stories <id,id,...> [--cwd <path>] [--audit-roster]',
|
|
171
170
|
summary:
|
|
172
171
|
'Close out a delivery run: roll up the delivered Stories’ signals and report the run’s loop health.',
|
|
173
172
|
flags: [
|
|
@@ -176,6 +175,10 @@ await runAsCli(import.meta.url, main, {
|
|
|
176
175
|
'Comma-separated delivered Story ids, singles or A-B ranges (required).',
|
|
177
176
|
],
|
|
178
177
|
['--cwd <path>', 'Repository root (default: process cwd).'],
|
|
178
|
+
[
|
|
179
|
+
'--audit-roster',
|
|
180
|
+
'Also select the cross-Story audit lens roster and post plan-run-audit-roster (off by default; the host then walks every listed lens).',
|
|
181
|
+
],
|
|
179
182
|
],
|
|
180
183
|
},
|
|
181
184
|
});
|
|
@@ -458,12 +458,19 @@ async function evaluateGreenWatch({
|
|
|
458
458
|
}
|
|
459
459
|
const headSha = headShaFn({ prRef, cwd });
|
|
460
460
|
const { verdict, reason } = classifyGreenVerdict({ digest, headSha });
|
|
461
|
-
if (verdict === 'fix-at-source') {
|
|
461
|
+
if (verdict === 'fix-at-source' || verdict === 'rerun-permitted') {
|
|
462
|
+
// Story #5343 — `rerun-permitted` is the one same-SHA green the rule
|
|
463
|
+
// admits: `file-ci-gap.js` recorded a proven `capacity` /
|
|
464
|
+
// `unreproducible-tier` verdict for THIS head, so there is no fix at
|
|
465
|
+
// source to make. Retiring the digest spends the allowance with it, which
|
|
466
|
+
// is what makes it exactly one: a second red writes a fresh digest that
|
|
467
|
+
// carries none.
|
|
462
468
|
retireDigestFn({ storyId, tempRoot, cwd });
|
|
463
469
|
const reArm = await reArmFn({ cwd, prNumber });
|
|
464
470
|
const reArmed = Boolean(reArm?.enabled);
|
|
465
471
|
logger.info?.(
|
|
466
|
-
`[pr-watch] green on
|
|
472
|
+
`[pr-watch] ${verdict === 'rerun-permitted' ? 'green admitted on the SAME head SHA' : 'green on a NEW head SHA'} ` +
|
|
473
|
+
`(${reason}) — digest retired, ` +
|
|
467
474
|
`auto-merge ${reArmed ? 're-armed' : `NOT re-armed (${reArm?.reason ?? 'unknown'})`}.`,
|
|
468
475
|
);
|
|
469
476
|
return { verdict, reason, exitCode: 0, headSha, reArmed };
|