mandrel 2.20.0 → 2.22.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.
Files changed (46) hide show
  1. package/.agents/README.md +1 -1
  2. package/.agents/agents/story-worker.md +15 -0
  3. package/.agents/instructions.md +14 -17
  4. package/.agents/rules/git-conventions.md +1 -1
  5. package/.agents/rules/known-tooling-behavior.md +114 -0
  6. package/.agents/scripts/check-context-budget.js +134 -2
  7. package/.agents/scripts/deliver-light.js +72 -8
  8. package/.agents/scripts/lib/audit-suite/selector.js +275 -162
  9. package/.agents/scripts/lib/config/temp-paths.js +113 -7
  10. package/.agents/scripts/lib/feedback-loop/graduator-core.js +604 -57
  11. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +72 -21
  12. package/.agents/scripts/lib/label-constants.js +12 -1
  13. package/.agents/scripts/lib/observability/runtime-friction.js +13 -1
  14. package/.agents/scripts/lib/observability/signals-writer.js +133 -14
  15. package/.agents/scripts/lib/observability/source-classifier.js +131 -1
  16. package/.agents/scripts/lib/orchestration/code-review.js +12 -0
  17. package/.agents/scripts/lib/orchestration/complexity-gate.js +119 -52
  18. package/.agents/scripts/lib/orchestration/deliver-recover.js +253 -6
  19. package/.agents/scripts/lib/orchestration/light-suitability.js +194 -11
  20. package/.agents/scripts/lib/orchestration/resolve-stories.js +17 -14
  21. package/.agents/scripts/lib/orchestration/retro-proposals.js +0 -0
  22. package/.agents/scripts/lib/orchestration/review-providers/degraded-gates.js +222 -0
  23. package/.agents/scripts/lib/orchestration/review-providers/findings-renderer.js +18 -3
  24. package/.agents/scripts/lib/orchestration/review-providers/native.js +82 -126
  25. package/.agents/scripts/lib/orchestration/review-providers/review-provider-factory.js +10 -0
  26. package/.agents/scripts/lib/orchestration/review-providers/scoped-lint.js +300 -0
  27. package/.agents/scripts/lib/orchestration/run-epilogue.js +51 -1
  28. package/.agents/scripts/lib/orchestration/single-story-close/gate-log.js +11 -1
  29. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +18 -8
  30. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-outcome.js +66 -0
  31. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +1 -1
  32. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +5 -1
  33. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +117 -4
  34. package/.agents/scripts/lib/orchestration/story-follow-ups.js +305 -10
  35. package/.agents/scripts/lib/story-body/story-body.js +248 -174
  36. package/.agents/scripts/lib/temp-retention.js +23 -8
  37. package/.agents/scripts/resolve-stories.js +52 -33
  38. package/.agents/scripts/single-story-confirm-merge.js +6 -8
  39. package/.agents/workflows/helpers/deliver-digest.md +8 -6
  40. package/.agents/workflows/helpers/deliver-light.md +45 -5
  41. package/.agents/workflows/helpers/deliver-reference.md +15 -12
  42. package/.agents/workflows/helpers/deliver-story-reference.md +56 -21
  43. package/.agents/workflows/helpers/deliver-story.md +8 -5
  44. package/.agents/workflows/helpers/plan-reference.md +5 -4
  45. package/docs/CHANGELOG.md +28 -0
  46. package/package.json +1 -1
