mandrel 2.23.0 → 2.25.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 (101) hide show
  1. package/.agents/agents/acceptance-critic.md +13 -19
  2. package/.agents/agents/auditor.md +16 -19
  3. package/.agents/agents/story-worker.md +41 -54
  4. package/.agents/audit-checklists/architecture.md +5 -1
  5. package/.agents/audit-checklists/quality.md +3 -0
  6. package/.agents/docs/agentrc-reference.json +2 -1
  7. package/.agents/docs/configuration.md +2 -1
  8. package/.agents/docs/workflows.md +1 -1
  9. package/.agents/rules/ci-remediation.md +68 -3
  10. package/.agents/schemas/agentrc.schema.json +6 -1
  11. package/.agents/schemas/baselines/baseline-envelope.schema.json +4 -0
  12. package/.agents/schemas/baselines/crap.schema.json +5 -1
  13. package/.agents/schemas/crap-report.schema.json +37 -0
  14. package/.agents/schemas/story-deliver-terminal.schema.json +6 -1
  15. package/.agents/scripts/acceptance-eval.js +35 -9
  16. package/.agents/scripts/audit-labels-bootstrap.js +16 -95
  17. package/.agents/scripts/audit-to-stories.js +33 -3
  18. package/.agents/scripts/check-context-budget.js +95 -14
  19. package/.agents/scripts/check-test-temp-hygiene.js +83 -0
  20. package/.agents/scripts/cleanup-repo-test-temp.js +7 -5
  21. package/.agents/scripts/deliver-light.js +22 -45
  22. package/.agents/scripts/deliver-recover.js +13 -0
  23. package/.agents/scripts/lib/audit-suite/lens-diff-floor.js +10 -25
  24. package/.agents/scripts/lib/audit-to-stories/audit-label-taxonomy.js +177 -0
  25. package/.agents/scripts/lib/audit-to-stories/build-story-body.js +35 -3
  26. package/.agents/scripts/lib/audit-to-stories/group-findings.js +20 -11
  27. package/.agents/scripts/lib/audit-to-stories/parse-audit-md.js +23 -9
  28. package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +9 -2
  29. package/.agents/scripts/lib/baseline-loader.js +0 -0
  30. package/.agents/scripts/lib/baselines/git-base.js +0 -0
  31. package/.agents/scripts/lib/baselines/kinds/crap.js +396 -51
  32. package/.agents/scripts/lib/baselines/kinds/maintainability.js +20 -32
  33. package/.agents/scripts/lib/baselines/preview-gates.js +58 -15
  34. package/.agents/scripts/lib/baselines/reader.js +10 -0
  35. package/.agents/scripts/lib/baselines/writer.js +7 -3
  36. package/.agents/scripts/lib/config-settings-schema-delivery.js +5 -0
  37. package/.agents/scripts/lib/crap-engine.js +97 -19
  38. package/.agents/scripts/lib/crap-utils.js +73 -13
  39. package/.agents/scripts/lib/env-loader.js +46 -16
  40. package/.agents/scripts/lib/escomplex-ast-compat.js +360 -0
  41. package/.agents/scripts/lib/findings/route-finding.js +123 -17
  42. package/.agents/scripts/lib/findings/severity.js +80 -2
  43. package/.agents/scripts/lib/git/cached-fetch.js +0 -0
  44. package/.agents/scripts/lib/label-constants.js +17 -0
  45. package/.agents/scripts/lib/maintainability-engine.js +83 -11
  46. package/.agents/scripts/lib/maintainability-unscorable.js +60 -0
  47. package/.agents/scripts/lib/maintainability-utils.js +14 -5
  48. package/.agents/scripts/lib/observability/runtime-friction.js +8 -0
  49. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +110 -17
  50. package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +548 -0
  51. package/.agents/scripts/lib/orchestration/deliver-recover.js +2 -2
  52. package/.agents/scripts/lib/orchestration/diff-magnitude.js +283 -0
  53. package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +71 -20
  54. package/.agents/scripts/lib/orchestration/light-backstop.js +124 -0
  55. package/.agents/scripts/lib/orchestration/light-escalation.js +248 -0
  56. package/.agents/scripts/lib/orchestration/light-suitability.js +254 -51
  57. package/.agents/scripts/lib/orchestration/plan-context.js +12 -13
  58. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +7 -0
  59. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +13 -1
  60. package/.agents/scripts/lib/orchestration/retro-proposals.js +0 -0
  61. package/.agents/scripts/lib/orchestration/run-scoped-config.js +276 -0
  62. package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +41 -10
  63. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +96 -34
  64. package/.agents/scripts/lib/orchestration/single-story-close/phases/post-land.js +70 -2
  65. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +129 -21
  66. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +73 -18
  67. package/.agents/scripts/lib/reserved-test-ids.js +77 -0
  68. package/.agents/scripts/lib/story-plan.js +137 -42
  69. package/.agents/scripts/lib/templates/decomposer-prompts.js +4 -4
  70. package/.agents/scripts/lib/ticket-body-sections.js +1 -1
  71. package/.agents/scripts/lib/util/poll-loop.js +18 -2
  72. package/.agents/scripts/lib/wave-runner/live-probe.js +7 -1
  73. package/.agents/scripts/lib/wave-runner/ready-set.js +73 -9
  74. package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +6 -3
  75. package/.agents/scripts/lib/workers/crap-worker.js +6 -3
  76. package/.agents/scripts/lib/workers/maintainability-worker.js +14 -9
  77. package/.agents/scripts/pr-watch-with-update.js +568 -213
  78. package/.agents/scripts/quality-preview.js +25 -0
  79. package/.agents/scripts/run-tests.js +10 -0
  80. package/.agents/scripts/single-story-close.js +6 -0
  81. package/.agents/scripts/single-story-init.js +7 -0
  82. package/.agents/scripts/stories-wave-tick.js +95 -10
  83. package/.agents/scripts/story-plan.js +65 -9
  84. package/.agents/scripts/update-crap-baseline.js +42 -4
  85. package/.agents/scripts/update-maintainability-baseline.js +31 -4
  86. package/.agents/templates/single-story-body.md +16 -8
  87. package/.agents/workflows/audit-architecture.md +69 -13
  88. package/.agents/workflows/audit-quality.md +26 -0
  89. package/.agents/workflows/audit-to-stories.md +30 -4
  90. package/.agents/workflows/deliver.md +81 -95
  91. package/.agents/workflows/helpers/audit-lens-core.md +25 -4
  92. package/.agents/workflows/helpers/deliver-light.md +21 -4
  93. package/.agents/workflows/helpers/deliver-reference.md +18 -3
  94. package/.agents/workflows/helpers/deliver-story-reference.md +127 -46
  95. package/.agents/workflows/helpers/deliver-story.md +104 -119
  96. package/.agents/workflows/helpers/plan-reference.md +34 -0
  97. package/.agents/workflows/plan.md +60 -76
  98. package/docs/CHANGELOG.md +38 -0
  99. package/lib/cli/update.js +14 -11
  100. package/lib/cli/version-check.js +9 -1
  101. package/package.json +1 -1
