mandrel 2.65.0 → 2.66.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 (56) hide show
  1. package/.agents/agents/acceptance-critic.md +5 -5
  2. package/.agents/agents/auditor.md +17 -18
  3. package/.agents/agents/plan-critic.md +5 -5
  4. package/.agents/agents/story-worker.md +5 -5
  5. package/.agents/docs/execution-reference.md +27 -5
  6. package/.agents/instructions.md +10 -12
  7. package/.agents/rules/ci-remediation.md +3 -3
  8. package/.agents/rules/gherkin-standards.md +3 -2
  9. package/.agents/rules/git-conventions-reference.md +12 -3
  10. package/.agents/rules/git-conventions.md +9 -7
  11. package/.agents/rules/testing-standards.md +8 -7
  12. package/.agents/runtime-deps.json +1 -1
  13. package/.agents/scripts/bootstrap.js +94 -89
  14. package/.agents/scripts/lib/baselines/duplication-scanner.js +17 -7
  15. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +78 -78
  16. package/.agents/scripts/lib/cli/standard-args.js +60 -76
  17. package/.agents/scripts/lib/cli-args.js +26 -0
  18. package/.agents/scripts/lib/config/gates/shared.js +3 -3
  19. package/.agents/scripts/lib/feedback-loop/graduate-steps.js +205 -0
  20. package/.agents/scripts/lib/feedback-loop/graduator-core.js +47 -782
  21. package/.agents/scripts/lib/feedback-loop/graduator-gh.js +449 -0
  22. package/.agents/scripts/lib/observability/close-telemetry.js +330 -0
  23. package/.agents/scripts/lib/observability/runtime-friction.js +2 -0
  24. package/.agents/scripts/lib/observability/signal-validator.js +17 -5
  25. package/.agents/scripts/lib/orchestration/code-review.js +22 -0
  26. package/.agents/scripts/lib/orchestration/plan-metrics.js +76 -63
  27. package/.agents/scripts/lib/orchestration/review-providers/review-provider-factory.js +23 -0
  28. package/.agents/scripts/lib/orchestration/run-epilogue.js +6 -0
  29. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +2 -0
  30. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +349 -263
  31. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +21 -7
  32. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +4 -0
  33. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +327 -314
  34. package/.agents/scripts/lib/orchestration/ticket-validator.js +19 -36
  35. package/.agents/scripts/lib/signals/detectors/common.js +63 -51
  36. package/.agents/scripts/lib/transpile.js +28 -3
  37. package/.agents/scripts/single-story-close.js +10 -2
  38. package/.agents/scripts/single-story-confirm-merge.js +267 -238
  39. package/.agents/skills/core/idea-refinement/SKILL.md +6 -6
  40. package/.agents/skills/stack/qa/qa-harness/SKILL.md +1 -2
  41. package/.agents/workflows/audit-architecture.md +5 -4
  42. package/.agents/workflows/audit-documentation.md +5 -5
  43. package/.agents/workflows/audit-performance.md +10 -10
  44. package/.agents/workflows/helpers/acceptance-self-eval.md +8 -8
  45. package/.agents/workflows/helpers/audit-lens-core.md +30 -57
  46. package/.agents/workflows/helpers/deliver-digest.md +2 -2
  47. package/.agents/workflows/helpers/deliver-reference.md +3 -1
  48. package/.agents/workflows/helpers/deliver-story.md +6 -1
  49. package/.agents/workflows/helpers/parallel-tooling.md +16 -18
  50. package/.agents/workflows/mandrel-deliver.md +1 -1
  51. package/.agents/workflows/mandrel-plan.md +6 -5
  52. package/docs/CHANGELOG.md +26 -0
  53. package/lib/cli/guarded-sync.js +87 -0
  54. package/lib/cli/sync-agents.js +9 -92
  55. package/lib/cli/sync-commands.js +9 -101
  56. package/package.json +2 -2
@@ -27,18 +27,18 @@ Per the core's Scope interpretation:
27
27
 
28
28
  ## Execution strategy
29
29
 