@@ -158,14 +158,13 @@ function readMaxWaitSecondsFlag() {
158
158
  * Resolve the PR number for the Story branch when one was not passed on
159
159
  * the CLI. Probes `gh pr list --head <branch> --state all` (the merged PR
160
160
  * is no longer `open`, so `--state all` is required). Returns `null` when
161
- * no PR is found.
161
+ * no PR is found. Exported for testing.
162
162
  *
163
- * @param {{ cwd: string, storyBranch: string, gh: object }} args
163
+ * @param {{ storyBranch: string, gh: object }} args
164
164
  * @returns {Promise<number|null>}
165
165
  */
166
- async function resolvePrNumber({ cwd, storyBranch, gh }) {
166
+ export async function resolvePrNumber({ storyBranch, gh }) {
167
167
  try {
168
- void cwd;
169
168
  const rows = await gh.pr.list(
170
169
  ['--head', storyBranch, '--state', 'all'],
171
170
  ['number', 'url'],
@@ -204,7 +203,7 @@ async function logConfirmResult(result, terminal, config) {
204
203
  status: terminal?.status,
205
204
  },
206
205
  });
207
- emitTerminalEnvelope(terminal);
206
+ emitTerminalEnvelope(terminal, { config });
208
207
  await emitTerminalFriction({ envelope: terminal, config });
209
208
  return { success: terminal.status !== 'failed', result, terminal };
210
209
  }
@@ -293,11 +292,11 @@ function buildConfirmTerminal({
293
292
  });
294
293
  }
295
294
 
296
- async function resolveConfirmPrNumber({ prParam, cwd, storyBranch, gh }) {
295
+ async function resolveConfirmPrNumber({ prParam, storyBranch, gh }) {
297
296
  const rawPr = prParam ?? readPrFlag();
298
297
  let prNumber = Number.parseInt(String(rawPr ?? ''), 10);
299
298
  if (!Number.isInteger(prNumber) || prNumber <= 0) {
300
- prNumber = await resolvePrNumber({ cwd, storyBranch, gh });
299
+ prNumber = await resolvePrNumber({ storyBranch, gh });
301
300
  }
302
301
  return Number.isInteger(prNumber) && prNumber > 0 ? prNumber : null;
303
302
  }
@@ -340,7 +339,6 @@ export async function runConfirmMerge({
340
339
 
341
340
  const prNumber = await resolveConfirmPrNumber({
342
341
  prParam,
343
- cwd,
344
342
  storyBranch,
345
343
  gh,
346
344
  });
@@ -20,16 +20,18 @@ description: >-
20
20
 
21
21
  ## 1. Dispatch — where the engine runs
22
22
 
23
- Read `stories[].dispatchMode` from the `resolve-stories.js` envelope. Two
24
- rules produce it, in order:
23
+ Read `stories[].dispatchMode` from the `resolve-stories.js` envelope.
24
+ `inline` names one indivisible resource — **the router's own session** — so one
25
+ rule produces it:
25
26
 
26
27
  1. **Run topology.** A run resolving **one** Story is `inline`
27
28
  whatever its shape — sub-agent isolation is load-bearing only against a
28
29
  *concurrent* sibling racing the same checkout, and a one-Story run has none.
29
- 2. **Body shape.** 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.
30
+ 2. **Every other run is `subagent`.** A multi-Story run dispatches every Story
31
+ as a sub-agent however trivial its shape: a lite body does not conjure a
32
+ second session for a sibling to run in, and the wave tick may hand you the
33
+ whole set on one beat. Shape still sets ceremony and is reported alongside;
34
+ the `route::lite` label is a human-visible hint, never the control signal.
33
35
 
34
36
  `inline` removes model-side fan-out only — no `story-worker` boot, no fresh
35
37
  acceptance-critic spawn. **`subagent` and `inline` run the same engine**: same
@@ -49,7 +49,9 @@ multi-capability enumeration). Size is enforced where ground truth is available:
49
49
  the diff backstop in step 4. Do not talk yourself past that one.
50
50
 
51
51
  Sensitivity is the exception and stays absolute: a footprint touching an auth,
52
- crypto, billing, or migration class routes `full` however small or mechanical.
52
+ crypto, billing, or migration class routes `full` however small or mechanical
53
+ and unlike a ceiling, it is **not overridable** (§ Recording a proceed-light
54
+ answer).
53
55
 
54
56
  ## Four invariants (do not skip one)
55
57
 
@@ -58,9 +60,11 @@ crypto, billing, or migration class routes `full` however small or mechanical.
58
60
  **and** a ledgered model verdict with a recorded reason. Both must agree on
59
61
  `lite`.
60
62
  2. **Over-scope stops — it never hard-fails.** An over-ceiling prompt STOPS and
61
- asks the operator to escalate to `/plan` or proceed light. Under `--yes` it
62
- fails closed to an **`escalated` terminal envelope** that ends the session
63
- Escalation is terminal).
63
+ asks the operator to escalate to `/plan` or proceed light. **Both answers
64
+ are executable** `--operator-proceed-light` records the second one
65
+ Recording a proceed-light answer). Under `--yes` it fails closed to an
66
+ **`escalated` terminal envelope** that ends the session (§ Escalation is
67
+ terminal).
64
68
  3. **Diff-derived backstop.** After implementation the ACTUAL change set is
65
69
  re-checked — the diff is the real scope signal — and an over-ceiling diff is
66
70
  blocked rather than landed.
@@ -89,7 +93,10 @@ crypto, billing, or migration class routes `full` however small or mechanical.
89
93
  `nextCommands`. Continue to step 2.
90
94
  - **`ask-operator`** — predicted scope exceeds the light ceilings. STOP and
91
95
  ask the operator to escalate to `/plan` or proceed light. Do not proceed
92
- on your own. This is a **question, not a terminal** — wait for the answer.
96
+ on your own. This is a **question, not a terminal** — wait for the answer,
97
+ then act on it: *escalate* leaves for `/plan`, *proceed light* re-runs the
98
+ same command with `--operator-proceed-light "<their reason>"`
99
+ (§ Recording a proceed-light answer).
93
100
  - **over-scope under `--yes`** — no `action` to branch on: the gate emits an
94
101
  **`escalated` terminal envelope** instead (exit 2). § Escalation is
95
102
  terminal governs; you are finished.
@@ -144,6 +151,39 @@ crypto, billing, or migration class routes `full` however small or mechanical.
144
151
  [`deliver-digest.md`](deliver-digest.md) § 5 — every close
145
152
  gate runs byte-identical to the full path.
146
153
 
154
+ ## Recording a proceed-light answer {#recording-a-proceed-light-answer}
155
+
156
+ The gate offers the operator two options, so **both** have to be executable.
157
+ Re-run the identical gate command with their answer appended:
158
+
159
+ ```bash
160
+ node .agents/scripts/deliver-light.js --prompt "<prompt>" … \
161
+ --operator-proceed-light "<the operator's reason, in their words>"
162
+ ```
163
+
164
+ The gate then proceeds light, records the decision in the receipt Story, and
165
+ returns it on the envelope's `override`. Do **not** instead re-shape the
166
+ prediction — shrinking `--refactors` until the gate stops objecting is
167
+ under-declaring the footprint, which is the one thing the coarse design must
168
+ not reward.
169
+
170
+ It is deliberately narrow, and a refusal is printed rather than silent:
171
+
172
+ - **Only a size prediction is waivable** — change kinds, magnitude,
173
+ uncertainty, deployable span. A sensitive-path class, a
174
+ migration-with-consumers span, and an unknown footprint (undeclared, glob,
175
+ no acceptance, unclassifiable) are refused: those are risk, not size, and
176
+ § Scope by effort keeps them absolute.
177
+ - **The ledgered verdict still stands on its own.** The override substitutes
178
+ for the predicted *shape* only; `--route lite --reason "<why>"` is still
179
+ required.
180
+ - **Attended-only.** With `--yes` it is a usage error, not a quiet no-op —
181
+ an unattended run has no operator whose answer this could be, and over-scope
182
+ there still fails closed (§ Escalation is terminal).
183
+
184
+ What licenses this at all is step 4: the operator waives a *guess*, never the
185
+ diff backstop, which re-checks the actual change set against ground truth.
186
+
147
187
  ## Escalation is terminal {#escalation-is-terminal}
148
188
 
149
189
  Over-scope under `--yes` emits a schema-validated `story-deliver-terminal`
@@ -61,23 +61,26 @@ a cache write at full rate; an inline continuation is a cache read at ~10%).
61
61
  retained in full for multi-Story waves, and the rule changes **where** the