@@ -36,6 +36,22 @@ whole surface exists to remove.
36
36
  Mixed ids and prose in one invocation is a **hard error**: refuse and ask which
37
37
  was meant, rather than guessing a mode and doing the wrong work.
38
38
 
39
+ ## Default-single split policy — what the seam means
40
+
41
+ The spine's two escape hatches from N=1 are narrow on purpose:
42
+
43
+ - **Near-zero overlap** — the pieces touch disjoint files and neither's
44
+ acceptance criteria can be scored without the other having landed.
45
+ - **Architectural seam** — different deployables, or a migration and its
46
+ consumer: work that cannot share one branch and one PR without one half
47
+ sitting unverifiable behind the other.
48
+
49
+ Everything else is one Story with `## Slicing` checkpoints. When N>1 does
50
+ apply, **every acceptance criterion belongs to exactly one Story** —
51
+ `assertAcceptancePartition` refuses a split whose criteria repeat across
52
+ siblings, because a verbatim-shared criterion is the signature of coupled work
53
+ cut in half rather than genuinely separable work.
54
+
39
55
  ## Unknown triage — AFK vs HITL
40
56
 
41
57
  Every open question interrogation surfaces is triaged by **who can resolve
@@ -98,6 +114,14 @@ Resume `/plan` at step 2 (Author) **in this same session** — the interrogation
98
114
  is still valid and re-paying for it buys nothing. This bounce-back is not an
99
115
  escalation.
100
116
 
117
+ **Under `--yes` the offer is recorded and planning proceeds** — it is *never*
118
+ auto-downgraded to light. An unattended run has nobody to confirm the reroute,
119
+ and a suggestion is not a confirmation. The same rule governs unknown triage
120
+ unattended: AFK unknowns are still researched, but no free-form operator
121
+ question is asked — each HITL unknown lands in Key Assumptions marked a
122
+ decision-made-by-default, so the record shows what was decided for the operator
123
+ rather than pretending it was decided with them.
124
+
101
125
  Escalation in the *other* direction — an over-scope prompt on the light path —
