mandrel 2.10.0 → 2.12.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 (42) hide show
  1. package/.agents/docs/configuration.md +35 -33
  2. package/.agents/rules/orchestration-error-handling.md +9 -1
  3. package/.agents/schemas/agentrc.schema.json +13 -8
  4. package/.agents/scripts/acceptance-eval.js +9 -5
  5. package/.agents/scripts/lib/audit-suite/audit-rules-reader.js +48 -0
  6. package/.agents/scripts/lib/audit-suite/selector.js +1 -26
  7. package/.agents/scripts/lib/baselines/env-overrides.js +33 -0
  8. package/.agents/scripts/lib/baselines/git-base.js +0 -0
  9. package/.agents/scripts/lib/baselines/preview-gates.js +5 -0
  10. package/.agents/scripts/lib/config/gates/maintainability.schema.js +10 -1
  11. package/.agents/scripts/lib/config/quality.js +13 -0
  12. package/.agents/scripts/lib/config-settings-schema.js +12 -16
  13. package/.agents/scripts/lib/orchestration/ceremony-routing.js +45 -0
  14. package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +97 -4
  15. package/.agents/scripts/lib/orchestration/check-baselines/phases/parse-args.js +7 -0
  16. package/.agents/scripts/lib/orchestration/complexity-gate.js +561 -184
  17. package/.agents/scripts/lib/orchestration/plan-context.js +69 -10
  18. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +117 -60
  19. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +21 -15
  20. package/.agents/scripts/lib/orchestration/resolve-stories.js +28 -7
  21. package/.agents/scripts/lib/orchestration/review-depth.js +9 -4
  22. package/.agents/scripts/lib/orchestration/single-story-close/gate-log.js +186 -0
  23. package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +21 -3
  24. package/.agents/scripts/lib/orchestration/spec-budget.js +78 -0
  25. package/.agents/scripts/lib/orchestration/story-body-gate.js +72 -0
  26. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +6 -0
  27. package/.agents/scripts/lib/orchestration/ticket-validator.js +18 -62
  28. package/.agents/scripts/plan-context.js +23 -5
  29. package/.agents/scripts/resolve-stories.js +2 -0
  30. package/.agents/workflows/deliver.md +28 -28
  31. package/.agents/workflows/helpers/acceptance-self-eval.md +16 -5
  32. package/.agents/workflows/helpers/deliver-digest.md +126 -0
  33. package/.agents/workflows/helpers/deliver-reference.md +30 -5
  34. package/.agents/workflows/helpers/deliver-story-reference.md +38 -12
  35. package/.agents/workflows/helpers/deliver-story.md +34 -37
  36. package/.agents/workflows/helpers/plan-reference.md +79 -44
  37. package/.agents/workflows/plan.md +11 -10
  38. package/docs/CHANGELOG.md +31 -0
  39. package/lib/cli/registry.js +31 -14
  40. package/lib/migrations/index.js +2 -0
  41. package/lib/migrations/steps/2.11.0-retire-max-seed-words.js +92 -0
  42. package/package.json +1 -1
@@ -1,52 +1,87 @@
1
1
  # /plan — on-demand reference appendix
2
2
 
3
3
  > **Applies when:** you are executing [`/plan`](../plan.md) and hit one of the
