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.
@@ -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: values['dry-run'] ? 'dry-run' : 'persist',
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 = await runPersistInvocation({
324
- values,
325
- config,
326
- provider,
327
- artifacts,
328
- metricsSince,
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 and reference detail
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 (Story #4542). |
35
- | `--route-downgrade-reason "<text>"` | Authored `lite` verdict + reason (Story #4722), ledgered per Story; shape-validated, fails closed to `full`. |
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 (1) the pieces have **near-zero overlap**
43
- (genuinely independent capabilities), or (2) there is an **architectural
44
- seam** (different deployables, migration vs consumer). Coupled work stays
45
- one Story — decompose it inside `## Slicing` as intra-session checkpoints,
46
- not sibling tickets. When N>1, every acceptance criterion belongs to exactly
47
- one Story (`assertAcceptancePartition` refuses coupled splits). **N=1 is the
48
- lean path:** one authoring prompt, folded `## Spec`, light risk/critic
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
- `--plan-dir` and derives source-ticket ids from its `sourceTickets[]`
64
- (Story #4554); the CLI also writes **`stories.template.json`** — the
65
- authoring skeleton step 2 starts from.
66
-
67
- The envelope carries docs context, codebase snapshot, the story-author
68
- prompt, `sourceTickets[]`, `duplicates[]` (open **Stories** overlapping the
69
- seed never Epics), and advisory `complexitySignals` (**no routing
70
- authority**, Story #4722). A genuinely trivial scope earns
71
- `--route-downgrade-reason "<why>"` at persist — shape-validated, failing
72
- closed to `full`. Detail:
73
- [`helpers/plan-reference.md` § Shape-derived routing](helpers/plan-reference.md).
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
- markdown string **or** a structured object; persist parses either,
86
- serializes the canonical markdown, and syncs the top-level `acceptance[]` /
87
- `verify[]` into the body — never dual-author those lists.
88
-
89
- ```jsonc
90
- // temp/plan-<slug>/stories.json
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 write Specs under `docs/`); optional `techspec.md` (**N===1 only** —
113
- folded into `## Spec`); optional `acceptance-manifest.json` (N>1 partition
114
- list — pass as `--plan-acceptance` or it is not read). For N=1,
115
- use the envelope `systemPrompts.story` and emit one cohesive Story.
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
- the source issues it replaces; persist refuses a partial map (shape:
120
- [`helpers/plan-reference.md` § Tickets mode](helpers/plan-reference.md)).
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 (ticket validator, body parse, DAG, capacity,
151
- budget, reachability, split-policy and supersede partitions, Spec fold)
152
- runs before the first `createIssue`. Then:
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
- Persist creates Story issue(s) with `type::story` plus a `plan-run::<id>`
164
- grouping label (**metadata only** never a delivery-resolution input, Story #4692); N>1 `depends_on` edges become `blocked by #<id>` body footers.
165
- `agent::ready` is the **terminal** flip, after all receipts are upserted — a
166
- ready Story is always fully persisted (Story #4541). stdout is pure JSON
167
- (logs on stderr).
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
- closes each superseded source as `not_planned` with a comment (default on).
171
- Detail (channels, close contract, resume, temp hygiene):
172
- [`helpers/plan-reference.md`](helpers/plan-reference.md)on a stranded
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
+ commandnever hand-delete issues.
174
159
 
175
160
  ## Constraints
176
161
 
177
- - `/plan` never starts delivery. No Epic ticket, no reconciler, no
178
- `delivery::single` marker.
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) — delivery entry point.
185
- - [`/audit-to-stories`](audit-to-stories.md) — audit findings → plan seed.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mandrel",
3
- "version": "2.12.0",
3
+ "version": "2.14.0",
4
4
  "description": "Claude Code-first opinionated workflow framework: instructions, skills, rules, and SDLC workflows that govern AI coding assistants.",
5
5
  "files": [
6
6
  ".agents/",