102
126
  is terminal and requires a fresh session. The rule that separates the two, and
103
127
  why it must not be flattened into symmetry:
@@ -294,6 +318,16 @@ Either way the critic is **maker-blind**: hand it the draft artifacts
294
318
  transcript or the reasons the planner believed its own draft is sound. A
295
319
  critic that reads the maker's case grades the case, not the draft.
296
320
 
321
+ ## What `--dry-run` actually gates
322
+
323
+ `plan-persist.js --dry-run` is the same command with GitHub writes suppressed,
324
+ and every gate runs before the first `createIssue` would fire — the validator,
325
+ the body parse, the DAG, the capacity and Spec-budget ceilings, the
326
+ reachability check, the split and supersede partitions, and the Tech Spec fold.
327
+ That is the whole point of running it first: a dry run that comes back clean
328
+ has already paid for every deterministic refusal, so the real persist has
329
+ nothing left to discover except network failure.
330
+
297
331
  ## Ready means fully persisted
298
332
 
299
333
  `agent::ready` is the **terminal** step, not part of the creating POST.
@@ -7,47 +7,43 @@ description:
7
7
 
8
8
  # /plan
9
9
 
10
- > **Lean spine.** Happy path + gate list; edge-case detail lives in on-demand
10
+ > **Lean spine.** Happy path + gate list; edge-case detail lives on demand in
11
11
  > [`helpers/plan-reference.md`](helpers/plan-reference.md).
12
12
 
13
13
  ## Inputs
14
14
 
15
15
  Single planning path — there is no Epic/Story router, no scope-triage
16
16
  `epic|story` verdict. **Derive the mode from what the operator typed, announce
17
- it, then act**:
17
+ it, act**:
18
18
 
19
19
  | Invocation | Mode | Behavior |
20
20
  | --- | --- | --- |
21
21
  | `/plan` | ask | Ask what to plan; nothing runs first. |
22
- | `/plan add a --json flag to doctor` | seed | Ideation from the prose: interrogate → author **one Story by default** → persist. |
23
- | `/plan temp/notes/idea.md` | seed-file | Same, from notes. An argument resolving to an existing file is a path, never prose. |
22
+ | `/plan add a --json flag to doctor` | seed | Ideation from prose: interrogate → author **one Story by default** → persist. |
23
+ | `/plan temp/notes/idea.md` | seed-file | Same, from notes. An existing file is a path, not prose. |
24
24
  | `/plan 4712[,4713…]` | tickets | Fetch issue(s), analyze into proper Stories (prefer N=1 rewrite). |
25
25
  | `/plan 4712`, already delivered | amends | Amend a shipped Story from a **delta envelope**, not a re-interrogation. |
26
26
 
27
27
  **Resolving a bare id.** Read live state rather than asking: `agent::done` can
28
28
  only be amended, an open unplanned issue can only be planned. **Announce the
29
- derivation** — "4712 is `agent::done` → amending" so a wrong read costs one
30
- correction. Ask **only** for an open Story already at `agent::ready`.
31
-
32
- `--body` is **not** a `/plan` entry; persist goes through `plan-persist.js`.
29
+ derivation** — "4712 is `agent::done` → amending". Ask **only** for an open
30
+ Story already at `agent::ready`.
33
31
 
34
32
  ## Saying what you want
35
33
 
36
- No flags to remember — state intent and the workflow fills in the CLI
37
- ([reference](helpers/plan-reference.md)). Run any script with `--help` rather
38
- than copying its surface here.
34
+ No flags to remember — state intent; the workflow fills in the CLI
35
+ ([reference](helpers/plan-reference.md)). Run scripts with `--help`.
39
36
 
40
37
  `--yes` is **runner-set, never operator-typed** — cron, `/loop`, and headless