62
62
  engine runs, never what runs — gates, PR, and terminal envelope are identical.
63
63
 
64
- **Lite-shaped Stories execute inline.** Before spawning anything,
65
- read the Story's `dispatchMode` from the resolver envelope
66
- (`stories[].dispatchMode`, derived by `resolveStoryDispatchMode` in
67
- `lib/orchestration/complexity-gate.js` **from the fetched Story body's own
68
- shape** `changes[]` count, acceptance count, creates-vs-refactors mix, and
69
- sensitive-path classes; the `route::lite` label is a human-visible hint only,
70
- never the control signal, so a lost or never-written label cannot misroute
71
- delivery): a Story with `dispatchMode: "inline"` executes
72
- [`deliver-story.md`](deliver-story.md) **inline in this session** — no
64
+ **Read the mode; never infer it from shape.** Before spawning
65
+ anything, read the Story's `dispatchMode` from the resolver envelope
66
+ (`stories[].dispatchMode`, produced by `resolveStoryDispatchMode` in
67
+ `lib/orchestration/complexity-gate.js`). A Story with `dispatchMode: "inline"`
68
+ executes [`deliver-story.md`](deliver-story.md) **inline in this session** no
73
69
  `story-worker` sub-agent boot and no fresh acceptance-critic sub-agents
