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
|
@@ -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
|
|
@@ -36,20 +36,16 @@
|
|
|
36
36
|
* --no-close-superseded Keep the source tickets open (no comment, no
|
|
37
37
|
* close) — for a genuinely partial supersede
|
|
38
38
|
* --dry-run Assemble + validate without GitHub writes
|
|
39
|
-
* --chain-on-clean Fast path (Story #4741; any plan since Story
|
|
40
|
-
* #5312): run the write-free dry-run first, and
|
|
41
|
-
* when it passes clean chain straight into the
|
|
42
|
-
* real persist in the SAME invocation, collapsing
|
|
43
|
-
* the two operator round-trips into one. A
|
|
44
|
-
* dry-run failure stops before any createIssue.
|
|
45
|
-
* Ignored when `--dry-run` is also set
|
|
46
39
|
* --force-review Operator-forced review stop before persist lands
|
|
47
40
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
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.
|
|
53
49
|
*
|
|
54
50
|
* stdout is reserved for the JSON result (Story #2278 discipline, extended to
|
|
55
51
|
* this CLI by Story #4541): `routeAllOutputToStderr()` runs before any
|
|
@@ -82,12 +78,11 @@ import {
|
|
|
82
78
|
} from './lib/orchestration/plan-persist/plan-context-source.js';
|
|
83
79
|
import {
|
|
84
80
|
runPlanPersist,
|
|
85
|
-
|
|
81
|
+
writePlanSummaryComment,
|
|
86
82
|
} from './lib/orchestration/plan-persist/run-plan-persist.js';
|
|
87
83
|
import {
|
|
88
84
|
buildPlanSummaryCommentBody,
|
|
89
85
|
buildWaveTable,
|
|
90
|
-
PLAN_SUMMARY_COMMENT_TYPE,
|
|
91
86
|
} from './lib/orchestration/plan-persist/summary.js';
|
|
92
87
|
import { resolveSourceTicketIds } from './lib/orchestration/plan-persist/supersede-ops.js';
|
|
93
88
|
import { createProvider } from './lib/provider-factory.js';
|
|
@@ -95,9 +90,8 @@ import { createProvider } from './lib/provider-factory.js';
|
|
|
95
90
|
export {
|
|
96
91
|
buildPlanSummaryCommentBody,
|
|
97
92
|
buildWaveTable,
|
|
98
|
-
PLAN_SUMMARY_COMMENT_TYPE,
|
|
99
93
|
runPlanPersist,
|
|
100
|
-
|
|
94
|
+
writePlanSummaryComment,
|
|
101
95
|
};
|
|
102
96
|
|
|
103
97
|
const CLI_OPTIONS = {
|
|
@@ -109,7 +103,6 @@ const CLI_OPTIONS = {
|
|
|
109
103
|
'close-superseded': { type: 'boolean', default: true },
|
|
110
104
|
'no-close-superseded': { type: 'boolean', default: false },
|
|
111
105
|
'dry-run': { type: 'boolean', default: false },
|
|
112
|
-
'chain-on-clean': { type: 'boolean', default: false },
|
|
113
106
|
'force-review': { type: 'boolean', default: false },
|
|
114
107
|
'epic-title': { type: 'string' },
|
|
115
108
|
'epic-goal': { type: 'string' },
|
|
@@ -120,7 +113,7 @@ const USAGE =
|
|
|
120
113
|
'Usage: plan-persist.js --stories <file> ' +
|
|
121
114
|
'[--tech-spec <file>] [--plan-dir <dir>] [--plan-context <file>] ' +
|
|
122
115
|
'[--source-tickets <ids>] [--no-close-superseded] ' +
|
|
123
|
-
'[--dry-run] [--
|
|
116
|
+
'[--dry-run] [--force-review] ' +
|
|
124
117
|
'[--epic-title <text> --epic-goal <text> | --epic <id>]';
|
|
125
118
|
|
|
126
119
|
async function readOptional(filePath, { required }) {
|
|
@@ -320,8 +313,9 @@ async function runPersistInvocation({
|
|
|
320
313
|
}
|
|
321
314
|
|
|
322
315
|
/**
|
|
323
|
-
*
|
|
324
|
-
* 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.
|
|
325
319
|
*
|
|
326
320
|
* Two passes over the **same** loaded artifacts:
|
|
327
321
|
*
|
|
@@ -334,6 +328,14 @@ async function runPersistInvocation({
|
|
|
334
328
|
* to gate this step went with the plan-side lite claim: a clean dry-run
|
|
335
329
|
* is the review the chain exists to fold.
|
|
336
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
|
+
*
|
|
337
339
|
* Exported for tests — this is where the round-trip collapse lives, so a
|
|
338
340
|
* regression here silently re-opens the second operator round-trip (or
|
|
339
341
|
* worse, persists a plan the dry-run never gated).
|
|
@@ -366,6 +368,14 @@ export async function runPersistChain({
|
|
|
366
368
|
metricsSince,
|
|
367
369
|
dryRun: false,
|
|
368
370
|
});
|
|
371
|
+
persistResult.repairs = mergeEvidence(
|
|
372
|
+
dryResult.repairs,
|
|
373
|
+
persistResult.repairs,
|
|
374
|
+
);
|
|
375
|
+
persistResult.warnings = mergeEvidence(
|
|
376
|
+
dryResult.warnings,
|
|
377
|
+
persistResult.warnings,
|
|
378
|
+
);
|
|
369
379
|
persistResult.chain = {
|
|
370
380
|
attempted: true,
|
|
371
381
|
persisted: true,
|
|
@@ -374,6 +384,51 @@ export async function runPersistChain({
|
|
|
374
384
|
return persistResult;
|
|
375
385
|
}
|
|
376
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
|
+
|
|
377
432
|
/**
|
|
378
433
|
* Attach the plan-metrics roll-up for **this** invocation.
|
|
379
434
|
*
|
|
@@ -440,10 +495,7 @@ async function main() {
|
|
|
440
495
|
const paths = resolveInputPaths(values);
|
|
441
496
|
const artifacts = await loadArtifacts(paths);
|
|
442
497
|
|
|
443
|
-
|
|
444
|
-
// (Story #4741). `--dry-run` always wins — an explicit dry-run never writes.
|
|
445
|
-
const useChain =
|
|
446
|
-
values['chain-on-clean'] === true && values['dry-run'] !== true;
|
|
498
|
+
const useChain = shouldChainPersist(values);
|
|
447
499
|
|
|
448
500
|
let result;
|
|
449
501
|
try {
|
|
@@ -482,7 +534,7 @@ runAsCli(import.meta.url, main, {
|
|
|
482
534
|
invocation:
|
|
483
535
|
'node .agents/scripts/plan-persist.js --stories <file> [--tech-spec <file>] [--dry-run] [options]',
|
|
484
536
|
summary:
|
|
485
|
-
'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.',
|
|
486
538
|
flags: [
|
|
487
539
|
['--stories <file>', 'Authored stories.json (required).'],
|
|
488
540
|
['--tech-spec <file>', 'Optional companion techspec.md.'],
|
|
@@ -493,7 +545,6 @@ runAsCli(import.meta.url, main, {
|
|
|
493
545
|
],
|
|
494
546
|
['--source-tickets <ids>', 'Ticket ids this plan supersedes.'],
|
|
495
547
|
['--dry-run', 'Validate and report; create nothing.'],
|
|
496
|
-
['--chain-on-clean', 'Persist immediately when the dry run is clean.'],
|
|
497
548
|
['--no-close-superseded', 'Leave superseded source tickets open.'],
|
|
498
549
|
[
|
|
499
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 };
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Order: audit (SCA) → lint (includes docs:check + the arch-cycles ratchet) →
|
|
9
9
|
* full test suite → unified baselines → the standalone ratchets
|
|
10
|
-
* (dead-exports ×2, context-budget, cyclomatic,
|
|
10
|
+
* (dead-exports ×2, context-budget, workflow-citations, cyclomatic,
|
|
11
|
+
* schema-references).
|
|
11
12
|
*
|
|
12
13
|
* The `audit` step runs `npm audit --audit-level=high`, matching CI's
|
|
13
14
|
* "Dependency Vulnerability Audit (SCA)" gate so a local green no longer hides
|
|
@@ -33,9 +34,14 @@
|
|
|
33
34
|
* being added here, so a green `verify` still hid both. Like their neighbours
|
|
34
35
|
* they are pure-Node and cost milliseconds.
|
|
35
36
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
37
|
+
* Story #5340 closed the `check-workflow-citations.js` gap the same way, once
|
|
38
|
+
* demoting it to a report made it free: it had been exempted here because
|
|
39
|
+
* `tests/check-workflow-citations.test.js` re-ran the same ratchet, so a
|
|
40
|
+
* `verify` step would have double-paid it. That test now asserts the report,
|
|
41
|
+
* not a ceiling, so the step is the only local surface that prints the count.
|
|
42
|
+
*
|
|
43
|
+
* Still NOT mirrored: `check-baseline-scope.js` runs in CI's `baselines` job
|
|
44
|
+
* only — `.agents/rules/known-tooling-behavior.md` entry 2 carries the current
|
|
39
45
|
* coverage table. (`prune-baseline-orphans.js --check` used to sit in that
|
|
40
46
|
* list; it no longer runs in CI at all — the un-attributed duplicate of the
|
|
41
47
|
* scope gate's `extra` direction reds every open PR on inherited rows.) Nor are the CI gates this command structurally cannot
|
|
@@ -50,8 +56,8 @@ import { spawnSync } from 'node:child_process';
|
|
|
50
56
|
import { runAsCli } from './lib/cli-utils.js';
|
|
51
57
|
|
|
52
58
|
/**
|
|
53
|
-
* A gate step: `node .agents/scripts/<script>` plus any extra args.
|
|
54
|
-
* the
|
|
59
|
+
* A gate step: `node .agents/scripts/<script>` plus any extra args. Eight of
|
|
60
|
+
* the eleven steps share exactly that shape, so spelling it once leaves the list
|
|
55
61
|
* below readable as what it actually is — a gate *order* — instead of a wall
|
|
56
62
|
* of spawn tuples.
|
|
57
63
|
*
|
|
@@ -79,6 +85,7 @@ const STEPS = [
|
|
|
79
85
|
gate('dead-exports', 'check-dead-exports.js'),
|
|
80
86
|
gate('dead-exports-production', 'check-dead-exports.js', '--production'),
|
|
81
87
|
gate('context-budget', 'check-context-budget.js'),
|
|
88
|
+
gate('workflow-citations', 'check-workflow-citations.js'),
|
|
82
89
|
gate('cyclomatic', 'check-cyclomatic.js'),
|
|
83
90
|
gate('schema-references', 'check-schema-references.js'),
|
|
84
91
|
];
|
|
@@ -70,7 +70,6 @@ import { handleRemoteVerificationFailure } from './lib/orchestration/story-init-
|
|
|
70
70
|
import {
|
|
71
71
|
STATE_LABELS,
|
|
72
72
|
transitionTicketState,
|
|
73
|
-
upsertStructuredComment,
|
|
74
73
|
} from './lib/orchestration/ticketing.js';
|
|
75
74
|
import { createProvider } from './lib/provider-factory.js';
|
|
76
75
|
import { buildProtectionCtx } from './lib/single-story-sweep/protection-ctx.js';
|
|
@@ -770,28 +769,13 @@ export async function runSingleStoryInit({
|
|
|
770
769
|
remoteProbe: { remoteUrl: remote.remoteUrl, detail: remote.detail },
|
|
771
770
|
};
|
|
772
771
|
|
|
773
|
-
//
|
|
774
|
-
//
|
|
775
|
-
//
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
storyId,
|
|
781
|
-
'story-init',
|
|
782
|
-
renderSingleStoryInitComment(result),
|
|
783
|
-
);
|
|
784
|
-
progress(
|
|
785
|
-
'COMMENT',
|
|
786
|
-
`📝 Upserted story-init structured comment on #${storyId}.`,
|
|
787
|
-
);
|
|
788
|
-
} catch (err) {
|
|
789
|
-
Logger.error(
|
|
790
|
-
`[single-story-init] ⚠️ Failed to upsert story-init structured comment: ${err?.message ?? err}`,
|
|
791
|
-
);
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
|
|
772
|
+
// Story #5343 — init posts no `story-init` comment. It was one GitHub write
|
|
773
|
+
// per Story restating what its own envelope already carries; the envelope is
|
|
774
|
+
// on stdout and on disk (below), `deliver-recover.js` classifies state from
|
|
775
|
+
// labels, the PR probe and disk artifacts, and `run-scoped-config.js` reads
|
|
776
|
+
// the base-branch pin off that same envelope. The `agent::executing` flip
|
|
777
|
+
// above is what makes the claim visible during the install window.
|
|
778
|
+
//
|
|
795
779
|
// Story #4685 — route the full result to a temp log and emit a single-line
|
|
796
780
|
// summary carrying the fields the orchestrating agent acts on (workCwd,
|
|
797
781
|
// remoteVerified). The `## Spec` names this the hot-path stdout to quiet.
|
|
@@ -820,40 +804,6 @@ export async function runSingleStoryInit({
|
|
|
820
804
|
return { success: true, result };
|
|
821
805
|
}
|
|
822
806
|
|
|
823
|
-
export function renderSingleStoryInitComment(result) {
|
|
824
|
-
const payload = {
|
|
825
|
-
storyId: result.storyId,
|
|
826
|
-
epicId: null,
|
|
827
|
-
standalone: true,
|
|
828
|
-
storyBranch: result.storyBranch,
|
|
829
|
-
baseBranch: result.baseBranch,
|
|
830
|
-
runScopedConfig: result.runScopedConfig,
|
|
831
|
-
worktreeEnabled: result.worktreeEnabled,
|
|
832
|
-
workCwd: result.workCwd,
|
|
833
|
-
worktreeCreated: result.worktreeCreated,
|
|
834
|
-
dependenciesInstalled: result.dependenciesInstalled,
|
|
835
|
-
installStatus: result.installStatus,
|
|
836
|
-
remoteVerified: result.remoteVerified,
|
|
837
|
-
remoteProbe: result.remoteProbe,
|
|
838
|
-
};
|
|
839
|
-
return [
|
|
840
|
-
'## Story init (standalone)',
|
|
841
|
-
'',
|
|
842
|
-
`- **standalone:** \`true\``,
|
|
843
|
-
`- **storyBranch:** \`${result.storyBranch}\``,
|
|
844
|
-
`- **baseBranch:** \`${result.baseBranch}\``,
|
|
845
|
-
`- **workCwd:** \`${result.workCwd}\``,
|
|
846
|
-
`- **worktreeEnabled:** \`${result.worktreeEnabled}\``,
|
|
847
|
-
`- **remoteVerified:** \`${result.remoteVerified}\``,
|
|
848
|
-
`- **dependenciesInstalled:** \`${result.dependenciesInstalled}\``,
|
|
849
|
-
'',
|
|
850
|
-
'```json',
|
|
851
|
-
JSON.stringify(payload, null, 2),
|
|
852
|
-
'```',
|
|
853
|
-
'',
|
|
854
|
-
].join('\n');
|
|
855
|
-
}
|
|
856
|
-
|
|
857
807
|
runAsCli(import.meta.url, runSingleStoryInit, {
|
|
858
808
|
source: 'single-story-init',
|
|
859
809
|
usage: {
|