mandrel 2.64.0 → 2.65.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 (67) hide show
  1. package/.agents/agents/acceptance-critic.md +3 -2
  2. package/.agents/agents/auditor.md +3 -2
  3. package/.agents/agents/plan-critic.md +3 -2
  4. package/.agents/agents/story-worker.md +2 -2
  5. package/.agents/audit-checklists/quality.md +3 -0
  6. package/.agents/docs/agentrc-reference.json +1 -9
  7. package/.agents/docs/configuration.md +8 -7
  8. package/.agents/schemas/agentrc.schema.json +6 -13
  9. package/.agents/schemas/audit-rules.schema.json +1 -1
  10. package/.agents/schemas/story-deliver-terminal.schema.json +5 -0
  11. package/.agents/scripts/bootstrap.js +8 -2
  12. package/.agents/scripts/check-context-budget.js +1 -1
  13. package/.agents/scripts/lib/ITicketingProvider.js +1 -3
  14. package/.agents/scripts/lib/audit-suite/findings.js +1 -17
  15. package/.agents/scripts/lib/audit-suite/frontmatter.js +0 -28
  16. package/.agents/scripts/lib/audit-suite/index.js +0 -6
  17. package/.agents/scripts/lib/audit-suite/selector.js +0 -31
  18. package/.agents/scripts/lib/bootstrap/agents-md-fold.js +156 -0
  19. package/.agents/scripts/lib/bootstrap/commit-push.js +1 -1
  20. package/.agents/scripts/lib/bootstrap/manifest.js +2 -2
  21. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +13 -29
  22. package/.agents/scripts/lib/config/review-chain-default.js +13 -0
  23. package/.agents/scripts/lib/config-settings-schema-delivery.js +2 -2
  24. package/.agents/scripts/lib/config-settings-schema-quality.js +11 -13
  25. package/.agents/scripts/lib/doc-tiers.js +25 -6
  26. package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
  27. package/.agents/scripts/lib/observability/metrics-ledger.js +0 -72
  28. package/.agents/scripts/lib/orchestration/code-review.js +11 -6
  29. package/.agents/scripts/lib/orchestration/epic-rollup.js +29 -12
  30. package/.agents/scripts/lib/orchestration/merge-block-class.js +20 -4
  31. package/.agents/scripts/lib/orchestration/merge-poll.js +41 -22
  32. package/.agents/scripts/lib/orchestration/required-checks.js +147 -0
  33. package/.agents/scripts/lib/orchestration/review-providers/code-review.js +203 -0
  34. package/.agents/scripts/lib/orchestration/review-providers/review-provider-factory.js +6 -4
  35. package/.agents/scripts/lib/orchestration/review-providers/security-review.js +3 -2
  36. package/.agents/scripts/lib/orchestration/single-story-close/failed-terminal.js +1 -0
  37. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +0 -12
  38. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +27 -11
  39. package/.agents/scripts/lib/orchestration/single-story-close/phases/post-land.js +112 -82
  40. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +72 -5
  41. package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +12 -87
  42. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +3 -0
  43. package/.agents/scripts/lib/templates/decomposer-prompts.js +5 -24
  44. package/.agents/scripts/providers/github/issues.js +14 -23
  45. package/.agents/scripts/sync-claude-agents.js +1 -1
  46. package/.agents/workflows/audit-quality.md +42 -7
  47. package/.agents/workflows/helpers/acceptance-self-eval.md +1 -1
  48. package/.agents/workflows/helpers/code-review.md +15 -38
  49. package/.agents/workflows/helpers/deliver-reference.md +4 -2
  50. package/.agents/workflows/helpers/deliver-story.md +3 -0
  51. package/.agents/workflows/helpers/plan-reference.md +9 -8
  52. package/.agents/workflows/mandrel-deliver.md +2 -1
  53. package/.agents/workflows/mandrel-plan.md +10 -7
  54. package/.agents/workflows/mandrel-update.md +5 -3
  55. package/docs/CHANGELOG.md +31 -0
  56. package/lib/cli/claude-code-version.js +73 -0
  57. package/lib/cli/doctor.js +2 -2
  58. package/lib/cli/registry.js +9 -0
  59. package/lib/cli/uninstall.js +37 -9
  60. package/lib/migrations/index.js +2 -0
  61. package/lib/migrations/steps/2.65.0-fold-claude-md-into-agents-md.js +38 -0
  62. package/package.json +2 -1
  63. package/.agents/scripts/lib/audit-suite/lens-diff-floor.js +0 -99
  64. package/.agents/scripts/lib/audit-suite/runner.js +0 -205
  65. package/.agents/scripts/lib/audit-suite/substitutions.js +0 -96
  66. package/.agents/scripts/lib/audit-suite/workflow-loader.js +0 -37
  67. package/.agents/scripts/lib/orchestration/story-close/phases/local-lens-review.js +0 -234
