mandrel 1.60.0 → 1.61.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.
- package/.agents/README.md +74 -32
- package/.agents/docs/SDLC.md +8 -9
- package/.agents/docs/configuration.md +61 -4
- package/.agents/docs/quality-gates.md +796 -0
- package/.agents/docs/workflows.md +2 -3
- package/.agents/runtime-deps.json +2 -2
- package/.agents/scripts/README.md +1 -1
- package/.agents/scripts/agents-bootstrap-github.js +23 -119
- package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +46 -0
- package/.agents/scripts/lib/bootstrap/gh-preflight.js +7 -9
- package/.agents/scripts/lib/bootstrap/manifest.js +21 -1
- package/.agents/scripts/lib/bootstrap/merge-methods.js +31 -16
- package/.agents/scripts/lib/bootstrap/project-bootstrap.js +32 -11
- package/.agents/scripts/lib/config/sync-agentrc.js +1 -1
- package/.agents/scripts/lib/detect-package-manager.js +72 -0
- package/.agents/scripts/lib/errors/index.js +4 -4
- package/.agents/scripts/lib/label-taxonomy.js +2 -2
- package/.agents/scripts/lib/onboard/detect-stack.js +10 -10
- package/.agents/scripts/lib/onboard/init-tail.js +218 -0
- package/.agents/scripts/lib/onboard/scaffold-docs.js +18 -3
- package/.agents/scripts/lib/runtime-deps/preflight.js +6 -6
- package/.agents/scripts/lib/worktree/node-modules-strategy.js +5 -2
- package/.agents/workflows/agents-update.md +14 -29
- package/.agents/workflows/helpers/agents-sync-config.md +3 -2
- package/.agents/workflows/plan.md +45 -3
- package/README.md +18 -30
- package/bin/mandrel.js +235 -16
- package/docs/CHANGELOG.md +24 -0
- package/lib/cli/doctor.js +45 -3
- package/lib/cli/init.js +66 -7
- package/lib/cli/registry.js +41 -145
- package/lib/cli/sync.js +122 -23
- package/lib/cli/uninstall.js +42 -7
- package/lib/cli/update.js +145 -192
- package/lib/cli/version-helpers.js +59 -0
- package/package.json +6 -6
- package/.agents/workflows/onboard.md +0 -208
- package/lib/cli/__tests__/migrate.test.js +0 -268
- package/lib/cli/__tests__/sync-local-zone.test.js +0 -247
- package/lib/cli/__tests__/sync.test.js +0 -372
- package/lib/cli/__tests__/update-changelog-surface.test.js +0 -357
- package/lib/cli/__tests__/update-major.test.js +0 -217
- package/lib/cli/__tests__/update-reexec.test.js +0 -513
- package/lib/cli/__tests__/update.test.js +0 -696
- package/lib/cli/__tests__/version-check.test.js +0 -398
- package/lib/migrations/__tests__/index.test.js +0 -216
|
@@ -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
|
|
168
|
-
*
|
|
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
|
|
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
|
-
*
|
|
8
|
-
* what it found before scaffolding missing
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
42
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
- **
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
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
|
|
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. **
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
92
|
+
5. **runMigrations** — applies any version-keyed migration steps for the
|
|
99
93
|
crossed range.
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
@@ -68,8 +68,9 @@ node .agents/scripts/sync-agentrc.js
|
|
|
68
68
|
The script:
|
|
69
69
|
|
|
70
70
|
1. Reads `.agentrc.json` from the working directory (`--cwd <path>` to
|
|
71
|
-
override). When the file is missing, prints
|
|
72
|
-
|
|
71
|
+
override). When the file is missing, prints an error asking the
|
|
72
|
+
operator to run `mandrel init` (new project) or
|
|
73
|
+
`node .agents/scripts/bootstrap.js` (existing project) and exits 1.
|
|
73
74
|
2. Validates the parsed config against the framework AJV schema
|
|
74
75
|
(`getAgentrcValidator()`). On any failure, prints a single-line error
|
|
75
76
|
list and exits 1 — the operator must fix the typo / missing required
|
|
@@ -58,19 +58,61 @@ commands (story-sized Epic ↘ Story; epic-sized Story draft ↗ Epic) is now
|
|
|
58
58
|
an **internal branch switch** inside this router: same skills, same
|
|
59
59
|
helpers, no command hop and no operator re-entry.
|
|
60
60
|
|
|
61
|
+
## First-run preflight
|
|
62
|
+
|
|
63
|
+
Before routing to a path helper, run a **first-run preflight** to catch
|
|
64
|
+
common day-0 issues that would silently degrade every downstream task.
|
|
65
|
+
|
|
66
|
+
### When the preflight fires
|
|
67
|
+
|
|
68
|
+
The preflight runs when **any** of these is true:
|
|
69
|
+
|
|
70
|
+
1. One or more `project.docsContextFiles` entries are absent under the
|
|
71
|
+
configured `docsRoot`.
|
|
72
|
+
2. One or more present `docsContextFiles` still carry the
|
|
73
|
+
`<!-- MANDREL:STUB -->` marker (i.e. they are un-edited scaffolded stubs).
|
|
74
|
+
3. The last `mandrel doctor` verdict cached in `temp/doctor-result.json`
|
|
75
|
+
records `"verdict": "unready"`. An **absent** cache file is no signal —
|
|
76
|
+
doctor may simply never have run; only an explicit recorded unready
|
|
77
|
+
verdict fires this signal.
|
|
78
|
+
|
|
79
|
+
### Preflight procedure
|
|
80
|
+
|
|
81
|
+
1. **Detect the condition.** Check the three signals above. When none is
|
|
82
|
+
true, skip the preflight entirely — no operator interaction, no delay.
|
|
83
|
+
2. **Offer to flesh out docs.** Summarize the found condition to the
|
|
84
|
+
operator (e.g. "3 docsContextFiles are missing" or "architecture.md
|
|
85
|
+
still carries the stub marker") and ask:
|
|
86
|
+
> *Do you want to flesh out these docs from the codebase before planning?
|
|
87
|
+
> [y/N]*
|
|
88
|
+
3. **On acceptance.** Walk through each affected file, read relevant
|
|
89
|
+
codebase artifacts (source files, README, existing docs), and write real
|
|
90
|
+
content to replace the stub. Then re-run `mandrel doctor` to confirm
|
|
91
|
+
readiness. If doctor passes, proceed to routing.
|
|
92
|
+
4. **On decline.** Log one line:
|
|
93
|
+
> *[plan] Proceeding with degraded doc context — planning quality may be
|
|
94
|
+
> reduced.*
|
|
95
|
+
Then continue to the normal routing procedure below.
|
|
96
|
+
|
|
97
|
+
The preflight is **never a hard stop** — declining continues planning with a
|
|
98
|
+
noted degradation. It only fires when there is a genuine signal (missing or
|
|
99
|
+
stubbed docs, or an unready doctor verdict).
|
|
100
|
+
|
|
61
101
|
## Procedure
|
|
62
102
|
|
|
63
103
|
1. **Parse args.** Exactly one of `<epicId>`, `--idea`, `--from-notes`, or
|
|
64
104
|
`--body` must be present; anything else is a usage error naming the four
|
|
65
105
|
forms. A `--body` invocation routes to the story path (no triage).
|
|
66
|
-
2. **
|
|
106
|
+
2. **First-run preflight.** Run the preflight above. Skip when all signals
|
|
107
|
+
are clear (healthy project).
|
|
108
|
+
3. **Triage (idea path only).** Run the
|
|
67
109
|
[`core/scope-triage`](../skills/core/scope-triage/SKILL.md) skill on the
|
|
68
110
|
seed. Record the verdict in chat (one line).
|
|
69
|
-
|
|
111
|
+
4. **Delegate.** Read the selected path helper **in full** and execute it
|
|
70
112
|
from its entry phase, forwarding the absorbed flags. The helper's phase
|
|
71
113
|
numbering, HITL gates, and scripts are unchanged — this router adds no
|
|
72
114
|
phase content.
|
|
73
|
-
|
|
115
|
+
5. **Internal returns.** When a path helper would historically have handed
|
|
74
116
|
off to the other planning command, switch helpers in-place and continue;
|
|
75
117
|
surface the switch to the operator as a one-line note.
|
|
76
118
|
|
package/README.md
CHANGED
|
@@ -25,37 +25,31 @@ provisions both as part of a cold start (`git init` → `gh repo create --push`
|
|
|
25
25
|
provisioning, grant the scope with `gh auth refresh -s project` (re-auth
|
|
26
26
|
in the browser when prompted) before running `bootstrap.js`.
|
|
27
27
|
|
|
28
|
-
See the [Compatibility matrix](docs/upgrade-major.md#compatibility-matrix)
|
|
29
|
-
section of `docs/upgrade-major.md` for the supported OS / Node /
|
|
30
|
-
package-manager combinations.
|
|
31
|
-
|
|
32
28
|
## Quickstart
|
|
33
29
|
|
|
34
|
-
The canonical cold-start path is one command, then
|
|
35
|
-
commands inside Claude Code:
|
|
30
|
+
The canonical cold-start path is one command, then one slash command:
|
|
36
31
|
|
|
37
32
|
```bash
|
|
38
|
-
npx mandrel init # install mandrel → sync → prompt → bootstrap
|
|
33
|
+
npx mandrel init # install mandrel → sync → prompt → bootstrap → onboarding tail → /plan handoff
|
|
39
34
|
```
|
|
40
35
|
|
|
41
36
|
```text
|
|
42
37
|
# then, inside Claude Code (commands load from .claude/commands/):
|
|
43
|
-
/onboard # guided first run: stack detect → docs → doctor → /plan
|
|
44
38
|
/plan # ideation -> PRD/Tech Spec -> Epic with child Stories
|
|
45
39
|
```
|
|
46
40
|
|
|
47
41
|
`npx mandrel init` installs `mandrel` (when `./.agents/` is absent),
|
|
48
42
|
materializes it via `mandrel sync`, then asks whether to **configure now**
|
|
49
|
-
(option 1 → runs `
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
review → retro → open PR).
|
|
43
|
+
(option 1 → runs `bootstrap.js`, then the onboarding tail: stack detection,
|
|
44
|
+
docs scaffolding offer, `mandrel doctor` readiness gate, and a `/plan`
|
|
45
|
+
handoff) or stop at **just the files** (option 2 → re-run `mandrel init`
|
|
46
|
+
any time to configure). Pass `--assume-yes` for a non-interactive run that
|
|
47
|
+
proceeds straight to configure (and forwards the flag to bootstrap). When
|
|
48
|
+
`./.agents/` is already present (you ran `npm install mandrel` first), `init`
|
|
49
|
+
skips the install/sync and goes straight to the prompt. Once `mandrel init`
|
|
50
|
+
completes, you land at the `/plan` handoff — run `/plan --idea "<seed>"` to
|
|
51
|
+
start planning your first Epic, then deliver it with `/deliver <id>` (wave
|
|
52
|
+
loop → validation → review → retro → open PR).
|
|
59
53
|
|
|
60
54
|
### Manual equivalent
|
|
61
55
|
|
|
@@ -108,29 +102,23 @@ npx mandrel update
|
|
|
108
102
|
|
|
109
103
|
1. **Resolve** the newest published version (a `npm view mandrel
|
|
110
104
|
version` registry probe) and the currently installed version.
|
|
111
|
-
2. **
|
|
112
|
-
|
|
113
|
-
[`docs/upgrade-major.md`](docs/upgrade-major.md), and exits non-zero
|
|
114
|
-
without touching anything. Re-run with `--major` to apply it.
|
|
115
|
-
3. **No-op short-circuit** — already on the newest version ⇒ nothing to do.
|
|
116
|
-
4. **Install** the target version with the project's package manager —
|
|
105
|
+
2. **No-op short-circuit** — already on the newest version ⇒ nothing to do.
|
|
106
|
+
3. **Install** the target version with the project's package manager —
|
|
117
107
|
auto-detected from the lockfile (`pnpm-lock.yaml` ⇒ pnpm, `yarn.lock` ⇒
|
|
118
108
|
yarn, otherwise npm) so the bump lands in your real lockfile. The
|
|
119
109
|
dependency bump is left **staged** on disk — `mandrel update` performs no
|
|
120
110
|
`git add` / `git commit`, so you review and commit the lockfile change
|
|
121
111
|
yourself.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
112
|
+
4. **Sync** — re-materialize `./.agents/` from the freshly installed payload.
|
|
113
|
+
5. **Migrate** — apply version-keyed migration steps for the crossed range.
|
|
114
|
+
6. **Doctor** — run the check registry to verify the resulting install.
|
|
115
|
+
7. **Surface** the target changelog section.
|
|
126
116
|
|
|
127
117
|
### Flags
|
|
128
118
|
|
|
129
119
|
- `--dry-run` — print the resolved target version and the ordered step
|
|
130
120
|
plan, then exit. No dependency is bumped, no file is written, no seam
|
|
131
121
|
runs.
|
|
132
|
-
- `--major` — apply a major-version crossing that the gate would otherwise
|
|
133
|
-
refuse. Review [`docs/upgrade-major.md`](docs/upgrade-major.md) first.
|
|
134
122
|
- `--install-cmd "<cmd>"` — override the auto-detected install command. The
|
|
135
123
|
package manager is normally detected from your lockfile
|
|
136
124
|
(`pnpm-lock.yaml` ⇒ `pnpm add -D …`, `yarn.lock` ⇒ `yarn add -D …`,
|