30
- This is a **heavyweight lens**: dispatch it as a single `subagent_type: auditor`
31
- call, or fan its resource dimensions out per-dimension across parallel `auditor`
32
- subagents (parallel-tooling Rule 3) and merge under the self-cross-check.
33
- Sequential inline execution is the fallback (see the core's Execution strategy).
30
+ Dispatch this lens as one `subagent_type: auditor` call. Fan its resource
31
+ dimensions out across parallel `auditor` subagents (parallel-tooling Rule 3),
32
+ merging under the self-cross-check, only when the operator explicitly asks for
33
+ per-dimension fan-out. Sequential inline execution is the fallback (see the
34
+ core's Execution strategy).
34
35
 
35
36
  > **Measurement is non-mutating, not forbidden.** This lens is read-only with
36
- > respect to source, but it MUST be allowed to *run* measurements. The
37
- > orchestrated path grants its measurement agents a `Bash` tool restricted to a
38
- > **non-mutating command allowlist** (profilers, timers, bundle-stat and
39
- > file-size probes — never a command that writes source, installs, or mutates
40
- > git/labels). See the allowlist in the harness-generated
41
- > `.claude/workflows/audit-performance.workflow.js`.
37
+ > respect to source, but the auditor MUST be allowed to *run* measurements. It
38
+ > runs only **non-mutating** commands — profilers, timers, bundle-stat and
39
+ > file-size probes — and never a command that writes source, installs
40
+ > packages, or mutates git state or labels. The one write is the report
41
+ > artifact.
42
42
 
43
43
  ## Step 0: Measure before you judge (mandatory)
44
44
 
@@ -32,8 +32,8 @@ per-criterion, mid-delivery, and evaluates the actual work product.
32
32
  authors the Story's verdict, and it covers **every** `acceptance[]` item in
33
33
  one file. Which pass is named by the ceremony decision
34
34
  (`verdictOwner: 'fresh-critic' | 'inline-self-eval'` from
35
- `resolveCeremonyForRisk`), and since Story #5343 that follows the
36
- **ceremony profile alone**:
35
+ `resolveCeremonyForRisk`), which follows the **ceremony profile
36
+ alone**:
37
37
 
38
38
  > ```bash
39
39
  > node <main-repo>/.agents/scripts/ceremony-derive.js --story <storyId> --cwd <workCwd>
@@ -43,9 +43,8 @@ per-criterion, mid-delivery, and evaluates the actual work product.
43
43
  > classes **for review depth**, and resolves the owner (`mode`, `reason`,
44
44
  > `verdictOwner`): **`minimal` / `standard` → `inline`** (the default — you
45
45
  > author the verdict yourself), **`strict` → `fresh`** (dispatch the
46
- > maker-blind critic). The derived level no longer routes this decision;
47
- > it escalates `review-depth.js` instead, which still resolves `deep` for
48
- > any sensitive path.
46
+ > maker-blind critic). The derived level feeds `review-depth.js`, not
47
+ > this decision; review depth resolves `deep` for any sensitive path.
49
48
 
50
49
  **Never run both**, and never run a preliminary self-assessment before
51
50
  dispatching a fresh critic — the redundant pre-pass buys no measurable
@@ -72,9 +71,10 @@ per-criterion, mid-delivery, and evaluates the actual work product.
72
71
  > system prompt, no entry-doc @-closure) carrying the maker-blind
73
72
  > invariant and the verdict schema standalone. With the kill-switch off
74
73
  > (`roleScopedAgents: false`), fall back to
75
- > `subagent_type: general-purpose`. This loop already runs inside a Story
76
- > delivery sub-agent, so the critic sits at nesting depth 2 — supported by
77
- > any harness that carries `Agent` into sub-agents (Claude Code ≥ 2.1.202).
74
+ > `subagent_type: general-purpose`. Under sub-agent dispatch this loop
75
+ > runs inside a `story-worker`, so the critic sits at nesting depth 2
76
+ > (depth 1 inline) — supported by any harness that carries `Agent` into
77
+ > sub-agents (Claude Code ≥ 2.1.202).
78
78
 
79
79
  Whichever pass owns it, the verdict:
80
80
  + Inspects the **change set it was handed** — the one `files` list above —
@@ -118,14 +118,12 @@ dropped finding is indistinguishable from a finding you never wrote.
118
118
  Use it instead of inventing a below-`Low` word of your own; a finding that
119
119
  cannot clear the evidence bar below is **dropped**, not filed as `Info`.
120
120
 
121
- ## Self-cross-check (mandatory — filter false positives before you finalize) {#self-cross-check}
121
+ ## Self-cross-check (the false-positive bar) {#self-cross-check}
122
122
 
123
- You are your own adversarial reviewer. After you have drafted the Detailed
124
- Findings but **before** you write the report artifact, re-open every finding
125
- and hold it to the bar below. This pass is **read-only** — it filters and
126
- tightens the findings you already have; it never invents new ones. It gives the
127
- sequential single-pass path the same false-positive filter the orchestrated
128
- path's independent adversarial reviewer applies.
123
+ A finding goes in the report only when it clears the bar and the exclusion
124
+ list below. The bar filters and tightens findings; it never invents new ones.
125
+ It is the one false-positive filter every execution path applies — no separate
126
+ adversarial reviewer runs after it.
129
127
 
130
128
  ### Per-finding evidence bar (keep or drop)
131
129
 
@@ -173,23 +171,18 @@ that rests on one of them:
173
171
  > delivery shipped and nothing in production ever calls. When a candidate is
174
172
  > genuinely one of the exclusions, cite the exclusion and drop it.
175
173
 
176
- ### Final re-open-and-drop pass (mandatory)
174
+ ### Recording the outcome
177
175
 
178
- 1. Walk your Detailed Findings once more, applying the bar and the exclusion
179
- list above. Remove every finding that fails.
180
- 2. Count what you kept (`k`) and what you dropped (`d`).
181
- 3. Record the outcome in the report's **Executive Summary** as a single line:
176
+ Record what you kept (`k`) and dropped (`d`) in the report's **Executive
177
+ Summary** as a single line:
182
178
 
183
- ```text
184
- Self-cross-check: kept <k> / dropped <d>.
185
- ```
186
-
187
- When `d > 0`, name the dropped findings (title + the bar/exclusion reason)
188
- in one short list under that line, so the filtering is auditable and never
189
- silent.
179
+ ```text
180
+ Self-cross-check: kept <k> / dropped <d>.
181
+ ```
190
182
 
191
- A lens that keeps every finding still records `dropped 0` — the line's absence
192
- is itself a defect (it means the pass did not run).
183
+ When `d > 0`, name the dropped findings (title + the bar/exclusion reason) in
184
+ one short list under that line, so the filtering is auditable and never
185
+ silent. A lens that keeps every finding still records `dropped 0`.
193
186
 
194
187
  ## Severity tally (mandatory, machine-readable) {#severity-tally}
195
188
 
@@ -261,55 +254,35 @@ available; every path emits the **identical** report contract (the finding-block
261
254
  skeleton above), so downstream consumers (`audit-to-stories`) are agnostic to
262
255
  which path produced it.
263
256
 
264
- 1. **Subagent dispatch (first-class).** Dispatch the lens as a single
257
+ 1. **One auditor per lens (the default).** Dispatch the lens as exactly one
265
258
  `subagent_type: auditor` call — the standalone boot context in
266
259
  [`../../agents/auditor.md`](../../agents/auditor.md) carries the read-only
267
260
  MUSTs, the finding-block skeleton, the severity scale, and the
268
261
  self-cross-check bar, so the child needs only the lens's own dimensions to
269
262
  run. The subagent returns the **report path plus the Executive Summary**
270
263
  (including the self-cross-check line); the parent never needs the full
271
- findings inline. This is the default: the auditor boots without the full
272
- project closure, so the spawn is cheap relative to running the lens inline
273
- in the parent's context.
274
-
275
- - **Per-dimension fan-out (heavyweight lenses).** `audit-architecture`,
276
- `audit-performance`, and `audit-documentation` carry enough independent
277
- dimensions to be worth fanning out: dispatch one `subagent_type: auditor`
278
- call **per dimension** in a single turn via
279
- [`parallel-tooling.md`](parallel-tooling.md) Rule 3, then **merge** the
264
+ findings inline. One auditor reads the repo once; per-dimension agents each
265
+ re-read it, so the single dispatch is the cheap path, not a compromise.
266
+
267
+ - **Per-dimension fan-out (operator request only).** Fan a lens out only
268
+ when the operator's invocation explicitly asks for it. Then dispatch one
269
+ `subagent_type: auditor` call **per dimension** in a single turn via
270
+ [`parallel-tooling.md`](parallel-tooling.md) Rule 3, and **merge** the
280
271
  per-dimension findings under this file's self-cross-check (the merge is
281
- where cross-dimension duplicates and false positives are dropped). Respect
282
- the nesting-depth budget and the concurrency cap that Rule 3 documents.
272
+ where cross-dimension duplicates and false positives are dropped). Never
273
+ fan out on your own judgment of a lens's size.
274
+ - **No nested fan-out.** An auditor never dispatches sub-agents of its own.
275
+ The fan-out, when requested, happens once, at the caller.
283
276
 
284
277
  2. **Sequential inline execution (documented fallback).** When subagent
285
278
  dispatch is unavailable, run the lens's steps turn-by-turn in the current
286
279
  context exactly as written, ending with the self-cross-check. This changes
287
280
  nothing about the report contract.
288
281
 
289
- > **Orchestrated dynamic-workflow path (optimization note).** Six lenses ship a
290
- > saved project workflow at `.claude/workflows/audit-<lens>.workflow.js` that,
291
- > **when Claude Code dynamic workflows are available** (runtime is Claude Code,
292
- > `disableWorkflows` unset, version `>= 2.1.154`), fans the dimensions out as
293
- > parallel read-only subagents and runs an independent adversarial cross-check
294
- > stage before synthesising the report. It derives its per-dimension prompts
295
- > from the *lens* markdown at run time — the lens stays the single source of
296
- > truth. This is a performance optimization over path 1, **not** a separate
297
- > contract, and it is not covered by the No-Shim / hard-cutover rule in
298
- > [`../../rules/git-conventions.md`](../../rules/git-conventions.md) because
299
- > there is one report contract and only the execution strategy varies — the
300
- > same capability-degradation pattern the protocol endorses for live-docs
301
- > fallback. **The host owns the choice.** Mandrel ships no in-repo strategy
302
- > selector and no force-override env var: Claude Code launches the saved
303
- > workflow when it can, and you get path 1 or 2 above when it cannot.
304
- > Suppress the orchestrated path with `CLAUDE_CODE_DISABLE_WORKFLOWS=1`
305
- > or `disableWorkflows: true` in `.claude/settings.json`. On the orchestrated
306
- > path the analysis subagents are granted only read/search tools (`Read`,
307
- > `Grep`, `Glob`) — the single write is the final report artifact.
308
-
309
282
  ## Parallel tooling {#parallel-tooling}
310
283
 
311
284
  When a lens batches independent reads/greps, runs a long shell (a scanner, a
312
- profiler, a suite time), or fans out per-dimension, apply
313
- [`parallel-tooling.md`](parallel-tooling.md): batch independent reads in one
314
- turn (Rule 1), run long shells via `run_in_background` + `Monitor` (Rule 2),
315
- and dispatch N independent units as N `Agent` calls in one turn (Rule 3).
285
+ profiler, a suite time), apply [`parallel-tooling.md`](parallel-tooling.md):
286
+ batch independent reads in one turn (Rule 1) and run long shells via
287
+ `run_in_background` (Rule 2). Rule 3 applies only to the caller of
288
+ an operator-requested per-dimension fan-out — never inside an auditor.
@@ -68,8 +68,8 @@ same derived level, so the two cannot disagree. A sensitive footprint
68
68
  therefore buys a **deep review**, not a fresh acceptance critic.
69
69
 
70
70
  > **The ceremony rule, stated once.** The **profile alone** names the verdict
71
- > owner (Story #5343, narrowed to that one input by #5366): `minimal` /
72
- > `standard` → `inline`, `strict` → `fresh`. Nothing else moves it — not the
71
+ > owner: `minimal` / `standard` → `inline`, `strict` → `fresh`. Nothing else
72
+ > moves it — not the
73
73
  > derived change level, not the footprint's sensitivity, and **not the
74
74
  > dispatch mode**: an `inline` Story under `strict` still spawns the fresh
75
75
  > maker-blind critic, one nesting level shallower than a dispatched one. The
@@ -78,7 +78,9 @@ but not a cycle.
78
78
  unblocked it:
79
79
  `node .agents/scripts/update-ticket-state.js --ticket <id> --state agent::ready`.
80
80
  Do not poll the label yourself while waiting — the HITL pause is the operator's
81
- turn, not a slow beat.
81
+ turn, not a slow beat. Before resuming, the operator raises session effort one
82
+ step, and raises effort before switching models
83
+ ([effort escalation](../../docs/execution-reference.md#session-effort-and-model)).
82
84
 
83
85
  Each beat re-probes live state: it re-resolves the graph, classifies **done**
84
86
  (`agent::done` or a closed issue — including foreign blockers that landed in
@@ -114,9 +114,14 @@ Do not open the PR or compose a terminal envelope.
114
114
  serialized against sibling Stories:
115
115
 
116
116
  ```bash
117
- node <main-repo>/.agents/scripts/single-story-close.js --story <storyId> --cwd <main-repo>
117
+ node <main-repo>/.agents/scripts/single-story-close.js --story <storyId> --cwd <main-repo> \
118
+ [--worker-tokens <n>]
118
119
  ```
119
120
 
121
+ When the host reported a total-token figure for the story-worker's Agent
122
+ dispatch, pass it as `--worker-tokens <n>` — close records it in its
123
+ result's local `telemetry`; omit it when the host reports none.
124
+
120
125
  **The whole delivery tail** — gates, PR, merge wait, `agent::done` flip,
121
126
  post-land tail in one process. Never background it, never delegate it to a
122
127
  child, and never end your turn while it is still running: "close is running"
@@ -29,15 +29,17 @@ the batch in parallel; serial calls cost N round-trips for no gain.
29
29
  - **Bounded fan-out:** keep the batch ≤ 10 calls per turn. Larger batches
30
30
  blow the context budget and obscure the failure surface if one call errors.
31
31
 
32
- ## Rule 2 — `run_in_background` + `Monitor` for long shells
32
+ ## Rule 2 — `run_in_background` for long shells
33
33
 
34
- Shell commands that exceed roughly 30 seconds (test suites, installs,
35
- multi-file lints, `git fetch --all`, container builds) **must** use the
36
- `Bash` tool's `run_in_background: true` flag and stream events via the
37
- `Monitor` tool. A synchronous `Bash` call holds the assistant turn open for
38
- the full duration and blocks every other parallel opportunity.
34
+ A shell command that can outrun the host's synchronous Bash ceiling, or that
35
+ would idle the turn while independent work waits (test suites, installs,
36
+ multi-file lints, `git fetch --all`, container builds), runs with the `Bash`
37
+ tool's `run_in_background: true` flag; its completion notification is the
38
+ signal to proceed. Attach `Monitor` only when you must act on output before
39
+ the command exits.
39
40
 
40
- - **Tool primitives:** `Bash(run_in_background: true)` + `Monitor`.
41
+ - **Tool primitives:** `Bash(run_in_background: true)`; `Monitor` only when
42
+ mid-run output matters.
41
43
  - **When:** `npm test`, `npm ci`, full-repo `eslint`/`biome` runs, long
42
44
  fetches, anything you would have prefixed with `nohup` in a terminal.
43
45
  - **Anti-pattern:** synchronous `Bash` with a 600 000 ms timeout used as a
@@ -90,17 +92,13 @@ the same shape as Rule 1 but at the sub-agent layer.
90
92
  ## When the rules conflict
91
93
 
92
94
  If a unit of work is both long (Rule 2) and independent (Rule 1 or 3),
93
- prefer the higher-numbered rule — the parallelism gain compounds the
94
- background-shell gain. Concretely: dispatch the `Agent` calls in one turn
95
- (Rule 3), and **inside** each sub-agent let it apply Rule 2 to its own
96
- long-running shells — and, within the supported nesting depth budget
97
- (verified depth 2, announced max depth 5), let it apply
98
- **Rule 3** to its own independent sub-units as well, not only Rule 2
99
- background shells. A sub-agent is a full orchestrator at its own level:
100
- recursive `Agent` fan-out is available to it, so the host does not need to
101
- micromanage the child's shell **or** dispatch strategy. Mind the depth
102
- budget and the compounding cost — every nesting level re-pays the
103
- always-loaded context (see [`instructions.md` § 4](../../instructions.md)).
95
+ dispatch the `Agent` calls in one turn (Rule 3), and **inside** each sub-agent let it apply Rule 2 to its own
96
+ long-running shells. A sub-agent does **not** fan out again on its own
97
+ initiative: every nesting level re-pays the always-loaded context (see
98
+ [`instructions.md` § 4](../../instructions.md)), and the cost compounds with
99
+ depth. The one exception is a dispatch the sub-agent's own workflow names
100
+ explicitly — for example the maker-blind acceptance critic a `strict`-profile
101
+ Story worker spawns — which stays legal at that depth.
104
102
 
105
103
  ## Constraints
106
104
 
@@ -144,7 +144,7 @@ resume what it names.
144
144
 
145
145
  **Reading the outcome.** Each close ends the Story in one schema-validated
146
146
  envelope — `landed` | `pending` | `blocked` | `failed`; statuses, exits and
147
- fields are digest § 5. `pending` is **not** a failure — run its `nextCommand`.
147
+ fields are digest § 6. `pending` is **not** a failure — run its `nextCommand`.
148
148
 
149
149
  **Branch model (authoritative).** `story-<id>` → PR → `main` (squash +
150
150
  required checks), per digest § 2; dependent Stories land sequentially. The
@@ -75,11 +75,12 @@ in Key Assumptions, each a decision-made-by-default.
75
75
  `duplicates[]` is non-empty (planning a duplicate of open work stays the
76
76
  operator's call): confirm the sharpened plan intent and settle it. Otherwise
77
77
  announce the sharpened intent and the advisory line, and continue to
78
- authoring. Everything else the envelope surfaced — `duplicates[]`, open
79
- `intake` rows, a truthy `memoryPoolAdvisory.recommend`, a truthy
80
- `complexitySignals.uiSurface` naming [`/prototype`](prototype.md) (never
81
- invoke it here) — collapses to **one advisory line** under the gate, which
82
- never reroutes the run ([ref](helpers/plan-reference.md)).
78
+ authoring. The advisory line names what the envelope surfaced — any
79
+ `duplicates[]` (the stop above), open `intake` rows, a truthy
80
+ `memoryPoolAdvisory.recommend`, a truthy `complexitySignals.uiSurface`
81
+ naming [`/prototype`](prototype.md) (never invoke it here) — as
82
+ **one advisory line** under the gate; the line itself never reroutes the run
83
+ ([ref](helpers/plan-reference.md)).
83
84
  Under `--yes`, auto-proceed.
84
85
 
85
86
  ### 2. Author
package/docs/CHANGELOG.md CHANGED
@@ -15,6 +15,32 @@ All notable changes to this project will be documented in this file.
15
15
  -->
16
16
  <!-- markdownlint-disable-file MD004 MD012 MD037 -->
17
17
 
18
+ ## [2.66.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.65.0...mandrel-v2.66.0) (2026-09-26)
19
+
20
+
21
+ ### Added
22
+
23
+ * record per-Story retry causes and per-provider review halts at close ([#5435](https://github.com/dsj1984/mandrel/issues/5435)) ([#5441](https://github.com/dsj1984/mandrel/issues/5441)) ([c7bfd5a](https://github.com/dsj1984/mandrel/commit/c7bfd5ab2be417889f45a85f92145704ac580b88))
24
+
25
+
26
+ ### Fixed
27
+
28
+ * make the quality instruments fail loudly instead of silently mis-measuring, and reclaim floor slack ([#5444](https://github.com/dsj1984/mandrel/issues/5444)) ([#5452](https://github.com/dsj1984/mandrel/issues/5452)) ([54eb9fd](https://github.com/dsj1984/mandrel/commit/54eb9fd4660a0476740e0f96daae34e7f1fc754f))
29
+
30
+
31
+ ### Performance
32
+
33
+ * stop tests and CLI start-up from spending real wall-clock on host state and sleeps ([#5445](https://github.com/dsj1984/mandrel/issues/5445)) ([#5451](https://github.com/dsj1984/mandrel/issues/5451)) ([8f74d51](https://github.com/dsj1984/mandrel/commit/8f74d51da489b3cc96a0c23053338a718a68708c))
34
+
35
+
36
+ ### Changed
37
+
38
+ * burn down the bootstrap hotspots across CRAP, cyclomatic and dead exports ([#5447](https://github.com/dsj1984/mandrel/issues/5447)) ([#5455](https://github.com/dsj1984/mandrel/issues/5455)) ([be9e666](https://github.com/dsj1984/mandrel/commit/be9e6665011547be9396670f9a7486951ca1cd81))
39
+ * burn down the orchestration and signals complexity outliers ([#5449](https://github.com/dsj1984/mandrel/issues/5449)) ([#5456](https://github.com/dsj1984/mandrel/issues/5456)) ([208cede](https://github.com/dsj1984/mandrel/commit/208cede77cb8186bf479cb6e190b8e2849347bbf))
40
+ * collapse duplicated CLI plumbing: standard-args flag switches and the sync-agents/sync-commands clone ([#5448](https://github.com/dsj1984/mandrel/issues/5448)) ([#5454](https://github.com/dsj1984/mandrel/issues/5454)) ([5df933b](https://github.com/dsj1984/mandrel/commit/5df933b7c567730eae0ed47b172a8ac670d0831e))
41
+ * decompose the single-story-close and confirm-merge hot path ([#5446](https://github.com/dsj1984/mandrel/issues/5446)) ([#5453](https://github.com/dsj1984/mandrel/issues/5453)) ([3b7a5ae](https://github.com/dsj1984/mandrel/commit/3b7a5ae2b7391e16dddfd18d64e7fe4528bdc48c))
42
+ * trim the always-on closure and document the effort policy ([#5437](https://github.com/dsj1984/mandrel/issues/5437)) ([#5439](https://github.com/dsj1984/mandrel/issues/5439)) ([61d3c66](https://github.com/dsj1984/mandrel/commit/61d3c666637d38144cee7abcc7a50f28c332201a))
43
+
18
44
  ## [2.65.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.64.0...mandrel-v2.65.0) (2026-09-24)
19
45
 
20
46
 
@@ -0,0 +1,87 @@
1
+ // lib/cli/guarded-sync.js
2
+ /**
3
+ * Shared body of `mandrel sync-commands` / `mandrel sync-agents`: run a
4
+ * `.agents/scripts/` projector in a child process, forwarding its exit code.
5
+ * Refuses first when `.agents/` does not match the running CLI (version
6
+ * marker, else the `agents-drift` check): `.claude/*` is gitignored, so a
7
+ * stale projection surfaces only when an agent hits a missing module.
8
+ * The marker is read from `cwd()`; `PROJECT_ROOT` only names this CLI's version.
9
+ */
10
+
11
+ import { spawnSync } from 'node:child_process';
12
+ import nodeFs from 'node:fs';
13
+ import path from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+
16
+ import { runAgentsDrift } from './registry.js';
17
+ import { readVersionMarker } from './sync.js';
18
+
19
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
20
+ // lib/cli/ → lib/ → project root
21
+ const PROJECT_ROOT = path.resolve(__dirname, '..', '..');
22
+
23
+ function resolveOwnPackageVersion(fsImpl) {
24
+ const parsed = JSON.parse(
25
+ fsImpl.readFileSync(path.join(PROJECT_ROOT, 'package.json'), 'utf8'),
26
+ );
27
+ return String(parsed.version);
28
+ }
29
+
30
+ function refusalReason(projectRoot, { fs, ownVersion, checkAgentsDrift, cwd }) {
31
+ const marker = readVersionMarker(projectRoot, fs);
32
+ if (marker) {
33
+ const own = ownVersion ?? resolveOwnPackageVersion(fs);
34
+ if (marker === own) return null;
35
+ return {
36
+ what: `the materialized .agents/ tree is v${marker} but the running CLI is v${own}`,
37
+ fix: 're-materialize .agents/ to the current version',
38
+ };
39
+ }
40
+ const drift = (checkAgentsDrift ?? (() => runAgentsDrift({ cwd })))();
41
+ if (drift.ok) return null;
42
+ return {
43
+ what: `.agents/ appears to have drifted from the installed package payload (${drift.detail})`,
44
+ fix: 'restore the materialized .agents/ payload',
45
+ };
46
+ }
47
+
48
+ /**
49
+ * @param {{ command: string, target: string, script: string }} spec
50
+ * @param {string[]} _argv - Unused; reserved for future flags.
51
+ * @param {object} [opts] - Injectable runner, cwd, fs, version and I/O seams.
52
+ */
53
+ export function runGuardedSync(
54
+ { command, target, script },
55
+ _argv = [],
56
+ {
57
+ runner = spawnSync,
58
+ cwd = () => process.cwd(),
59
+ fs = nodeFs,
60
+ ownVersion,
61
+ checkAgentsDrift,
62
+ writeErr = (s) => process.stderr.write(s),
63
+ exit = (code) => process.exit(code),
64
+ } = {},
65
+ ) {
66
+ const refusal = refusalReason(cwd(), {
67
+ fs,
68
+ ownVersion,
69
+ checkAgentsDrift,
70
+ cwd,
71
+ });
72
+ if (refusal) {
73
+ writeErr(
74
+ `mandrel ${command}: ${refusal.what} — refusing to project ${target} from a mismatched tree.\n` +
75
+ ` → Run \`mandrel sync\` to ${refusal.fix}, then re-run.\n`,
76
+ );
77
+ exit(1);
78
+ return;
79
+ }
80
+ const syncScript = path.join(PROJECT_ROOT, '.agents', 'scripts', script);
81
+ const result = runner(process.execPath, [syncScript], {
82
+ stdio: 'inherit',
83
+ env: process.env,
84
+ });
85
+ const exitCode = result.status ?? 1;
86
+ if (exitCode !== 0) exit(exitCode);
87
+ }
@@ -1,97 +1,14 @@
1
1
  // lib/cli/sync-agents.js
2
- /**
3
- * `mandrel sync-agents`: project `.agents/agents/` into `.claude/agents/` via
4
- * `.agents/scripts/sync-claude-agents.js`. Exact sibling of
5
- * `sync-commands.js` — same child-process delegation, same marker-gated
6
- * refusal and anchor rule (see that module's doc).
7
- */
2
+ /** `mandrel sync-agents`: project `.agents/agents/` into `.claude/agents/`. */
8
3
 
9
- import { spawnSync } from 'node:child_process';
10
- import nodeFs from 'node:fs';
11
- import path from 'node:path';
12
- import { fileURLToPath } from 'node:url';
4
+ import { runGuardedSync } from './guarded-sync.js';
13
5
 
14
- import { runAgentsDrift } from './registry.js';
15
- import { readVersionMarker } from './sync.js';
6
+ const SPEC = {
7
+ command: 'sync-agents',
8
+ target: '.claude/agents/',
9
+ script: 'sync-claude-agents.js',
10
+ };
16
11
 
17
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
- // lib/cli/ → lib/ → project root
19
- const PROJECT_ROOT = path.resolve(__dirname, '..', '..');
20
- const SYNC_SCRIPT = path.join(
21
- PROJECT_ROOT,
22
- '.agents',
23
- 'scripts',
24
- 'sync-claude-agents.js',
25
- );
26
-
27
- /**
28
- * @param {typeof nodeFs} fsImpl
29
- * @returns {string}
30
- */
31
- function resolveOwnPackageVersion(fsImpl) {
32
- const parsed = JSON.parse(
33
- fsImpl.readFileSync(path.join(PROJECT_ROOT, 'package.json'), 'utf8'),
34
- );
35
- return String(parsed.version);
36
- }
37
-
38
- /**
39
- * @param {string[]} _argv - Unused; reserved for future flags.
40
- * @param {{
41
- * runner?: typeof spawnSync,
42
- * cwd?: () => string,
43
- * fs?: typeof nodeFs,
44
- * ownVersion?: string,
45
- * checkAgentsDrift?: () => { ok: boolean, detail: string },
46
- * writeErr?: (s: string) => void,
47
- * exit?: (code: number) => void,
48
- * }} [opts]
49
- * @returns {void}
50
- */
51
- export default function run(
52
- _argv = [],
53
- {
54
- runner = spawnSync,
55
- cwd = () => process.cwd(),
56
- fs = nodeFs,
57
- ownVersion,
58
- checkAgentsDrift,
59
- writeErr = (s) => process.stderr.write(s),
60
- exit = (code) => process.exit(code),
61
- } = {},
62
- ) {
63
- const projectRoot = cwd();
64
- const resolvedOwnVersion = ownVersion ?? resolveOwnPackageVersion(fs);
65
- const marker = readVersionMarker(projectRoot, fs);
66
-
67
- if (marker) {
68
- if (marker !== resolvedOwnVersion) {
69
- writeErr(
70
- `mandrel sync-agents: the materialized .agents/ tree is v${marker} but the running CLI is v${resolvedOwnVersion} — refusing to project .claude/agents/ from a mismatched tree.\n` +
71
- ' → Run `mandrel sync` to re-materialize .agents/ to the current version, then re-run.\n',
72
- );
73
- exit(1);
74
- return;
75
- }
76
- } else {
77
- const drift = (checkAgentsDrift ?? (() => runAgentsDrift({ cwd })))();
78
- if (!drift.ok) {
79
- writeErr(
80
- `mandrel sync-agents: .agents/ appears to have drifted from the installed package payload (${drift.detail}) — refusing to project .claude/agents/ from a mismatched tree.\n` +
81
- ' → Run `mandrel sync` to restore the materialized .agents/ payload, then re-run.\n',
82
- );
83
- exit(1);
84
- return;
85
- }
86
- }
87
-
88
- const result = runner(process.execPath, [SYNC_SCRIPT], {
89
- stdio: 'inherit',
90
- env: process.env,
91
- });
92
-
93
- const exitCode = result.status ?? 1;
94
- if (exitCode !== 0) {
95
- exit(exitCode);
96
- }
12
+ export default function run(argv, opts) {
13
+ runGuardedSync(SPEC, argv, opts);
97
14
  }