mandrel 2.11.0 → 2.13.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/docs/workflows.md +2 -1
- package/.agents/rules/orchestration-error-handling.md +9 -1
- package/.agents/scripts/deliver-light.js +385 -0
- package/.agents/scripts/lib/audit-suite/audit-rules-reader.js +48 -0
- package/.agents/scripts/lib/audit-suite/selector.js +1 -26
- package/.agents/scripts/lib/orchestration/complexity-gate.js +68 -17
- package/.agents/scripts/lib/orchestration/light-suitability.js +439 -0
- package/.agents/scripts/lib/orchestration/plan-context.js +256 -15
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +6 -0
- package/.agents/scripts/lib/orchestration/resolve-stories.js +18 -1
- package/.agents/scripts/lib/orchestration/single-story-close/gate-log.js +186 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +21 -3
- package/.agents/scripts/plan-context.js +45 -3
- package/.agents/scripts/plan-persist.js +106 -9
- package/.agents/workflows/deliver-light.md +117 -0
- package/.agents/workflows/deliver.md +27 -29
- package/.agents/workflows/helpers/deliver-digest.md +126 -0
- package/.agents/workflows/helpers/deliver-reference.md +21 -0
- package/.agents/workflows/helpers/deliver-story-reference.md +6 -0
- package/.agents/workflows/helpers/deliver-story.md +31 -35
- package/.agents/workflows/plan.md +66 -84
- package/docs/CHANGELOG.md +20 -0
- package/package.json +1 -1
|
@@ -17,6 +17,12 @@
|
|
|
17
17
|
* --tickets 123[,456…] Analyze existing issue(s) into proper
|
|
18
18
|
* Stories. Envelope carries `sourceTickets[]`.
|
|
19
19
|
*
|
|
20
|
+
* --amends 123 | #123 Amendment (delta) planning. Composes a DELTA
|
|
21
|
+
* envelope from the prior Story's body, its
|
|
22
|
+
* acceptance criteria, and its delivered file map
|
|
23
|
+
* instead of re-interrogating the repo from
|
|
24
|
+
* scratch (Story #4741). Envelope carries `amends`.
|
|
25
|
+
*
|
|
20
26
|
* Flags:
|
|
21
27
|
* --out <path> Write the envelope to <path> (parent dirs created).
|
|
22
28
|
* `/plan` points this at `<plan-dir>/plan-context.json`,
|
|
@@ -83,6 +89,25 @@ export function parseTicketIds(raw) {
|
|
|
83
89
|
return [...new Set(ids)];
|
|
84
90
|
}
|
|
85
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Parse a single `--amends` id, tolerating a leading `#` (`#123` or `123`).
|
|
94
|
+
*
|
|
95
|
+
* @param {string} raw
|
|
96
|
+
* @returns {number}
|
|
97
|
+
*/
|
|
98
|
+
export function parseAmendsId(raw) {
|
|
99
|
+
if (typeof raw !== 'string' || raw.trim().length === 0) {
|
|
100
|
+
throw new Error('--amends requires a single prior Story id.');
|
|
101
|
+
}
|
|
102
|
+
const id = Number(raw.trim().replace(/^#/, ''));
|
|
103
|
+
if (!Number.isInteger(id) || id <= 0) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`--amends expects a positive integer Story id; got ${JSON.stringify(raw)}`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
return id;
|
|
109
|
+
}
|
|
110
|
+
|
|
86
111
|
/**
|
|
87
112
|
* Build the envelope and write it to `stdout` as a single JSON line
|
|
88
113
|
* (or pretty-printed with --pretty). Exported for tests.
|
|
@@ -96,6 +121,7 @@ export async function emitPlanContext({
|
|
|
96
121
|
seedFileContent,
|
|
97
122
|
seedText,
|
|
98
123
|
ticketIds,
|
|
124
|
+
amendsId,
|
|
99
125
|
provider,
|
|
100
126
|
config,
|
|
101
127
|
settings,
|
|
@@ -110,6 +136,7 @@ export async function emitPlanContext({
|
|
|
110
136
|
seedFileContent,
|
|
111
137
|
seedText,
|
|
112
138
|
ticketIds,
|
|
139
|
+
amendsId,
|
|
113
140
|
provider,
|
|
114
141
|
config,
|
|
115
142
|
settings,
|
|
@@ -139,14 +166,19 @@ export async function emitPlanContext({
|
|
|
139
166
|
duplicates: (envelope.duplicates ?? []).length,
|
|
140
167
|
// Advisory only (Story #4722): signals, no route — the planner owns
|
|
141
168
|
// the trivial-vs-standard verdict and persist validates it by shape.
|
|
169
|
+
// The nested `deliverLightSuggestion` is the recorded plan-side routing
|
|
170
|
+
// handshake (Story #4741 AC-6) — advisory, never an automatic reroute.
|
|
142
171
|
complexitySignals: envelope.complexitySignals
|
|
143
172
|
? {
|
|
144
173
|
artifactCount: envelope.complexitySignals.artifactCount,
|
|
145
174
|
riskHeuristicHits: envelope.complexitySignals.riskHeuristicHits,
|
|
146
175
|
sensitivePathClasses:
|
|
147
176
|
envelope.complexitySignals.sensitivePathClasses,
|
|
177
|
+
deliverLightSuggestion:
|
|
178
|
+
envelope.complexitySignals.deliverLightSuggestion ?? null,
|
|
148
179
|
}
|
|
149
180
|
: null,
|
|
181
|
+
amends: envelope.amends ? { id: envelope.amends.id } : null,
|
|
150
182
|
};
|
|
151
183
|
stdout.write(`${JSON.stringify(digest)}\n`);
|
|
152
184
|
} else {
|
|
@@ -221,6 +253,7 @@ async function main() {
|
|
|
221
253
|
seed: { type: 'string' },
|
|
222
254
|
'seed-file': { type: 'string' },
|
|
223
255
|
tickets: { type: 'string' },
|
|
256
|
+
amends: { type: 'string' },
|
|
224
257
|
out: { type: 'string' },
|
|
225
258
|
pretty: { type: 'boolean', default: false },
|
|
226
259
|
},
|
|
@@ -234,16 +267,24 @@ async function main() {
|
|
|
234
267
|
typeof seedFilePath === 'string' && seedFilePath.length > 0;
|
|
235
268
|
const hasTickets =
|
|
236
269
|
typeof values.tickets === 'string' && values.tickets.trim().length > 0;
|
|
237
|
-
const
|
|
270
|
+
const hasAmends =
|
|
271
|
+
typeof values.amends === 'string' && values.amends.trim().length > 0;
|
|
272
|
+
const entryForms = [hasSeed, hasSeedFile, hasTickets, hasAmends].filter(
|
|
273
|
+
Boolean,
|
|
274
|
+
).length;
|
|
238
275
|
if (entryForms !== 1) {
|
|
239
276
|
throw new Error(
|
|
240
|
-
'Pass exactly one of --seed "<text>", --seed-file <path>, or --
|
|
277
|
+
'Pass exactly one of --seed "<text>", --seed-file <path>, --tickets <ids>, or --amends <id>.',
|
|
241
278
|
);
|
|
242
279
|
}
|
|
243
280
|
|
|
244
281
|
let mode;
|
|
245
282
|
let ticketIds;
|
|
246
|
-
|
|
283
|
+
let amendsId;
|
|
284
|
+
if (hasAmends) {
|
|
285
|
+
mode = 'amends';
|
|
286
|
+
amendsId = parseAmendsId(values.amends);
|
|
287
|
+
} else if (hasTickets) {
|
|
247
288
|
mode = 'tickets';
|
|
248
289
|
ticketIds = parseTicketIds(values.tickets);
|
|
249
290
|
} else if (hasSeedFile) {
|
|
@@ -288,6 +329,7 @@ async function main() {
|
|
|
288
329
|
seedFilePath: hasSeedFile ? seedFilePath : undefined,
|
|
289
330
|
seedText: hasSeed ? seedText : undefined,
|
|
290
331
|
ticketIds,
|
|
332
|
+
amendsId,
|
|
291
333
|
provider,
|
|
292
334
|
config,
|
|
293
335
|
settings,
|
|
@@ -44,6 +44,15 @@
|
|
|
44
44
|
* --no-close-superseded Keep the source tickets open (no comment, no
|
|
45
45
|
* close) — for a genuinely partial supersede
|
|
46
46
|
* --dry-run Assemble + validate without GitHub writes
|
|
47
|
+
* --chain-on-clean Plan-diet fast path (Story #4741): run the
|
|
48
|
+
* write-free dry-run first, and — only when it
|
|
49
|
+
* passes clean AND the plan resolves to the `lite`
|
|
50
|
+
* route — chain straight into the real persist in
|
|
51
|
+
* the SAME invocation, collapsing the two operator
|
|
52
|
+
* round-trips into one. A dry-run failure stops
|
|
53
|
+
* before any createIssue; a full-route plan keeps
|
|
54
|
+
* its review round-trip (the chain declines, no
|
|
55
|
+
* writes). Ignored when `--dry-run` is also set
|
|
47
56
|
* --force-review Operator-forced review stop before persist lands
|
|
48
57
|
* --allow-over-budget / --allow-large-fan-out
|
|
49
58
|
*
|
|
@@ -111,6 +120,7 @@ const CLI_OPTIONS = {
|
|
|
111
120
|
'close-superseded': { type: 'boolean', default: true },
|
|
112
121
|
'no-close-superseded': { type: 'boolean', default: false },
|
|
113
122
|
'dry-run': { type: 'boolean', default: false },
|
|
123
|
+
'chain-on-clean': { type: 'boolean', default: false },
|
|
114
124
|
'force-review': { type: 'boolean', default: false },
|
|
115
125
|
'allow-over-budget': { type: 'boolean', default: false },
|
|
116
126
|
'allow-large-fan-out': { type: 'boolean', default: false },
|
|
@@ -122,7 +132,7 @@ const USAGE =
|
|
|
122
132
|
'[--plan-acceptance <file>] ' +
|
|
123
133
|
'[--source-tickets <ids>] [--no-close-superseded] ' +
|
|
124
134
|
'[--route-downgrade-reason <text>] ' +
|
|
125
|
-
'[--dry-run] [--force-review] ' +
|
|
135
|
+
'[--dry-run] [--chain-on-clean] [--force-review] ' +
|
|
126
136
|
'[--allow-over-budget] [--allow-large-fan-out]';
|
|
127
137
|
|
|
128
138
|
async function readOptional(filePath, { required }) {
|
|
@@ -229,8 +239,11 @@ async function runPersistInvocation({
|
|
|
229
239
|
provider,
|
|
230
240
|
artifacts,
|
|
231
241
|
metricsSince,
|
|
242
|
+
dryRun,
|
|
232
243
|
}) {
|
|
233
244
|
const paths = resolveInputPaths(values);
|
|
245
|
+
const effectiveDryRun =
|
|
246
|
+
typeof dryRun === 'boolean' ? dryRun : values['dry-run'] === true;
|
|
234
247
|
const settings = {
|
|
235
248
|
baseBranch: config.project?.baseBranch,
|
|
236
249
|
paths: config.project?.paths,
|
|
@@ -241,7 +254,7 @@ async function runPersistInvocation({
|
|
|
241
254
|
return recordPlanInvocation(
|
|
242
255
|
{
|
|
243
256
|
cli: 'plan-persist',
|
|
244
|
-
mode:
|
|
257
|
+
mode: effectiveDryRun ? 'dry-run' : 'persist',
|
|
245
258
|
config,
|
|
246
259
|
},
|
|
247
260
|
() =>
|
|
@@ -252,12 +265,83 @@ async function runPersistInvocation({
|
|
|
252
265
|
settings,
|
|
253
266
|
opts: {
|
|
254
267
|
...buildPersistOptions(values, paths, artifacts.planContextEnvelope),
|
|
268
|
+
dryRun: effectiveDryRun,
|
|
269
|
+
skipCleanup: effectiveDryRun,
|
|
255
270
|
metricsSince,
|
|
256
271
|
},
|
|
257
272
|
}),
|
|
258
273
|
);
|
|
259
274
|
}
|
|
260
275
|
|
|
276
|
+
/**
|
|
277
|
+
* Plan-diet fast path (Story #4741 AC-1/AC-3): chain the lite dry-run into the
|
|
278
|
+
* real persist in ONE operator invocation.
|
|
279
|
+
*
|
|
280
|
+
* Two passes over the **same** loaded artifacts:
|
|
281
|
+
*
|
|
282
|
+
* 1. A write-free dry-run. Every gate runs before any `createIssue` can
|
|
283
|
+
* happen, so a validation failure — which throws or returns reachability
|
|
284
|
+
* orphans — stops here, before a single issue exists (AC-3).
|
|
285
|
+
* 2. The real write, run **only** when the dry-run passed clean AND resolved
|
|
286
|
+
* to the `lite` route. Because it replays the identical artifacts, the
|
|
287
|
+
* persisted output is byte-identical to what the dry-run validated
|
|
288
|
+
* (AC-1). A full-route plan keeps its review round-trip: the chain
|
|
289
|
+
* declines and returns the dry-run result, mutating nothing.
|
|
290
|
+
*
|
|
291
|
+
* Exported for tests — this is where the round-trip collapse and its
|
|
292
|
+
* fail-closed guard live, so a regression here silently re-opens the second
|
|
293
|
+
* operator round-trip (or worse, persists a plan the dry-run never gated).
|
|
294
|
+
*
|
|
295
|
+
* @param {{ values: object, config: object, provider: object,
|
|
296
|
+
* artifacts: object, metricsSince: string }} args
|
|
297
|
+
* @returns {Promise<object>} the persist result, plus a `chain` receipt.
|
|
298
|
+
*/
|
|
299
|
+
export async function runPersistChain({
|
|
300
|
+
values,
|
|
301
|
+
config,
|
|
302
|
+
provider,
|
|
303
|
+
artifacts,
|
|
304
|
+
metricsSince,
|
|
305
|
+
}) {
|
|
306
|
+
const dryResult = await runPersistInvocation({
|
|
307
|
+
values,
|
|
308
|
+
config,
|
|
309
|
+
provider,
|
|
310
|
+
artifacts,
|
|
311
|
+
metricsSince,
|
|
312
|
+
dryRun: true,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
if (dryResult.route?.route !== 'lite') {
|
|
316
|
+
dryResult.chain = {
|
|
317
|
+
attempted: true,
|
|
318
|
+
persisted: false,
|
|
319
|
+
reason: 'route-not-lite',
|
|
320
|
+
};
|
|
321
|
+
Logger.info(
|
|
322
|
+
'[plan-persist] --chain-on-clean: dry-run clean but the plan did not ' +
|
|
323
|
+
'resolve to the lite route — declining the auto-persist; run persist ' +
|
|
324
|
+
'explicitly after review.',
|
|
325
|
+
);
|
|
326
|
+
return dryResult;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const persistResult = await runPersistInvocation({
|
|
330
|
+
values,
|
|
331
|
+
config,
|
|
332
|
+
provider,
|
|
333
|
+
artifacts,
|
|
334
|
+
metricsSince,
|
|
335
|
+
dryRun: false,
|
|
336
|
+
});
|
|
337
|
+
persistResult.chain = {
|
|
338
|
+
attempted: true,
|
|
339
|
+
persisted: true,
|
|
340
|
+
reason: 'lite-dry-run-clean',
|
|
341
|
+
};
|
|
342
|
+
return persistResult;
|
|
343
|
+
}
|
|
344
|
+
|
|
261
345
|
/**
|
|
262
346
|
* Attach the plan-metrics roll-up for **this** invocation.
|
|
263
347
|
*
|
|
@@ -318,15 +402,28 @@ async function main() {
|
|
|
318
402
|
const paths = resolveInputPaths(values);
|
|
319
403
|
const artifacts = await loadArtifacts(paths);
|
|
320
404
|
|
|
405
|
+
// `--chain-on-clean` collapses the dry-run + persist operator round-trips
|
|
406
|
+
// (Story #4741). `--dry-run` always wins — an explicit dry-run never writes.
|
|
407
|
+
const useChain =
|
|
408
|
+
values['chain-on-clean'] === true && values['dry-run'] !== true;
|
|
409
|
+
|
|
321
410
|
let result;
|
|
322
411
|
try {
|
|
323
|
-
result =
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
412
|
+
result = useChain
|
|
413
|
+
? await runPersistChain({
|
|
414
|
+
values,
|
|
415
|
+
config,
|
|
416
|
+
provider,
|
|
417
|
+
artifacts,
|
|
418
|
+
metricsSince,
|
|
419
|
+
})
|
|
420
|
+
: await runPersistInvocation({
|
|
421
|
+
values,
|
|
422
|
+
config,
|
|
423
|
+
provider,
|
|
424
|
+
artifacts,
|
|
425
|
+
metricsSince,
|
|
426
|
+
});
|
|
330
427
|
} catch (err) {
|
|
331
428
|
if (err?.code === 'PLAN_REACHABILITY_ORPHANS') {
|
|
332
429
|
process.stdout.write(`${err.message}\n`);
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
---
|
|
2
|
+
description:
|
|
3
|
+
Single-session delivery for genuinely small work. Judges a prompt's
|
|
4
|
+
predicted footprint, authors a receipt Story, then lands it through the same
|
|
5
|
+
single-story-init / single-story-close engine — every close gate unchanged.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# /deliver-light "<prompt>" | --amends '#<id>' "<prompt>"
|
|
9
|
+
|
|
10
|
+
> **Thin entry point, not a second engine.** `/deliver-light` removes the
|
|
11
|
+
> `/plan` session for small work — nothing else. It runs a suitability gate,
|
|
12
|
+
> authors a minimal receipt Story, then hands off to the SAME scripts
|
|
13
|
+
> [`/deliver`](deliver.md) uses. Read
|
|
14
|
+
> [`helpers/deliver-digest.md`](helpers/deliver-digest.md) once first — the
|
|
15
|
+
> engine invariants, gates, and terminal-envelope contract below are its.
|
|
16
|
+
|
|
17
|
+
## Role
|
|
18
|
+
|
|
19
|
+
For a genuinely trivial change — a one-file fix, a small addition, a small
|
|
20
|
+
amendment — the multi-session plan→deliver ceremony buys nothing the bare model
|
|
21
|
+
lacks except **gates and landing**. `/deliver-light` keeps exactly those: one
|
|
22
|
+
session straight to execution from an operator prompt, landing through the
|
|
23
|
+
unchanged close path. It never relaxes a close gate, never bypasses the PR to
|
|
24
|
+
`main`, and never lands over-scope work silently.
|
|
25
|
+
|
|
26
|
+
## Four invariants (do not skip one)
|
|
27
|
+
|
|
28
|
+
1. **Suitability gate.** The prompt's predicted footprint is judged by the
|
|
29
|
+
shared shape machinery (`deriveStoryShape` / `deriveChangeLevel`) **and** a
|
|
30
|
+
ledgered model verdict with a recorded reason. Both must agree on `lite`.
|
|
31
|
+
2. **Over-scope stops — it never hard-fails.** An over-ceiling prompt STOPS and
|
|
32
|
+
asks the operator to escalate to `/plan` or proceed light. Under `--yes` it
|
|
33
|
+
fails closed to recommending `/plan`.
|
|
34
|
+
3. **Diff-derived backstop.** After implementation the ACTUAL change set is
|
|
35
|
+
re-checked — the diff is the real scope signal — and an over-ceiling diff is
|
|
36
|
+
blocked rather than landed.
|
|
37
|
+
4. **Minimal receipt Story.** A `type::story` is authored inline so `refs #`,
|
|
38
|
+
history, telemetry, and the `agent::executing → agent::done` state machine
|
|
39
|
+
all survive.
|
|
40
|
+
|
|
41
|
+
## Procedure
|
|
42
|
+
|
|
43
|
+
1. **Predict + gate.** Form the predicted footprint (new files, edited files,
|
|
44
|
+
acceptance count) and your ledgered verdict (a recorded reason for `lite`),
|
|
45
|
+
then run the gate:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
node .agents/scripts/deliver-light.js --prompt "<prompt>" \
|
|
49
|
+
--creates <csv> --refactors <csv> --acceptance <n> \
|
|
50
|
+
--route lite --reason "<why this is trivial>" [--amends '#<id>'] [--yes]
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Branch on `action` in the JSON envelope:
|
|
54
|
+
- **`proceed-light`** — the receipt Story is authored; read `storyId` and
|
|
55
|
+
`nextCommands`. Continue to step 2.
|
|
56
|
+
- **`ask-operator`** — predicted scope exceeds the light ceilings. STOP and
|
|
57
|
+
ask the operator to escalate to `/plan` or proceed light. Do not proceed
|
|
58
|
+
on your own.
|
|
59
|
+
- **`escalate-plan`** — over-scope under `--yes`: recommend `/plan` and stop.
|
|
60
|
+
`/deliver-light` never lands over-scope work.
|
|
61
|
+
|
|
62
|
+
`--amends '#<id>'` is the canonical light case — shape-checked identically; a
|
|
63
|
+
heavy amendment escalates to `/plan` like any other over-scope prompt.
|
|
64
|
+
|
|
65
|
+
2. **Init (same engine).** From the main checkout, synchronously, with the
|
|
66
|
+
maximum Bash timeout:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
node .agents/scripts/single-story-init.js --story <storyId>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Capture `workCwd`; `remoteVerified: false` → flip `agent::blocked` and stop.
|
|
73
|
+
This is [`/deliver`](deliver.md)'s worktree/branch/lease/label engine,
|
|
74
|
+
invoked, not reimplemented.
|
|
75
|
+
|
|
76
|
+
3. **Implement + self-eval.** `cd` into `workCwd`, implement the change, run
|
|
77
|
+
`npm test` once in the worktree, then run the bounded acceptance self-eval
|
|
78
|
+
loop ([`helpers/deliver-story.md`](helpers/deliver-story.md) Step 1a). Commit
|
|
79
|
+
on `story-<id>` with `(refs #<storyId>)`.
|
|
80
|
+
|
|
81
|
+
4. **Diff backstop.** Before close, re-check the ACTUAL diff:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
node .agents/scripts/deliver-light.js --backstop --story <storyId>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Exit `3` (`blocked: true`) means the landed diff exceeds the light ceilings
|
|
88
|
+
(file count or a sensitive-path class). STOP, flip `agent::blocked`, and
|
|
89
|
+
escalate to `/plan` — do not land.
|
|
90
|
+
|
|
91
|
+
5. **Close and land (same engine).** Exactly [`/deliver`](deliver.md)'s close:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
node .agents/scripts/single-story-close.js --story <storyId> --cwd <main-repo>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Branch on the terminal envelope's `status` per
|
|
98
|
+
[`helpers/deliver-digest.md`](helpers/deliver-digest.md) § 5 — every close
|
|
99
|
+
gate runs byte-identical to the full path.
|
|
100
|
+
|
|
101
|
+
## Constraints
|
|
102
|
+
|
|
103
|
+
- **Land or block — never a silent local build.** The close push is the only
|
|
104
|
+
sanctioned landing.
|
|
105
|
+
- **No parallel engine.** `/deliver-light` invokes `single-story-init.js` and
|
|
106
|
+
`single-story-close.js`; it never reimplements worktree, branch, PR, or merge
|
|
107
|
+
mechanics.
|
|
108
|
+
- **State only via `update-ticket-state.js`.** Drive every `agent::*`
|
|
109
|
+
transition through the script; report state, not process.
|
|
110
|
+
|
|
111
|
+
## See also
|
|
112
|
+
|
|
113
|
+
- [`/deliver`](deliver.md) — the multi-Story / planned delivery entry point.
|
|
114
|
+
- [`helpers/deliver-story.md`](helpers/deliver-story.md) — the one Story
|
|
115
|
+
delivery engine both entry points share.
|
|
116
|
+
- [`helpers/deliver-digest.md`](helpers/deliver-digest.md) — engine invariants,
|
|
117
|
+
gates, and the terminal-envelope contract.
|
|
@@ -10,7 +10,9 @@ description:
|
|
|
10
10
|
> **Lean spine.** Happy path + gate list. Sequencing edge cases, dispatch
|
|
11
11
|
> mechanics, lite-route inline execution, checklist threading, ceremony, and
|
|
12
12
|
> the per-run epilogue live in the on-demand
|
|
13
|
-
> [`helpers/deliver-reference.md`](helpers/deliver-reference.md).
|
|
13
|
+
> [`helpers/deliver-reference.md`](helpers/deliver-reference.md). What every
|
|
14
|
+
> delivery always needs is bundled into one read:
|
|
15
|
+
> [`helpers/deliver-digest.md`](helpers/deliver-digest.md) (Story #4736).
|
|
14
16
|
|
|
15
17
|
## Role
|
|
16
18
|
|
|
@@ -20,20 +22,22 @@ owns input resolution and sequencing only — every Story runs through
|
|
|
20
22
|
`epic/<id>` integration branch, no `--no-ff` wave merges.
|
|
21
23
|
|
|
22
24
|
The dependency graph is **discovered, not declared**: `resolve-stories.js`
|
|
23
|
-
reads it from live state (body edges ∪ native GitHub `blocked_by` edges,
|
|
24
|
-
blocker resolved against its real issue state). You never hand it a graph
|
|
25
|
+
reads it from live state (body edges ∪ native GitHub `blocked_by` edges, each
|
|
26
|
+
blocker resolved against its real issue state). You never hand it a graph and
|
|
25
27
|
there is no batch label — which is what lets you deliver Stories **across plan
|
|
26
|
-
runs and over time**.
|
|
27
|
-
|
|
28
|
+
runs and over time**. `plan-run::<id>` is filter metadata, never a resolution
|
|
29
|
+
input.
|
|
28
30
|
Per-Story routes are **body-derived** too (#4722); `route::lite` is a hint
|
|
29
|
-
only.
|
|
31
|
+
only. Ahead of that: a **single-Story run runs the engine inline** whatever the
|
|
32
|
+
shape (#4736) — sub-agent isolation only earns its cost against a concurrent
|
|
33
|
+
sibling.
|
|
30
34
|
|
|
31
35
|
## Inputs
|
|
32
36
|
|
|
33
37
|
| Invocation | Behavior |
|
|
34
38
|
| --- | --- |
|
|
35
|
-
| `/deliver <storyId>` | Deliver one Story via `helpers/deliver-story.md
|
|
36
|
-
| `/deliver <storyId> <storyId> ...` | Resolve the set with `resolve-stories.js`, then sequence by the discovered graph via `stories-wave-tick.js
|
|
39
|
+
| `/deliver <storyId>` | Deliver one Story via `helpers/deliver-story.md`, executed **inline in this session** — no `story-worker` spawn. |
|
|
40
|
+
| `/deliver <storyId> <storyId> ...` | Resolve the set with `resolve-stories.js`, then sequence by the discovered graph via `stories-wave-tick.js`, dispatching role-scoped sub-agents. Default concurrency is **3**. |
|
|
37
41
|
|
|
38
42
|
Any named ticket that is not `type::story`, or still carrying an `Epic: #N`
|
|
39
43
|
footer, is a **hard error** naming the id and the fix (close or re-plan as a v2
|
|
@@ -43,18 +47,16 @@ Story). Resolution refuses the whole set rather than silently under-delivering.
|
|
|
43
47
|
|
|
44
48
|
| Flag | Meaning |
|
|
45
49
|
| --- | --- |
|
|
46
|
-
| `--concurrency <n>` | **Optional** per-run override of the fan-out cap. Omit it to honor `delivery.deliverRunner.concurrencyCap` (config default **3**,
|
|
50
|
+
| `--concurrency <n>` | **Optional** per-run override of the fan-out cap. Omit it to honor `delivery.deliverRunner.concurrencyCap` (config default **3**, incl. any `.agentrc.local.json` override); pass **only** for a one-run cap. `1` = sequential. |
|
|
47
51
|
| `--yes` | Suppress the multi-Story confirmation gate. |
|
|
48
52
|
| `--steal` | Forwarded to `single-story-init.js` / lease steal. |
|
|
49
53
|
| `--wait-merge` | Force close-and-land (the default; `delivery.routing.closeAndLand`). |
|
|
50
54
|
| `--no-wait-merge` | Opt out; stop at `agent::closing` for a human land. |
|
|
51
55
|
|
|
52
56
|
**Operator-merge implies no-wait.** `--no-auto-merge` and
|
|
53
|
-
`delivery.ci.autoMerge: "strict"`
|
|
54
|
-
`agent::
|
|
55
|
-
(
|
|
56
|
-
still waits and still blocks, because that is a fault to report, not an operator
|
|
57
|
-
decision to respect.
|
|
57
|
+
`delivery.ci.autoMerge: "strict"` rest the Story at `agent::closing`, not
|
|
58
|
+
`agent::blocked` — a genuine *arm failure* still waits and still blocks
|
|
59
|
+
([`helpers/deliver-reference.md` § Operator-merge](helpers/deliver-reference.md)).
|
|
58
60
|
|
|
59
61
|
## Procedure
|
|
60
62
|
|
|
@@ -64,8 +66,8 @@ decision to respect.
|
|
|
64
66
|
present the order in step 2. You do **not** thread them into step 3 — the
|
|
65
67
|
tick re-resolves the graph itself every beat. Resolution hard-errors
|
|
66
68
|
(exit 1) on a named id that is not a Story, carries an `Epic: #N` footer, or
|
|
67
|
-
whose native edges cannot be read — a missing gate would co-dispatch
|
|
68
|
-
|
|
69
|
+
whose native edges cannot be read — a missing gate would co-dispatch against
|
|
70
|
+
an unlanded blocker.
|
|
69
71
|
|
|
70
72
|
2. **Confirm (N>1).** Present the order; wait unless `--yes`.
|
|
71
73
|
|
|
@@ -78,10 +80,8 @@ decision to respect.
|
|
|
78
80
|
```
|
|
79
81
|
|
|
80
82
|
**Do not add `--concurrency` unless the operator explicitly asked for a
|
|
81
|
-
per-run cap
|
|
82
|
-
|
|
83
|
-
override. An explicit `--concurrency <n>` wins over config for that run, so a
|
|
84
|
-
filled-in literal (e.g. `3`) silently defeats the operator's override.
|
|
83
|
+
per-run cap** — an explicit value wins over config, so a filled-in literal
|
|
84
|
+
silently defeats a `.agentrc.local.json` override (see Flags).
|
|
85
85
|
|
|
86
86
|
Each beat re-probes live state to derive done / in-flight itself; you never
|
|
87
87
|
compute them (Story #4594). `--dispatched` is the one thing you must supply —
|
|
@@ -129,10 +129,10 @@ review depth reading the same level) and the mechanism table:
|
|
|
129
129
|
|
|
130
130
|
## Reading a Story's outcome
|
|
131
131
|
|
|
132
|
-
Each Story's delivery ends in exactly one schema-validated terminal envelope
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
132
|
+
Each Story's delivery ends in exactly one schema-validated terminal envelope —
|
|
133
|
+
`landed` | `pending` | `blocked` | `failed`. Statuses, exits, and fields:
|
|
134
|
+
[`helpers/deliver-digest.md`](helpers/deliver-digest.md) § 5, over the shipped
|
|
135
|
+
[schema](../schemas/story-deliver-terminal.schema.json) (Story #4543).
|
|
136
136
|
|
|
137
137
|
`pending` is **not** a failure: the bounded merge wait expired with the PR
|
|
138
138
|
healthy (or a human owns the merge), nothing was mutated, and the
|
|
@@ -146,11 +146,9 @@ For a Story in an unclear state — including the merged-but-label-stale one a
|
|
|
146
146
|
|
|
147
147
|
## Constraints
|
|
148
148
|
|
|
149
|
-
- **Land or block — never a silent local build
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
(`delivery.routing.closeAndLand: true`); use `--no-wait-merge` only when a
|
|
153
|
-
human lands the PR.
|
|
149
|
+
- **Land or block — never a silent local build** (digest § 2). Attended
|
|
150
|
+
delivers default to close-and-land (`delivery.routing.closeAndLand: true`);
|
|
151
|
+
use `--no-wait-merge` only when a human lands the PR.
|
|
154
152
|
- `/deliver` never plans — tickets come from [`/plan`](plan.md). The router
|
|
155
153
|
performs no git/label mutations; `deliver-story` owns every script.
|
|
156
154
|
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: >-
|
|
3
|
+
The deliver path's one bundled framework read (Story #4736). Carries what
|
|
4
|
+
every Story delivery always needs — dispatch decision, engine invariants,
|
|
5
|
+
the change-set/ceremony incantation, the acceptance-eval gate, and the
|
|
6
|
+
terminal envelope contract — so the engine reads one file instead of
|
|
7
|
+
re-reading the helper/schema set each session.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Deliver digest (read once per session)
|
|
11
|
+
|
|
12
|
+
> **Bundle, not a procedure.** [`deliver-story.md`](deliver-story.md) is still
|
|
13
|
+
> the steps. This file is the material those steps referenced across five
|
|
14
|
+
> separate files and a JSON schema — bundled so one read covers the whole happy
|
|
15
|
+
> path. Situational material (lease preflight, recovery routers, merge-wait
|
|
16
|
+
> budgets, CI remediation) stays on demand in
|
|
17
|
+
> [`deliver-story-reference.md`](deliver-story-reference.md) and
|
|
18
|
+
> [`deliver-reference.md`](deliver-reference.md); read those **only** when an
|
|
19
|
+
> envelope or a failure routes you there.
|
|
20
|
+
|
|
21
|
+
## 1. Dispatch — where the engine runs
|
|
22
|
+
|
|
23
|
+
Read `stories[].dispatchMode` from the `resolve-stories.js` envelope. Two
|
|
24
|
+
rules produce it, in order:
|
|
25
|
+
|
|
26
|
+
1. **Run topology (#4736).** A run resolving **one** Story is `inline`
|
|
27
|
+
whatever its shape — sub-agent isolation is load-bearing only against a
|
|
28
|
+
*concurrent* sibling racing the same checkout, and a one-Story run has none.
|
|
29
|
+
2. **Body shape (#4722).** In a multi-Story run, a lite-shaped body is
|
|
30
|
+
`inline`; a full-shaped body, an unparseable one, or a footprint touching a
|
|
31
|
+
sensitive-path class is `subagent`. The `route::lite` label is a
|
|
32
|
+
human-visible hint, never the control signal.
|
|
33
|
+
|
|
34
|
+
`inline` removes model-side fan-out only — no `story-worker` boot, no fresh
|
|
35
|
+
acceptance-critic spawn. **`subagent` and `inline` run the same engine**: same
|
|
36
|
+
gates, same PR to `main`, same terminal envelope, byte for byte.
|
|
37
|
+
|
|
38
|
+
## 2. Engine invariants
|
|
39
|
+
|
|
40
|
+
| Trait | Contract |
|
|
41
|
+
| --- | --- |
|
|
42
|
+
| Ticket type | `type::story` only; an `Epic: #N` footer means **stop and re-plan** |
|
|
43
|
+
| Branch | `story-<id>`, seeded from `project.baseBranch` (`main`) |
|
|
44
|
+
| Merge target | `main` via PR (squash + required checks) — never a direct push |
|
|
45
|
+
| Integration branch | **None** — no `epic/<id>`, no `--no-ff` wave merge |
|
|
46
|
+
| Gates | Every close gate runs regardless of route; no route bypasses one |
|
|
47
|
+
| State | Only via `update-ticket-state.js --ticket <id> --state <state>` |
|
|
48
|
+
| Paths | Prefix every path-based tool with the absolute `workCwd` — `cd` does not scope them |
|
|
49
|
+
|
|
50
|
+
**Land or block.** Worktree → `story-<id>` → close-validation → PR to `main` is
|
|
51
|
+
the only sanctioned landing. A silent local build is not a delivery.
|
|
52
|
+
|
|
53
|
+
## 3. Change set — computed once, handed to everyone
|
|
54
|
+
|
|
55
|
+
One enumeration per Story (#4593). A critic that re-runs its own `git diff`
|
|
56
|
+
can score a different set than the one that routed it:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
node --input-type=module -e '
|
|
60
|
+
import { computeChangeSet } from "<main-repo>/.agents/scripts/lib/orchestration/change-set.js";
|
|
61
|
+
const { files } = computeChangeSet({ baseRef: "main", headRef: "story-<storyId>" });
|
|
62
|
+
console.log(JSON.stringify(files));
|
|
63
|
+
'
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Derive the level with `deriveChangeLevel`
|
|
67
|
+
([`review-depth.js`](../../scripts/lib/orchestration/review-depth.js)) over
|
|
68
|
+
that one list: a sensitive path registered in `audit-rules.json` → `high`, none
|
|
69
|
+
→ `low`, an unenumerable diff (`files === null`) → `null`. Resolve
|
|
70
|
+
fresh-vs-inline critics with `resolveCeremonyForRisk`
|
|
71
|
+
([`ceremony-routing.js`](../../scripts/lib/orchestration/ceremony-routing.js)):
|
|
72
|
+
`minimal` → always inline, `strict` → always fresh, `standard` → `high`/`null`
|
|
73
|
+
→ fresh and `low` → inline unless the `freshCriticSampleRate` floor forces
|
|
74
|
+
fresh. An `inline` dispatch mode overrides all of it to inline critics. Close's
|
|
75
|
+
`review-depth.js` reads the same derived level, so the two cannot disagree.
|
|
76
|
+
|
|
77
|
+
## 4. Acceptance self-eval (Step 1a, required)
|
|
78
|
+
|
|
79
|
+
**One verdict-owner per cluster** (#4723) — the fresh critic *or* the inline
|
|
80
|
+
self-eval, named by `verdictOwner`, never both and never a warm-up pass. It
|
|
81
|
+
scores each `acceptance[]` item against the change set above, with `verify[]`
|
|
82
|
+
output as evidence. Bounded by `delivery.acceptanceEval.maxRounds` (default 2).
|
|
83
|
+
Then score the authored verdict:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
node <main-repo>/.agents/scripts/acceptance-eval.js \
|
|
87
|
+
--story <storyId> --verdict <verdict-path>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`proceed` → close. `redraft` → one more round inside the cap. `block` → **do
|
|
91
|
+
not close**: post a `friction` comment and flip `agent::blocked`.
|
|
92
|
+
Per-round mechanics: [`acceptance-self-eval.md`](acceptance-self-eval.md).
|
|
93
|
+
|
|
94
|
+
## 5. Terminal envelope — the return contract
|
|
95
|
+
|
|
96
|
+
`single-story-close.js` emits exactly one envelope on stdout between
|
|
97
|
+
`--- STORY DELIVER TERMINAL ---` markers, schema-validated against
|
|
98
|
+
[`story-deliver-terminal.schema.json`](../../schemas/story-deliver-terminal.schema.json)
|
|
99
|
+
(#4543 — the SSOT; read the JSON only when you need a field this table omits).
|
|
100
|
+
Relay it verbatim; never hand-compose one, never substitute prose.
|
|
101
|
+
|
|
102
|
+
| `status` | Exit | Meaning | You do |
|
|
103
|
+
| --- | --- | --- | --- |
|
|
104
|
+
| `landed` | 0 | PR merged, `agent::done`, tail ran (`tail.*: false` degrades the report, not the land) | Relay it. Done. |
|
|
105
|
+
| `pending` | 3 | **Resumable, not a failure** — the bounded wait expired healthy, or a human owns the merge. Nothing was mutated. | Run `nextCommand`. |
|
|
106
|
+
| `blocked` | 1 | Hard block; `blocked.blockClass` names it | `checks-failed` → fix + resume; else relay |
|
|
107
|
+
| `failed` | 1 | A phase crashed; `phase` names which | Diagnose, fix, re-run close |
|
|
108
|
+
|
|
109
|
+
Required fields: `kind` (`story-deliver-terminal`), `storyId`, `status`,
|
|
110
|
+
`phase`, `elapsedSeconds`, `nextCommand`. `phase` is one of `init`,
|
|
111
|
+
`wrong-tree-guard`, `close-validation`, `base-sync`, `push`, `pull-request`,
|
|
112
|
+
`code-review`, `auto-merge`, `confirm-merge`, `post-land`, `done`. `gates`
|
|
113
|
+
reports every gate as `passed` / `failed` / `skipped` — a skipped gate is
|
|
114
|
+
reported, never omitted, so a missing gate is never read as a passing one.
|
|
115
|
+
|
|
116
|
+
**Gate output is captured, not streamed (#4736).** Close writes gate lines to
|
|
117
|
+
`temp/orchestration/close-gates-<storyId>.log` and reports a one-line digest on
|
|
118
|
+
success; a failed gate replays its tail inline. `AGENT_LOG_LEVEL=verbose`
|
|
119
|
+
restores live streaming.
|
|
120
|
+
|
|
121
|
+
## 6. When to leave this file
|
|
122
|
+
|
|
123
|
+
- Unclear state / a re-run refusal → `deliver-recover.js --story <id>` (read-only).
|
|
124
|
+
- Lease, sweep, worktree-scope detail → [`deliver-story-reference.md`](deliver-story-reference.md).
|
|
125
|
+
- CI red after the PR opens → [`rules/ci-remediation.md`](../../rules/ci-remediation.md).
|
|
126
|
+
- Sequencing, epilogue, checklist threading → [`deliver-reference.md`](deliver-reference.md).
|