41
- dispatch set it to mean *nobody is at the keyboard*, which is what auto-proceeds
42
- the gates below. Never offer it to an operator or add it to an attended run.
38
+ dispatch set it to mean *nobody is at the keyboard*, which auto-proceeds the
39
+ gates below (#1 and #2). Never offer it to an operator or an attended run.
43
40
 
44
41
  ## Default-single split policy
45
42
 
46
43
  Author **one Story** unless the pieces have **near-zero overlap** or sit across
47
- an **architectural seam** (different deployables, migration vs consumer).
48
- Coupled work stays one Story — `## Slicing` checkpoints, not sibling tickets;
49
- when N>1 every acceptance criterion belongs to exactly one Story
50
- (`assertAcceptancePartition` refuses coupled splits). **N=1 is the lean path.**
44
+ an **architectural seam**. Coupled work stays one Story — `## Slicing`
45
+ checkpoints, not sibling tickets
46
+ ([detail](helpers/plan-reference.md)). **N=1 is lean.**
51
47
 
52
48
  ## Procedure
53
49
 
@@ -63,54 +59,46 @@ node .agents/scripts/plan-context.js --seed "<seed>" \
63
59
  and derives source ids from its `sourceTickets[]`; the CLI also writes
64
60
  **`stories.template.json`**, step 2's skeleton.
65
61
 
66
- The envelope carries docs context, the story-author
67
- prompt, `sourceTickets[]`, `duplicates[]` (open **Stories**, never Epics), and
68
- advisory `complexitySignals` (**no routing authority**). A trivial scope earns
69
- `--route-downgrade-reason "<why>"` at persist — shape-validated, failing closed
70
- to `full` ([detail](helpers/plan-reference.md)).
71
-
72
- **Triage each unknown by resolver**
73
- ([detail](helpers/plan-reference.md)): an **AFK** unknown (research settles
74
- it) is resolved before authoring, never assumed; a **HITL** unknown (an
75
- operator call) goes to Gate #1 as "needs your decision". Under `--yes`, do
76
- not ask free-form operator questions AFK unknowns are still researched;
77
- only HITL unknowns land in Key Assumptions, each marked a
62
+ The envelope carries docs context, the story-author prompt, `sourceTickets[]`,
63
+ `duplicates[]` (open **Stories**, never Epics) and advisory `complexitySignals`
64
+ (**no routing authority**). A trivial scope can claim the lite route at
65
+ persist — shape-validated, failing closed to `full`
66
+ ([detail](helpers/plan-reference.md)).
67
+
68
+ **Triage each unknown by resolver** ([detail](helpers/plan-reference.md)): an
69
+ **AFK** unknown (research settles it) is resolved before authoring, never
70
+ assumed; a **HITL** unknown (an operator call) goes to Gate #1 as "needs your
71
+ decision". Under `--yes` do not ask free-form operator questions AFK unknowns
72
+ are still researched; only HITL unknowns land in Key Assumptions, each marked a
78
73
  decision-made-by-default.
79
74
 
80
75
  **Gate #1** — STOP to confirm the sharpened plan intent and any
81
76
  duplicate-candidate review. Under `--yes`, auto-proceed.
82
77
 
83
- On a truthy `deliverLightSuggestion.suggested`, offer —
84
- **advisory, never an automatic reroute** — to deliver the seed instead of
85
- planning it. On confirm, route **in this session** into
86
- [`helpers/deliver-light.md`](helpers/deliver-light.md), filling its gate from
87
- this envelope, not the raw seed; an `ask-operator` verdict returns here to
88
- step 2 with the interrogation intact. Under `--yes` it is recorded and planning
89
- proceeds — never auto-downgraded to light.
90
-
91
- A truthy `complexitySignals.uiSurface` marks a UI-touching plan: name
92
- [`/prototype`](prototype.md) as an operator option — never invoke it here.
78
+ On a truthy `deliverLightSuggestion.suggested`, offer — advisory, never an
79
+ automatic reroute — to deliver the seed instead; on confirm, route **in this
80
+ session** into [`helpers/deliver-light.md`](helpers/deliver-light.md), its gate
81
+ filled from this envelope. A truthy `complexitySignals.uiSurface` marks a
82
+ UI-touching plan: name [`/prototype`](prototype.md) as an operator option,
83
+ never invoke it here. [Both offers](helpers/plan-reference.md).
93
84
 
94
85
  ### 2. Author
95
86
 
96
87
  **One-shot authoring.** Start from `stories.template.json`; author
97
- `stories.json` in one pass. `body` is a markdown string **or** a structured
98
- object; persist parses either, serializes the canonical markdown, and syncs
99
- top-level `acceptance[]` /
100
- `verify[]` into it — never dual-author those lists.
88
+ `stories.json` in one pass. `body` is markdown **or** a structured object;
89
+ persist parses either, serializes canonical markdown and syncs top-level
90
+ `acceptance[]` / `verify[]` into it — never dual-author those lists.
101
91
 
102
- **Grounding = your reads + Phase 8.** Nothing inventories the repo for you:
103
- read each file you cite, then persist's file-assumption gate hard-errors on
104
- every `{path, assumption}` that misses the real tree.
105
-
106
- Entry fields (the `stories.template.json` shape):
92
+ **Grounding = your reads + Phase 8.** Nothing inventories the repo: read each
93
+ file you cite; persist's file-assumption gate hard-errors on any
94
+ `{path, assumption}` absent from the tree. Entry fields:
107
95
  [reference](helpers/plan-reference.md).
108
96
 
109
97
  Artifacts under `temp/plan-<slug>/`: `stories.json` (**length 1 by default**;
110
- over-budget Specs fail closed — split or tighten, never under `docs/`);
111
- optional `techspec.md` (**N===1 only** — folded into `## Spec`); optional
112
- `acceptance-manifest.json` (N>1 partition list — `--plan-acceptance`). For N=1
113
- use the envelope `systemPrompts.story`; split only under the policy above.
98
+ over-budget Specs fail closed — split or tighten, never under `docs/`); optional
99
+ `techspec.md` (**N===1 only** — folded into `## Spec`); optional
100
+ `acceptance-manifest.json` (N>1 partition — `--plan-acceptance`). For N=1 use
101
+ the envelope `systemPrompts.story`; split only under the policy above.
114
102
 