74
70
  (sub-agent boots are the dominant deliver-phase token cost at trivial scope) —
75
71
  threading the same `docsDigestPath` / `checklistPath` / change-set discipline
76
72
  as a spawned worker. Inline removes model-side fan-out only: every
77
73
  `single-story-close.js` gate, the PR to `main`, and the terminal envelope are
78
- identical. Everything else — a full-shaped body, a missing/unparseable body,
79
- or a footprint intersecting a sensitive-path class (sensitivity wins and
80
- keeps the fresh acceptance critic) takes the standard sub-agent path.
74
+ identical.
75
+
76
+ **A trivial shape does not buy that session.** Only the
77
+ one-Story rule above yields `inline`; every Story of a multi-Story run comes
78
+ back `subagent` however lite its body, because the ready set below may offer
79
+ several Stories on one beat and a session cannot be split between them. The
80
+ Story's derived shape is still reported (it sets ceremony, and a sensitive
81
+ footprint keeps the fresh acceptance critic), and the `route::lite` label
82
+ remains a human-visible hint only, never the control signal — a lost or
83
+ never-written label cannot misroute delivery.
81
84
 
82
85
  **Dispatch each `ready` Story (role-scoped by default).** When
83
86
  `delivery.routing.roleScopedAgents` is enabled (the **default**) and the host
@@ -133,23 +133,25 @@ and the `rules/security-baseline.md` MUSTs all run exactly as for a
133
133
  full-ceremony Story. The lite route's `preserves` field is the machine-readable
134
134
  record of those non-negotiables; there is no lite-specific gate bypass.
135
135
 
136
- **Deliver derives the route from the Story body's shape.**
137
- Persist stamps a lite cohort's Stories with the `route::lite` label as a
138
- *human-visible hint only* (and ledgers the authored verdict — recorded
139
- reason plus per-Story shape evidence — on the `story-plan-state`
140
- checkpoint); the label is never the control signal. `/deliver` computes the
141
- route from the fetched Story body via `resolveStoryDispatchMode`
142
- (`lib/orchestration/complexity-gate.js`) — the same shape taxonomy
143
- `deriveChangeLevel` applies to the landed diff at close: `changes[]` count,
144
- acceptance count, creates-vs-refactors mix, sensitive-path classes. A
145
- lite-shaped Story executes **inline in the deliver session** even when the
146
- label is absent or its write failed with no `story-worker` sub-agent boot,
147
- and the Step 1a acceptance self-eval runs its critics **inline** (no
148
- fresh-context acceptance-critic sub-agent dispatch; sub-agent boots are the
149
- dominant deliver-phase token cost at trivial scope). A footprint
150
- intersecting a sensitive-path class derives `full` sensitivity wins, and
151
- the Story keeps its fresh acceptance critic. Inline execution
152
- changes the isolation only: the engine, every script gate, and the
136
+ **Deliver derives the route from the Story body's shape — and the
137
+ dispatch mode from the run.** Persist stamps a lite cohort's Stories with the
138
+ `route::lite` label as a *human-visible hint only* (and ledgers the authored
139
+ verdict — recorded reason plus per-Story shape evidence — on the
140
+ `story-plan-state` checkpoint); the label is never the control signal.
141
+ `/deliver` computes the route from the fetched Story body via
142
+ `resolveStoryDispatchMode` (`lib/orchestration/complexity-gate.js`) — the same
143
+ shape taxonomy `deriveChangeLevel` applies to the landed diff at close:
144
+ `changes[]` count, acceptance count, creates-vs-refactors mix, sensitive-path
145
+ classes. A footprint intersecting a sensitive-path class derives `full`
146
+ sensitivity wins, and the Story keeps its fresh acceptance critic.
147
+
148
+ That derived route sets ceremony. It does **not** set the dispatch mode,
149
+ because `inline` names one indivisible resource the router's own session —
150
+ and only run topology can say whether it is free: a **single-Story run**
151
+ executes inline, and every Story of a multi-Story run dispatches as a
152
+ `story-worker` sub-agent whatever its shape a lite shape makes the work
153
+ cheap, it does not conjure a second session for a sibling. Inline
154
+ execution changes the isolation only: the engine, every script gate, and the
153
155
  terminal envelope are byte-identical either way.