@@ -100,19 +100,20 @@ not re-deriving which assumptions were really the agent's to make.
100
100
 
101
101
  ## Gate #1 → the one advisory line
102
102
 
103
- Gate #1 stops for exactly two things — the sharpened plan intent and any HITL
104
- unknown — and everything else the envelope surfaced collapses to **one
105
- advisory line** beneath it. Nothing on that line stops the run,
106
- reroutes it, or is invoked by `/mandrel-plan`; each item names something the
107
- operator may prefer to do instead, and the run proceeds either way. Under
103
+ Gate #1 stops only when there is at least one HITL unknown or
104
+ `duplicates[]` is non-empty; otherwise the run announces the sharpened plan
105
+ intent plus the advisory line and continues to authoring. Everything else the
106
+ envelope surfaced collapses to **one advisory line** beneath the gate.
107
+ Nothing on that line reroutes the run or is invoked by `/mandrel-plan`; each
108
+ item names something the operator may prefer to do instead. Under
108
109
  `--yes` the line is recorded and planning continues — an unattended run has
109
110
  nobody to take an offer.
110
111
 
111
112
  The line names, in order, whichever of these the envelope carries:
112
113
 
113
114
  - **`duplicates[]`** — open Stories the seed resembles (never Epics). Name
114
- the top one or two by id and title; a plan that duplicates open work is
115
- still the operator's call.
115
+ the top one or two by id and title. A plan that duplicates open work is
116
+ still the operator's call, so a non-empty list also stops Gate #1.
116
117
  - **Open `intake` rows** (`priorFeedback`) — CI-gap intake filings written by
117
118
  [`file-ci-gap.js`](../../scripts/file-ci-gap.js) when a delivery reached an
118
119
  Option-2 verdict in [`ci-remediation.md`](../../rules/ci-remediation.md).
@@ -339,7 +340,7 @@ On `dispatch: true`, dispatch **one fresh-context, maker-blind sub-agent**.
339
340
  When `delivery.routing.roleScopedAgents` is enabled (the **default**), use
340
341
  `subagent_type: plan-critic` — it boots on the role-scoped
341
342
  [`plan-critic`](../../agents/plan-critic.md) context (its own system prompt,
342
- no `CLAUDE.md` @-closure) that carries the maker-blind invariant, the
343
+ no entry-doc @-closure) that carries the maker-blind invariant, the
343
344
  `pre-mortem` charter, and the output shape standalone. When the kill-switch
344
345
  is off (`roleScopedAgents: false`) or the host cannot spawn at this depth,
345
346
  fall back to a generic sub-agent and hand it the same charter. Either way the
@@ -76,7 +76,8 @@ to an attended run.
76
76
  it cannot read — a missing gate would co-dispatch against an unlanded
77
77
  blocker.
78
78
 
79
- 2. **Confirm (N>1).** Present the order; wait unless `--yes`.
79
+ 2. **Announce (N>1).** Present the resolved order and proceed — do not wait
80
+ for confirmation; the operator can interject mid-run to change it.
80
81
 
81
82
  3. **Run the beat.** One command per beat, repeated until the envelope reports
82
83
  the run `done`:
@@ -71,13 +71,16 @@ assumed; a **HITL** unknown goes to Gate #1. Under `--yes` do not ask free-form
71
71
  operator questions — AFK unknowns are still researched; only HITL unknowns land
72
72
  in Key Assumptions, each a decision-made-by-default.
73
73
 
