mandrel 2.36.0 → 2.38.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 (225) hide show
  1. package/.agents/README.md +16 -16
  2. package/.agents/agents/plan-critic.md +1 -1
  3. package/.agents/docs/SDLC.md +40 -40
  4. package/.agents/docs/configuration.md +41 -16
  5. package/.agents/docs/execution-reference.md +3 -3
  6. package/.agents/docs/quality-gates.md +2 -2
  7. package/.agents/docs/workflows.md +6 -6
  8. package/.agents/instructions.md +5 -5
  9. package/.agents/rules/changelog-style.md +3 -3
  10. package/.agents/rules/ci-remediation.md +1 -1
  11. package/.agents/rules/git-conventions-reference.md +4 -4
  12. package/.agents/rules/testing-standards.md +56 -0
  13. package/.agents/schemas/agentrc.schema.json +9 -9
  14. package/.agents/schemas/story-deliver-terminal.schema.json +2 -2
  15. package/.agents/schemas/validation-evidence.schema.json +1 -1
  16. package/.agents/scripts/acceptance-eval.js +1 -1
  17. package/.agents/scripts/apply-quality-bootstrap.js +1 -1
  18. package/.agents/scripts/audit-to-stories.js +2 -2
  19. package/.agents/scripts/boot-sweep.js +7 -1
  20. package/.agents/scripts/check-context-budget.js +195 -103
  21. package/.agents/scripts/check-cyclomatic.js +112 -42
  22. package/.agents/scripts/check-doc-links.js +32 -4
  23. package/.agents/scripts/check-generated-validator.js +202 -0
  24. package/.agents/scripts/check-lifecycle-lint.js +2 -68
  25. package/.agents/scripts/check-schema-references.js +2 -28
  26. package/.agents/scripts/check-test-temp-hygiene.js +1 -1
  27. package/.agents/scripts/check-workflow-cli-lint.js +1 -1
  28. package/.agents/scripts/deliver-light.js +10 -10
  29. package/.agents/scripts/deliver-recover.js +1 -1
  30. package/.agents/scripts/drain-pending-cleanup.js +19 -2
  31. package/.agents/scripts/evidence-gate.js +1 -1
  32. package/.agents/scripts/generate-workflows-doc.js +1 -1
  33. package/.agents/scripts/lib/audit-suite/selector.js +1 -1
  34. package/.agents/scripts/lib/audit-to-stories/audit-label-taxonomy.js +1 -1
  35. package/.agents/scripts/lib/audit-to-stories/build-story-body.js +1 -1
  36. package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +5 -5
  37. package/.agents/scripts/lib/audit-to-stories/wire-dependencies.js +1 -1
  38. package/.agents/scripts/lib/baselines/components.js +32 -2
  39. package/.agents/scripts/lib/baselines/env-overrides.js +1 -1
  40. package/.agents/scripts/lib/bdd-runner-detect.js +1 -1
  41. package/.agents/scripts/lib/bdd-scenario-budget.js +1 -1
  42. package/.agents/scripts/lib/bdd-scenario-scanner.js +3 -3
  43. package/.agents/scripts/lib/bdd-step-index.js +1 -1
  44. package/.agents/scripts/lib/bootstrap/branch-protection.js +2 -2
  45. package/.agents/scripts/lib/bootstrap/commit-push.js +2 -2
  46. package/.agents/scripts/lib/bootstrap/install-ledger.js +1 -1
  47. package/.agents/scripts/lib/bootstrap/issue-forms-template.js +1 -1
  48. package/.agents/scripts/lib/bootstrap/prompt.js +1 -1
  49. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +2 -2
  50. package/.agents/scripts/lib/checks/core-bare-clean.js +1 -1
  51. package/.agents/scripts/lib/checks/loop-health.js +1 -1
  52. package/.agents/scripts/lib/checks/story-init-not-backgrounded.js +1 -1
  53. package/.agents/scripts/lib/cli-args.js +1 -3
  54. package/.agents/scripts/lib/close-validation/gates.js +1 -1
  55. package/.agents/scripts/lib/close-validation/process.js +1 -1
  56. package/.agents/scripts/lib/command-header.js +1 -1
  57. package/.agents/scripts/lib/config/delivery-routing.js +1 -1
  58. package/.agents/scripts/lib/config/explain.js +1 -1
  59. package/.agents/scripts/lib/config/runners.js +2 -2
  60. package/.agents/scripts/lib/config/runtime.js +1 -1
  61. package/.agents/scripts/lib/config/sync-agentrc.js +1 -1
  62. package/.agents/scripts/lib/config/temp-paths.js +2 -2
  63. package/.agents/scripts/lib/config-settings-schema-delivery.js +3 -3
  64. package/.agents/scripts/lib/config-settings-schema-quality.js +6 -6
  65. package/.agents/scripts/lib/config-settings-schema.js +49 -9
  66. package/.agents/scripts/lib/coverage-baseline.js +2 -2
  67. package/.agents/scripts/lib/cpu-pool.js +90 -10
  68. package/.agents/scripts/lib/crap-utils.js +6 -2
  69. package/.agents/scripts/lib/cyclomatic-ceiling.js +28 -6
  70. package/.agents/scripts/lib/cyclomatic-scope.js +144 -0
  71. package/.agents/scripts/lib/dependency-version.js +86 -0
  72. package/.agents/scripts/lib/duplicate-search.js +2 -2
  73. package/.agents/scripts/lib/errors/index.js +1 -1
  74. package/.agents/scripts/lib/feedback-loop/graduator-core.js +2 -2
  75. package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +5 -5
  76. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +2 -2
  77. package/.agents/scripts/lib/findings/classify-finding.js +1 -1
  78. package/.agents/scripts/lib/findings/promote-finding.js +12 -12
  79. package/.agents/scripts/lib/findings/route-finding.js +2 -2
  80. package/.agents/scripts/lib/generated/agentrc-validator.js +16 -0
  81. package/.agents/scripts/lib/git-branch-lifecycle.js +2 -2
  82. package/.agents/scripts/lib/install-cmd-parser.js +1 -1
  83. package/.agents/scripts/lib/knip-entry-sync.js +1 -57
  84. package/.agents/scripts/lib/label-constants.js +2 -2
  85. package/.agents/scripts/lib/label-taxonomy.js +1 -1
  86. package/.agents/scripts/lib/maintainability-utils.js +108 -10
  87. package/.agents/scripts/lib/observability/source-classifier.js +1 -0
  88. package/.agents/scripts/lib/onboard/init-tail.js +4 -4
  89. package/.agents/scripts/lib/onboard/scaffold-docs.js +2 -2
  90. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +1 -1
  91. package/.agents/scripts/lib/orchestration/code-review.js +4 -4
  92. package/.agents/scripts/lib/orchestration/complexity-gate.js +5 -5
  93. package/.agents/scripts/lib/orchestration/deliver-recover.js +3 -3
  94. package/.agents/scripts/lib/orchestration/docs-digest.js +3 -3
  95. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +2 -2
  96. package/.agents/scripts/lib/orchestration/light-backstop.js +1 -1
  97. package/.agents/scripts/lib/orchestration/light-escalation.js +6 -6
  98. package/.agents/scripts/lib/orchestration/light-suitability.js +19 -19
  99. package/.agents/scripts/lib/orchestration/plan-context.js +4 -4
  100. package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +2 -2
  101. package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +2 -2
  102. package/.agents/scripts/lib/orchestration/plan-metrics.js +1 -1
  103. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +1 -1
  104. package/.agents/scripts/lib/orchestration/plan-persist/plan-context-source.js +3 -3
  105. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +6 -6
  106. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +86 -25
  107. package/.agents/scripts/lib/orchestration/plan-persist/summary.js +3 -3
  108. package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +6 -6
  109. package/.agents/scripts/lib/orchestration/planning/authoring-context.js +7 -7
  110. package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +1 -1
  111. package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +3 -3
  112. package/.agents/scripts/lib/orchestration/pr-watch.js +1 -30
  113. package/.agents/scripts/lib/orchestration/reassert-status-column.js +2 -2
  114. package/.agents/scripts/lib/orchestration/remote-verifier.js +2 -2
  115. package/.agents/scripts/lib/orchestration/resolve-stories.js +4 -4
  116. package/.agents/scripts/lib/orchestration/review-providers/codex.js +3 -3
  117. package/.agents/scripts/lib/orchestration/review-providers/findings-renderer.js +1 -1
  118. package/.agents/scripts/lib/orchestration/review-providers/native.js +9 -2
  119. package/.agents/scripts/lib/orchestration/review-providers/review-depth.js +2 -2
  120. package/.agents/scripts/lib/orchestration/review-providers/security-review.js +3 -3
  121. package/.agents/scripts/lib/orchestration/run-epilogue.js +2 -2
  122. package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +2 -2
  123. package/.agents/scripts/lib/orchestration/single-story-close/phases/conventional-subject.js +1 -1
  124. package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +5 -1
  125. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +52 -1
  126. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +1 -1
  127. package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +3 -3
  128. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +2 -2
  129. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +1 -1
  130. package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +2 -5
  131. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +14 -12
  132. package/.agents/scripts/lib/orchestration/story-follow-ups.js +1 -1
  133. package/.agents/scripts/lib/orchestration/ticket-lease.js +34 -9
  134. package/.agents/scripts/lib/orchestration/ticket-validator.js +1 -1
  135. package/.agents/scripts/lib/orchestration/ticketing/reads.js +6 -4
  136. package/.agents/scripts/lib/plan-phase-cleanup.js +1 -1
  137. package/.agents/scripts/lib/preflight-runner.js +3 -3
  138. package/.agents/scripts/lib/qa/qa-session.js +1 -1
  139. package/.agents/scripts/lib/runtime-deps/manifest.js +1 -7
  140. package/.agents/scripts/lib/runtime-deps/scan-imports.js +3 -58
  141. package/.agents/scripts/lib/signals/schema.js +1 -1
  142. package/.agents/scripts/lib/single-story-sweep/sweep-lock.js +284 -25
  143. package/.agents/scripts/lib/skills/walk-skill-files.js +1 -1
  144. package/.agents/scripts/lib/source-text/strip-js-comments.js +115 -0
  145. package/.agents/scripts/lib/story-adjacency.js +2 -2
  146. package/.agents/scripts/lib/templates/decomposer-prompts.js +1 -1
  147. package/.agents/scripts/lib/test-isolate/runner.js +3 -3
  148. package/.agents/scripts/lib/test-runner-contract.js +14 -6
  149. package/.agents/scripts/lib/test-tiers.js +135 -18
  150. package/.agents/scripts/lib/ticket-body-sections.js +1 -1
  151. package/.agents/scripts/lib/transpile.js +16 -3
  152. package/.agents/scripts/lib/util/concurrent-map.js +1 -1
  153. package/.agents/scripts/lib/util/parse-id-list.js +1 -1
  154. package/.agents/scripts/lib/wave-runner/live-probe.js +4 -4
  155. package/.agents/scripts/lib/wave-runner/ready-set.js +1 -1
  156. package/.agents/scripts/lib/workflow-closure.js +1 -1
  157. package/.agents/scripts/lib/worktree/lifecycle/pending-cleanup.js +170 -32
  158. package/.agents/scripts/lib/worktree/node-modules-strategy.js +2 -5
  159. package/.agents/scripts/lint-issue-body.js +3 -3
  160. package/.agents/scripts/plan-context.js +4 -4
  161. package/.agents/scripts/plan-critics.js +2 -2
  162. package/.agents/scripts/plan-persist.js +2 -2
  163. package/.agents/scripts/plan-run-epilogue.js +2 -2
  164. package/.agents/scripts/pr-watch-with-update.js +305 -137
  165. package/.agents/scripts/providers/github/board-add.js +1 -1
  166. package/.agents/scripts/providers/github/errors.js +1 -1
  167. package/.agents/scripts/providers/github/tickets.js +114 -21
  168. package/.agents/scripts/quality-preview.js +162 -70
  169. package/.agents/scripts/resolve-stories.js +3 -3
  170. package/.agents/scripts/resync-status-column.js +1 -1
  171. package/.agents/scripts/run-lint.js +1 -1
  172. package/.agents/scripts/run-test-profile.js +8 -5
  173. package/.agents/scripts/run-tests.js +79 -14
  174. package/.agents/scripts/single-story-close.js +2 -4
  175. package/.agents/scripts/single-story-init.js +10 -6
  176. package/.agents/scripts/stories-wave-tick.js +8 -8
  177. package/.agents/skills/core/idea-refinement/SKILL.md +5 -5
  178. package/.agents/skills/core/scope-triage/SKILL.md +3 -3
  179. package/.agents/skills/skills.index.json +1 -1
  180. package/.agents/skills/stack/qa/qa-harness/SKILL.md +1 -1
  181. package/.agents/workflows/audit-data-model.md +1 -1
  182. package/.agents/workflows/audit-documentation.md +2 -2
  183. package/.agents/workflows/audit-to-stories.md +11 -11
  184. package/.agents/workflows/git-deliver.md +4 -4
  185. package/.agents/workflows/helpers/_merge-conflict-template.md +1 -1
  186. package/.agents/workflows/helpers/audit-lens-core.md +1 -1
  187. package/.agents/workflows/helpers/code-review.md +4 -4
  188. package/.agents/workflows/helpers/deliver-light.md +26 -26
  189. package/.agents/workflows/helpers/deliver-reference.md +15 -7
  190. package/.agents/workflows/helpers/deliver-story-reference.md +9 -4
  191. package/.agents/workflows/helpers/deliver-story.md +6 -7
  192. package/.agents/workflows/helpers/diagnose.md +2 -2
  193. package/.agents/workflows/helpers/parallel-tooling.md +1 -1
  194. package/.agents/workflows/helpers/plan-reference.md +9 -9
  195. package/.agents/workflows/helpers/qa-core.md +8 -8
  196. package/.agents/workflows/helpers/worktree-lifecycle.md +6 -6
  197. package/.agents/workflows/{deliver.md → mandrel-deliver.md} +15 -19
  198. package/.agents/workflows/{plan.md → mandrel-plan.md} +9 -9
  199. package/.agents/workflows/memory-consolidate.md +4 -4
  200. package/.agents/workflows/prototype.md +3 -3
  201. package/.agents/workflows/qa-assist.md +12 -12
  202. package/.agents/workflows/qa-explore.md +6 -6
  203. package/.agents/workflows/qa-run.md +1 -1
  204. package/README.md +7 -7
  205. package/docs/CHANGELOG.md +43 -0
  206. package/lib/cli/doctor.js +1 -1
  207. package/lib/cli/init.js +1 -1
  208. package/lib/cli/migrate.js +100 -94
  209. package/lib/migrations/helpers/retire-agentrc-key.js +200 -0
  210. package/lib/migrations/index.js +32 -33
  211. package/lib/migrations/steps/2.1.0-retire-mi-drop-knobs.js +21 -80
  212. package/lib/migrations/steps/2.1.0-retire-verify-concurrency-cap.js +15 -87
  213. package/lib/migrations/steps/2.11.0-retire-max-seed-words.js +13 -71
  214. package/lib/migrations/steps/2.2.0-retire-epic-ac-tags.js +1 -1
  215. package/lib/migrations/steps/2.20.0-retire-codebase-snapshot.js +13 -101
  216. package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +13 -100
  217. package/package.json +7 -3
  218. package/.agents/scripts/lib/audit-suite/frontmatter-lint.js +0 -32
  219. package/.agents/scripts/lib/baselines/maintainability-baseline-save.js +0 -37
  220. package/.agents/scripts/lib/cli/parse-numeric.js +0 -60
  221. package/.agents/scripts/lib/close-validation/telemetry.js +0 -79
  222. package/.agents/scripts/lib/orchestration/label-transitions.js +0 -44
  223. package/.agents/scripts/lib/orchestration/parked-follow-ons.js +0 -147
  224. package/.agents/scripts/lib/orchestration/phase-runner.js +0 -88
  225. package/.agents/scripts/lib/orchestration/recut.js +0 -56