115
103
  **Tickets mode:** every Story authors a top-level `supersedes[]`; persist
116
104
  refuses a partial map ([shape](helpers/plan-reference.md)).
@@ -123,28 +111,27 @@ node .agents/scripts/plan-critics.js \
123
111
  [--tech-spec temp/plan-<slug>/techspec.md]
124
112
  ```
125
113
 
126
- Run **before** persist — the last point a finding folds into a re-author round.
114
+ Run **before** persist — the last point a finding folds into a re-author.
127
115
  It exits 0 on **any** verdict (verdicts route work, they do not gate) and exits
128
116
  **1** only on a usage/IO error — no critic ran, no skip ledgered: **do not
129
- proceed to Persist**; fix and re-run.
117
+ proceed to Persist**, fix and re-run.
130
118
 
131
119
  - **Both `dispatch: false`** — proceed to Persist (each skip is ledgered).
132
120
  - **Either `dispatch: true`** — dispatch **one fresh-context, maker-blind
133
121
  sub-agent per firing critic** (hand it only the draft artifacts, never the
134
122
  authoring transcript), fold findings into Gate #2 or a re-author round, re-run
135
123
  this step. Pre-mortem triggers (incl. the external-dependency probe), the
136
- advisory-only `textHygiene.findings[]` lints, and the dispatch shape:
137
- [`helpers/plan-reference.md` § Critic dispatch detail](helpers/plan-reference.md).
124
+ advisory-only `textHygiene.findings[]` lints and dispatch shape:
125
+ [reference § Critic dispatch detail](helpers/plan-reference.md).
138
126
 
139
127
  ### 3. Persist
140
128
 
141
129
  **Gate #2** — STOP for approval before persist **only** when the operator asked
142
130
  to review (`--force-review`). Under `--yes`, auto-proceed.
143
131
 
144
- Run persist with `--dry-run` **first** — same command, GitHub writes suppressed;
145
- every gate (validator, body parse, DAG, capacity, budget, reachability,
146
- split/supersede partitions, Spec fold) runs before the first `createIssue`.
147
- Then:
132
+ Run persist with `--dry-run` **first** — same command, writes suppressed;
133
+ every gate runs before the first `createIssue`
134
+ ([the list](helpers/plan-reference.md)):
148
135
 
149
136
  ```bash
150
137
  node .agents/scripts/plan-persist.js \
@@ -155,28 +142,25 @@ node .agents/scripts/plan-persist.js \
155
142
  [--source-tickets 123,456]
156
143
  ```
157
144
 
158
- At lite shape, `--chain-on-clean` chains a clean dry-run into the real persist
159
- in one round-trip; a full plan keeps its review round-trip.
160
-
161
- Persist creates `type::story` issue(s) plus a `plan-run::<id>` grouping label
162
- (**metadata only**); N>1 `depends_on` edges become `blocked by #<id>` footers.
163
- `agent::ready` is the **terminal** flip after all receipts land. stdout is pure
164
- JSON.
145
+ At lite shape, `--chain-on-clean` folds a clean dry-run into the real persist;
146
+ a full plan keeps its review trip.
165
147
 