74
- **Gate #1** — STOP for exactly two things: confirm the sharpened plan intent,
75
- and settle any HITL unknown the operator owns. Everything else the envelope
76
- surfaced — `duplicates[]`, open `intake` rows, a truthy
77
- `memoryPoolAdvisory.recommend`, a truthy `complexitySignals.uiSurface` naming
78
- [`/prototype`](prototype.md) (never invoke it here) — collapses to
79
- **one advisory line** under the gate; none of it stops the run or reroutes
80
- it ([ref](helpers/plan-reference.md)). Under `--yes`, auto-proceed.
74
+ **Gate #1** — STOP only when a HITL unknown the operator owns exists or
75
+ `duplicates[]` is non-empty (planning a duplicate of open work stays the
76
+ operator's call): confirm the sharpened plan intent and settle it. Otherwise
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)).
83
+ Under `--yes`, auto-proceed.
81
84
 
82
85
  ### 2. Author
83
86
 
@@ -200,13 +200,15 @@ non-optional.
200
200
 
201
201
  ## Step 4 — Review the surfaced changelog and update consumer-side guidance
202
202
 
203
- Framework upgrades change behaviour the consumer's own `AGENTS.md` /
204
- `CLAUDE.md` and runbooks often encode. Step 1 already printed the changelog
203
+ Framework upgrades change behaviour the consumer's own `AGENTS.md` and
204
+ runbooks often encode. Step 1 already printed the changelog
205
205
  for the applied range — that output is your source of truth (re-read the
206
206
  transcript or the GitHub Releases page if it scrolled past). For each entry
207
207
  between the installed and target versions:
208
208
 
209
- 1. **Consumer `AGENTS.md` / `CLAUDE.md`.** Update instructions so a fresh
209
+ 1. **Consumer `AGENTS.md`.** It is the entry doc — `mandrel update` folds a
210
+ root `CLAUDE.md` into it and deletes `CLAUDE.md`, so reconcile the folded
211
+ content here. Update instructions so a fresh
210
212
  agent reading them in isolation produces output that passes the
211
213
  framework's new validators; remove or rewrite instructions that
212
214
  contradict a tightened rule.
