mandrel 1.60.0 → 1.62.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 (49) hide show
  1. package/.agents/README.md +74 -32
  2. package/.agents/docs/SDLC.md +18 -12
  3. package/.agents/docs/configuration.md +61 -4
  4. package/.agents/docs/quality-gates.md +796 -0
  5. package/.agents/docs/workflows.md +3 -4
  6. package/.agents/runtime-deps.json +2 -2
  7. package/.agents/scripts/README.md +1 -1
  8. package/.agents/scripts/agents-bootstrap-github.js +23 -119
  9. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +46 -0
  10. package/.agents/scripts/lib/bootstrap/gh-preflight.js +7 -9
  11. package/.agents/scripts/lib/bootstrap/manifest.js +21 -1
  12. package/.agents/scripts/lib/bootstrap/merge-methods.js +31 -16
  13. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +32 -11
  14. package/.agents/scripts/lib/config/sync-agentrc.js +1 -1
  15. package/.agents/scripts/lib/detect-package-manager.js +72 -0
  16. package/.agents/scripts/lib/errors/index.js +4 -4
  17. package/.agents/scripts/lib/label-taxonomy.js +2 -2
  18. package/.agents/scripts/lib/onboard/detect-stack.js +10 -10
  19. package/.agents/scripts/lib/onboard/init-tail.js +218 -0
  20. package/.agents/scripts/lib/onboard/scaffold-docs.js +18 -3
  21. package/.agents/scripts/lib/runtime-deps/preflight.js +6 -6
  22. package/.agents/scripts/lib/worktree/node-modules-strategy.js +5 -2
  23. package/.agents/workflows/agents-update.md +14 -29
  24. package/.agents/workflows/deliver.md +87 -26
  25. package/.agents/workflows/helpers/agents-sync-config.md +3 -2
  26. package/.agents/workflows/helpers/deliver-epic.md +12 -5
  27. package/.agents/workflows/helpers/deliver-stories.md +13 -7
  28. package/.agents/workflows/plan.md +48 -4
  29. package/README.md +18 -30
  30. package/bin/mandrel.js +235 -16
  31. package/docs/CHANGELOG.md +36 -0
  32. package/lib/cli/doctor.js +45 -3
  33. package/lib/cli/init.js +66 -7
  34. package/lib/cli/registry.js +42 -146
  35. package/lib/cli/sync.js +122 -23
  36. package/lib/cli/uninstall.js +42 -7
  37. package/lib/cli/update.js +257 -198
  38. package/lib/cli/version-helpers.js +59 -0
  39. package/package.json +6 -6
  40. package/.agents/workflows/onboard.md +0 -208
  41. package/lib/cli/__tests__/migrate.test.js +0 -268
  42. package/lib/cli/__tests__/sync-local-zone.test.js +0 -247
  43. package/lib/cli/__tests__/sync.test.js +0 -372
  44. package/lib/cli/__tests__/update-changelog-surface.test.js +0 -357
  45. package/lib/cli/__tests__/update-major.test.js +0 -217
  46. package/lib/cli/__tests__/update-reexec.test.js +0 -513
  47. package/lib/cli/__tests__/update.test.js +0 -696
  48. package/lib/cli/__tests__/version-check.test.js +0 -398
  49. package/lib/migrations/__tests__/index.test.js +0 -216