166
- In tickets mode persist resolves source ids **envelope-first** and closes each
167
- as `not_planned` with a comment (default on;
168
- [detail](helpers/plan-reference.md)).
148
+ Persist creates `type::story` issue(s), a **metadata-only** `plan-run::<id>`
149
+ label, and `blocked by #<id>` footers for N>1 `depends_on` edges.
150
+ `agent::ready` is the **terminal** flip after receipts land; stdout is pure
151
+ JSON. Tickets mode also comments on and closes each source id
152
+ ([detail](helpers/plan-reference.md)).
169
153
 
170
154
  ## Constraints
171
155
 
172
156
  - `/plan` starts delivery **only** through a confirmed Gate #1 light route —
173
157
  never off its own authored Stories, which land via [`/deliver`](deliver.md).
174
- - Duplicate search targets open Stories (`type::story`), not Epics.
175
- - Deterministic gates still fail closed under `--yes`.
158
+ - Duplicate search targets open Stories (`type::story`), not Epics; and
159
+ deterministic gates still fail closed under `--yes`.
176
160
 
177
161
  ## See also
178
162
 
179
- - [`/deliver`](deliver.md), [`/audit-to-stories`](audit-to-stories.md),
180
- [`helpers/plan-reference.md`](helpers/plan-reference.md) on-demand detail.
181
- - [`core/scope-triage`](../skills/core/scope-triage/SKILL.md) — optional
182
- split-advisory notes only (no routing verdict).
163
+ [`/deliver`](deliver.md), [`/audit-to-stories`](audit-to-stories.md),
164
+ [`helpers/plan-reference.md`](helpers/plan-reference.md) (on-demand detail),
165
+ [`core/scope-triage`](../skills/core/scope-triage/SKILL.md) — optional
166
+ split-advisory notes only (no routing verdict).
package/docs/CHANGELOG.md CHANGED
@@ -2,6 +2,44 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [2.25.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.24.0...mandrel-v2.25.0) (2026-08-01)
6
+
7
+
8
+ ### Added
9
+
10
+ * enforce the no-rerun verifier: disarm auto-merge on the first red and gate re-arming on the head SHA moving ([#4865](https://github.com/dsj1984/mandrel/issues/4865)) ([#4867](https://github.com/dsj1984/mandrel/issues/4867)) ([b95267f](https://github.com/dsj1984/mandrel/commit/b95267fe49cd27ca7cd42bb3bc7ae0c6ceaa5a3a))
11
+
12
+
13
+ ### Fixed
14
+
15
+ * **crap:** measure coordinate mixing, not ordinary drift, in the preview gate (refs [#4871](https://github.com/dsj1984/mandrel/issues/4871)) ([#4881](https://github.com/dsj1984/mandrel/issues/4881)) ([63bcb66](https://github.com/dsj1984/mandrel/commit/63bcb66ab46471cfc1b8ad957d60dd1526d0f679))
16
+ * escape the remaining raw NUL delimiters so these diffs are reviewable ([#4896](https://github.com/dsj1984/mandrel/issues/4896)) ([992fc46](https://github.com/dsj1984/mandrel/commit/992fc46b7675736c8fc7af546600cc59ef5ec96d))
17
+ * make the sanctioned CI watch survive a late-arriving required context, and pass --repo through as a real gh flag ([#4890](https://github.com/dsj1984/mandrel/issues/4890)) ([#4893](https://github.com/dsj1984/mandrel/issues/4893)) ([00c23f4](https://github.com/dsj1984/mandrel/commit/00c23f45f57f6b9d11c7314b92f337a31dda4c2b))
18
+ * normalize audit severity and labels without re-minting fingerprints, carry dedupe provenance, and give the lenses a built-but-not-wired mandate ([#4877](https://github.com/dsj1984/mandrel/issues/4877)) ([#4888](https://github.com/dsj1984/mandrel/issues/4888)) ([04f8937](https://github.com/dsj1984/mandrel/commit/04f89373e4dcaa0a47d5501381fb02b5e29bcef6))
19
+ * pin the run's base branch at init and fail closed when config changed mid-run (refs [#4891](https://github.com/dsj1984/mandrel/issues/4891)) ([#4894](https://github.com/dsj1984/mandrel/issues/4894)) ([8099020](https://github.com/dsj1984/mandrel/commit/8099020a9ea2e662c19a1c8a3d9e32d6269df863))
20
+ * **plan:** reconcile the persist body contract and stop acceptance-eval burning a round on every read (refs [#4874](https://github.com/dsj1984/mandrel/issues/4874)) ([#4882](https://github.com/dsj1984/mandrel/issues/4882)) ([33512b4](https://github.com/dsj1984/mandrel/commit/33512b48702e50d9ebaf98dffc396a9ce0091290))
21
+ * ratchet context-budget shrinkage and stop the baseline updaters writing on --help (refs [#4872](https://github.com/dsj1984/mandrel/issues/4872)) ([#4879](https://github.com/dsj1984/mandrel/issues/4879)) ([8d445a4](https://github.com/dsj1984/mandrel/commit/8d445a45e8a4c57b65573213042fb4aec557af08))
22
+ * stamp the version cache with the real clock and end unquoted env values at an unescaped comment marker (refs [#4878](https://github.com/dsj1984/mandrel/issues/4878)) ([#4883](https://github.com/dsj1984/mandrel/issues/4883)) ([d73ee10](https://github.com/dsj1984/mandrel/commit/d73ee10c2f15c3698705281a5979c7394eb03fa4))
23
+ * stop close-resume opening duplicate empty PRs and stop the PR watcher reporting green on a blocked PR ([#4873](https://github.com/dsj1984/mandrel/issues/4873)) ([#4884](https://github.com/dsj1984/mandrel/issues/4884)) ([9d12f8d](https://github.com/dsj1984/mandrel/commit/9d12f8dcd4badbe3b657f1526c284f7689bf04ee))
24
+ * stop the CRAP row builder silently mixing transpiled and original line coordinates, and refuse to compare on an unsound basis ([#4866](https://github.com/dsj1984/mandrel/issues/4866)) ([#4868](https://github.com/dsj1984/mandrel/issues/4868)) ([a6c8c88](https://github.com/dsj1984/mandrel/commit/a6c8c8866c72b1372eb5fd5b7232fb3833e33f93))
25
+ * stop the test suite writing fixture friction signals into the real ledger, and stop unresolvable Story ids reaching an auto-filed ticket ([#4892](https://github.com/dsj1984/mandrel/issues/4892)) ([#4895](https://github.com/dsj1984/mandrel/issues/4895)) ([a607de4](https://github.com/dsj1984/mandrel/commit/a607de49a09c778c1fbefe68d4c57d6a75f0b648))
26
+ * warn about the un-waivable light-path verdict up front, de-conflict on actual footprints, and reconcile the concurrency override ([#4875](https://github.com/dsj1984/mandrel/issues/4875)) ([#4880](https://github.com/dsj1984/mandrel/issues/4880)) ([16d4958](https://github.com/dsj1984/mandrel/commit/16d49584227d783695bb36043e29c64a58aaab39))
27
+
28
+
29
+ ### Changed
30
+
31
+ * reclaim workflow-spine byte headroom and move the close-and-land tail to the orchestrator (refs [#4876](https://github.com/dsj1984/mandrel/issues/4876)) ([#4885](https://github.com/dsj1984/mandrel/issues/4885)) ([7d981cd](https://github.com/dsj1984/mandrel/commit/7d981cde4f8c3d1047a09e64d1f193b85fbfd4b6))
32
+ * widen workflow-spine and agent-boot headroom to a real editing margin (refs [#4876](https://github.com/dsj1984/mandrel/issues/4876)) ([#4889](https://github.com/dsj1984/mandrel/issues/4889)) ([45f5482](https://github.com/dsj1984/mandrel/commit/45f548238c9c82a1e36df11320b73c8f9dea27f5))
33
+
34
+ ## [2.24.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.23.0...mandrel-v2.24.0) (2026-07-31)
35
+
36
+
37
+ ### Fixed
38
+
39
+ * **deliver:** hold the Story assignee-lease until the PR is confirmed merged (refs [#4860](https://github.com/dsj1984/mandrel/issues/4860)) ([#4861](https://github.com/dsj1984/mandrel/issues/4861)) ([d722a65](https://github.com/dsj1984/mandrel/commit/d722a6558f2fa1af6aa428d2f8549101c4bedc23))
40
+ * **maintainability:** score the Babel AST the escomplex kernel actually parses ([#4859](https://github.com/dsj1984/mandrel/issues/4859)) ([279b86f](https://github.com/dsj1984/mandrel/commit/279b86fa49c48454b5e8421ca1e0bb01f54a3bba))
41
+ * scope the light diff backstop by change magnitude, and recycle the receipt instead of orphaning it ([#4856](https://github.com/dsj1984/mandrel/issues/4856)) ([#4857](https://github.com/dsj1984/mandrel/issues/4857)) ([114b479](https://github.com/dsj1984/mandrel/commit/114b4797f248bf00064206f32f490413e28276fb))
42
+
5
43
  ## [2.23.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.22.0...mandrel-v2.23.0) (2026-07-30)
6
44
 
7
45
 
package/lib/cli/update.js CHANGED
@@ -247,10 +247,15 @@ function resolveProjectRoot() {
247
247
  * `mandrel` version via the daily freshness cache (`version-check.js`).
248
248
  *
249
249
  * When `bypassCache` is `true` (the default for an explicit `mandrel update`
250
- * call — Story #4046 A1b), the cache freshness window is effectively zeroed by
251
- * passing `now` as far in the future, which makes `isStale` treat any existing
252
- * cache as stale and always issue exactly one network probe. The cache is still
253
- * written so the post-update `version-current` advisory has a fresh baseline.
250
+ * call — Story #4046 A1b), `isStale` is asked to skip its freshness read via
251
+ * `forceRefresh`, so any existing cache is ignored and exactly one network
252
+ * probe is issued. The cache is still written so the post-update
253
+ * `version-current` advisory has a fresh baseline.
254
+ *
255
+ * The bypass deliberately does **not** shift `now`: the same clock is stamped
256
+ * into the refreshed `checkedAt`, so a shifted clock would persist a
257
+ * ~48h-future timestamp and defeat the 24h window for every later passive
258
+ * check until real time caught up (Story #4878).
254
259
  *
255
260
  * When `bypassCache` is `false` (passive staleness checks only), the normal
256
261
  * 24h-cache semantics apply: a fresh cache returns the cached version with
@@ -281,16 +286,14 @@ async function defaultResolveTargetVersion({
281
286
  bypassCache = false,
282
287
  log = () => {},
283
288
  } = {}) {
284
- // When bypassCache is true, push `now` far enough into the future that any
285
- // cached checkedAt value is guaranteed to be older than the STALE_AFTER_MS
286
- // window, forcing a fresh network probe (Story #4046 A1b).
287
- const effectiveNow = bypassCache
288
- ? new Date(now.getTime() + 48 * 60 * 60 * 1000)
289
- : now;
289
+ // Bypass the cache by telling isStale to skip its freshness read never by
290
+ // shifting `now`, which is also the clock persisted as `checkedAt`
291
+ // (Story #4046 A1b forcing the probe; Story #4878 keeping the stamp honest).
290
292
  const result = await isStale({
291
293
  cachePath,
292
- now: effectiveNow,
294
+ now,
293
295
  runner,
296
+ forceRefresh: bypassCache,
294
297
  fs,
295
298
  log,
296
299
  });
@@ -142,10 +142,17 @@ export function refreshCache({
142
142
  * The freshness comparison uses `>=` so an exactly-24h-old cache counts as
143
143
  * stale (the boundary is treated as "due for refresh").
144
144
  *
145
+ * `forceRefresh` skips the freshness read entirely and always probes. It is
146
+ * the supported way to bypass the cache: callers must **not** fake staleness
147
+ * by shifting `now` forward, because `now` is also the clock stamped into the
148
+ * refreshed `checkedAt` — a shifted clock persists a future timestamp and
149
+ * defeats the freshness window for every later reader (Story #4878).
150
+ *
145
151
  * @param {{
146
152
  * cachePath: string,
147
153
  * now?: Date,
148
154
  * runner: () => (string | Promise<string>),
155
+ * forceRefresh?: boolean,
149
156
  * fs?: typeof import('node:fs'),
150
157
  * log?: (msg: string) => void,
151
158
  * }} opts
@@ -160,10 +167,11 @@ export async function isStale({
160
167
  cachePath,
161
168
  now = new Date(),
162
169
  runner,
170
+ forceRefresh = false,
163
171
  fs = nodeFs,
164
172
  log = () => {},
165
173
  }) {
166
- const cached = readCache({ cachePath, fs });
174
+ const cached = forceRefresh ? null : readCache({ cachePath, fs });
167
175
 
168
176
  if (cached) {
169
177
  const ageMs = now.getTime() - new Date(cached.checkedAt).getTime();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mandrel",
3
- "version": "2.23.0",
3
+ "version": "2.25.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/",