4
- > situations below — the ceremony-lite route, `--tickets` supersede authoring,
5
- > critic dispatch detail, a failed persist, or source-id resolution. The spine
6
- > stays resident; this file is read on demand.
7
-
8
- ## Ceremony-lite complexity gate (`complexityRoute`)
9
-
10
- The envelope's `complexityRoute` field is a **deterministic, conservative**
11
- plan-time gate (Story #4683) that routes a genuinely trivial single-artifact
12
- seed onto a collapsed path so it stops paying the full two-session
13
- plan/deliver ceremony that measurably buys no quality at that size:
14
-
15
- - **`route: "lite"`** a trivial scope (seed ≤ `maxSeedWords` words **and** ≤
16
- `maxArtifacts` enumerated items). Collapse the ceremony: author **one minimal
17
- Story** and skip the fresh-critic / Tech-Spec ceremony a one-artifact scope
18
- does not earn. The lite route is **not** licence to drop a non-negotiable —
19
- its `preserves` field enumerates exactly what still holds: the Story ticket,
20
- the PR-to-`main` landing, every repo quality gate, and the security baseline.
21
- Those gates still run in `single-story-close.js` regardless of route.
22
- - **`route: "full"`** everything else. The gate fails toward `full` on any
23
- doubt (empty seed, over the word ceiling, a multi-capability enumeration, or
24
- the gate disabled via `planning.complexityGate.enabled=false`), so a real
25
- capability slice never loses ceremony. Author normally under the split policy.
26
-
27
- **Planner downgrade (audited, Story #4707).** Seed word count is a poor
28
- complexity proxy, so a `full` verdict you judge genuinely trivial (one
29
- artifact, one obvious change) may be downgraded to `lite` but **only** by
30
- passing `--route-downgrade-reason "<why>"` to persist. The reason is recorded
31
- on every created Story's `story-plan-state` checkpoint, making the judgment
32
- auditable; without a recorded reason the deterministic verdict stands, and the
33
- gate itself is unchanged (it still fails toward `full`).
34
-
35
- **The route persists with the Story (Story #4707).** Persist labels every
36
- Story of a lite-routed plan with the **`route::lite`** marker and ledgers the
37
- route (including any downgrade reason) on its `story-plan-state` checkpoint;
38
- a full-routed Story carries no marker. `/deliver` reads the marker to execute
39
- a lite Story **inline** — no story-worker or acceptance-critic sub-agent
40
- boots while every `single-story-close.js` gate runs unchanged. The
41
- `route::*` axis is runtime-derived: hand-authored `route::*` entries in
42
- `labels[]` are dropped by persist.
43
-
44
- The threshold and its override knob (`planning.complexityGate.{enabled,
45
- maxSeedWords, maxArtifacts}`) are documented in
46
- [`.agents/docs/configuration.md`](../../docs/configuration.md) under
47
- `### planning`; the defaults live on `DEFAULT_COMPLEXITY_GATE` in
4
+ > situations below — shape-derived complexity routing, `--tickets` supersede
5
+ > authoring, critic dispatch detail, a failed persist, or source-id
6
+ > resolution. The spine stays resident; this file is read on demand.
7
+
8
+ ## Shape-derived complexity routing (`complexitySignals`)
9
+
10
+ Complexity routes on the **objective shape of the authored work**, never on
11
+ seed word count (Story #4722 a detailed prompt can describe trivial work, a
12
+ terse one complex work; `maxSeedWords` is removed). The pipeline stages the
13
+ decision:
14
+
15
+ - **Signals, not routing.** The envelope's `complexitySignals` field is
16
+ advisory only (`routingAuthority: false`): enumerated-artifact count (with
17
+ the configured `maxArtifacts` threshold beside it as one input),
18
+ `planning.riskHeuristics` phrases present in the seed, the repo state of
19
+ predicted paths (existing paths predict refactors; missing predict
20
+ creates), and the `audit-rules.json` sensitive-path classes the predicted
21
+ footprint intersects.
22
+ - **You author the verdict.** Judge the signals: a genuinely trivial scope
23
+ (small additive footprint, no risk hits, no sensitive class) earns a `lite`
24
+ claim via `plan-persist.js --route-downgrade-reason "<why>"`. The reason is
25
+ recorded on every created Story's `story-plan-state` checkpoint, making the
26
+ judgment auditable; without a recorded reason the conservative default
27
+ (`full`) stands.
28
+ - **Persist backstops the claim deterministically.** After authoring, the
29
+ work has measurable shape, so persist validates the `lite` claim against
30
+ each Story's own shape `changes[]` count, acceptance-criteria count,
31
+ creates-vs-refactors mix, glob-free footprint, and sensitive-path classes,
32
+ against the framework `STORY_SHAPE_CEILINGS` and **fails closed to
33
+ `full`** when any Story exceeds them (the refusal is ledgered on the
34
+ checkpoint too). The lite route is **not** licence to drop a
35
+ non-negotiable every decision's `preserves` field enumerates what still
36
+ holds: the Story ticket, the PR-to-`main` landing, every repo quality gate,
37
+ and the security baseline. Those gates run in `single-story-close.js`
38
+ regardless of route.
39
+
40
+ **The label is a hint; deliver re-derives (Story #4722).** Persist labels a
41
+ lite cohort's Stories with **`route::lite`** as a *human-visible hint only*
42
+ `/deliver` computes the route from each fetched Story body via the same shape
43
+ function at dispatch, so neither a lost label nor an unread marker can
44
+ misroute delivery: a lite-shaped Story executes **inline** (no story-worker
45
+ or acceptance-critic sub-agent boots) even with the label absent, and a
46
+ sensitive-footprint Story routes `full` and keeps its fresh critic even with
47
+ the label present. The `route::*` axis stays runtime-derived: hand-authored
48
+ `route::*` entries in `labels[]` are dropped by persist.
49
+
50
+ The knobs (`planning.complexityGate.{enabled, maxArtifacts}`) are documented
51
+ in [`.agents/docs/configuration.md`](../../docs/configuration.md) under
52
+ `### planning`; the defaults live on `DEFAULT_COMPLEXITY_GATE` and the shape
53
+ ceilings on `STORY_SHAPE_CEILINGS` in
48
54
  [`lib/orchestration/complexity-gate.js`](../../scripts/lib/orchestration/complexity-gate.js).
49
55
 
56
+ ## Correct-by-construction authoring template (Story #4723)
57
+
58
+ `plan-context.js --out` writes `stories.template.json` as a
59
+ **correct-by-construction** skeleton, built from the same repo snapshot the
60
+ `complexitySignals` probed:
61
+
62
+ - **`verify[]` placeholders already end with a valid `(tier)` tag.** Keep
63
+ every filled entry's trailing tag one of `(unit)` / `(contract)` /
64
+ `(e2e)` / `(validate)` (or use the `manual:<reason>` escape) — a tierless
65
+ entry is exactly the mechanical persist round-trip the template exists to
66
+ prevent.
67
+ - **`changes[]` arrive pre-resolved to creates-vs-refactors.** Every path
68
+ the seed predicted is probed against the repo: an existing path is
69
+ emitted with `assumption: "refactors-existing"`, a missing one with
70
+ `assumption: "creates"`. Trust the pre-resolved assumption — verify
71
+ against the repo before overriding one (authoring `creates` for a file
72
+ that exists at base is a validator rejection). The persist gates stay
73
+ authoritative: they probe the base branch ref, not the working tree.
74
+ - **Keep `## Spec` near contract-level prose.** Persist emits an
75
+ **advisory** warning past ~250 words (`SPEC_SOFT_WORD_BUDGET`) — it never
76
+ fails the persist, but it is the nudge toward the #4707 contract-level
77
+ Spec (interfaces, invariants, load-bearing constraints; no per-file
78
+ behavior narration). The hard fail-closed ceiling (~1500 tokens,
79
+ `spec-spill.js`) is unchanged.
80
+
81
+ A faithfully-filled skeleton — placeholders replaced, pre-resolved entries
82
+ kept, tags valid — passes the persist ticket validators with no
83
+ round-trip.
84
+
50
85
  ## Tickets mode — authoring `supersedes[]`
51
86
 
52
87
  In `--tickets` mode each Story carries a top-level `supersedes` array claiming
@@ -32,7 +32,7 @@ Epic/Story router, no scope-triage `epic|story` verdict:
32
32
  | `--tickets <ids>` | Issue ids to analyze; closed as superseded at persist. |
33
33
  | `--no-close-superseded` | Keep the source issues open — no supersede comment, no close. |
34
34
  | `--force-review` | STOP at gate #2 for operator review — the only review gate (Story #4542). |
35
- | `--route-downgrade-reason "<text>"` | Audited `full`→`lite` downgrade (Story #4707), ledgered per Story. |
35
+ | `--route-downgrade-reason "<text>"` | Authored `lite` verdict + reason (Story #4722), ledgered per Story; shape-validated, fails closed to `full`. |
36
36
  | `--allow-over-budget` | Permit a plan exceeding `maxTickets`. |
37
37
  | `--yes` | Non-interactive: auto-proceed gate #1 and gate #2 HITL waits. |
38
38
  | `--dry-run` | Author + validate without GitHub writes; run as a pre-pass. |
@@ -66,11 +66,11 @@ authoring skeleton step 2 starts from.
66
66
 
67
67
  The envelope carries docs context, codebase snapshot, the story-author
68
68
  prompt, `sourceTickets[]`, `duplicates[]` (open **Stories** overlapping the
69
- seed — never Epics), and the `complexityRoute` signal:
70
- `"lite"` (trivial single-artifact scope author one minimal Story, skip
71
- fresh-critic / Tech-Spec ceremony; every close gate still runs) or `"full"`
72
- (everything else; fails toward `full` on any doubt). Detail:
73
- [`helpers/plan-reference.md` § Ceremony-lite gate](helpers/plan-reference.md).
69
+ seed — never Epics), and advisory `complexitySignals` (**no routing
70
+ authority**, Story #4722). A genuinely trivial scope earns
71
+ `--route-downgrade-reason "<why>"` at persist shape-validated, failing
72
+ closed to `full`. Detail:
73
+ [`helpers/plan-reference.md` § Shape-derived routing](helpers/plan-reference.md).
74
74
  Under `--yes`, do not ask free-form operator questions — unresolved
75
75
  unknowns land in Key Assumptions.
76
76
 
@@ -79,8 +79,9 @@ duplicate-candidate review. Under `--yes`, auto-proceed.
79
79
 
80
80
  ### 2. Author
81
81
 
82
- **One-shot authoring (Story #4707).** Start from `stories.template.json`
83
- (or the skeleton below); author `stories.json` in one pass. `body` is a
82
+ **One-shot authoring (Story #4707).** Start from `stories.template.json`;
83
+ author `stories.json` in one pass. Entries are pre-resolved (#4723); keep
84
+ tiers/assumptions valid. `body` is a
84
85
  markdown string **or** a structured object; persist parses either,
85
86
  serializes the canonical markdown, and syncs the top-level `acceptance[]` /
86
87
  `verify[]` into the body — never dual-author those lists.
@@ -182,7 +183,7 @@ persist, re-run the same command; never hand-delete issues.
182
183
 
183
184
  - [`/deliver`](deliver.md) — delivery entry point.
184
185
  - [`/audit-to-stories`](audit-to-stories.md) — audit findings → plan seed.
185
- - [`helpers/plan-reference.md`](helpers/plan-reference.md) — ceremony-lite,
186
- supersede, critic, and persist-resume detail.
186
+ - [`helpers/plan-reference.md`](helpers/plan-reference.md) — on-demand
187
+ detail.
187
188
  - [`core/scope-triage`](../skills/core/scope-triage/SKILL.md) — optional
188
189
  split-advisory notes only (no routing verdict).
package/docs/CHANGELOG.md CHANGED
@@ -2,6 +2,37 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [2.12.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.11.0...mandrel-v2.12.0) (2026-07-24)
6
+
7
+
8
+ ### Performance
9
+
10
+ * **orchestration:** cut deliver-path cost — inline single-Story dispatch, bounded close output, bundled reads (refs [#4736](https://github.com/dsj1984/mandrel/issues/4736)) ([#4737](https://github.com/dsj1984/mandrel/issues/4737)) ([81e62a8](https://github.com/dsj1984/mandrel/commit/81e62a87ca49add2aeb5b5a6b9ac11014edc3374))
11
+
12
+
13
+ ### Changed
14
+
15
+ * **orchestration:** thread injectedRules seam through route composition layers ([#4734](https://github.com/dsj1984/mandrel/issues/4734)) ([6e1daad](https://github.com/dsj1984/mandrel/commit/6e1daadbc24210fdb34e4892c0077256f2ccd051))
16
+
17
+ ## [2.11.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.10.0...mandrel-v2.11.0) (2026-07-23)
18
+
19
+
20
+ ### Added
21
+
22
+ * **migration:** strip retired planning.complexityGate.maxSeedWords on consumer upgrade ([#4729](https://github.com/dsj1984/mandrel/issues/4729)) ([fd75b54](https://github.com/dsj1984/mandrel/commit/fd75b54b0f7390a3d562a25e90cbe6c5911b4f66))
23
+ * route on the work, not the words — shape-derived complexity routing honored end-to-end (refs [#4722](https://github.com/dsj1984/mandrel/issues/4722)) ([#4725](https://github.com/dsj1984/mandrel/issues/4725)) ([f166ca1](https://github.com/dsj1984/mandrel/commit/f166ca14785bad97657fdda0f41e282e47db2ea3))
24
+ * single-owner acceptance verification + plan-template hardening ([#4723](https://github.com/dsj1984/mandrel/issues/4723)) ([#4728](https://github.com/dsj1984/mandrel/issues/4728)) ([954f55a](https://github.com/dsj1984/mandrel/commit/954f55aedda3199d1fdd48b11884a9513012e2c4))
25
+
26
+
27
+ ### Fixed
28
+
29
+ * **doctor:** include .agents/local/workflows/ in the commands-in-sync expected set (refs [#4724](https://github.com/dsj1984/mandrel/issues/4724)) ([#4726](https://github.com/dsj1984/mandrel/issues/4726)) ([b96dcd4](https://github.com/dsj1984/mandrel/commit/b96dcd446879a9da28e8d86c1a493c2bae62d2c8))
30
+
31
+
32
+ ### Changed
33
+
34
+ * **orchestration:** extract spec-budget and story-body parse gate from ticket-validator ([#4730](https://github.com/dsj1984/mandrel/issues/4730)) ([fb0c3c3](https://github.com/dsj1984/mandrel/commit/fb0c3c31795f6178b2ba740b2fb00760921e1178))
35
+
5
36
  ## [2.10.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.9.0...mandrel-v2.10.0) (2026-07-23)
6
37
 
7
38
 
@@ -225,10 +225,20 @@ function runGhAuth({ runner = spawn, env = process.env } = {}) {
225
225
  // ---------------------------------------------------------------------------
226
226
 
227
227
  /**
228
- * Dry-run the sync-claude-commands logic: compare `.agents/workflows/*.md`
229
- * sources to the generated flat command tree `.claude/commands/*.md`
230
- * destinations and report parity (the projection is a flat `/<name>` command
231
- * surface; the #3576 plugin projection was reverted).
228
+ * Dry-run the sync-claude-commands logic: compare the union of the two
229
+ * projection sources `.agents/workflows/*.md` (the installed payload) and
230
+ * `.agents/local/workflows/*.md` (consumer-authored, prune-exempt, projected
231
+ * since 1.75.0 / #4244) to the generated flat command tree
232
+ * `.claude/commands/*.md` destinations and report parity (the projection is
233
+ * a flat `/<name>` command surface; the #3576 plugin projection was
234
+ * reverted). A dest command backed only by a local workflow is in sync, not
235
+ * stale (#4721).
236
+ *
237
+ * Union semantics mirror the sync script's payload-wins shadowing (`byRel`):
238
+ * a basename projects iff at least one source's copy is non-excluded — an
239
+ * excluded payload copy never enters `byRel`, so it does not shadow a
240
+ * projectable local copy. Filtering each source dir independently and
241
+ * unioning the surviving basenames reproduces that exactly.
232
242
  *
233
243
  * Resolution anchor (Story #3588): the root defaults to `process.cwd()` —
234
244
  * the consumer project directory where `mandrel sync` materializes both
@@ -272,28 +282,35 @@ function runCommandsInSync({ projectRoot, cwd, readDir, readFile } = {}) {
272
282
  }
273
283
  });
274
284
 
275
- const srcDir = path.join(root, '.agents', 'workflows');
285
+ const srcDirs = [
286
+ path.join(root, '.agents', 'workflows'),
287
+ path.join(root, '.agents', 'local', 'workflows'),
288
+ ];
276
289
  const destDir = path.join(root, '.claude', 'commands');
277
290
 
278
291
  // Only top-level .md files are synced (helpers/ subdirectory excluded by
279
292
  // the sync script — they are path-included modules, not slash commands).
280
293
  // Workflows whose frontmatter carries `command: false` (#4482) opt out of
281
- // projection and must not count toward the expected command set.
282
- const sources = listDir(srcDir)
283
- .filter((f) => !f.startsWith('.'))
284
- .filter((f) => {
294
+ // projection and must not count toward the expected command set. An absent
295
+ // local dir degrades to [] via the listDir catch, so payload-only
296
+ // consumers are unchanged.
297
+ const expected = new Set();
298
+ for (const srcDir of srcDirs) {
299
+ for (const f of listDir(srcDir)) {
300
+ if (f.startsWith('.')) continue;
285
301
  const content = readSource(path.join(srcDir, f));
286
- return content == null || !isCommandExcluded(content);
287
- })
288
- .sort();
302
+ if (content != null && isCommandExcluded(content)) continue;
303
+ expected.add(f);
304
+ }
305
+ }
306
+ const sources = [...expected].sort();
289
307
  const dests = listDir(destDir)
290
308
  .filter((f) => !f.startsWith('.'))
291
309
  .sort();
292
310
 
293
- const srcSet = new Set(sources);
294
311
  const dstSet = new Set(dests);
295
312
  const missing = sources.filter((f) => !dstSet.has(f));
296
- const extra = dests.filter((f) => !srcSet.has(f));
313
+ const extra = dests.filter((f) => !expected.has(f));
297
314
 
298
315
  if (missing.length === 0 && extra.length === 0) {
299
316
  return { ok: true, detail: `${sources.length} commands up to date` };
@@ -55,6 +55,7 @@
55
55
  import { retireMiDropKnobs } from './steps/2.1.0-retire-mi-drop-knobs.js';
56
56
  import { retireVerifyConcurrencyCap } from './steps/2.1.0-retire-verify-concurrency-cap.js';
57
57
  import { retireEpicAcTags } from './steps/2.2.0-retire-epic-ac-tags.js';
58
+ import { retireMaxSeedWords } from './steps/2.11.0-retire-max-seed-words.js';
58
59
 
59
60
  /**
60
61
  * Ordered registry of migration steps. MUST stay sorted ascending by
@@ -71,6 +72,7 @@ export const migrations = [
71
72
  retireMiDropKnobs,
72
73
  retireVerifyConcurrencyCap,
73
74
  retireEpicAcTags,
75
+ retireMaxSeedWords,
74
76
  ];
75
77
 
76
78
  /**
@@ -0,0 +1,92 @@
1
+ // lib/migrations/steps/2.11.0-retire-max-seed-words.js
2
+ /**
3
+ * Story #4722 follow-up — strip the retired
4
+ * `planning.complexityGate.maxSeedWords` knob from a consumer's
5
+ * `.agentrc.json`.
6
+ *
7
+ * #4722 (PR #4725) hard-cutover-removed word-count complexity routing:
8
+ * the route derives from the authored Story's shape, never from seed word
9
+ * count, and `maxSeedWords` was dropped from the runtime AJV schema and
10
+ * the published mirror. The `complexityGate` block carries
11
+ * `additionalProperties: false`, so a consumer whose config still sets
12
+ * `maxSeedWords` hits a hard validation failure on upgrade, not a
13
+ * warning. This step strips the key before that check runs — the same
14
+ * contract-cutover pattern as `2.1.0-retire-mi-drop-knobs.js`.
15
+ */
16
+
17
+ import nodeFs from 'node:fs';
18
+ import path from 'node:path';
19
+
20
+ const AGENTRC_FILENAME = '.agentrc.json';
21
+
22
+ /**
23
+ * @param {unknown} ctx
24
+ * @returns {string}
25
+ */
26
+ function resolveAgentrcPath(ctx) {
27
+ const projectRoot = ctx?.projectRoot ?? process.cwd();
28
+ return path.join(projectRoot, AGENTRC_FILENAME);
29
+ }
30
+
31
+ /**
32
+ * @param {unknown} ctx
33
+ * @param {typeof nodeFs} fsImpl
34
+ * @returns {object | null}
35
+ */
36
+ function readAgentrcConfig(ctx, fsImpl) {
37
+ try {
38
+ const raw = fsImpl.readFileSync(resolveAgentrcPath(ctx), 'utf8');
39
+ return JSON.parse(raw);
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * @param {object | null} config
47
+ * @returns {boolean}
48
+ */
49
+ function hasRetiredKey(config) {
50
+ const gate = config?.planning?.complexityGate;
51
+ return Boolean(gate) && Object.hasOwn(gate, 'maxSeedWords');
52
+ }
53
+
54
+ export const retireMaxSeedWords = {
55
+ version: '2.11.0',
56
+ description:
57
+ 'strip retired planning.complexityGate.maxSeedWords from .agentrc.json ' +
58
+ '(complexity routes on Story shape, never seed word count — Story #4722)',
59
+ /**
60
+ * @param {{ projectRoot?: string, fs?: typeof nodeFs }} [ctx]
61
+ * @returns {boolean}
62
+ */
63
+ detect(ctx) {
64
+ const fsImpl = ctx?.fs ?? nodeFs;
65
+ return hasRetiredKey(readAgentrcConfig(ctx, fsImpl));
66
+ },
67
+ /**
68
+ * @param {{ projectRoot?: string, fs?: typeof nodeFs }} [ctx]
69
+ * @returns {void}
70
+ */
71
+ apply(ctx) {
72
+ const fsImpl = ctx?.fs ?? nodeFs;
73
+ const config = readAgentrcConfig(ctx, fsImpl);
74
+ if (!config) return;
75
+
76
+ const gate = config.planning?.complexityGate;
77
+ if (gate && Object.hasOwn(gate, 'maxSeedWords')) {
78
+ delete gate.maxSeedWords;
79
+ if (Object.keys(gate).length === 0) {
80
+ delete config.planning.complexityGate;
81
+ if (Object.keys(config.planning).length === 0) {
82
+ delete config.planning;
83
+ }
84
+ }
85
+ }
86
+
87
+ fsImpl.writeFileSync(
88
+ resolveAgentrcPath(ctx),
89
+ `${JSON.stringify(config, null, 2)}\n`,
90
+ );
91
+ },
92
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mandrel",
3
- "version": "2.10.0",
3
+ "version": "2.12.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/",