package/docs/CHANGELOG.md CHANGED
@@ -15,6 +15,37 @@ 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.65.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.64.0...mandrel-v2.65.0) (2026-09-24)
19
+
20
+
21
+ ### ⚠ BREAKING CHANGES
22
+
23
+ * consumers must run Claude Code >= 2.1.277, which loads a root AGENTS.md; `mandrel update` folds the root CLAUDE.md into AGENTS.md and deletes CLAUDE.md.
24
+
25
+ ### Added
26
+
27
+ * add a low-effort model bug review to close and pin role-agent effort ([#5426](https://github.com/dsj1984/mandrel/issues/5426)) ([#5431](https://github.com/dsj1984/mandrel/issues/5431)) ([ec39643](https://github.com/dsj1984/mandrel/commit/ec3964390210e140ebaa324bbfe27b67fd470fa4))
28
+ * **audit-quality:** add a property-based coverage dimension (refs [#5425](https://github.com/dsj1984/mandrel/issues/5425)) ([#5428](https://github.com/dsj1984/mandrel/issues/5428)) ([f78fd62](https://github.com/dsj1984/mandrel/commit/f78fd62edda9060580b1cbacaec1cc82875040d1))
29
+ * cut consumers over to AGENTS.md: bootstrap wiring, CLAUDE.md fold migration, uninstall revert, Claude Code version doctor check ([#5410](https://github.com/dsj1984/mandrel/issues/5410)) ([#5411](https://github.com/dsj1984/mandrel/issues/5411)) ([df69d59](https://github.com/dsj1984/mandrel/commit/df69d59e7ced7342f321b8435ab50b317655f7f3))
30
+ * make AGENTS.md mandrel's always-loaded entry doc: host-faithful closure resolver, delete CLAUDE.md, rewrite AGENTS.md ([#5409](https://github.com/dsj1984/mandrel/issues/5409)) ([#5413](https://github.com/dsj1984/mandrel/issues/5413)) ([929e5bb](https://github.com/dsj1984/mandrel/commit/929e5bb6834e1a25152163ac34a87d327034ce05))
31
+ * trim legacy story-author scaffolding and drop two needless workflow stops ([#5427](https://github.com/dsj1984/mandrel/issues/5427)) ([#5429](https://github.com/dsj1984/mandrel/issues/5429)) ([7b39d36](https://github.com/dsj1984/mandrel/commit/7b39d3637b4911ad7122d6cbaef3c19c88704101))
32
+
33
+
34
+ ### Fixed
35
+
36
+ * merge wait: fail fast on a red required check without waiting for unrelated jobs ([#5415](https://github.com/dsj1984/mandrel/issues/5415)) ([#5420](https://github.com/dsj1984/mandrel/issues/5420)) ([381d8ff](https://github.com/dsj1984/mandrel/commit/381d8ff4145ec3210d984936904368d51cde426a))
37
+
38
+
39
+ ### Performance
40
+
41
+ * close: per-phase timing, faster merge observation, concurrent post-land GitHub steps ([#5417](https://github.com/dsj1984/mandrel/issues/5417)) ([#5423](https://github.com/dsj1984/mandrel/issues/5423)) ([2d3c537](https://github.com/dsj1984/mandrel/commit/2d3c5373402ad0e212951b70ca2880204da3a8f3))
42
+ * epic rollup: stop reading every Epic's sub-issues when a Story has no native parent ([#5414](https://github.com/dsj1984/mandrel/issues/5414)) ([#5419](https://github.com/dsj1984/mandrel/issues/5419)) ([025c650](https://github.com/dsj1984/mandrel/commit/025c650409a757b47be9847a4584e5e45fdef8e8))
43
+
44
+
45
+ ### Changed
46
+
47
+ * retire the Story-scope local-lens pass from close ([#5416](https://github.com/dsj1984/mandrel/issues/5416)) ([#5422](https://github.com/dsj1984/mandrel/issues/5422)) ([ecd86da](https://github.com/dsj1984/mandrel/commit/ecd86daa1a9d1ddff7f58e7f832e491a9c58cb68))
48
+
18
49
  ## [2.64.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.63.0...mandrel-v2.64.0) (2026-09-19)
19
50
 
20
51
 
@@ -0,0 +1,73 @@
1
+ // lib/cli/claude-code-version.js
2
+ /**
3
+ * `mandrel doctor` check `claude-code-version`: an AGENTS.md-only project
4
+ * needs a Claude Code host that loads AGENTS.md at all. Below the floor the
5
+ * framework never hydrates, so the check fails. A missing `claude` binary,
6
+ * unparseable output, or a project still carrying CLAUDE.md skips with
7
+ * `ok: true` so headless/CI hosts are never blocked. The `claude --version`
8
+ * spawn is injected by the registry (fixed argv, never user input); with no
9
+ * runner the check skips.
10
+ *
11
+ * @module cli/claude-code-version
12
+ */
13
+
14
+ import fs from 'node:fs';
15
+ import path from 'node:path';
16
+
17
+ import { compareVersions } from './version-helpers.js';
18
+
19
+ /** Oldest Claude Code release that loads a root AGENTS.md. */
20
+ export const CLAUDE_CODE_AGENTS_MD_FLOOR = '2.1.277';
21
+
22
+ /**
23
+ * @param {string} text
24
+ * @returns {string|null} the leading `x.y.z`, or null
25
+ */
26
+ export function parseClaudeVersion(text) {
27
+ const match = /^\s*v?(\d+\.\d+\.\d+)/.exec(text ?? '');
28
+ return match ? match[1] : null;
29
+ }
30
+
31
+ /**
32
+ * @param {{
33
+ * projectRoot?: string,
34
+ * cwd?: () => string,
35
+ * existsSync?: (p: string) => boolean,
36
+ * runClaude?: () => { status: number|null, stdout: string, error?: NodeJS.ErrnoException },
37
+ * }} [opts]
38
+ * @returns {{ ok: boolean, detail: string, remedy?: string }}
39
+ */
40
+ export function runClaudeCodeVersion({
41
+ projectRoot,
42
+ cwd = () => process.cwd(),
43
+ existsSync = fs.existsSync,
44
+ runClaude = () => ({ status: null, stdout: '', error: new Error('none') }),
45
+ } = {}) {
46
+ const root = projectRoot ?? cwd();
47
+ if (existsSync(path.join(root, 'CLAUDE.md'))) {
48
+ return { ok: true, detail: 'skipped: CLAUDE.md present' };
49
+ }
50
+ if (!existsSync(path.join(root, 'AGENTS.md'))) {
51
+ return { ok: true, detail: 'skipped: no AGENTS.md' };
52
+ }
53
+ const r = runClaude();
54
+ if (r.error || r.status !== 0) {
55
+ return { ok: true, detail: 'skipped: claude not found on PATH' };
56
+ }
57
+ const version = parseClaudeVersion(r.stdout);
58
+ if (!version) {
59
+ return {
60
+ ok: true,
61
+ detail: 'skipped: unparseable `claude --version` output',
62
+ };
63
+ }
64
+ const detail = `Claude Code ${version} (required >=${CLAUDE_CODE_AGENTS_MD_FLOOR} to load AGENTS.md)`;
65
+ if (compareVersions(version, CLAUDE_CODE_AGENTS_MD_FLOOR) >= 0) {
66
+ return { ok: true, detail };
67
+ }
68
+ return {
69
+ ok: false,
70
+ detail,
71
+ remedy: `Upgrade Claude Code to >=${CLAUDE_CODE_AGENTS_MD_FLOOR} (e.g. \`claude update\`) — older hosts do not load AGENTS.md, so the framework never hydrates.`,
72
+ };
73
+ }
package/lib/cli/doctor.js CHANGED
@@ -54,7 +54,7 @@ function formatSummary(passed, total) {
54
54
  }
55
55
 
56
56
  /**
57
- * Informational line for the `CLAUDE.md` always-loaded closure (file count,
57
+ * Informational line for the host entry doc's always-loaded closure (file count,
58
58
  * KB). Never counted toward the verdict; a resolve failure degrades to a
59
59
  * neutral line.
60
60
  *
@@ -73,7 +73,7 @@ export function formatClosureReport({
73
73
  return `ℹ ${label} always-loaded closure unavailable\n`;
74
74
  }
75
75
  if (files.length === 0) {
76
- return `ℹ ${label} no CLAUDE.md closure found\n`;
76
+ return `ℹ ${label} no entry-doc closure found (CLAUDE.md or AGENTS.md)\n`;
77
77
  }
78
78
  const kb = (tierTotalBytes(files) / 1024).toFixed(1);
79
79
  return `ℹ ${label} ${files.length} file(s), ${kb} KB always-loaded\n`;
@@ -28,6 +28,7 @@ import { isCommandExcluded } from '../../.agents/scripts/lib/command-header.js';
28
28
  import { getDeliveryRouting } from '../../.agents/scripts/lib/config/delivery-routing.js';
29
29
  import { isResolvable } from '../../.agents/scripts/lib/runtime-deps/dep-resolution.js';
30
30
  import { describeParserMajorError } from '../../.agents/scripts/lib/runtime-deps/parser-major.js';
31
+ import { runClaudeCodeVersion } from './claude-code-version.js';
31
32
  import {
32
33
  defaultResolvePackageRoot,
33
34
  listFiles as listPayloadFiles,
@@ -958,6 +959,14 @@ export const registry = [
958
959
  name: 'agents-drift',
959
960
  run: (opts) => runAgentsDrift(opts),
960
961
  },
962
+ {
963
+ name: 'claude-code-version',
964
+ run: (opts) =>
965
+ runClaudeCodeVersion({
966
+ runClaude: () => spawn('claude', ['--version'], { timeout: 10_000 }),
967
+ ...opts,
968
+ }),
969
+ },
961
970
  {
962
971
  name: 'merge-driver',
963
972
  run: (opts) => runMergeDriver(opts),
@@ -21,8 +21,8 @@ import {
21
21
  BOOTSTRAP_COMMAND,
22
22
  GITIGNORE_BLOCKS,
23
23
  SYNC_COMMAND,
24
+ SYSTEM_PROMPT_AGENTS_MD,
24
25
  SYSTEM_PROMPT_BLOCK,
25
- SYSTEM_PROMPT_CLAUDE_MD,
26
26
  SYSTEM_PROMPT_IMPORT,
27
27
  } from '../../.agents/scripts/lib/bootstrap/project-bootstrap.js';
28
28
  import {
@@ -60,17 +60,17 @@ const FRAMEWORK_NPM_SCRIPTS = Object.freeze({
60
60
  */
61
61
 
62
62
  /**
63
- * Strip the system-prompt import block from `CLAUDE.md`. The file is deleted
64
- * only when byte-identical to the install template — a heading-only heuristic
65
- * would delete an operator's all-headings file.
63
+ * Strip the system-prompt import block from a root entry doc. The file is
64
+ * deleted only when byte-identical to the install template — a heading-only
65
+ * heuristic would delete an operator's all-headings file.
66
66
  *
67
+ * @param {string} rel - `AGENTS.md`, or a legacy un-migrated `CLAUDE.md`.
67
68
  * @param {string} projectRoot
68
69
  * @param {typeof fs} fsImpl
69
70
  * @returns {ReversalOutcome}
70
71
  */
71
- function revertClaudeMd(projectRoot, fsImpl) {
72
- const target = path.join(projectRoot, 'CLAUDE.md');
73
- const rel = 'CLAUDE.md';
72
+ function revertEntryDoc(rel, projectRoot, fsImpl) {
73
+ const target = path.join(projectRoot, rel);
74
74
  if (!fsImpl.existsSync(target)) {
75
75
  return { kind: 'skipped', target: rel, detail: 'file absent' };
76
76
  }
@@ -78,12 +78,12 @@ function revertClaudeMd(projectRoot, fsImpl) {
78
78
  if (!original.includes(SYSTEM_PROMPT_IMPORT)) {
79
79
  return { kind: 'skipped', target: rel, detail: 'import already absent' };
80
80
  }
81
- if (original.trim() === SYSTEM_PROMPT_CLAUDE_MD.trim()) {
81
+ if (original.trim() === SYSTEM_PROMPT_AGENTS_MD.trim()) {
82
82
  fsImpl.rmSync(target, { force: true });
83
83
  return {
84
84
  kind: 'reverted',
85
85
  target: rel,
86
- detail: 'removed install-created CLAUDE.md',
86
+ detail: `removed install-created ${rel}`,
87
87
  };
88
88
  }
89
89
  // Full block first, then the bare import line for a hand-edited block.
@@ -109,6 +109,33 @@ function revertClaudeMd(projectRoot, fsImpl) {
109
109
  };
110
110
  }
111
111
 
112
+ /**
113
+ * @param {string} projectRoot
114
+ * @param {typeof fs} fsImpl
115
+ * @returns {ReversalOutcome}
116
+ */
117
+ function revertAgentsMd(projectRoot, fsImpl) {
118
+ return revertEntryDoc('AGENTS.md', projectRoot, fsImpl);
119
+ }
120
+
121
+ /**
122
+ * Pre-2.65 ledgers name CLAUDE.md. An un-migrated CLAUDE.md reverts as
123
+ * before; once the update migration folded it away, the wiring lives in
124
+ * AGENTS.md, so that is reverted instead. CLAUDE.md is never recreated.
125
+ *
126
+ * @param {string} projectRoot
127
+ * @param {typeof fs} fsImpl
128
+ * @returns {ReversalOutcome}
129
+ */
130
+ function revertClaudeMd(projectRoot, fsImpl) {
131
+ const legacy = fsImpl.existsSync(path.join(projectRoot, 'CLAUDE.md'));
132
+ return revertEntryDoc(
133
+ legacy ? 'CLAUDE.md' : 'AGENTS.md',
134
+ projectRoot,
135
+ fsImpl,
136
+ );
137
+ }
138
+
112
139
  /**
113
140
  * Splice the sync hook and any legacy plugin-enablement keys out of
114
141
  * `.claude/settings.json`, keeping everything else; delete the file if empty.
@@ -405,6 +432,7 @@ function revertPreCommitHook(projectRoot, fsImpl) {
405
432
  * @type {Readonly<Record<string, (root: string, fsImpl: typeof fs, executedAction?: string) => ReversalOutcome>>}
406
433
  */
407
434
  const REVERSAL_BY_TARGET = Object.freeze({
435
+ 'AGENTS.md': revertAgentsMd,
408
436
  'CLAUDE.md': revertClaudeMd,
409
437
  '.claude/settings.json': revertClaudeSettings,
410
438
  '.claude/plugins/mandrel': revertClaudeCommands,
@@ -16,6 +16,7 @@ import { retireDeliveryLimitKnobs } from './steps/2.57.0-retire-delivery-limit-k
16
16
  import { retirePlanningLimitKnobs } from './steps/2.57.0-retire-planning-limit-knobs.js';
17
17
  import { retireAuditResultsAutoFile } from './steps/2.60.0-retire-audit-results-autofile.js';
18
18
  import { baselineMergeQueueShape } from './steps/2.63.0-baseline-merge-queue-shape.js';
19
+ import { foldClaudeMdIntoAgentsMdStep } from './steps/2.65.0-fold-claude-md-into-agents-md.js';
19
20
  import { stripRemovedAgentrcKeys } from './steps/strip-removed-agentrc-keys.js';
20
21
 
21
22
  /**
@@ -40,6 +41,7 @@ export const migrations = [
40
41
  retireAuditResultsAutoFile,
41
42
  stripRemovedAgentrcKeys,
42
43
  baselineMergeQueueShape,
44
+ foldClaudeMdIntoAgentsMdStep,
43
45
  ];
44
46
 
45
47
  export { compareVersions };
@@ -0,0 +1,38 @@
1
+ // lib/migrations/steps/2.65.0-fold-claude-md-into-agents-md.js
2
+ /**
3
+ * Fold a consumer's root CLAUDE.md into AGENTS.md and delete CLAUDE.md.
4
+ * The host reads CLAUDE.md exclusively whenever it exists, so a surviving one
5
+ * would shadow the AGENTS.md wiring. Shares the bootstrap fold contract.
6
+ */
7
+
8
+ import nodeFs from 'node:fs';
9
+ import path from 'node:path';
10
+
11
+ import {
12
+ foldClaudeMdIntoAgentsMd,
13
+ LEGACY_ENTRY_DOC,
14
+ } from '../../../.agents/scripts/lib/bootstrap/agents-md-fold.js';
15
+
16
+ export const foldClaudeMdIntoAgentsMdStep = {
17
+ version: '2.65.0',
18
+ description:
19
+ 'fold the root CLAUDE.md into AGENTS.md and delete it — AGENTS.md is ' +
20
+ 'now the entry doc (Story #5410)',
21
+ /**
22
+ * @param {{ projectRoot?: string, fs?: typeof nodeFs }} [ctx]
23
+ * @param {typeof nodeFs} [fsImpl]
24
+ * @returns {boolean}
25
+ */
26
+ detect(ctx, fsImpl = ctx?.fs ?? nodeFs) {
27
+ const root = ctx?.projectRoot ?? process.cwd();
28
+ return fsImpl.existsSync(path.join(root, LEGACY_ENTRY_DOC));
29
+ },
30
+ /**
31
+ * @param {{ projectRoot?: string, fs?: typeof nodeFs }} [ctx]
32
+ * @param {typeof nodeFs} [fsImpl]
33
+ * @returns {void}
34
+ */
35
+ apply(ctx, fsImpl = ctx?.fs ?? nodeFs) {
36
+ foldClaudeMdIntoAgentsMd(ctx?.projectRoot ?? process.cwd(), fsImpl);
37
+ },
38
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mandrel",
3
- "version": "2.64.0",
3
+ "version": "2.65.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/",
@@ -102,6 +102,7 @@
102
102
  "@cucumber/gherkin": "^42.0.1",
103
103
  "c8": "^11.0.0",
104
104
  "chokidar": "^5.0.0",
105
+ "fast-check": "^4.10.2",
105
106
  "husky": "^9.1.7",
106
107
  "jscpd": "^4.2.5",
107
108
  "knip": "^6.17.1",
@@ -1,99 +0,0 @@
1
- /**
2
- * Close-scope lens diff-floor: a diff under the changed-line floor that
3
- * touches no sensitive path skips lens materialization. Fail-open — every
4
- * degraded input (unknown count, unreadable manifest, floor `0`) means "do
5
- * not skip". Sensitive classes come from the same selector as review depth,
6
- * so the two never disagree. Total; no I/O beyond the injected git spawn.
7
- */
8
-
9
- import { gitSpawn } from '../git-utils.js';
10
- import { readNumstatRows } from '../orchestration/diff-magnitude.js';
11
- import { selectSensitivePathClasses } from './selector.js';
12
-
13
- /** Measured: zero-yield closes clustered well under this size. */
14
- export const DEFAULT_LENS_DIFF_FLOOR = 40;
15
-
16
- /**
17
- * Whole-diff additions + deletions (no companion exemption: the question is
18
- * "is this diff small"). Shares the numstat parse with the light path's
19
- * backstop. `null` on any git failure; binary rows count 0.
20
- *
21
- * @param {{
22
- * baseRef: string,
23
- * headRef: string,
24
- * cwd?: string,
25
- * gitSpawnFn?: typeof gitSpawn,
26
- * }} args
27
- * @returns {number|null}
28
- */
29
- export function countChangedLines({
30
- baseRef,
31
- headRef,
32
- cwd = process.cwd(),
33
- gitSpawnFn = gitSpawn,
34
- } = {}) {
35
- const rows = readNumstatRows({ baseRef, headRef, cwd, gitSpawnFn });
36
- if (rows === null) return null;
37
- return rows.reduce((total, row) => total + row.additions + row.deletions, 0);
38
- }
39
-
40
- /**
41
- * Skip only when the floor is enabled, the count is known and below it, and
42
- * no sensitive-path class matches; every other state names its reason.
43
- *
44
- * @param {{
45
- * changedFiles?: string[]|null,
46
- * changedLineCount?: number|null,
47
- * floor?: number,
48
- * injectedRules?: object,
49
- * selectSensitivePathClassesFn?: typeof selectSensitivePathClasses,
50
- * }} [input]
51
- * @returns {{
52
- * skip: boolean,
53
- * reason: 'floor-disabled'|'line-count-unknown'|'at-or-above-floor'|'sensitive-classes-unknown'|'sensitive-path-hit'|'below-floor',
54
- * floor: number,
55
- * changedLineCount: number|null,
56
- * sensitiveClasses: string[],
57
- * }}
58
- */
59
- export function evaluateLensDiffFloor(input = {}) {
60
- const floorRaw = input.floor;
61
- const floor =
62
- typeof floorRaw === 'number' && Number.isInteger(floorRaw) && floorRaw >= 0
63
- ? floorRaw
64
- : DEFAULT_LENS_DIFF_FLOOR;
65
- const count =
66
- typeof input.changedLineCount === 'number' &&
67
- Number.isFinite(input.changedLineCount) &&
68
- input.changedLineCount >= 0
69
- ? Math.floor(input.changedLineCount)
70
- : null;
71
- const verdict = (skip, reason, sensitiveClasses = []) => ({
72
- skip,
73
- reason,
74
- floor,
75
- changedLineCount: count,
76
- sensitiveClasses,
77
- });
78
-
79
- if (floor <= 0) return verdict(false, 'floor-disabled');
80
- if (count === null) return verdict(false, 'line-count-unknown');
81
- if (count >= floor) return verdict(false, 'at-or-above-floor');
82
-
83
- const select =
84
- input.selectSensitivePathClassesFn ?? selectSensitivePathClasses;
85
- let classes;
86
- try {
87
- classes = select({
88
- changedFiles: Array.isArray(input.changedFiles) ? input.changedFiles : [],
89
- injectedRules: input.injectedRules,
90
- });
91
- } catch {
92
- return verdict(false, 'sensitive-classes-unknown');
93
- }
94
- const matched = Array.isArray(classes) ? classes : [];
95
- if (matched.length > 0) {
96
- return verdict(false, 'sensitive-path-hit', matched);
97
- }
98
- return verdict(true, 'below-floor');
99
- }