154
156
 
155
157
  ---
@@ -247,10 +249,10 @@ Resolve fresh-vs-inline acceptance critics per AC-cluster with
247
249
  floor forces `fresh`). Review depth reads the same derived level via
248
250
  `review-depth.js` inside close, so the two decisions cannot disagree.
249
251
 
250
- **Lite-route override.** When the Story's body derives the
251
- lite shape (`resolveStoryDispatchMode` → `inline`), run every
252
- acceptance critic **inline** — do not spawn fresh-context critic sub-agents
253
- regardless of what the profile would otherwise resolve. The self-eval rigor
252
+ **Inline-dispatch override.** When the Story dispatches
253
+ `inline` (`resolveStoryDispatchMode` → `inline`, i.e. a single-Story run), run
254
+ every acceptance critic **inline** — do not spawn fresh-context critic
255
+ sub-agents regardless of what the profile would otherwise resolve. The self-eval rigor
254
256
  (scoring each `acceptance[]` item against the one computed change set, with
255
257
  `verify[]` output as evidence) is unchanged; only the sub-agent boot is
256
258
  removed. Hard gates are untouched.
@@ -745,6 +747,39 @@ unconfirmed merge is a **contract violation** — the parent cannot distinguish
745
747
  "still working" from "done but silent". `pending` is the honest,
746
748
  machine-readable alternative: "not finished, here is exactly how to continue."
747
749
 
750
+ ### The envelope also lands on disk
751
+
752
+ Stdout has exactly one reader — the turn that launched the close — and that
753
+ reader is not always still listening. A child that reports progress and ends
754
+ its turn while its close is mid-gate-chain is behaving reasonably, but the
755
+ envelope it never relayed is gone, and reconstructing the Story's state from
756
+ labels costs a recovery round trip plus a full resume of the child. Observed
757
+ four times across three workers in a single consumer run, on unrelated
758
+ footprints, and not new to that run.
759
+
760
+ So `emitTerminalEnvelope` — the one writer behind every emit site — also
761
+ persists the validated envelope to
762
+ `<tempRoot>/orchestration/story-deliver-terminal-<storyId>.json`:
763
+
764
+ - **It is the same object**, not a summary. Read it and branch exactly as you
765
+ would on stdout; the copy is written before the markers are, so a caller
766
+ that saw them can rely on the file.
767
+ - **It is best-effort.** A failed write returns null and changes nothing about
768
+ the emitted envelope or the exit code — a landed PR must never become a
769
+ crash because a temp directory was unwritable.
770
+ - **It is a fallback, not a licence.** A worker still holds its turn until the
771
+ envelope arrives; see [`agents/story-worker.md`](../../agents/story-worker.md).
772
+
773
+ `deliver-recover.js` reads the same artifact, plus the freshness of
774
+ `close-gates-<storyId>.log`, to split the one genuinely ambiguous row of its
775
+ table. `agent::executing` with no PR used to answer "Implementation never
776
+ finished" — false for the whole duration of a close, whose gates and push
777
+ happen before any PR exists, and actively hazardous, because acting on its
778
+ re-init suggestion can put a second close on one PR. It now answers
779
+ `close-in-flight` (a gate log touched inside the window: wait, then re-probe)
780
+ or `close-envelope-on-disk` (the close already reached a verdict: relay it),
781
+ and falls back to the original verdict only when neither artifact exists.
782
+
748
783
  ### Exit-code compatibility note (`--no-wait-merge`)
