mandrel 2.12.0 → 2.14.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/schemas/story-deliver-terminal.schema.json +60 -6
- package/.agents/scripts/deliver-light.js +446 -0
- package/.agents/scripts/lib/orchestration/complexity-gate.js +7 -4
- 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/story-deliver-terminal.js +122 -6
- package/.agents/scripts/plan-context.js +45 -3
- package/.agents/scripts/plan-persist.js +106 -9
- package/.agents/workflows/deliver-light.md +148 -0
- package/.agents/workflows/plan.md +66 -84
- package/docs/CHANGELOG.md +15 -0
- package/package.json +1 -1
|
@@ -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,148 @@
|
|
|
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 an **`escalated` terminal envelope** that ends the session
|
|
34
|
+
(§ Escalation is terminal).
|
|
35
|
+
3. **Diff-derived backstop.** After implementation the ACTUAL change set is
|
|
36
|
+
re-checked — the diff is the real scope signal — and an over-ceiling diff is
|
|
37
|
+
blocked rather than landed.
|
|
38
|
+
4. **Minimal receipt Story.** A `type::story` is authored inline so `refs #`,
|
|
39
|
+
history, telemetry, and the `agent::executing → agent::done` state machine
|
|
40
|
+
all survive.
|
|
41
|
+
|
|
42
|
+
## Procedure
|
|
43
|
+
|
|
44
|
+
1. **Predict + gate.** Form the predicted footprint (new files, edited files,
|
|
45
|
+
acceptance count) and your ledgered verdict (a recorded reason for `lite`),
|
|
46
|
+
then run the gate:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
node .agents/scripts/deliver-light.js --prompt "<prompt>" \
|
|
50
|
+
--creates <csv> --refactors <csv> --acceptance <n> \
|
|
51
|
+
--route lite --reason "<why this is trivial>" [--amends '#<id>'] [--yes]
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Branch on `action` in the JSON envelope:
|
|
55
|
+
- **`proceed-light`** — the receipt Story is authored; read `storyId` and
|
|
56
|
+
`nextCommands`. Continue to step 2.
|
|
57
|
+
- **`ask-operator`** — predicted scope exceeds the light ceilings. STOP and
|
|
58
|
+
ask the operator to escalate to `/plan` or proceed light. Do not proceed
|
|
59
|
+
on your own. This is a **question, not a terminal** — wait for the answer.
|
|
60
|
+
- **over-scope under `--yes`** — no `action` to branch on: the gate emits an
|
|
61
|
+
**`escalated` terminal envelope** instead (exit 2). § Escalation is
|
|
62
|
+
terminal governs; you are finished.
|
|
63
|
+
|
|
64
|
+
`--amends '#<id>'` is the canonical light case — shape-checked identically; a
|
|
65
|
+
heavy amendment escalates to `/plan` like any other over-scope prompt.
|
|
66
|
+
|
|
67
|
+
2. **Init (same engine).** From the main checkout, synchronously, with the
|
|
68
|
+
maximum Bash timeout:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
node .agents/scripts/single-story-init.js --story <storyId>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Capture `workCwd`; `remoteVerified: false` → flip `agent::blocked` and stop.
|
|
75
|
+
This is [`/deliver`](deliver.md)'s worktree/branch/lease/label engine,
|
|
76
|
+
invoked, not reimplemented.
|
|
77
|
+
|
|
78
|
+
3. **Implement + self-eval.** `cd` into `workCwd`, implement the change, run
|
|
79
|
+
`npm test` once in the worktree, then run the bounded acceptance self-eval
|
|
80
|
+
loop ([`helpers/deliver-story.md`](helpers/deliver-story.md) Step 1a). Commit
|
|
81
|
+
on `story-<id>` with `(refs #<storyId>)`.
|
|
82
|
+
|
|
83
|
+
4. **Diff backstop.** Before close, re-check the ACTUAL diff:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
node .agents/scripts/deliver-light.js --backstop --story <storyId>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Exit `3` (`blocked: true`) means the landed diff exceeds the light ceilings
|
|
90
|
+
(file count or a sensitive-path class). STOP, flip `agent::blocked`, and
|
|
91
|
+
escalate to `/plan` — do not land.
|
|
92
|
+
|
|
93
|
+
5. **Close and land (same engine).** Exactly [`/deliver`](deliver.md)'s close:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
node .agents/scripts/single-story-close.js --story <storyId> --cwd <main-repo>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Branch on the terminal envelope's `status` per
|
|
100
|
+
[`helpers/deliver-digest.md`](helpers/deliver-digest.md) § 5 — every close
|
|
101
|
+
gate runs byte-identical to the full path.
|
|
102
|
+
|
|
103
|
+
## Escalation is terminal {#escalation-is-terminal}
|
|
104
|
+
|
|
105
|
+
Over-scope under `--yes` emits a schema-validated `story-deliver-terminal`
|
|
106
|
+
envelope with **`status: "escalated"`**, `storyId: null`, and a `nextCommand`
|
|
107
|
+
naming the `/plan` invocation that owns the work.
|
|
108
|
+
|
|
109
|
+
**That envelope IS this session's terminal output.** Relay it and stop. There is
|
|
110
|
+
no remaining step, no degraded fallback, and no smaller version of the work to
|
|
111
|
+
attempt.
|
|
112
|
+
|
|
113
|
+
**Invoking `/plan` in this same session is forbidden.** Hand the operator the
|
|
114
|
+
`nextCommand`; `/plan` runs in a **fresh** session.
|
|
115
|
+
|
|
116
|
+
This is not style — it is the empirical finding that motivated the envelope.
|
|
117
|
+
A mandrel-bench 2.13.0 light-arm run read the escalation and continued anyway:
|
|
118
|
+
it invoked `/plan` in-session and delivered. The in-session plan authored **one**
|
|
119
|
+
Story against the scenario's 3–5 contract, where a fresh `/plan` session on the
|
|
120
|
+
identical seed authored **four**. Planning inside a session already framed as
|
|
121
|
+
small work under-decomposes, so walking past the escalation silently produced
|
|
122
|
+
the very outcome the guard exists to prevent. The gate's decision was right both
|
|
123
|
+
times; only the outcome's finality was missing.
|
|
124
|
+
|
|
125
|
+
Nothing is left half-started: an escalated run creates **no receipt Story, no
|
|
126
|
+
`story-<id>` branch, and no worktree** — the escalation path returns before
|
|
127
|
+
every creation call site, and `escalation.created` records all three as `false`
|
|
128
|
+
in a shape the schema pins, so a later run finds nothing to trip over.
|
|
129
|
+
|
|
130
|
+
## Constraints
|
|
131
|
+
|
|
132
|
+
- **Land, block, or escalate — never a silent local build.** The close push is
|
|
133
|
+
the only sanctioned landing; an `escalated` terminal is the only sanctioned
|
|
134
|
+
ending that delivers nothing, and it ends the session
|
|
135
|
+
(§ Escalation is terminal).
|
|
136
|
+
- **No parallel engine.** `/deliver-light` invokes `single-story-init.js` and
|
|
137
|
+
`single-story-close.js`; it never reimplements worktree, branch, PR, or merge
|
|
138
|
+
mechanics.
|
|
139
|
+
- **State only via `update-ticket-state.js`.** Drive every `agent::*`
|
|
140
|
+
transition through the script; report state, not process.
|
|
141
|
+
|
|
142
|
+
## See also
|
|
143
|
+
|
|
144
|
+
- [`/deliver`](deliver.md) — the multi-Story / planned delivery entry point.
|
|
145
|
+
- [`helpers/deliver-story.md`](helpers/deliver-story.md) — the one Story
|
|
146
|
+
delivery engine both entry points share.
|
|
147
|
+
- [`helpers/deliver-digest.md`](helpers/deliver-digest.md) — engine invariants,
|
|
148
|
+
gates, and the terminal-envelope contract.
|
|
@@ -7,8 +7,7 @@ description:
|
|
|
7
7
|
|
|
8
8
|
# /plan --seed "<text>" | --seed-file <path> | --tickets <ids>
|
|
9
9
|
|
|
10
|
-
> **Lean spine.** Happy path + gate list; edge-case
|
|
11
|
-
> lives in the on-demand
|
|
10
|
+
> **Lean spine.** Happy path + gate list; edge-case detail lives in on-demand
|
|
12
11
|
> [`helpers/plan-reference.md`](helpers/plan-reference.md).
|
|
13
12
|
|
|
14
13
|
## Inputs
|
|
@@ -20,9 +19,9 @@ Epic/Story router, no scope-triage `epic|story` verdict:
|
|
|
20
19
|
| --- | --- |
|
|
21
20
|
| `/plan --seed "<text>"` / `--seed-file <path>` | Ideation from chat text or on-disk notes: interrogate → author **one Story by default** → persist. |
|
|
22
21
|
| `/plan --tickets 123[,456…]` | Fetch issue(s), analyze into proper Stories (prefer N=1 rewrite). |
|
|
22
|
+
| `/plan --amends #<id>` | Amend a shipped Story from a **delta envelope** (prior body + acceptance + delivered file map), not a from-scratch re-interrogation (#4741). |
|
|
23
23
|
|
|
24
|
-
`--body` is **not** a `/plan` entry; persist always goes through
|
|
25
|
-
`plan-persist.js`.
|
|
24
|
+
`--body` is **not** a `/plan` entry; persist always goes through `plan-persist.js`.
|
|
26
25
|
|
|
27
26
|
## Flags
|
|
28
27
|
|
|
@@ -30,23 +29,24 @@ Epic/Story router, no scope-triage `epic|story` verdict:
|
|
|
30
29
|
| --- | --- |
|
|
31
30
|
| `--seed "<text>"` / `--seed-file <path>` | Seed text / pre-authored notes path. |
|
|
32
31
|
| `--tickets <ids>` | Issue ids to analyze; closed as superseded at persist. |
|
|
32
|
+
| `--amends #<id>` | Prior Story to amend; emits a delta envelope, not a full re-interrogation (#4741). |
|
|
33
|
+
| `--chain-on-clean` | Persist: chain a clean lite dry-run into the real persist in one round-trip; full-route plans keep the review round-trip (#4741). |
|
|
33
34
|
| `--no-close-superseded` | Keep the source issues open — no supersede comment, no close. |
|
|
34
|
-
| `--force-review` | STOP at gate #2 for operator review — the only review gate (
|
|
35
|
-
| `--route-downgrade-reason "<text>"` | Authored `lite` verdict + reason (
|
|
35
|
+
| `--force-review` | STOP at gate #2 for operator review — the only review gate (#4542). |
|
|
36
|
+
| `--route-downgrade-reason "<text>"` | Authored `lite` verdict + reason (#4722); shape-validated, fails closed to `full`. |
|
|
36
37
|
| `--allow-over-budget` | Permit a plan exceeding `maxTickets`. |
|
|
37
38
|
| `--yes` | Non-interactive: auto-proceed gate #1 and gate #2 HITL waits. |
|
|
38
39
|
| `--dry-run` | Author + validate without GitHub writes; run as a pre-pass. |
|
|
39
40
|
|
|
40
41
|
## Default-single split policy
|
|
41
42
|
|
|
42
|
-
Author **one Story** unless
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
profile — no Epic-scale ceremony.
|
|
43
|
+
Author **one Story** unless the pieces have **near-zero overlap** (genuinely
|
|
44
|
+
independent capabilities) or sit across an **architectural seam** (different
|
|
45
|
+
deployables, migration vs consumer). Coupled work stays one Story —
|
|
46
|
+
`## Slicing` intra-session checkpoints, not sibling tickets; when N>1 every
|
|
47
|
+
acceptance criterion belongs to exactly one Story
|
|
48
|
+
(`assertAcceptancePartition` refuses coupled splits). **N=1 is the lean
|
|
49
|
+
path:** one authoring prompt, folded `## Spec`, no Epic-scale ceremony.
|
|
50
50
|
|
|
51
51
|
## Procedure
|
|
52
52
|
|
|
@@ -55,69 +55,52 @@ profile — no Epic-scale ceremony.
|
|
|
55
55
|
```bash
|
|
56
56
|
node .agents/scripts/plan-context.js --seed "<seed>" \
|
|
57
57
|
--out temp/plan-<slug>/plan-context.json
|
|
58
|
-
# or: --seed-file <path>
|
|
59
|
-
# or: --tickets 123,456
|
|
58
|
+
# or: --seed-file <path> | --tickets 123,456 | --amends #<id>
|
|
60
59
|
```
|
|
61
60
|
|
|
62
|
-
**Always pass `--out`.** Persist auto-discovers that envelope from
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
Under `--yes`, do not ask free-form operator questions — unresolved
|
|
75
|
-
unknowns land in Key Assumptions.
|
|
61
|
+
**Always pass `--out`.** Persist auto-discovers that envelope from `--plan-dir`
|
|
62
|
+
and derives source-ticket ids from its `sourceTickets[]` (#4554); the CLI also
|
|
63
|
+
writes **`stories.template.json`** — the authoring skeleton step 2 starts from.
|
|
64
|
+
|
|
65
|
+
The envelope carries docs context, the codebase snapshot, the story-author
|
|
66
|
+
prompt, `sourceTickets[]`, `duplicates[]` (open **Stories**, never Epics), and
|
|
67
|
+
advisory `complexitySignals` (**no routing authority**, #4722). A trivial scope
|
|
68
|
+
earns `--route-downgrade-reason "<why>"` at persist — shape-validated, failing
|
|
69
|
+
closed to `full`
|
|
70
|
+
([detail](helpers/plan-reference.md)).
|
|
71
|
+
Under `--yes`, do not ask free-form operator questions — unresolved unknowns
|
|
72
|
+
land in Key Assumptions.
|
|
76
73
|
|
|
77
74
|
**Gate #1** — STOP to confirm the sharpened plan intent and any
|
|
78
|
-
duplicate-candidate review. Under `--yes`, auto-proceed.
|
|
75
|
+
duplicate-candidate review. Under `--yes`, auto-proceed. When
|
|
76
|
+
`complexitySignals.deliverLightSuggestion.suggested` is `true`, surface an
|
|
77
|
+
**advisory** `/deliver-light` suggestion — the operator decides; under `--yes`
|
|
78
|
+
it is recorded and planning proceeds, **never an automatic reroute** (#4741).
|
|
79
79
|
|
|
80
80
|
### 2. Author
|
|
81
81
|
|
|
82
82
|
**One-shot authoring (Story #4707).** Start from `stories.template.json`;
|
|
83
83
|
author `stories.json` in one pass. Entries are pre-resolved (#4723); keep
|
|
84
|
-
tiers/assumptions valid. `body` is a
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
[
|
|
92
|
-
|
|
93
|
-
"slug": "hyphen-case-slug", // ^[a-z0-9][a-z0-9-]*$
|
|
94
|
-
"type": "story",
|
|
95
|
-
"title": "Short descriptive title",
|
|
96
|
-
"body": {
|
|
97
|
-
"goal": "One sentence: why this Story exists.",
|
|
98
|
-
"spec": "Optional — contract and invariants.",
|
|
99
|
-
"changes": [{ "path": "path/to/file.ext", "assumption": "refactors-existing" }], // creates | refactors-existing | deletes
|
|
100
|
-
"non_goals": [],
|
|
101
|
-
"reason_to_exist": "One coherent reason this Story exists."
|
|
102
|
-
},
|
|
103
|
-
"acceptance": ["A testable, observable criterion"],
|
|
104
|
-
"verify": ["exact command (unit|contract|e2e|validate)"],
|
|
105
|
-
"depends_on": [] // sibling Story slugs, N>1 only
|
|
106
|
-
}
|
|
107
|
-
]
|
|
108
|
-
```
|
|
84
|
+
tiers/assumptions valid. `body` is a markdown string **or** a structured
|
|
85
|
+
object; persist parses either, serializes the canonical markdown, and syncs the
|
|
86
|
+
top-level `acceptance[]` / `verify[]` into it — never dual-author those lists.
|
|
87
|
+
|
|
88
|
+
Each entry (the `stories.template.json` shape): `slug`
|
|
89
|
+
(`^[a-z0-9][a-z0-9-]*$`), `type: "story"`, `title`, `body` (`goal`, optional
|
|
90
|
+
`spec`, `changes[{path, assumption}]` — `creates|refactors-existing|deletes`,
|
|
91
|
+
`non_goals`, `reason_to_exist`), top-level `acceptance[]`, `verify[]`
|
|
92
|
+
(`… (unit|contract|e2e|validate)`), `depends_on[]` (N>1 only).
|
|
109
93
|
|
|
110
94
|
Artifacts under `temp/plan-<slug>/`: `stories.json`
|
|
111
95
|
(**length 1 by default**; over-budget Specs fail closed — split or tighten,
|
|
112
|
-
never
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
Split only under the policy above.
|
|
96
|
+
never under `docs/`); optional `techspec.md` (**N===1 only** — folded into
|
|
97
|
+
`## Spec`); optional `acceptance-manifest.json` (N>1 partition list — pass as
|
|
98
|
+
`--plan-acceptance`). For N=1, use the envelope `systemPrompts.story` and emit
|
|
99
|
+
one cohesive Story. Split only under the policy above.
|
|
117
100
|
|
|
118
|
-
**Tickets mode:** every Story authors a top-level `supersedes[]` claiming
|
|
119
|
-
|
|
120
|
-
[
|
|
101
|
+
**Tickets mode:** every Story authors a top-level `supersedes[]` claiming the
|
|
102
|
+
source issues it replaces; persist refuses a partial map
|
|
103
|
+
([shape](helpers/plan-reference.md)).
|
|
121
104
|
|
|
122
105
|
### 2.5 Critics
|
|
123
106
|
|
|
@@ -147,9 +130,9 @@ ledgered: **do not proceed to Persist**; fix and re-run.
|
|
|
147
130
|
**only** trigger). Under `--yes`, auto-proceed.
|
|
148
131
|
|
|
149
132
|
Run persist with `--dry-run` **first** — same command, GitHub writes
|
|
150
|
-
suppressed; every gate (
|
|
151
|
-
|
|
152
|
-
|
|
133
|
+
suppressed; every gate (validator, body parse, DAG, capacity, budget,
|
|
134
|
+
reachability, split/supersede partitions, Spec fold) runs before the first
|
|
135
|
+
`createIssue`. Then:
|
|
153
136
|
|
|
154
137
|
```bash
|
|
155
138
|
node .agents/scripts/plan-persist.js \
|
|
@@ -160,30 +143,29 @@ node .agents/scripts/plan-persist.js \
|
|
|
160
143
|
[--source-tickets 123,456] [...flags from the table above]
|
|
161
144
|
```
|
|
162
145
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
(
|
|
146
|
+
At lite shape, `--chain-on-clean` chains that dry-run into the real persist in
|
|
147
|
+
one round-trip **only** when it is clean and `lite`; a full-route plan keeps
|
|
148
|
+
its review round-trip (#4741).
|
|
149
|
+
|
|
150
|
+
Persist creates `type::story` issue(s) plus a `plan-run::<id>` grouping label
|
|
151
|
+
(**metadata only**, #4692); N>1 `depends_on` edges become `blocked by #<id>`
|
|
152
|
+
footers. `agent::ready` is the **terminal** flip after all receipts land — a
|
|
153
|
+
ready Story is fully persisted (#4541). stdout is pure JSON.
|
|
168
154
|
|
|
169
|
-
In `--tickets` mode persist resolves source ids **envelope-first** and
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
persist, re-run the same command; never hand-delete issues.
|
|
155
|
+
In `--tickets` mode persist resolves source ids **envelope-first** and closes
|
|
156
|
+
each as `not_planned` with a comment (default on;
|
|
157
|
+
[detail](helpers/plan-reference.md)). On a stranded persist, re-run the same
|
|
158
|
+
command — never hand-delete issues.
|
|
174
159
|
|
|
175
160
|
## Constraints
|
|
176
161
|
|
|
177
|
-
- `/plan` never starts delivery
|
|
178
|
-
`
|
|
179
|
-
- Duplicate search targets open Stories (`type::story`), not Epics.
|
|
162
|
+
- `/plan` never starts delivery — no Epic ticket, no reconciler. Duplicate
|
|
163
|
+
search targets open Stories (`type::story`), not Epics.
|
|
180
164
|
- Deterministic gates still fail closed under `--yes`.
|
|
181
165
|
|
|
182
166
|
## See also
|
|
183
167
|
|
|
184
|
-
- [`/deliver`](deliver.md)
|
|
185
|
-
|
|
186
|
-
- [`helpers/plan-reference.md`](helpers/plan-reference.md) — on-demand
|
|
187
|
-
detail.
|
|
168
|
+
- [`/deliver`](deliver.md), [`/audit-to-stories`](audit-to-stories.md),
|
|
169
|
+
[`helpers/plan-reference.md`](helpers/plan-reference.md) — on-demand detail.
|
|
188
170
|
- [`core/scope-triage`](../skills/core/scope-triage/SKILL.md) — optional
|
|
189
171
|
split-advisory notes only (no routing verdict).
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [2.14.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.13.0...mandrel-v2.14.0) (2026-07-24)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
* **deliver-light:** make escalate-plan a terminal outcome (refs [#4746](https://github.com/dsj1984/mandrel/issues/4746)) ([#4747](https://github.com/dsj1984/mandrel/issues/4747)) ([53f3151](https://github.com/dsj1984/mandrel/commit/53f3151cff5a22e8ce938ec286cc2898985eb892))
|
|
11
|
+
|
|
12
|
+
## [2.13.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.12.0...mandrel-v2.13.0) (2026-07-24)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
* **orchestration:** add /deliver-light — validated single-session delivery with full gates (refs [#4740](https://github.com/dsj1984/mandrel/issues/4740)) ([#4742](https://github.com/dsj1984/mandrel/issues/4742)) ([605c20b](https://github.com/dsj1984/mandrel/commit/605c20b5ce9c17c6dc7c40356a2e496bb2f25a44))
|
|
18
|
+
* plan-phase turn diet: same artifacts, fewer round-trips, amendment-aware envelope ([#4741](https://github.com/dsj1984/mandrel/issues/4741)) ([#4744](https://github.com/dsj1984/mandrel/issues/4744)) ([65b6790](https://github.com/dsj1984/mandrel/commit/65b67900ae58aea8a15e8a96e28895b1283df079))
|
|
19
|
+
|
|
5
20
|
## [2.12.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.11.0...mandrel-v2.12.0) (2026-07-24)
|
|
6
21
|
|
|
7
22
|
|
package/package.json
CHANGED