@@ -52,10 +52,10 @@ export class GhVersionError extends Error {
52
52
  /**
53
53
  * Raised when a framework runtime dependency (e.g. `ajv`) cannot be
54
54
  * resolved from the consumer's `node_modules/`. Surfaces during the
55
- * `/agents-bootstrap-github` preflight to redirect operators to
56
- * `/agents-bootstrap-project`, which is the workflow responsible for
57
- * merging and installing the framework's runtime deps. `missing` carries
58
- * the package specifiers that failed to resolve so the CLI can render an
55
+ * `agents-bootstrap-github` preflight to redirect operators to the
56
+ * correct remediation (`mandrel init` for new projects, or
57
+ * `npm install mandrel` for existing ones). `missing` carries the
58
+ * package specifiers that failed to resolve so the CLI can render an
59
59
  * actionable hint.
60
60
  */
61
61
  export class MissingRuntimeDepsError extends Error {
@@ -164,8 +164,8 @@ export const STATUS_FIELD_OPTIONS = ['Todo', 'In Progress', 'Done'];
164
164
  *
165
165
  * GitHub's GraphQL surface does not expose a public `createProjectV2View`
166
166
  * mutation, so bootstrap creates these via the REST Projects V2 views
167
- * endpoint best-effort and falls back to documenting the filter strings in
168
- * `docs/project-board.md` when the endpoint is unavailable.
167
+ * endpoint best-effort; when the endpoint is unavailable the views must be
168
+ * configured manually in the GitHub Projects UI.
169
169
  *
170
170
  * @type {Array<{ name: string, filter: string, groupBy: string,
171
171
  * layout?: 'table'|'board'|'roadmap' }>}
@@ -1,11 +1,12 @@
1
1
  /**
2
- * detect-stack.js — Consumer stack detection for `/onboard`
2
+ * detect-stack.js — Consumer stack detection for `mandrel init`
3
3
  *
4
4
  * Inspects a consumer repository root and reports the package manager,
5
5
  * test runner, and primary language it can infer from on-disk signals
6
6
  * (lockfiles, `package.json` contents, and source-file extensions). The
7
- * guided `/onboard` flow (Feature #3514) uses this to tell the operator
8
- * what it found before scaffolding missing `docsContextFiles`.
7
+ * `mandrel init` configure-path tail (Feature #3514, Story #4045) uses
8
+ * this to tell the operator what it found before scaffolding missing
9
+ * `docsContextFiles`.
9
10
  *
10
11
  * The detection functions are seam-injectable: each takes an injected
11
12
  * filesystem facade (`exists` / `readFile` / `listExtensions`) so they
@@ -19,6 +20,7 @@
19
20
 
20
21
  import fs from 'node:fs';
21
22
  import path from 'node:path';
23
+ import { detectPackageManager as detectPm } from '../detect-package-manager.js';
22
24
 
23
25
  /**
24
26
  * Filesystem facade. Pure detection logic talks to disk only through
@@ -163,18 +165,16 @@ export const defaultFsFacade = {
163
165
  * when no lockfile is found but a `package.json` exists, and `null` when
164
166
  * the repo has no Node manifest at all.
165
167
  *
168
+ * Delegates to the shared `detectPackageManager` helper
169
+ * (Story #4048 B3 — one implementation per concept). The `fsFacade.exists`
170
+ * seam is forwarded directly.
171
+ *
166
172
  * @param {string} root - Repository root.
167
173
  * @param {FsFacade} [fsFacade=defaultFsFacade]
168
174
  * @returns {'pnpm'|'yarn'|'bun'|'npm'|null}
169
175
  */
170
176
  export function detectPackageManager(root, fsFacade = defaultFsFacade) {
171
- const { exists } = fsFacade;
172
- if (exists(path.join(root, 'pnpm-lock.yaml'))) return 'pnpm';
173
- if (exists(path.join(root, 'yarn.lock'))) return 'yarn';
174
- if (exists(path.join(root, 'bun.lockb'))) return 'bun';
175
- if (exists(path.join(root, 'package-lock.json'))) return 'npm';
176
- if (exists(path.join(root, 'package.json'))) return 'npm';
177
- return null;
177
+ return detectPm(root, fsFacade.exists);
178
178
  }
179
179
 
180
180
  /**
@@ -0,0 +1,218 @@
1
+ /**
2
+ * init-tail.js — post-bootstrap onboarding tail for `mandrel init`.
3
+ *
4
+ * Called by `mandrel init` after `bootstrap.js` completes successfully on
5
+ * the "configure now" path. Sequences the four phases that walk an operator
6
+ * from a freshly bootstrapped project to a ready-to-plan workspace:
7
+ *
8
+ * Phase 1 — Detect the consumer stack (lib/onboard/detect-stack.js).
9
+ * Phase 2 — Offer to scaffold missing docsContextFiles (scaffold-docs.js).
10
+ * Phase 3 — Run `mandrel doctor` as a readiness gate.
11
+ * Phase 4 — Print the /plan handoff next-step text.
12
+ *
13
+ * The whole tail is idempotent: re-running after an already-onboarded project
14
+ * re-detects, re-checks, and re-offers scaffolding without duplicating stubs
15
+ * (the scaffolder only writes genuinely absent files) and without modifying
16
+ * anything (doctor is read-only).
17
+ *
18
+ * Injectable seams: `runDoctor`, `stdout`, `confirmScaffold`, and `isTTY`
19
+ * allow the unit suite to drive every branch without real I/O.
20
+ *
21
+ * Story #4045 (refs #4045).
22
+ */
23
+
24
+ import { spawnSync as defaultSpawnSync } from 'node:child_process';
25
+ import fs from 'node:fs';
26
+ import path from 'node:path';
27
+
28
+ import { detectStack } from './detect-stack.js';
29
+ import { STUB_MARKER, scaffoldDocs } from './scaffold-docs.js';
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Phase text constants
33
+ // ---------------------------------------------------------------------------
34
+
35
+ /**
36
+ * Text printed at the end of the init tail to hand the operator off to /plan.
37
+ *
38
+ * @type {string}
39
+ */
40
+ export const PLAN_HANDOFF_TEXT =
41
+ '\n✅ Mandrel is ready. Start your first Epic:\n\n' +
42
+ ' /plan --idea "<one-line description of what you want to build>"\n\n' +
43
+ 'Or, if you already have a `type::epic` Issue open:\n\n' +
44
+ ' /plan <epicId>\n';
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Internal helpers
48
+ // ---------------------------------------------------------------------------
49
+
50
+ /**
51
+ * Format a stack-detection result as a human-readable report line.
52
+ *
53
+ * @param {{ packageManager: string|null, testRunner: string|null, primaryLanguage: string|null }} stack
54
+ * @returns {string}
55
+ */
56
+ function formatStackReport(stack) {
57
+ const pm = stack.packageManager ?? '(unknown)';
58
+ const runner = stack.testRunner ?? '(unknown)';
59
+ const lang = stack.primaryLanguage ?? '(unknown)';
60
+ return (
61
+ '\n[init] Stack detection:\n' +
62
+ ` Package manager : ${pm}\n` +
63
+ ` Test runner : ${runner}\n` +
64
+ ` Primary language: ${lang}\n`
65
+ );
66
+ }
67
+
68
+ /**
69
+ * Format a list of missing docs as a human-readable report (no prompt).
70
+ *
71
+ * @param {string[]} missing
72
+ * @returns {string}
73
+ */
74
+ function formatMissingList(missing) {
75
+ if (missing.length === 0) return '';
76
+ const list = missing.map((f) => ` • ${f}`).join('\n');
77
+ return (
78
+ '\n[init] The following docsContextFiles are missing — agents load\n' +
79
+ 'these before every task:\n' +
80
+ `${list}\n`
81
+ );
82
+ }
83
+
84
+ /** Prompt text shown only on a TTY when asking to scaffold. */
85
+ const SCAFFOLD_PROMPT = '\nScaffold stubs now? [y/N]: ';
86
+
87
+ /**
88
+ * Synchronous y/N read from stdin (fd 0). Returns `false` on any read error
89
+ * or when the user enters something other than `y` / `yes`.
90
+ *
91
+ * @returns {boolean}
92
+ */
93
+ function syncConfirm() {
94
+ let answer = '';
95
+ try {
96
+ const buf = fs.readFileSync(0, 'utf8');
97
+ answer = buf.split('\n', 1)[0].trim().toLowerCase();
98
+ } catch {
99
+ answer = '';
100
+ }
101
+ return answer === 'y' || answer === 'yes';
102
+ }
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // Public API
106
+ // ---------------------------------------------------------------------------
107
+
108
+ /**
109
+ * Run the post-bootstrap init tail.
110
+ *
111
+ * @param {object} [opts]
112
+ * @param {string} [opts.root] - Project root (defaults to `process.cwd()`).
113
+ * @param {(msg: string) => void} [opts.stdout] - Output sink (defaults to
114
+ * `process.stdout.write` bound to `process.stdout`).
115
+ * @param {() => boolean} [opts.confirmScaffold] - Read the operator's y/N
116
+ * answer for the scaffold offer. Returns `true` to scaffold. In non-TTY
117
+ * contexts defaults to `false` (no write without operator confirmation).
118
+ * @param {(extraArgs?: string[]) => { status: number|null }} [opts.runDoctor]
119
+ * - Run `mandrel doctor`; injectable for tests.
120
+ * @param {boolean} [opts.isTTY] - Whether stdin is a TTY (defaults to
121
+ * `Boolean(process.stdin.isTTY)`).
122
+ * @returns {{
123
+ * stack: { packageManager: string|null, testRunner: string|null, primaryLanguage: string|null },
124
+ * scaffoldResult: object,
125
+ * doctorStatus: number,
126
+ * ok: boolean,
127
+ * }}
128
+ */
129
+ export function runInitTail({
130
+ root,
131
+ stdout = (s) => process.stdout.write(s),
132
+ confirmScaffold,
133
+ runDoctor,
134
+ isTTY,
135
+ } = {}) {
136
+ const projectRoot = root ?? process.cwd();
137
+ const tty = isTTY ?? Boolean(process.stdin.isTTY);
138
+
139
+ // When confirmScaffold is explicitly injected (e.g. from tests), always use
140
+ // it. When using the default, auto-decline on non-TTY so the scaffolder
141
+ // never writes unattended.
142
+ const usingDefaultConfirm = confirmScaffold == null;
143
+ const confirmFn = confirmScaffold ?? (() => syncConfirm());
144
+
145
+ // Default doctor runner — spawns `mandrel doctor` via the locally installed
146
+ // bin; inherits stdio so the report streams to the terminal.
147
+ const mandrelBin = path.join(
148
+ projectRoot,
149
+ 'node_modules',
150
+ 'mandrel',
151
+ 'bin',
152
+ 'mandrel.js',
153
+ );
154
+ const defaultRunDoctor = (extraArgs = []) =>
155
+ defaultSpawnSync(process.execPath, [mandrelBin, 'doctor', ...extraArgs], {
156
+ cwd: projectRoot,
157
+ stdio: 'inherit',
158
+ });
159
+
160
+ const doctorFn = runDoctor ?? defaultRunDoctor;
161
+
162
+ // --- Phase 1: Detect the stack -------------------------------------------
163
+ let stack;
164
+ try {
165
+ stack = detectStack(projectRoot);
166
+ } catch {
167
+ stack = { packageManager: null, testRunner: null, primaryLanguage: null };
168
+ }
169
+ stdout(formatStackReport(stack));
170
+
171
+ // --- Phase 2: Offer to scaffold missing docsContextFiles -----------------
172
+ const preview = scaffoldDocs({ root: projectRoot, write: false });
173
+ let scaffoldResult = preview;
174
+
175
+ if (preview.missing.length === 0) {
176
+ stdout('\n[init] All docsContextFiles are present.\n');
177
+ } else {
178
+ stdout(formatMissingList(preview.missing));
179
+ // On non-TTY without an injected confirm, auto-decline so the scaffolder
180
+ // never writes unattended. On TTY (or with an injected confirm seam), show
181
+ // the prompt and consult the confirm function.
182
+ const canPrompt = tty || !usingDefaultConfirm;
183
+ if (canPrompt) stdout(SCAFFOLD_PROMPT);
184
+ const accepted = canPrompt ? confirmFn() : false;
185
+ if (accepted) {
186
+ scaffoldResult = scaffoldDocs({ root: projectRoot, write: true });
187
+ if (scaffoldResult.created.length > 0) {
188
+ stdout(
189
+ `[init] Scaffolded ${scaffoldResult.created.length} stub(s). ` +
190
+ `Each carries a \`${STUB_MARKER}\` marker — replace placeholder ` +
191
+ 'content before planning.\n',
192
+ );
193
+ }
194
+ } else {
195
+ stdout(
196
+ '[init] Scaffolding declined. docsContextFiles are still missing — ' +
197
+ 'agents will load degraded context until you create them.\n',
198
+ );
199
+ }
200
+ }
201
+
202
+ // --- Phase 3: Readiness gate (mandrel doctor) ----------------------------
203
+ stdout('\n[init] Running mandrel doctor…\n');
204
+ const doctorResult = doctorFn();
205
+ const doctorStatus = doctorResult?.status ?? 1;
206
+
207
+ if (doctorStatus !== 0) {
208
+ stdout(
209
+ '\n[init] ❌ Doctor check failed. Resolve the remedies above and\n' +
210
+ 'then re-run: mandrel init\n',
211
+ );
212
+ return { stack, scaffoldResult, doctorStatus, ok: false };
213
+ }
214
+
215
+ // --- Phase 4: Handoff to /plan -------------------------------------------
216
+ stdout(PLAN_HANDOFF_TEXT);
217
+ return { stack, scaffoldResult, doctorStatus, ok: true };
218
+ }
@@ -24,6 +24,15 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
24
24
  const AGENT_ROOT = path.resolve(__dirname, '../../..');
25
25
  const DOCS_TEMPLATE_DIR = path.join(AGENT_ROOT, 'templates', 'docs');
26
26
 
27
+ /**
28
+ * Deterministic marker written into every scaffolded stub. The `/plan`
29
+ * first-run preflight (and any tooling that wants to detect unedited stubs)
30
+ * keys off this exact string — do not change it without a hard cutover.
31
+ *
32
+ * @type {string}
33
+ */
34
+ export const STUB_MARKER = '<!-- MANDREL:STUB -->';
35
+
27
36
  /**
28
37
  * Build the generic placeholder stub for a docsContextFile that has no
29
38
  * dedicated template. Derives a human-readable title from the filename.
@@ -39,8 +48,9 @@ function genericStub(fileName) {
39
48
  .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
40
49
  .join(' ');
41
50
  return (
51
+ `${STUB_MARKER}\n` +
42
52
  `# ${title}\n\n` +
43
- '> **Stub generated by guided onboarding.** This file is one of the\n' +
53
+ '> **Stub generated by `mandrel init`.** This file is one of the\n' +
44
54
  '> `project.docsContextFiles` mandatory-reads — agents load it before\n' +
45
55
  '> every task. Replace this placeholder with real content.\n'
46
56
  );
@@ -48,7 +58,9 @@ function genericStub(fileName) {
48
58
 
49
59
  /**
50
60
  * Read the dedicated template body for a docsContextFile, or fall back to the
51
- * generic stub when no template ships for that name.
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
63
+ * stubs regardless of whether a dedicated template was used.
52
64
  *
53
65
  * @param {string} fileName
54
66
  * @param {import('node:fs')} fsImpl
@@ -57,7 +69,10 @@ function genericStub(fileName) {
57
69
  function stubContentFor(fileName, fsImpl) {
58
70
  const templatePath = path.join(DOCS_TEMPLATE_DIR, fileName);
59
71
  if (fsImpl.existsSync(templatePath)) {
60
- return fsImpl.readFileSync(templatePath, 'utf8');
72
+ const body = fsImpl.readFileSync(templatePath, 'utf8');
73
+ // Prepend the marker only when absent (idempotent; the template files
74
+ // themselves are kept marker-free so they read cleanly as documentation).
75
+ return body.startsWith(STUB_MARKER) ? body : `${STUB_MARKER}\n${body}`;
61
76
  }
62
77
  return genericStub(fileName);
63
78
  }
@@ -12,7 +12,7 @@
12
12
  */
13
13
 
14
14
  import fs from 'node:fs';
15
- import path from 'node:path';
15
+ import { detectPackageManager as detectPm } from '../detect-package-manager.js';
16
16
 
17
17
  /**
18
18
  * Resolve each required package via the injected `resolve` seam and collect
@@ -38,17 +38,17 @@ export function checkRuntimeDeps({ required, resolve }) {
38
38
  /**
39
39
  * Detect the consumer's package manager from lockfile presence so the
40
40
  * remediation message names the right install command. Defaults to `npm`.
41
- * Mirrors `bootstrap/project-bootstrap.js#detectPackageManager` but stays
42
- * decoupled (and seam-injectable) so the preflight imports nothing heavy.
41
+ *
42
+ * Delegates to the shared `detectPackageManager` helper
43
+ * (Story #4048 B3 — one implementation per concept). The `exists` seam
44
+ * is forwarded directly; `null` (no manifest) coerces to `'npm'`.
43
45
  *
44
46
  * @param {string} root
45
47
  * @param {(p: string) => boolean} [exists=fs.existsSync]
46
48
  * @returns {'pnpm'|'yarn'|'npm'}
47
49
  */
48
50
  export function detectPackageManager(root, exists = fs.existsSync) {
49
- if (exists(path.join(root, 'pnpm-lock.yaml'))) return 'pnpm';
50
- if (exists(path.join(root, 'yarn.lock'))) return 'yarn';
51
- return 'npm';
51
+ return detectPm(root, exists) ?? 'npm';
52
52
  }
53
53
 
54
54
  /** Map a detected package manager to its install command. */
@@ -20,6 +20,7 @@
20
20
  import { spawnSync } from 'node:child_process';
21
21
  import fs from 'node:fs';
22
22
  import path from 'node:path';
23
+ import { detectPackageManager } from '../detect-package-manager.js';
23
24
 
24
25
  function sleepSync(ms) {
25
26
  if (!Number.isFinite(ms) || ms <= 0) return;
@@ -110,10 +111,12 @@ export function selectInstallCommand(strategy, wtPath, fsLike = fs) {
110
111
  if (strategy === 'pnpm-store') {
111
112
  return { cmd: 'pnpm', args: ['install', '--frozen-lockfile'] };
112
113
  }
113
- if (fsLike.existsSync(path.join(wtPath, 'pnpm-lock.yaml'))) {
114
+ // Shared lockfile probe (Story #4048 B3 — one implementation per concept).
115
+ const pm = detectPackageManager(wtPath, (p) => fsLike.existsSync(p)) ?? 'npm';
116
+ if (pm === 'pnpm') {
114
117
  return { cmd: 'pnpm', args: ['install', '--frozen-lockfile'] };
115
118
  }
116
- if (fsLike.existsSync(path.join(wtPath, 'yarn.lock'))) {
119
+ if (pm === 'yarn') {
117
120
  return { cmd: 'yarn', args: ['install', '--frozen-lockfile'] };
118
121
  }
119
122
  return { cmd: 'npm', args: ['ci'] };
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  description: >-
3
3
  npm-era upgrade wraparound for a Mandrel consumer. Runs `mandrel update`
4
- (resolve newest non-major version → install → re-materialize `.agents/` →
4
+ (resolve newest published version → install → re-materialize `.agents/` →
5
5
  migrate → doctor → surface changelog) as the single mechanical step, then
6
6
  walks the operator through the judgment wraparound the CLI deliberately
7
7
  leaves unowned: reconcile `.agentrc.json`, install the Epic #1386
@@ -23,7 +23,7 @@ description: >-
23
23
 
24
24
  ## Overview
25
25
 
26
- `/agents-update` advances the consumer repo to the newest non-major
26
+ `/agents-update` advances the consumer repo to the newest published
27
27
  `mandrel` release, re-materializes `.agents/`, and regenerates the
28
28
  flat `.claude/commands/` tree (invoked as `/<name>`) against the new workflow
29
29
  set — then reconciles the consumer's own config, harness allowlist, and
@@ -40,11 +40,10 @@ The upgrade contract:
40
40
  - **CI honours the committed lockfile.** Consumer CI runs `npm ci` against
41
41
  the committed `package-lock.json`, so it installs exactly the version the
42
42
  lockfile pins — never "whatever the registry's newest is today."
43
- - **The major axis is gated.** `mandrel update` refuses to cross a major
44
- boundary (e.g. `1.x 2.0`) without an explicit `--major`, printing a
45
- pointer to `docs/upgrade-major.md` and exiting non-zero without touching
46
- anything. Routine minor/patch bumps within the current major are never
47
- gated.
43
+ - **Majors apply like any other bump.** Mandrel ships hard cutovers
44
+ (`.agents/rules/git-conventions.md` § Contract Cutovers), so a major
45
+ crossing is applied directly the surfaced changelog is the migration
46
+ guide.
48
47
  - **The CLI never commits.** The npm bump rewrites `package.json` /
49
48
  `package-lock.json` and leaves them **staged on disk** for operator review;
50
49
  `mandrel update` performs no `git add` / `git commit`. Staging and
@@ -55,8 +54,7 @@ The upgrade contract:
55
54
  authoritative writer of the generated flat command tree
56
55
  (`.claude/commands/`) is
57
56
  [`sync-claude-commands.js`](../scripts/sync-claude-commands.js), which
58
- prepends the `<!-- AUTO-GENERATED -->` header that
59
- `/agents-bootstrap-project` parity-checks. Nothing else copies workflow
57
+ prepends the `<!-- AUTO-GENERATED -->` header. Nothing else copies workflow
60
58
  files.
61
59
 
62
60
  > **Persona**: `devops-engineer` · **Skills**:
@@ -71,7 +69,7 @@ mandrel update --dry-run
71
69
  mandrel update
72
70
  ```
73
71
 
74
- `mandrel update --dry-run` resolves the newest non-major version and prints
72
+ `mandrel update --dry-run` resolves the newest published version and prints
75
73
  the ordered step plan (`npm-update → runSync → runMigrations → doctor →
76
74
  surface changelog`) without invoking any effectful seam — no dependency bump,
77
75
  no sync, no migrations, no doctor, nothing written. Read the planned target
@@ -82,23 +80,19 @@ version before applying.
82
80
  1. **Resolve target** — the newest published `mandrel` version (via
83
81
  the daily freshness cache in `temp/version-check.json`) and the currently
84
82
  installed version.
85
- 2. **Major gate** — if the newest version crosses a major boundary, the run
86
- declines, prints the `docs/upgrade-major.md` pointer, and exits non-zero
87
- without touching anything. Re-run with `--major` only after reviewing that
88
- runbook.
89
- 3. **No-op short-circuit** — already on the newest version ⇒ prints
83
+ 2. **No-op short-circuit** — already on the newest version prints
90
84
  `Already up to date` and exits 0.
91
- 4. **Install** — bumps the dependency (default
85
+ 3. **Install** — bumps the dependency (default
92
86
  `npm install mandrel@<target>`; pass
93
87
  `--install-cmd "<pm> <args>"` for a pnpm/yarn workspace). The lockfile
94
88
  change is left **staged** for review; the CLI never commits.
95
- 5. **runSync** — re-materializes `.agents/` from the freshly installed
89
+ 4. **runSync** — re-materializes `.agents/` from the freshly installed
96
90
  payload, which also regenerates the flat `.claude/commands/` tree via
97
91
  `sync-claude-commands.js`.
98
- 6. **runMigrations** — applies any version-keyed migration steps for the
92
+ 5. **runMigrations** — applies any version-keyed migration steps for the
99
93
  crossed range.
100
- 7. **doctor** — runs the check registry to verify the resulting install.
101
- 8. **Surface changelog** — prints the `docs/CHANGELOG.md` section(s) covering
94
+ 6. **doctor** — runs the check registry to verify the resulting install.
95
+ 7. **Surface changelog** — prints the `docs/CHANGELOG.md` section(s) covering
102
96
  the applied range `(current, target]`. Capture this output — Step 4
103
97
  reconciles the consumer's own instructions against it.
104
98
 
@@ -371,12 +365,6 @@ no-op.
371
365
 
372
366
  ## Troubleshooting
373
367
 
374
- - **`a newer MAJOR version (X.0.0) is available`** — `mandrel update`
375
- hit the major gate and exited non-zero without touching anything. A
376
- major crossing is a breaking upgrade. Read `docs/upgrade-major.md`,
377
- then re-run `mandrel update --major` only after you have absorbed the
378
- migration steps that runbook describes.
379
-
380
368
  - **`doctor reported failures: …`** — the dependency bumped and `.agents/`
381
369
  re-materialized, but a doctor check failed (and the run exited
382
370
  non-zero). Run `mandrel doctor` for the per-check remedies. The lockfile
@@ -402,9 +390,6 @@ no-op.
402
390
  - **Idempotent.** A second `mandrel update` immediately after a successful
403
391
  run resolves the same newest version, hits the no-op short-circuit, and
404
392
  prints `Already up to date` — exit 0, nothing bumped.
405
- - **Non-major only by default.** The major axis is gated behind an explicit
406
- `--major`; routine minor/patch bumps within the current major apply
407
- without a gate.
408
393
  - **No auto-commit.** `mandrel update` leaves the lockfile bump staged on
409
394
  disk and never runs git. The operator reviews the surfaced changelog and
410
395
  writes the commit message (Step 5) — the CLI does not know whether the