@@ -89,7 +89,7 @@ export function branchExistsViaTrackingRef(branch, cwd) {
89
89
  /**
90
90
  * Pure: classify how a `story-<id>` branch should be seeded from the (local,
91
91
  * remote) ref-presence matrix. This is the single source of truth for
92
- * `single-story-init.js#decideStoryBranchSeed` (v2 `/deliver` path).
92
+ * `single-story-init.js#decideStoryBranchSeed` (v2 `/mandrel-deliver` path).
93
93
  *
94
94
  * The init path previously re-implemented the same `local → no-op, remote →
95
95
  * fetch, else create` decision tree; it now delegates here so the branching
@@ -122,7 +122,7 @@ export function classifyBranchSeed({ localHas, remoteHas }) {
122
122
  * The two callers differ in exactly two behavioural axes, both of which are
123
123
  * parameters here — no other conditional branching is introduced:
124
124
  * - **`baseRef`** — the ref to branch from on `create` (`main` for v2
125
- * `/deliver`; pre-v2 Epic close used the Epic branch).
125
+ * `/mandrel-deliver`; pre-v2 Epic close used the Epic branch).
126
126
  * - **`swallowCreateRace`** — when `true`, a `git branch` that exits
127
127
  * non-zero with an "already exists" stderr is treated as reuse rather
128
128
  * than a fatal error (pre-v2 concurrent wave dispatch). When `false`, any create failure
@@ -17,7 +17,7 @@ import { spawnSync as defaultSpawnSync } from 'node:child_process';
17
17
  * @param {string} installCmd
18
18
  * @returns {{ bin: string, args: string[], shell: boolean }}
19
19
  */
20
- export function parseInstallCmd(installCmd) {
20
+ function parseInstallCmd(installCmd) {
21
21
  const tokens = String(installCmd ?? '')
22
22
  .trim()
23
23
  .split(/\s+/)
@@ -59,6 +59,7 @@ import {
59
59
  resolveKnipEntryPatterns,
60
60
  __testing as resolverTesting,
61
61
  } from './knip-config-resolver.js';
62
+ import { stripJsComments } from './source-text/strip-js-comments.js';
62
63
 
63
64
  // Only `resolveEntrySync` and `renderEntrySyncReport` are public — they are
64
65
  // what `check-knip-entries.js` calls. Everything else is module-private and
@@ -89,63 +90,6 @@ const INVOCATION_SURFACES = Object.freeze([
89
90
  { kind: 'dir', at: SCRIPTS_DIR, test: /\.js$/ },
90
91
  ]);
91
92
 
92
- /**
93
- * Strip line and block comments from JavaScript source, preserving string and
94
- * template literals so a `'https://…'` or a `` `${x}//y` `` is not mangled.
95
- *
96
- * Comments are stripped before scanning `.js` surfaces so a JSDoc paragraph
97
- * that merely *names* a CLI ("superseded by `node .agents/scripts/foo.js`")
98
- * cannot confer liveness on it. Several such mentions exist today; every one
99
- * of them is prose about a script's internals, not a call.
100
- *
101
- * Replaces comment bodies with equivalent whitespace rather than deleting
102
- * them, so byte offsets and line numbers survive for any future caller that
103
- * wants to report a position.
104
- *
105
- * @param {string} source
106
- * @returns {string}
107
- */
108
- function stripJsComments(source) {
109
- const text = String(source ?? '');
110
- let out = '';
111
- let i = 0;
112
- const blank = (s) => s.replace(/[^\n]/g, ' ');
113
- while (i < text.length) {
114
- const two = text.slice(i, i + 2);
115
- if (two === '//') {
116
- const end = text.indexOf('\n', i);
117
- const stop = end === -1 ? text.length : end;
118
- out += blank(text.slice(i, stop));
119
- i = stop;
120
- } else if (two === '/*') {
121
- const end = text.indexOf('*/', i + 2);
122
- const stop = end === -1 ? text.length : end + 2;
123
- out += blank(text.slice(i, stop));
124
- i = stop;
125
- } else {
126
- const ch = text[i];
127
- if (ch === "'" || ch === '"' || ch === '`') {
128
- let j = i + 1;
129
- while (j < text.length) {
130
- if (text[j] === '\\') {
131
- j += 2;
132
- continue;
133
- }
134
- if (text[j] === ch) break;
135
- j += 1;
136
- }
137
- const stop = Math.min(j + 1, text.length);
138
- out += text.slice(i, stop);
139
- i = stop;
140
- } else {
141
- out += ch;
142
- i += 1;
143
- }
144
- }
145
- }
146
- return out;
147
- }
148
-
149
93
  /**
150
94
  * Recursively list files under `dir`, skipping `node_modules` and `.git`.
151
95
  *
@@ -15,7 +15,7 @@ export const AGENT_LABELS = {
15
15
  // the Story PR is opened against `main`. It flips to
16
16
  // `agent::done` only after the close pipeline confirms the PR merge
17
17
  // landed; if the close is killed mid-flight, the Story remains at
18
- // `agent::closing` so `/deliver` can pick up at the
18
+ // `agent::closing` so `/mandrel-deliver` can pick up at the
19
19
  // post-merge phase rather than re-running preflight. The label is the
20
20
  // distinguishing signal between "hung close" and "finished work".
21
21
  CLOSING: 'agent::closing',
@@ -101,7 +101,7 @@ export const ACCEPTANCE_NA = ACCEPTANCE_LABELS.N_A;
101
101
  * loop). `meta::framework-gap` is applied to issues that surface a defect or
102
102
  * missing capability in the framework itself; `meta::consumer-improvement`
103
103
  * is applied to issues that surface improvements to a consumer project
104
- * (workflow tweaks, ergonomic asks, doc polish). The `/plan` Phase 0
104
+ * (workflow tweaks, ergonomic asks, doc polish). The `/mandrel-plan` Phase 0
105
105
  * fetcher (see `lib/feedback-loop/prior-feedback-fetcher.js`) reads open
106
106
  * issues carrying either label and surfaces them to the planner so retro
107
107
  * signals are routed into durable substrates rather than lost in chat.
@@ -40,7 +40,7 @@ export const LABEL_TAXONOMY = [
40
40
  name: AGENT_LABELS.READY,
41
41
  color: LABEL_COLORS.AGENT,
42
42
  description:
43
- 'Parking state — frozen dispatch manifest exists; awaiting local /deliver',
43
+ 'Parking state — frozen dispatch manifest exists; awaiting local /mandrel-deliver',
44
44
  },
45
45
  {
46
46
  name: AGENT_LABELS.EXECUTING,
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { minimatch } from 'minimatch';
3
+ import { Minimatch } from 'minimatch';
4
4
  import { canonicalise as canonicalisePath } from './baselines/path-canon.js';
5
5
  import { POOL_SERIAL_THRESHOLD, runOnPool } from './cpu-pool.js';
6
6
  import { Logger } from './Logger.js';
@@ -27,6 +27,57 @@ const IGNORED_DIRS = new Set([
27
27
  '.next',
28
28
  ]);
29
29
 
30
+ /**
31
+ * Compiled-matcher cache for `isIgnoredByGlobs`, keyed on the pattern list.
32
+ *
33
+ * `minimatch(path, glob)` re-parses `glob` into an AST on every call, so the
34
+ * scan was paying O(files × globs) glob *compilations* to answer O(files ×
35
+ * globs) glob *matches* — 12–17 ms over the 619-file tree against 0.5 ms
36
+ * once the patterns are compiled (Story #5109). The number of distinct
37
+ * pattern lists in a process is tiny (one per gate), and each list is
38
+ * config-derived and immutable, so an unbounded `Map` keyed on the joined
39
+ * patterns is bounded in practice.
40
+ *
41
+ * Each entry also memoises the per-path verdict. The baseline gates ask the
42
+ * same question about the same path many times over — `crap.json` carries
43
+ * thousands of method rows spread over a few hundred files, and the evaluate
44
+ * phase filters every row — so the second and later asks about a path cost a
45
+ * `Map` lookup instead of a globstar walk. Both caches are keyed on
46
+ * config-derived, immutable inputs, and both are bounded by the tree the
47
+ * process is scanning.
48
+ *
49
+ * `\u0000` is the join separator because it cannot occur in a glob, so two
50
+ * different lists cannot collide onto one key.
51
+ *
52
+ * @type {Map<string, {matchers: import('minimatch').Minimatch[], verdicts: Map<string, boolean>}>}
53
+ */
54
+ const IGNORE_MATCHER_CACHE = new Map();
55
+
56
+ /**
57
+ * Compile (once) the `Minimatch` instances for a pattern list, alongside the
58
+ * per-path verdict memo that shares their lifetime.
59
+ *
60
+ * Non-string patterns are dropped rather than compiled — `minimatch()` would
61
+ * have thrown on them, and the functional call site never fed it any, so
62
+ * dropping preserves the observable result set.
63
+ *
64
+ * @param {string[]} ignoreGlobs
65
+ * @returns {{matchers: import('minimatch').Minimatch[], verdicts: Map<string, boolean>}}
66
+ */
67
+ function ignoreMatcherEntry(ignoreGlobs) {
68
+ const key = ignoreGlobs.join('\u0000');
69
+ let entry = IGNORE_MATCHER_CACHE.get(key);
70
+ if (entry) return entry;
71
+ entry = {
72
+ matchers: ignoreGlobs
73
+ .filter((g) => typeof g === 'string')
74
+ .map((g) => new Minimatch(g, { dot: true })),
75
+ verdicts: new Map(),
76
+ };
77
+ IGNORE_MATCHER_CACHE.set(key, entry);
78
+ return entry;
79
+ }
80
+
30
81
  /**
31
82
  * Test whether an absolute (or repo-relative) file path matches any of the
32
83
  * configured `ignoreGlobs`. This is the single source of truth for how the
@@ -39,22 +90,62 @@ const IGNORED_DIRS = new Set([
39
90
  *
40
91
  * Matching mirrors `scanDirectory`: the path is reduced to a canonicalised,
41
92
  * POSIX, repo-relative form and tested against each glob with minimatch's
42
- * `{ dot: true }` so dot-prefixed roots like `.agents/` match.
93
+ * `{ dot: true }` so dot-prefixed roots like `.agents/` match. The patterns
94
+ * are compiled at most once per distinct list (see `IGNORE_MATCHER_CACHE`);
95
+ * the matched set is identical to the functional `minimatch()` call this
96
+ * replaced, which is what the `gate-scan-fast-path` test pins over the real
97
+ * configured `ignoreGlobs`.
43
98
  *
44
99
  * @param {string} filePath absolute or relative path to the source file
45
100
  * @param {string[]} ignoreGlobs minimatch patterns; empty/absent is a no-op
46
101
  * @param {string} [cwd] root for repo-relative resolution; defaults to cwd
47
102
  * @returns {boolean} true when the file matches at least one ignore glob
48
103
  */
49
- export function isIgnoredByGlobs(filePath, ignoreGlobs = [], cwd) {
50
- if (!Array.isArray(ignoreGlobs) || ignoreGlobs.length === 0) return false;
51
- const matchCwd = cwd ?? process.cwd();
104
+ /**
105
+ * Reduce an absolute-or-relative path to the canonicalised, POSIX,
106
+ * repo-relative form the ignore patterns are written against.
107
+ *
108
+ * @param {string} filePath
109
+ * @param {string} matchCwd
110
+ * @returns {string}
111
+ */
112
+ function canonicalRelPath(filePath, matchCwd) {
52
113
  const absFilePath = path.isAbsolute(filePath)
53
114
  ? filePath
54
115
  : path.resolve(matchCwd, filePath);
55
116
  const rawRel = path.relative(matchCwd, absFilePath).replace(/\\/g, '/');
56
- const relPath = canonicalisePath(rawRel);
57
- return ignoreGlobs.some((g) => minimatch(relPath, g, { dot: true }));
117
+ return canonicalisePath(rawRel);
118
+ }
119
+
120
+ /**
121
+ * Answer "does this path match any of the entry's patterns", consulting and
122
+ * populating the entry's verdict memo.
123
+ *
124
+ * Split out of `isIgnoredByGlobs` rather than inlined: the memo's
125
+ * hit/miss test is a branch, and folding it into the caller pushed that
126
+ * function from cyclomatic 4 to 5 — over the per-method CRAP contract the
127
+ * pre-push preview enforces. The lookup belongs beside the cache it reads
128
+ * anyway, and the hot function keeps a flat shape.
129
+ *
130
+ * @param {{matchers: import('minimatch').Minimatch[], verdicts: Map<string, boolean>}} entry
131
+ * @param {string} relPath Canonicalised, POSIX, repo-relative path.
132
+ * @returns {boolean}
133
+ */
134
+ function memoisedIgnoreVerdict(entry, relPath) {
135
+ const memoised = entry.verdicts.get(relPath);
136
+ if (memoised !== undefined) return memoised;
137
+ const verdict = entry.matchers.some((m) => m.match(relPath));
138
+ entry.verdicts.set(relPath, verdict);
139
+ return verdict;
140
+ }
141
+
142
+ export function isIgnoredByGlobs(filePath, ignoreGlobs = [], cwd) {
143
+ if (!Array.isArray(ignoreGlobs) || ignoreGlobs.length === 0) return false;
144
+ const matchCwd = cwd ?? process.cwd();
145
+ return memoisedIgnoreVerdict(
146
+ ignoreMatcherEntry(ignoreGlobs),
147
+ canonicalRelPath(filePath, matchCwd),
148
+ );
58
149
  }
59
150
 
60
151
  /**
@@ -107,7 +198,7 @@ export function scanDirectory(dir, fileList = [], opts = {}) {
107
198
  * Calculates maintainability scores for a list of file paths.
108
199
  *
109
200
  * Each file's transpile-then-analyze unit is dispatched to a
110
- * worker_threads pool sized to `os.availableParallelism()`. Workers
201
+ * worker_threads pool whose width `runOnPool` resolves. Workers
111
202
  * are recycled across files so TypeScript loads at most once per
112
203
  * worker. The pool is bypassed for batches of fewer than
113
204
  * `SERIAL_THRESHOLD` files because spawn overhead dominates at small
@@ -129,9 +220,16 @@ export function scanDirectory(dir, fileList = [], opts = {}) {
129
220
  * and nothing said so.
130
221
  *
131
222
  * @param {string[]} paths
223
+ * @param {{serialThreshold?: number}} [opts] `serialThreshold` overrides the
224
+ * pool-vs-serial cutover for this call only. Production callers omit it;
225
+ * the parity tests use it to drive the pooled path on a small fixture set
226
+ * rather than materialising 256 files to clear the cutover.
132
227
  * @returns {Promise<Record<string, number>>}
133
228
  */
134
- export async function calculateAll(paths) {
229
+ export async function calculateAll(paths, opts = {}) {
230
+ const serialThreshold = Number.isFinite(opts?.serialThreshold)
231
+ ? opts.serialThreshold
232
+ : SERIAL_THRESHOLD;
135
233
  const cwd = process.cwd();
136
234
  const indexed = paths.map((p) => ({
137
235
  abs: p,
@@ -139,7 +237,7 @@ export async function calculateAll(paths) {
139
237
  }));
140
238
 
141
239
  let perFile;
142
- if (indexed.length < SERIAL_THRESHOLD) {
240
+ if (indexed.length < serialThreshold) {
143
241
  perFile = indexed.map(({ abs, relPath }) => {
144
242
  try {
145
243
  return { relPath, ...scoreFile(abs) };
@@ -94,6 +94,7 @@ const FRAMEWORK_SCRIPT_BASENAMES = Object.freeze([
94
94
  'check-cyclomatic.js',
95
95
  'check-dead-exports.js',
96
96
  'check-doc-links.js',
97
+ 'check-generated-validator.js',
97
98
  'check-gherkin-corpus.js',
98
99
  'check-knip-entries.js',
99
100
  'check-lifecycle-lint.js',
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * Phase 1 — Offer to scaffold missing docsContextFiles (scaffold-docs.js).
9
9
  * Phase 2 — Run `mandrel doctor` as a readiness gate.
10
- * Phase 3 — Print the /plan handoff next-step text.
10
+ * Phase 3 — Print the /mandrel-plan handoff next-step text.
11
11
  *
12
12
  * The whole tail is idempotent: re-running after an already-onboarded project
13
13
  * re-checks and re-offers scaffolding without duplicating stubs (the scaffolder
@@ -31,13 +31,13 @@ import { STUB_MARKER, scaffoldDocs } from './scaffold-docs.js';
31
31
  // ---------------------------------------------------------------------------
32
32
 
33
33
  /**
34
- * Text printed at the end of the init tail to hand the operator off to /plan.
34
+ * Text printed at the end of the init tail to hand the operator off to /mandrel-plan.
35
35
  *
36
36
  * @type {string}
37
37
  */
38
38
  export const PLAN_HANDOFF_TEXT =
39
39
  '\n✅ Mandrel is ready. Start your first project:\n\n' +
40
- ' /plan --seed "<one-line description of what you want to build>"\n';
40
+ ' /mandrel-plan --seed "<one-line description of what you want to build>"\n';
41
41
 
42
42
  // ---------------------------------------------------------------------------
43
43
  // Internal helpers
@@ -200,7 +200,7 @@ export async function runInitTail({
200
200
  return { scaffoldResult, doctorStatus, ok: false };
201
201
  }
202
202
 
203
- // --- Phase 3: Handoff to /plan -------------------------------------------
203
+ // --- Phase 3: Handoff to /mandrel-plan -------------------------------------------
204
204
  stdout(PLAN_HANDOFF_TEXT);
205
205
  return { scaffoldResult, doctorStatus, ok: true };
206
206
  }
@@ -25,7 +25,7 @@ const AGENT_ROOT = path.resolve(__dirname, '../../..');
25
25
  const DOCS_TEMPLATE_DIR = path.join(AGENT_ROOT, 'templates', 'docs');
26
26
 
27
27
  /**
28
- * Deterministic marker written into every scaffolded stub. The `/plan`
28
+ * Deterministic marker written into every scaffolded stub. The `/mandrel-plan`
29
29
  * first-run preflight (and any tooling that wants to detect unedited stubs)
30
30
  * keys off this exact string — do not change it without a hard cutover.
31
31
  *
@@ -59,7 +59,7 @@ function genericStub(fileName) {
59
59
  /**
60
60
  * Read the dedicated template body for a docsContextFile, or fall back to the
61
61
  * generic stub when no template ships for that name. Either path prepends the
62
- * {@link STUB_MARKER} so the `/plan` first-run preflight can detect un-edited
62
+ * {@link STUB_MARKER} so the `/mandrel-plan` first-run preflight can detect un-edited
63
63
  * stubs regardless of whether a dedicated template was used.
64
64
  *
65
65
  * @param {string} fileName
@@ -170,7 +170,7 @@ export function decideAcceptanceEval({ verdict, maxRounds, round: roundIn }) {
170
170
  /**
171
171
  * Build the per-criterion acceptance-eval signal payload for the retro /
172
172
  * feedback substrate. Carries which acceptance items needed rework and the
173
- * round count so `/plan` Phase 0 feedback fetch and the retro can
173
+ * round count so `/mandrel-plan` Phase 0 feedback fetch and the retro can
174
174
  * surface acceptance churn. PII-free by construction — it carries only
175
175
  * acceptance-item indices, verdicts, and the terminal decision.
176
176
  *
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Story #1155 (Epic #1142, 5.40.0) — extracted the helper-driven
5
5
  * `epic-code-review` invocation into a callable module so the
6
- * `/deliver` runner can run Phase D without spawning a child
6
+ * `/mandrel-deliver` runner can run Phase D without spawning a child
7
7
  * process or routing through an LLM-driven helper.
8
8
  *
9
9
  * Story #2831 (Epic #2815, Pluggable Code Review) — refactored to load
@@ -32,11 +32,11 @@
32
32
  * `code-review` and `audit-results` findings contracts into this one
33
33
  * `verification-results` marker.
34
34
  * - Treats severity.critical > 0 as a halting blocker — the merged
35
- * `/deliver` runner consults `halted` and refuses to advance
35
+ * `/mandrel-deliver` runner consults `halted` and refuses to advance
36
36
  * to Phase E (retro) when set.
37
37
  *
38
38
  * Halting on critical findings is the in-process replacement for the
39
- * helper's "operator must remediate before /deliver" gate.
39
+ * helper's "operator must remediate before /mandrel-deliver" gate.
40
40
  */
41
41
 
42
42
  import { hasSurvivingCritical } from '../audit-suite/findings.js';
@@ -129,7 +129,7 @@ function resolveScopeEnvelope(opts, config) {
129
129
  }
130
130
 
131
131
  /**
132
- * In-process wrapper that the `/deliver` runner and the
132
+ * In-process wrapper that the `/mandrel-deliver` runner and the
133
133
  * `/single-story-deliver` close path consume.
134
134
  *
135
135
  * Story #2252 — emits `code-review.start` immediately on entry and
@@ -132,7 +132,7 @@ export const LITE_ROUTE_LABEL = 'route::lite';
132
132
  * footprint: `trivial` < `moderate` < `substantial`.
133
133
  * - `maxUncertainty` — is the shape determined by the request
134
134
  * (`determined`), or does it still need the design
135
- * decisions `/plan` exists to resolve
135
+ * decisions `/mandrel-plan` exists to resolve
136
136
  * (`needs-design`)?
137
137
  * - `maxDeployables` — named deployable roots (`apps/<x>`, `packages/<x>`, …)
138
138
  * the footprint spans; more than one is epic by
@@ -325,7 +325,7 @@ const EFFORT_RULES = Object.freeze([
325
325
  UNCERTAINTY_SCALE.indexOf(s.uncertainty) >
326
326
  UNCERTAINTY_SCALE.indexOf(c.maxUncertainty),
327
327
  reason: (s) =>
328
- `the shape is not determined by the request (uncertainty "${s.uncertainty}") — the design decisions /plan exists to resolve are still open; full route`,
328
+ `the shape is not determined by the request (uncertainty "${s.uncertainty}") — the design decisions /mandrel-plan exists to resolve are still open; full route`,
329
329
  },
330
330
  {
331
331
  code: SHAPE_CODES.DEPLOYABLE_SPAN,
@@ -657,7 +657,7 @@ function buildEffortShape({
657
657
  /**
658
658
  * Derive the complexity route from an authored Story's **effort and risk**
659
659
  * (Story #4722 AC-3/AC-4; re-anchored off artifact cardinality by Story #4764)
660
- * — the single shape function persist's backstop and `/deliver`'s dispatch
660
+ * — the single shape function persist's backstop and `/mandrel-deliver`'s dispatch
661
661
  * derivation both read, so the two can never disagree about the same body.
662
662
  *
663
663
  * `lite` requires **every** signal to agree, against
@@ -804,7 +804,7 @@ export function deriveStoryShape({
804
804
  }
805
805
 
806
806
  /**
807
- * Decide how `/deliver` executes a Story: **run topology, and nothing else.**
807
+ * Decide how `/mandrel-deliver` executes a Story: **run topology, and nothing else.**
808
808
  *
809
809
  * **`inline` names one indivisible resource: the router's own session.** Two
810
810
  * Stories cannot both own it, so exactly one premise can grant it —
@@ -839,7 +839,7 @@ export function deriveStoryShape({
839
839
  * module header's non-negotiables.
840
840
  *
841
841
  * @param {{ storyCount?: unknown }} [args] `storyCount` is the number of
842
- * Stories the invoking `/deliver` run resolved. Omitted (or not exactly 1)
842
+ * Stories the invoking `/mandrel-deliver` run resolved. Omitted (or not exactly 1)
843
843
  * means the run cannot be shown sibling-free and therefore dispatches as a
844
844
  * sub-agent — never an assumed 1.
845
845
  * @returns {{ mode: 'inline'|'subagent', reasons: string[] }}
@@ -21,7 +21,7 @@
21
21
  * - `closing` with a red PR → enter the fix loop. Waiting is pointless; no
22
22
  * budget turns a failed check green.
23
23
  * - `closing` with a MERGED PR → run confirm. **This is the strand a
24
- * `/deliver` re-run refuses outright**, because `single-story-init.js`
24
+ * `/mandrel-deliver` re-run refuses outright**, because `single-story-init.js`
25
25
  * hard-errors on an already-closed Story — so before this surface, the
26
26
  * merged-but-label-stale Story had no automated way back.
27
27
  * - `done` with a drifted board → run resync. The GitHub Projects bot won
@@ -367,7 +367,7 @@ export function decideRecovery({
367
367
  nextCommand: NEXT_COMMANDS.confirmMerge(storyId),
368
368
  detail:
369
369
  `PR #${pr.number} is MERGED but the Story is at \`${label ?? 'no state label'}\`. ` +
370
- `A /deliver re-run cannot fix this — single-story-init.js hard-errors on an ` +
370
+ `A /mandrel-deliver re-run cannot fix this — single-story-init.js hard-errors on an ` +
371
371
  `already-closed Story. The confirm CLI is idempotent and flips the label from ` +
372
372
  `the already-merged PR, then runs the land tail.`,
373
373
  evidence,
@@ -482,7 +482,7 @@ export function decideRecovery({
482
482
  nextCommand: NEXT_COMMANDS.close(storyId),
483
483
  detail:
484
484
  `Story is at \`${label ?? 'no agent:: state label'}\` — not mid-delivery, so there ` +
485
- `is no strand to recover. Deliver it normally via /deliver ${storyId}.`,
485
+ `is no strand to recover. Deliver it normally via /mandrel-deliver ${storyId}.`,
486
486
  evidence,
487
487
  };
488
488
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * docs-digest.js — per-run docs digest builder (Story #4338).
3
3
  *
4
- * `/deliver` story sub-agents previously re-read every file in
4
+ * `/mandrel-deliver` story sub-agents previously re-read every file in
5
5
  * `project.docsContextFiles` on every Story, re-paying the full docs payload
6
6
  * per child. This module produces a single **digest** — one compact markdown
7
7
  * outline per configured doc — that the parent threads into every child prompt
@@ -17,7 +17,7 @@
17
17
  * generate-and-write export so the planner-context surface
18
18
  * (`plan-context.js` / `authoring-context.js`) can produce a session docs
19
19
  * digest without duplicating the mkdir+writeFile plumbing shared by
20
- * `plan-context.js` / `authoring-context.js` and the `/deliver` workflow.
20
+ * `plan-context.js` / `authoring-context.js` and the `/mandrel-deliver` workflow.
21
21
  */
22
22
 
23
23
  import fs from 'node:fs';
@@ -145,7 +145,7 @@ export async function buildDocsDigest({ docsContextFiles, docsRoot } = {}) {
145
145
  * Build the docs digest and write it to `outputPath`, returning `null` (no
146
146
  * write) when there is nothing to digest. This is the single shared
147
147
  * generate-and-persist export both digest producers call: the per-run
148
- * `/deliver` docs digest (`helpers/deliver-story.md`) and the planner-
148
+ * `/mandrel-deliver` docs digest (`helpers/deliver-story.md`) and the planner-
149
149
  * context digest (`plan-context.js` → `authoring-context.js`, Story
150
150
  * #4433). Callers own path construction (temp-root layout, run id, etc.)
151
151
  * so both surfaces can keep — or deliberately share — their own convention;
@@ -3,7 +3,7 @@
3
3
  * kernel shared by the three per-surface lease guards.
4
4
  *
5
5
  * The per-surface lease guards — today `single-story-lease-guard.js`;
6
- * historically also the retired Epic-tier deliver/plan guards —
6
+ * historically also the retired Epic-tier deliver/mandrel-plan guards —
7
7
  * each carried their own copy of
8
8
  * the operator-handle resolution and the fail-closed acquire wrapper around
9
9
  * `ticket-lease.acquireLease` (refuse any foreign assignee, throwing an
@@ -20,7 +20,7 @@
20
20
  *
21
21
  * - **Operator candidates** — each surface supplies its own ordered
22
22
  * candidate list (e.g. `--as` flag → `github.operatorHandle` →
23
- * `git user.email` for `/deliver`; bare `operatorHandle` for the
23
+ * `git user.email` for `/mandrel-deliver`; bare `operatorHandle` for the
24
24
  * plan/standalone paths).
25
25
  * - **Missing-handle behaviour** — `'null'` (return null; the caller fails
26
26
  * closed at acquire time) vs `'throw'` (refuse immediately with surface
@@ -71,7 +71,7 @@ function runDiffBackstop({
71
71
  *
72
72
  * A refusal also **preserves** the work before it reports (Story #4875): the
73
73
  * implementation is finished and the recycle command hands the receipt to
74
- * `/plan`, so leaving it on an untracked local branch that routine cleanup may
74
+ * `/mandrel-plan`, so leaving it on an untracked local branch that routine cleanup may
75
75
  * reap is not an acceptable end state. Preservation is best-effort and its
76
76
  * outcome is reported either way — a failed push degrades the message, never
77
77
  * the verdict or the exit code.
@@ -7,9 +7,9 @@
7
7
  * {@link module:lib/orchestration/light-suitability}:
8
8
  *
9
9
  * 1. **Recycling the receipt.** A blocked diff backstop used to tell the
10
- * operator to "escalate to `/plan`", which authored a brand-new Story and
10
+ * operator to "escalate to `/mandrel-plan`", which authored a brand-new Story and
11
11
  * left the receipt open with no successor — orphaning its branch, its
12
- * worktree, and a finished implementation. Naming the receipt as `/plan`'s
12
+ * worktree, and a finished implementation. Naming the receipt as `/mandrel-plan`'s
13
13
  * *input* recycles it instead: tickets mode already fetches a ticket,
14
14
  * rewrites it into properly-planned Stories, and closes the source as
15
15
  * superseded.
@@ -45,13 +45,13 @@ import {
45
45
  } from '../observability/runtime-friction.js';
46
46
 
47
47
  /**
48
- * The `/plan` invocation that owns a Story the light path could not land.
48
+ * The `/mandrel-plan` invocation that owns a Story the light path could not land.
49
49
  *
50
50
  * @param {number} storyId
51
51
  * @returns {string}
52
52
  */
53
53
  function buildRecycleCommand(storyId) {
54
- return `/plan ${storyId}`;
54
+ return `/mandrel-plan ${storyId}`;
55
55
  }
56
56
 
57
57
  /**
@@ -106,7 +106,7 @@ export async function recordGateRefusal({
106
106
 
107
107
  /**
108
108
  * Handle a blocked diff backstop: record the refusal as friction and return the
109
- * `/plan` invocation that recycles the receipt.
109
+ * `/mandrel-plan` invocation that recycles the receipt.
110
110
  *
111
111
  * Lives here rather than in the CLI so the shell stays a shell — the backstop
112
112
  * mode's job is to branch and print, not to decide what a refusal means.
@@ -152,7 +152,7 @@ export async function handleBlockedBackstop({
152
152
  * recoverable (Story #4875).
153
153
  *
154
154
  * A blocked backstop refuses the *land*, not the *work*: the implementation is
155
- * complete and the recycle command hands the receipt to `/plan`, which will
155
+ * complete and the recycle command hands the receipt to `/mandrel-plan`, which will
156
156
  * want it. Before this, that work existed only as a local `story-<id>` branch
157
157
  * with no remote ref — an untracked branch is exactly what the routine merged-
158
158
  * branch sweeps and worktree reaping treat as disposable, so the only copy of a