749
784
 
750
785
  Every close flag keeps its meaning, but the **exit code** of a
@@ -22,8 +22,8 @@ mandatoryReads: [deliver-digest.md]
22
22
 
23
23
  ## Overview
24
24
 
25
- The **one** delivery engine in v2 — every Story (a lite-**shaped** Story
26
- runs inline with inline critics; engine, gates, envelope byte-identical):
25
+ The **one** delivery engine in v2 — every Story (a **one-Story** run
26
+ goes inline with inline critics; engine, gates, envelope byte-identical):
27
27
 
28
28
  ```text
29
29
  single-story-init.js → implement + commits → derived-level ceremony
@@ -148,10 +148,13 @@ budget is exhausted. Reference § Step 7.
148
148
 
149
149
  ## Recovering a stranded Story {#recover}
150
150
 
151
- Unclear state (killed run, lost envelope, a re-run refusal incl.
152
- merged-but-label-stale)? Do not guess — probe **read-only** with
151
+ **Lost envelope first: read it off disk.** Close persists each to
152
+ `temp/orchestration/story-deliver-terminal-<storyId>.json`; branch on it per
153
+ digest § 5. Otherwise (killed run, re-run refusal, merged-but-label-stale)
154
+ do not guess — probe **read-only** with
153
155
  `node .agents/scripts/deliver-recover.js --story <storyId>`; it prints the
154
- **one** next command with its evidence, never a menu.
156
+ **one** next command with its evidence, never a menu. A live close answers
157
+ `close-in-flight`: wait, never re-init underneath it.
155
158
 
156
159
  ## Idempotence & constraints
157
160
 
@@ -120,10 +120,11 @@ decision:
120
120
  lite cohort's Stories with **`route::lite`** as a *human-visible hint only* —
121
121
  `/deliver` computes the route from each fetched Story body via the same shape
122
122
  function at dispatch, so neither a lost label nor an unread marker can
123
- misroute delivery: a lite-shaped Story executes **inline** (no story-worker
124
- or acceptance-critic sub-agent boots) even with the label absent, and a
125
- sensitive-footprint Story routes `full` and keeps its fresh critic even with
126
- the label present. The `route::*` axis stays runtime-derived: hand-authored
123
+ misroute delivery: a lite-shaped Story derives `lite` even with the label
124
+ absent, and a sensitive-footprint Story routes `full` and keeps its fresh
125
+ critic even with the label present. The derived route sets ceremony, not
126
+ where the engine runs sub-agent boots are collapsed by a **single-Story
127
+ run**, never by a trivial shape. The `route::*` axis stays runtime-derived: hand-authored
127
128
  `route::*` entries in `labels[]` are dropped by persist.
128
129
 
129
130
  The knobs (`planning.complexityGate.{enabled, maxArtifacts}`) are documented
package/docs/CHANGELOG.md CHANGED
@@ -2,6 +2,34 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [2.22.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.21.0...mandrel-v2.22.0) (2026-07-30)
6
+
7
+
8
+ ### Fixed
9
+
10
+ * **context-budget:** hold agentBoot baseline rows in sync with the tree (refs [#4830](https://github.com/dsj1984/mandrel/issues/4830)) ([#4831](https://github.com/dsj1984/mandrel/issues/4831)) ([8a14f8b](https://github.com/dsj1984/mandrel/commit/8a14f8b85951819ef391382355fa2ba4d7998d43))
11
+ * correct the always-on instruction spine and give its budget one honest definition (refs [#4821](https://github.com/dsj1984/mandrel/issues/4821)) ([#4822](https://github.com/dsj1984/mandrel/issues/4822)) ([436d7f0](https://github.com/dsj1984/mandrel/commit/436d7f05d36bd35a6b1a934f19e9a85b37b69f93))
12
+ * feedback-loop: give an auto-filed issue a stable identity, an actionable body, and no path to the live API from a test ([#4837](https://github.com/dsj1984/mandrel/issues/4837)) ([#4838](https://github.com/dsj1984/mandrel/issues/4838)) ([43bbf57](https://github.com/dsj1984/mandrel/commit/43bbf578fc129c9b15ad068925722732fd264c7f))
13
+ * make an inline dispatch verdict mean the engine can actually run inline (refs [#4829](https://github.com/dsj1984/mandrel/issues/4829)) ([#4832](https://github.com/dsj1984/mandrel/issues/4832)) ([f554830](https://github.com/dsj1984/mandrel/commit/f55483085eb201aa126275bb0ed6250fc5a452a3))
14
+ * make the framework limb of the feedback loop reachable (refs [#4824](https://github.com/dsj1984/mandrel/issues/4824)) ([#4827](https://github.com/dsj1984/mandrel/issues/4827)) ([91816a2](https://github.com/dsj1984/mandrel/commit/91816a2dceda127a0cdf1ad3f48f53d7770cbf0b))
15
+ * make the friction roll-up file its proposals and make an all-empty result legible (refs [#4828](https://github.com/dsj1984/mandrel/issues/4828)) ([#4835](https://github.com/dsj1984/mandrel/issues/4835)) ([c017f9a](https://github.com/dsj1984/mandrel/commit/c017f9a9145122b15e41b55a9126a35171f3f1b9))
16
+ * review: stop the scoped-lint gate failing open silently, and fix why its runner cannot execute ([#4839](https://github.com/dsj1984/mandrel/issues/4839)) ([#4840](https://github.com/dsj1984/mandrel/issues/4840)) ([8f3f007](https://github.com/dsj1984/mandrel/commit/8f3f0072abef020439d555af698829ebfc8900f8))
17
+ * rules: ship a known-tooling-behavior rule so measured gate quirks stop being rediscovered per session ([#4825](https://github.com/dsj1984/mandrel/issues/4825)) ([#4826](https://github.com/dsj1984/mandrel/issues/4826)) ([2c20403](https://github.com/dsj1984/mandrel/commit/2c2040318055026307518de57b1433c11c56efc1))
18
+ * **test:** probe bins on either path separator so [#4839](https://github.com/dsj1984/mandrel/issues/4839)'s tests pass on Windows ([#4841](https://github.com/dsj1984/mandrel/issues/4841)) ([98e6048](https://github.com/dsj1984/mandrel/commit/98e6048774b7483717931d0b4a63179800815941))
19
+
20
+
21
+ ### Changed
22
+
23
+ * restore clean-code gate headroom: cover CRAP-capacity shells, flatten selectAudits, table-drive story-body parsing ([#4842](https://github.com/dsj1984/mandrel/issues/4842)) ([#4843](https://github.com/dsj1984/mandrel/issues/4843)) ([cc5dc5e](https://github.com/dsj1984/mandrel/commit/cc5dc5e7eaf423612495441f0f8f9007dbb9b8a4))
24
+
25
+ ## [2.21.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.20.0...mandrel-v2.21.0) (2026-07-28)
26
+
27
+
28
+ ### Added
29
+
30
+ * deliver-light: make the gate's `proceed-light` answer representable, attended-only, and auditable ([#4815](https://github.com/dsj1984/mandrel/issues/4815)) ([#4817](https://github.com/dsj1984/mandrel/issues/4817)) ([74aa28e](https://github.com/dsj1984/mandrel/commit/74aa28e57d0192bcad698461d600806a77de092f))
31
+ * persist the close terminal envelope and teach recovery a live-close state (refs [#4816](https://github.com/dsj1984/mandrel/issues/4816)) ([#4819](https://github.com/dsj1984/mandrel/issues/4819)) ([ffa61d8](https://github.com/dsj1984/mandrel/commit/ffa61d8f56f39c33705bfa379cefdcaaeb523115))
32
+
5
33
  ## [2.20.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.19.0...mandrel-v2.20.0) (2026-07-27)
6
34
 
7
35
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mandrel",
3
- "version": "2.20.0",
3
+ "version": "2.22.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/",