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
|
@@ -25,11 +25,11 @@ by `node .agents/scripts/generate-workflows-doc.js`; `npm run docs:check`
|
|
|
25
25
|
fails when it drifts from the on-disk workflow set. To change a command’s
|
|
26
26
|
description, edit the workflow file’s front-matter and regenerate.
|
|
27
27
|
|
|
28
|
-
## Commands (
|
|
28
|
+
## Commands (26)
|
|
29
29
|
|
|
30
30
|
| Command | Description |
|
|
31
31
|
| --- | --- |
|
|
32
|
-
| `/agents-update` | npm-era upgrade wraparound for a Mandrel consumer. Runs `mandrel update` (resolve newest
|
|
32
|
+
| `/agents-update` | npm-era upgrade wraparound for a Mandrel consumer. Runs `mandrel update` (resolve newest published version → install → re-materialize `.agents/` → migrate → doctor → surface changelog) as the single mechanical step, then walks the operator through the judgment wraparound the CLI deliberately leaves unowned: reconcile `.agentrc.json`, install the Epic #1386 quality-gate surface, refresh the harness permission allowlist, reconcile the consumer's `AGENTS.md` / runbooks against the surfaced changelog, and stage + commit the staged lockfile bump. |
|
|
33
33
|
| `/audit-architecture` | Audit architectural boundaries, module coupling, and layering violations; emit a structured findings report keyed to High/Medium/Low severity. |
|
|
34
34
|
| `/audit-clean-code` | Audit code smells, dead code, complexity hotspots, and maintainability-index outliers; emit a structured findings report. |
|
|
35
35
|
| `/audit-dependencies` | Audit `package.json` for unused, outdated, and major-version-stale dependencies; surface Node-engine drift and propose upgrade batches. |
|
|
@@ -51,7 +51,6 @@ description, edit the workflow file’s front-matter and regenerate.
|
|
|
51
51
|
| `/git-merge-pr` | Analyze, validate, resolve conflicts, and merge a given pull request by number. |
|
|
52
52
|
| `/git-pr-all` | Stage all outstanding changes, commit, push to a feature branch, and open a pull request with native auto-merge enabled. |
|
|
53
53
|
| `/git-push` | Commit all outstanding changes then push to the remote repository. |
|
|
54
|
-
| `/onboard` | Guided first-run onboarding for a freshly installed Mandrel. Detects the consumer stack, offers to scaffold any missing docsContextFiles, runs `mandrel doctor` as a readiness gate, and hands off to a started /plan. The whole path is designed to take about 15 minutes from a clean checkout to a planned Epic. |
|
|
55
54
|
| `/plan` | Unified planning entry point. Routes a seed idea (via scope triage) or an existing Epic ID to the right planning path — the full Epic pipeline (PRD, Tech Spec, Acceptance Spec, decomposition) or the standalone-Story authoring path — and absorbs every planning flag. |
|
|
56
55
|
| `/qa-assist` | Human-led QA assist loop — ingest one operator observation, enrich it with repro + root-cause (file:line) + a coverage verdict, ask clarifying questions when it is ambiguous, and append a redacted ledger item to a persistent, resumable rolling session under temp/qa/ |
|
|
57
56
|
| `/qa-explore` | Agent-led exploratory-QA loop — the agent Plans a surface with an explicit static-vs-drive method choice, drives it (browser MCP or static), and captures ledger items read-only, then Triages — a bounded per-surface session, HITL-gated at every phase transition, routed through the shared dedup/coverage/classification/missing-test/redaction/session core under temp/qa/ |
|
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
"minimatch": "^10.0.0",
|
|
8
8
|
"picomatch": "^4.0.4",
|
|
9
9
|
"string-argv": "^0.3.2",
|
|
10
|
-
"typescript": ">=5.0.0",
|
|
11
10
|
"typhonjs-escomplex": "^0.1.0"
|
|
12
11
|
},
|
|
13
12
|
"optionalDependencies": {
|
|
14
13
|
"@commitlint/load": "^21.0.0",
|
|
15
14
|
"chokidar": "^5.0.0",
|
|
16
|
-
"jscpd": "^4.0.0"
|
|
15
|
+
"jscpd": "^4.0.0",
|
|
16
|
+
"typescript": ">=5.0.0"
|
|
17
17
|
}
|
|
18
18
|
}
|
|
@@ -87,7 +87,7 @@ when Stryker itself fails to run.
|
|
|
87
87
|
- [`/docs/architecture.md`](../../docs/architecture.md) — system
|
|
88
88
|
architecture; the "Key Scripts" section lists the standard
|
|
89
89
|
orchestration entrypoints.
|
|
90
|
-
- [
|
|
90
|
+
- [`.agents/docs/quality-gates.md`](../docs/quality-gates.md) — coverage,
|
|
91
91
|
CRAP, and maintainability baselines + floors.
|
|
92
92
|
- `package.json` `scripts` — the canonical list of standard CLIs
|
|
93
93
|
(`test`, `verify`, `coverage:update`, …).
|
|
@@ -15,13 +15,7 @@
|
|
|
15
15
|
* @see docs/v5-implementation-plan.md Sprint 1C
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import fs from 'node:fs';
|
|
19
|
-
import path from 'node:path';
|
|
20
18
|
import { applyBranchProtection } from './lib/bootstrap/branch-protection.js';
|
|
21
|
-
import {
|
|
22
|
-
CI_WORKFLOW_RELATIVE_PATH,
|
|
23
|
-
renderCiWorkflow,
|
|
24
|
-
} from './lib/bootstrap/ci-workflow-template.js';
|
|
25
19
|
import {
|
|
26
20
|
compareSemver,
|
|
27
21
|
MIN_GH_VERSION,
|
|
@@ -55,7 +49,7 @@ import {
|
|
|
55
49
|
import { createProvider } from './lib/provider-factory.js';
|
|
56
50
|
|
|
57
51
|
const PROJECTS_DOC_POINTER =
|
|
58
|
-
'
|
|
52
|
+
'Configure the board views manually in the GitHub Projects UI.';
|
|
59
53
|
|
|
60
54
|
/**
|
|
61
55
|
* Detect that an error is a not-found / 404 signal across the surfaces
|
|
@@ -266,106 +260,6 @@ async function ensureProjectFields(provider, project, log) {
|
|
|
266
260
|
return fields;
|
|
267
261
|
}
|
|
268
262
|
|
|
269
|
-
/**
|
|
270
|
-
* Create or additively-merge branch protection on `baseBranch` (typically
|
|
271
|
-
* `main`) so the `delivery.quality.prGate.checks` suite is required
|
|
272
|
-
* before merge. Behaviour rules:
|
|
273
|
-
*
|
|
274
|
-
* - `enforceBranchProtection: false` → skip, log the opt-out, return a
|
|
275
|
-
* `{ status: 'skipped' }` summary.
|
|
276
|
-
* - `prGate.checks` empty or absent → skip with a clear log, since there
|
|
277
|
-
* is nothing to enforce.
|
|
278
|
-
* - Existing protection rule → preserve every existing required-check
|
|
279
|
-
* context and append only the missing prGate names.
|
|
280
|
-
* - No existing rule → create a fresh one carrying just the prGate
|
|
281
|
-
* contexts plus minimal sensible defaults (strict status checks).
|
|
282
|
-
*
|
|
283
|
-
* Errors (insufficient scopes, repo permission denied, etc.) are logged
|
|
284
|
-
* and return a `{ status: 'failed' }` summary so the bootstrap CLI
|
|
285
|
-
* surfaces a non-fatal warning rather than aborting the entire run —
|
|
286
|
-
* matching how the project-board provisioning steps degrade.
|
|
287
|
-
*/
|
|
288
|
-
async function ensureMainBranchProtection(
|
|
289
|
-
provider,
|
|
290
|
-
{ baseBranch, prGate },
|
|
291
|
-
log,
|
|
292
|
-
) {
|
|
293
|
-
if (prGate?.enforceBranchProtection === false) {
|
|
294
|
-
log(
|
|
295
|
-
`[bootstrap] Branch protection on '${baseBranch}': skipped (delivery.quality.prGate.enforceBranchProtection=false).`,
|
|
296
|
-
);
|
|
297
|
-
return { status: 'skipped', reason: 'opt-out' };
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
const checkNames = (prGate?.checks ?? [])
|
|
301
|
-
.map((c) => c?.name)
|
|
302
|
-
.filter((n) => typeof n === 'string' && n.length > 0);
|
|
303
|
-
if (checkNames.length === 0) {
|
|
304
|
-
log(
|
|
305
|
-
`[bootstrap] Branch protection on '${baseBranch}': skipped (no prGate.checks configured).`,
|
|
306
|
-
);
|
|
307
|
-
return { status: 'skipped', reason: 'no-checks' };
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
try {
|
|
311
|
-
const result = await provider.setBranchProtection(baseBranch, {
|
|
312
|
-
contexts: checkNames,
|
|
313
|
-
});
|
|
314
|
-
const verb = result.created ? 'Created' : 'Updated';
|
|
315
|
-
const addedSuffix = result.added.length
|
|
316
|
-
? ` (added: ${result.added.join(', ')})`
|
|
317
|
-
: ' (all required checks already present)';
|
|
318
|
-
log(
|
|
319
|
-
`[bootstrap] Branch protection on '${baseBranch}': ${verb} rule${addedSuffix}.`,
|
|
320
|
-
);
|
|
321
|
-
return { status: result.created ? 'created' : 'merged', ...result };
|
|
322
|
-
} catch (err) {
|
|
323
|
-
log(
|
|
324
|
-
`[bootstrap] Branch protection on '${baseBranch}': failed — ${err.message}. Proceeding without it.`,
|
|
325
|
-
);
|
|
326
|
-
return { status: 'failed', reason: err.message };
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
* Render the stabilized-quality-gates CI workflow template into a project
|
|
332
|
-
* checkout. Idempotent on the byte level: when `.github/workflows/ci.yml`
|
|
333
|
-
* already matches the rendered template, no write occurs and the action is
|
|
334
|
-
* `unchanged`. When the file is absent the action is `created`. When the
|
|
335
|
-
* file exists with operator-authored differences the helper preserves it
|
|
336
|
-
* and returns `custom-workflow-skip` along with the rendered body so the
|
|
337
|
-
* bootstrap caller (or `/agents-update`) can offer a side-by-side diff.
|
|
338
|
-
*
|
|
339
|
-
* Network-free; safe to invoke under tests with a tmp `projectRoot`.
|
|
340
|
-
*
|
|
341
|
-
* @param {object} args
|
|
342
|
-
* @param {string} args.projectRoot - Repo root (must contain or accept
|
|
343
|
-
* `.github/workflows/`).
|
|
344
|
-
* @param {object} [args.template] - Forwarded to `renderCiWorkflow`.
|
|
345
|
-
* @param {boolean} [args.write=true] - When `false`, the helper computes
|
|
346
|
-
* the would-be action without touching disk. Used by the
|
|
347
|
-
* bootstrap CLI's dry-run mode.
|
|
348
|
-
* @returns {{ action: 'created'|'unchanged'|'custom-workflow-skip',
|
|
349
|
-
* path: string, rendered: string }}
|
|
350
|
-
*/
|
|
351
|
-
export function ensureCiWorkflow(args) {
|
|
352
|
-
const projectRoot = args.projectRoot;
|
|
353
|
-
const rendered = renderCiWorkflow(args.template);
|
|
354
|
-
const target = path.join(projectRoot, CI_WORKFLOW_RELATIVE_PATH);
|
|
355
|
-
if (!fs.existsSync(target)) {
|
|
356
|
-
if (args.write !== false) {
|
|
357
|
-
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
358
|
-
fs.writeFileSync(target, rendered, 'utf8');
|
|
359
|
-
}
|
|
360
|
-
return { action: 'created', path: target, rendered };
|
|
361
|
-
}
|
|
362
|
-
const existing = fs.readFileSync(target, 'utf8');
|
|
363
|
-
if (existing === rendered) {
|
|
364
|
-
return { action: 'unchanged', path: target, rendered };
|
|
365
|
-
}
|
|
366
|
-
return { action: 'custom-workflow-skip', path: target, rendered };
|
|
367
|
-
}
|
|
368
|
-
|
|
369
263
|
/**
|
|
370
264
|
* Run the idempotent bootstrap sequence.
|
|
371
265
|
*
|
|
@@ -397,6 +291,7 @@ export function ensureCiWorkflow(args) {
|
|
|
397
291
|
* github?: object,
|
|
398
292
|
* baseBranch?: string,
|
|
399
293
|
* githubAdminApproved?: boolean,
|
|
294
|
+
* isTTY?: boolean,
|
|
400
295
|
* }} [opts] - `githubAdminApproved` MUST be `true` for any GitHub mutation to
|
|
401
296
|
* occur; any other value (absent / `false`) is treated as "not approved"
|
|
402
297
|
* and the run is a verified no-op.
|
|
@@ -463,12 +358,12 @@ export async function runBootstrap(config, opts = {}) {
|
|
|
463
358
|
// Consumer-facing bootstrap promotes the framework's CI-gates-only
|
|
464
359
|
// stance: branch protection with enforce_admins + 0-approval-count and
|
|
465
360
|
// the squash-only merge-method allowlist. Behavior-shifting drift on
|
|
466
|
-
//
|
|
467
|
-
// with a clear stderr message rather than silently apply.
|
|
361
|
+
// branch protection routes through the HITL confirm gate — non-TTY runs
|
|
362
|
+
// abort with a clear stderr message rather than silently apply. The
|
|
363
|
+
// merge-method step differs by design (Story #4045 A4): non-TTY without an
|
|
364
|
+
// assume override default-applies the framework stance with an explicit
|
|
365
|
+
// log line (see mergeMethodsHitlConfirm below).
|
|
468
366
|
//
|
|
469
|
-
// The legacy `ensureMainBranchProtection` helper is preserved (re-
|
|
470
|
-
// exported below) so the Epic #1142 Story #1157 contract tests stay
|
|
471
|
-
// green; `applyBranchProtection` is its consumer-parity successor.
|
|
472
367
|
// Post-reshape: bootstrap reads from the new `project` + `github` blocks
|
|
473
368
|
// exclusively. The legacy "agent settings" opt was removed in Epic #2880.
|
|
474
369
|
const projectCfg = opts.project ?? config.project ?? {};
|
|
@@ -493,10 +388,22 @@ export async function runBootstrap(config, opts = {}) {
|
|
|
493
388
|
hitlConfirm,
|
|
494
389
|
log,
|
|
495
390
|
});
|
|
391
|
+
|
|
392
|
+
// Merge-methods gate (Story #4045 A4): under non-TTY without an explicit
|
|
393
|
+
// assume override there is no operator to consult, and the default HITL
|
|
394
|
+
// gate declines every non-TTY prompt — which would make applyMergeMethods'
|
|
395
|
+
// documented non-TTY default-apply branch unreachable. Skip the gate in
|
|
396
|
+
// that case so the merge-method stance default-applies with its explicit
|
|
397
|
+
// log line. Interactive runs (and explicit --assume-yes/--assume-no, and
|
|
398
|
+
// injected gates) keep the loud confirm/decline behaviour.
|
|
399
|
+
const stdoutIsTTY = opts.isTTY ?? Boolean(process.stdout.isTTY);
|
|
400
|
+
const mergeMethodsHitlConfirm =
|
|
401
|
+
opts.hitlConfirm ??
|
|
402
|
+
(stdoutIsTTY || opts.assumeYes || opts.assumeNo ? hitlConfirm : undefined);
|
|
496
403
|
const mergeMethods = await applyMergeMethods({
|
|
497
404
|
provider,
|
|
498
405
|
settings,
|
|
499
|
-
hitlConfirm,
|
|
406
|
+
hitlConfirm: mergeMethodsHitlConfirm,
|
|
500
407
|
log,
|
|
501
408
|
});
|
|
502
409
|
|
|
@@ -539,8 +446,9 @@ async function main() {
|
|
|
539
446
|
}
|
|
540
447
|
|
|
541
448
|
// Preflight runtime deps before the dynamic config-resolver import so
|
|
542
|
-
// a
|
|
543
|
-
//
|
|
449
|
+
// a consumer who hasn't installed framework runtime deps yet gets a
|
|
450
|
+
// clear hint (`run mandrel init` or `npm install mandrel`) instead of
|
|
451
|
+
// a raw `ERR_MODULE_NOT_FOUND`.
|
|
544
452
|
try {
|
|
545
453
|
await preflightRuntimeDeps();
|
|
546
454
|
} catch (err) {
|
|
@@ -569,7 +477,6 @@ async function main() {
|
|
|
569
477
|
process.exit(1);
|
|
570
478
|
}
|
|
571
479
|
|
|
572
|
-
const installWorkflows = process.argv.includes('--install-workflows');
|
|
573
480
|
// Epic #1235 Story 5 — flags let CI / non-interactive callers pin the
|
|
574
481
|
// HITL gate's answer deterministically. The bootstrap is non-interactive
|
|
575
482
|
// by default in non-TTY contexts (the gate returns false and aborts);
|
|
@@ -594,7 +501,6 @@ async function main() {
|
|
|
594
501
|
|
|
595
502
|
try {
|
|
596
503
|
const result = await runBootstrap(config, {
|
|
597
|
-
installWorkflows,
|
|
598
504
|
project: config.project,
|
|
599
505
|
github: config.github,
|
|
600
506
|
assumeYes,
|
|
@@ -617,12 +523,10 @@ async function main() {
|
|
|
617
523
|
}
|
|
618
524
|
}
|
|
619
525
|
|
|
620
|
-
// Re-export internal helpers for test consumers (no production caller imports them).
|
|
621
526
|
// Re-export the gh-preflight surface so existing test consumers can keep
|
|
622
527
|
// importing it from this module after the Story #3349 split.
|
|
623
528
|
export {
|
|
624
529
|
compareSemver,
|
|
625
|
-
ensureMainBranchProtection,
|
|
626
530
|
isApiAccessNotFoundError,
|
|
627
531
|
MIN_GH_VERSION,
|
|
628
532
|
parseGhVersion,
|
|
@@ -28,6 +28,9 @@
|
|
|
28
28
|
* @module bootstrap/ci-workflow-template
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
+
import fs from 'node:fs';
|
|
32
|
+
import path from 'node:path';
|
|
33
|
+
|
|
31
34
|
/**
|
|
32
35
|
* @typedef {object} CiTemplateOptions
|
|
33
36
|
* @property {string} [nodeVersion='22'] - Node major to install via setup-node.
|
|
@@ -169,3 +172,46 @@ ${crapBlock}`;
|
|
|
169
172
|
* path without re-deriving it.
|
|
170
173
|
*/
|
|
171
174
|
export const CI_WORKFLOW_RELATIVE_PATH = '.github/workflows/ci.yml';
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Render the stabilized-quality-gates CI workflow template into a project
|
|
178
|
+
* checkout. Idempotent on the byte level: when `.github/workflows/ci.yml`
|
|
179
|
+
* already matches the rendered template, no write occurs and the action is
|
|
180
|
+
* `unchanged`. When the file is absent the action is `created`. When the
|
|
181
|
+
* file exists with operator-authored differences the helper preserves it
|
|
182
|
+
* and returns `custom-workflow-skip` along with the rendered body so the
|
|
183
|
+
* bootstrap caller (or `/agents-update`) can offer a side-by-side diff.
|
|
184
|
+
*
|
|
185
|
+
* Network-free; safe to invoke under tests with a tmp `projectRoot`.
|
|
186
|
+
*
|
|
187
|
+
* Moved from `agents-bootstrap-github.js` to this module so it lives
|
|
188
|
+
* alongside `renderCiWorkflow` and `CI_WORKFLOW_RELATIVE_PATH`
|
|
189
|
+
* (Story #4048 B2 — dead-code deletion from the bootstrap entry point).
|
|
190
|
+
*
|
|
191
|
+
* @param {object} args
|
|
192
|
+
* @param {string} args.projectRoot - Repo root (must contain or accept
|
|
193
|
+
* `.github/workflows/`).
|
|
194
|
+
* @param {object} [args.template] - Forwarded to `renderCiWorkflow`.
|
|
195
|
+
* @param {boolean} [args.write=true] - When `false`, the helper computes
|
|
196
|
+
* the would-be action without touching disk. Used by the
|
|
197
|
+
* bootstrap CLI's dry-run mode.
|
|
198
|
+
* @returns {{ action: 'created'|'unchanged'|'custom-workflow-skip',
|
|
199
|
+
* path: string, rendered: string }}
|
|
200
|
+
*/
|
|
201
|
+
export function ensureCiWorkflow(args) {
|
|
202
|
+
const projectRoot = args.projectRoot;
|
|
203
|
+
const rendered = renderCiWorkflow(args.template);
|
|
204
|
+
const target = path.join(projectRoot, CI_WORKFLOW_RELATIVE_PATH);
|
|
205
|
+
if (!fs.existsSync(target)) {
|
|
206
|
+
if (args.write !== false) {
|
|
207
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
208
|
+
fs.writeFileSync(target, rendered, 'utf8');
|
|
209
|
+
}
|
|
210
|
+
return { action: 'created', path: target, rendered };
|
|
211
|
+
}
|
|
212
|
+
const existing = fs.readFileSync(target, 'utf8');
|
|
213
|
+
if (existing === rendered) {
|
|
214
|
+
return { action: 'unchanged', path: target, rendered };
|
|
215
|
+
}
|
|
216
|
+
return { action: 'custom-workflow-skip', path: target, rendered };
|
|
217
|
+
}
|
|
@@ -38,14 +38,12 @@ const GH_SCOPES_UNREADABLE_NOTE =
|
|
|
38
38
|
* Framework runtime deps the consumer must have installed in
|
|
39
39
|
* `node_modules/` before this script reaches the dynamic
|
|
40
40
|
* `config-resolver` import. `ajv` is the sentinel — if it cannot
|
|
41
|
-
* resolve, the
|
|
42
|
-
* Step 2c/2d dependency-install never ran). The list mirrors the floor
|
|
43
|
-
* in `agents-bootstrap-project.md` Step 2c; keep them in sync.
|
|
41
|
+
* resolve, the framework runtime dependencies are not installed.
|
|
44
42
|
*/
|
|
45
43
|
const REQUIRED_RUNTIME_DEPS = Object.freeze(['ajv']);
|
|
46
44
|
|
|
47
45
|
const RUNTIME_DEPS_HINT =
|
|
48
|
-
'Run
|
|
46
|
+
'Run `mandrel init` (for a fresh project) or `npm install mandrel` (for an existing one) to install the framework runtime dependencies, then re-run this command.';
|
|
49
47
|
|
|
50
48
|
/**
|
|
51
49
|
* Default runner: synchronously execs `gh <args>` and returns
|
|
@@ -273,12 +271,12 @@ function classifyProjectScopes(scopeLine) {
|
|
|
273
271
|
/**
|
|
274
272
|
* Preflight the framework's runtime dependencies before dynamic-importing
|
|
275
273
|
* `config-resolver.js` (which transitively pulls in `ajv` via
|
|
276
|
-
* `config-settings-schema.js`). A
|
|
277
|
-
*
|
|
278
|
-
*
|
|
274
|
+
* `config-settings-schema.js`). A consumer who has not installed the
|
|
275
|
+
* framework runtime deps will not have `ajv` available, and the raw
|
|
276
|
+
* `ERR_MODULE_NOT_FOUND` from the dynamic import is opaque. This
|
|
279
277
|
* preflight converts that into a {@link MissingRuntimeDepsError} that
|
|
280
|
-
* names the missing packages and points the operator at the
|
|
281
|
-
*
|
|
278
|
+
* names the missing packages and points the operator at the correct
|
|
279
|
+
* remediation (`mandrel init` / `npm install mandrel`).
|
|
282
280
|
*
|
|
283
281
|
* The `resolver` seam lets tests inject a stub without touching the real
|
|
284
282
|
* module graph; production uses `import.meta.resolve(specifier)`.
|
|
@@ -150,7 +150,17 @@ export function buildMutationManifest(ctx = {}) {
|
|
|
150
150
|
|
|
151
151
|
// --- repo-config ------------------------------------------------------
|
|
152
152
|
// Local repository configuration files the bootstrap seeds or extends.
|
|
153
|
+
// Also includes git-init, which is the most irreversible local mutation
|
|
154
|
+
// the bootstrap performs (B1 — uninstall ledger must record it).
|
|
153
155
|
entries.push(
|
|
156
|
+
{
|
|
157
|
+
phaseGroup: PHASE_GROUPS.REPO_CONFIG,
|
|
158
|
+
target: rel('.git'),
|
|
159
|
+
action: 'run',
|
|
160
|
+
detail:
|
|
161
|
+
'Initialize the local git repository (git init + first commit) when absent. No-op when already a git repo.',
|
|
162
|
+
reversible: false,
|
|
163
|
+
},
|
|
154
164
|
{
|
|
155
165
|
phaseGroup: PHASE_GROUPS.REPO_CONFIG,
|
|
156
166
|
target: rel('package.json'),
|
|
@@ -209,6 +219,16 @@ export function buildMutationManifest(ctx = {}) {
|
|
|
209
219
|
? `${ctx.answers.owner}/${ctx.answers.repo}`
|
|
210
220
|
: 'the GitHub repository';
|
|
211
221
|
entries.push(
|
|
222
|
+
{
|
|
223
|
+
// B1: GitHub repo creation is the most irreversible remote mutation —
|
|
224
|
+
// record it so the uninstall ledger always lists it.
|
|
225
|
+
phaseGroup: PHASE_GROUPS.GITHUB_ADMIN,
|
|
226
|
+
target: `${repoSlug} (repo)`,
|
|
227
|
+
action: 'create',
|
|
228
|
+
detail:
|
|
229
|
+
'Create the GitHub repository (gh repo create --source=. --push) when absent. No-op when already pushed.',
|
|
230
|
+
reversible: false,
|
|
231
|
+
},
|
|
212
232
|
{
|
|
213
233
|
phaseGroup: PHASE_GROUPS.GITHUB_ADMIN,
|
|
214
234
|
target: `${repoSlug} labels`,
|
|
@@ -238,7 +258,7 @@ export function buildMutationManifest(ctx = {}) {
|
|
|
238
258
|
target: `${repoSlug} merge methods`,
|
|
239
259
|
action: 'configure',
|
|
240
260
|
detail:
|
|
241
|
-
'Set the allowed pull-request merge methods to the framework stance.',
|
|
261
|
+
'Set the allowed pull-request merge methods to the framework stance (squash-only, auto-merge enabled).',
|
|
242
262
|
reversible: false,
|
|
243
263
|
},
|
|
244
264
|
);
|
|
@@ -17,10 +17,13 @@
|
|
|
17
17
|
* No drift (live settings already match the target): no-op, returns
|
|
18
18
|
* `{ status: 'unchanged' }`.
|
|
19
19
|
*
|
|
20
|
-
* Drift (any field differs from the target stance):
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* `{ status: 'skipped', reason: 'hitl-declined' }` without writing
|
|
20
|
+
* Drift (any field differs from the target stance): when a `hitlConfirm`
|
|
21
|
+
* gate is supplied, the proposed payload routes through it — on approval
|
|
22
|
+
* the PATCH is issued; on decline the module returns
|
|
23
|
+
* `{ status: 'skipped', reason: 'hitl-declined' }` without writing (a loud
|
|
24
|
+
* decline, never silent). When NO gate is supplied (non-TTY, no operator
|
|
25
|
+
* present — Story #4045 A4), the framework stance is default-applied with
|
|
26
|
+
* an explicit log line.
|
|
24
27
|
*/
|
|
25
28
|
|
|
26
29
|
export const TARGET_MERGE_METHODS = Object.freeze({
|
|
@@ -81,20 +84,32 @@ export async function applyMergeMethods({
|
|
|
81
84
|
return { status: 'unchanged' };
|
|
82
85
|
}
|
|
83
86
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
87
|
+
let approved;
|
|
88
|
+
if (typeof hitlConfirm === 'function') {
|
|
89
|
+
approved = await hitlConfirm({
|
|
90
|
+
summary:
|
|
91
|
+
'Repo merge-method settings diverge from the framework hands-off-pipeline stance.',
|
|
92
|
+
current,
|
|
93
|
+
proposed: target,
|
|
94
|
+
});
|
|
95
|
+
if (!approved) {
|
|
96
|
+
log(
|
|
97
|
+
'[bootstrap] Merge methods: HITL declined — leaving operator settings ' +
|
|
98
|
+
'untouched. Note: auto-merge will remain disabled until the merge-method ' +
|
|
99
|
+
'settings match the framework stance (allow_squash_merge: true, ' +
|
|
100
|
+
'allow_auto_merge: true, delete_branch_on_merge: true).',
|
|
101
|
+
);
|
|
102
|
+
return { status: 'skipped', reason: 'hitl-declined', diff };
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
// Non-TTY: no operator present to confirm. Default-apply the framework
|
|
106
|
+
// stance and log explicitly so the consequence is never silent.
|
|
94
107
|
log(
|
|
95
|
-
'[bootstrap] Merge methods:
|
|
108
|
+
'[bootstrap] Merge methods: non-TTY — applying framework stance automatically ' +
|
|
109
|
+
'(allow_squash_merge, allow_auto_merge, delete_branch_on_merge). ' +
|
|
110
|
+
'To opt out, pass a hitlConfirm gate or set github.mergeMethods overrides in .agentrc.json.',
|
|
96
111
|
);
|
|
97
|
-
|
|
112
|
+
approved = true;
|
|
98
113
|
}
|
|
99
114
|
|
|
100
115
|
try {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* bootstrap/project-bootstrap — deterministic
|
|
3
|
-
* `/agents-bootstrap-project` workflow (Story #2074, hard cutover).
|
|
2
|
+
* bootstrap/project-bootstrap — deterministic project bootstrap steps.
|
|
4
3
|
*
|
|
5
4
|
* Each exported `ensure*` function is one step of the bootstrap. Every step
|
|
6
5
|
* is idempotent and additive — re-running on an already-bootstrapped clone
|
|
@@ -16,6 +15,8 @@ import { spawnSync as defaultSpawnSync } from 'node:child_process';
|
|
|
16
15
|
import fs from 'node:fs';
|
|
17
16
|
import os from 'node:os';
|
|
18
17
|
import path from 'node:path';
|
|
18
|
+
import { pathToFileURL } from 'node:url';
|
|
19
|
+
import { detectPackageManager as detectPm } from '../detect-package-manager.js';
|
|
19
20
|
import { LEDGER_RELATIVE_PATH } from './install-ledger.js';
|
|
20
21
|
import { PHASE_GROUPS, previewMutationManifest } from './manifest.js';
|
|
21
22
|
import { applyQualityBootstrap } from './quality-bootstrap.js';
|
|
@@ -61,8 +62,8 @@ export const GITIGNORE_BLOCKS = Object.freeze({
|
|
|
61
62
|
block:
|
|
62
63
|
'\n# Project-scoped MCP config carries secrets — keep out of git.\n.mcp.json\n',
|
|
63
64
|
},
|
|
64
|
-
// Story #3894: `.env` holds real secrets (
|
|
65
|
-
// put `GITHUB_TOKEN` here). It MUST be ignored by default so a cold-start
|
|
65
|
+
// Story #3894: `.env` holds real secrets (`mandrel init` instructs operators
|
|
66
|
+
// to put `GITHUB_TOKEN` here). It MUST be ignored by default so a cold-start
|
|
66
67
|
// provision never stages/pushes it. The pattern matches a bare `.env` (with
|
|
67
68
|
// an optional trailing slash) but deliberately NOT `.env.example`, the
|
|
68
69
|
// committed placeholder that `security-baseline.md` § Secrets Management
|
|
@@ -112,7 +113,23 @@ function writeJson(p, obj, fsImpl = fs) {
|
|
|
112
113
|
fsImpl.writeFileSync(p, `${JSON.stringify(obj, null, 2)}\n`, 'utf8');
|
|
113
114
|
}
|
|
114
115
|
|
|
115
|
-
/**
|
|
116
|
+
/**
|
|
117
|
+
* The minimum Node.js patch version mandrel requires.
|
|
118
|
+
*
|
|
119
|
+
* Rationale: `22.22.1` is the Node 22 LTS patch that ships with the
|
|
120
|
+
* `node:sqlite` built-in (`--experimental-sqlite` removed from flag
|
|
121
|
+
* requirement in 22.5.0, but the module stabilised at 22.22.1 per the
|
|
122
|
+
* Node 22 LTS changelog). Pinning to this patch prevents silent failures on
|
|
123
|
+
* older 22.x installs that lack the built-in. CI exercises `node-version: 22`
|
|
124
|
+
* which resolves to the latest 22.x (always ≥ 22.22.1 in the current GHA
|
|
125
|
+
* environment), so CI validates the contract without exercising the exact
|
|
126
|
+
* patch floor — the floor is enforced at install time, not in CI.
|
|
127
|
+
*
|
|
128
|
+
* Single source of truth: `registry.js` and any other consumer MUST import
|
|
129
|
+
* this constant rather than duplicating it.
|
|
130
|
+
*
|
|
131
|
+
* Matches `package.json` `engines.node` (`>=22.22.1 <25`).
|
|
132
|
+
*/
|
|
116
133
|
export const REQUIRED_NODE_FLOOR = '22.22.1';
|
|
117
134
|
export const REQUIRED_NODE_CEILING_MAJOR = 25;
|
|
118
135
|
|
|
@@ -152,16 +169,18 @@ export function checkNodeVersion(version = process.versions.node) {
|
|
|
152
169
|
|
|
153
170
|
/**
|
|
154
171
|
* Detect the package manager based on lockfile presence. Defaults to
|
|
155
|
-
* `npm` when no lock is found
|
|
172
|
+
* `npm` when no lock is found (including the `null` case from the shared
|
|
173
|
+
* helper where no Node manifest exists at all).
|
|
174
|
+
*
|
|
175
|
+
* Delegates to the shared `detectPackageManager` helper
|
|
176
|
+
* (Story #4048 B3 — one implementation per concept).
|
|
156
177
|
*
|
|
157
178
|
* @param {string} projectRoot
|
|
158
179
|
* @param {typeof fs} [fsImpl]
|
|
180
|
+
* @returns {'pnpm'|'yarn'|'npm'}
|
|
159
181
|
*/
|
|
160
182
|
export function detectPackageManager(projectRoot, fsImpl = fs) {
|
|
161
|
-
|
|
162
|
-
return 'pnpm';
|
|
163
|
-
if (fsImpl.existsSync(path.join(projectRoot, 'yarn.lock'))) return 'yarn';
|
|
164
|
-
return 'npm';
|
|
183
|
+
return detectPm(projectRoot, (p) => fsImpl.existsSync(p)) ?? 'npm';
|
|
165
184
|
}
|
|
166
185
|
|
|
167
186
|
/**
|
|
@@ -328,7 +347,9 @@ export async function validateAgentrc(ctx) {
|
|
|
328
347
|
if (!fsImpl.existsSync(schemaModule)) {
|
|
329
348
|
return { ok: false, errors: ['config-settings-schema.js not found'] };
|
|
330
349
|
}
|
|
331
|
-
|
|
350
|
+
// pathToFileURL handles Windows drive letters and percent-encoding
|
|
351
|
+
// correctly (same fix as commit 2e3d210b in lib/transpile.js).
|
|
352
|
+
const mod = await import(pathToFileURL(schemaModule).href);
|
|
332
353
|
const validate = mod.getAgentrcValidator();
|
|
333
354
|
const data = readJsonIfExists(
|
|
334
355
|
path.join(ctx.projectRoot, '.agentrc.json'),
|
|
@@ -69,7 +69,7 @@ export function syncAgentrc(opts) {
|
|
|
69
69
|
status: 'missing-config',
|
|
70
70
|
changes: [],
|
|
71
71
|
errors: [
|
|
72
|
-
`No .agentrc.json at ${configPath}. Run /
|
|
72
|
+
`No .agentrc.json at ${configPath}. Run \`mandrel init\` (new project) or \`node .agents/scripts/bootstrap.js\` to create it.`,
|
|
73
73
|
],
|
|
74
74
|
configPath,
|
|
75
75
|
wrote: false,
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* detect-package-manager — shared lockfile-probe helper (Story #4048 B3).
|
|
3
|
+
*
|
|
4
|
+
* Five independent copies of this lockfile probe existed across the codebase:
|
|
5
|
+
* - `lib/cli/update.js#detectPackageManager`
|
|
6
|
+
* - `lib/bootstrap/project-bootstrap.js#detectPackageManager`
|
|
7
|
+
* - `lib/runtime-deps/preflight.js#detectPackageManager`
|
|
8
|
+
* - `lib/onboard/detect-stack.js#detectPackageManager`
|
|
9
|
+
* - `lib/worktree/node-modules-strategy.js#selectInstallCommand` (inline)
|
|
10
|
+
*
|
|
11
|
+
* This module is the single authoritative implementation. It uses the
|
|
12
|
+
* strictest semantics from the prior copies: detects `bun` in addition to
|
|
13
|
+
* pnpm/yarn/npm, returns `null` when the directory has no Node manifest at
|
|
14
|
+
* all (not even `package.json`), and optionally reports `workspaceRoot` for
|
|
15
|
+
* pnpm (the `update.js` caller's unique requirement).
|
|
16
|
+
*
|
|
17
|
+
* All callers must handle `null` explicitly — it means the directory carries
|
|
18
|
+
* no recognizable Node toolchain, so callers that need a concrete fallback
|
|
19
|
+
* should coerce: `detectPm(root) ?? 'npm'`.
|
|
20
|
+
*
|
|
21
|
+
* Injectable seams: the `exists` parameter replaces `fs.existsSync` so
|
|
22
|
+
* callers can drive the function with an in-memory fixture in unit tests.
|
|
23
|
+
*
|
|
24
|
+
* Builtins only — this module runs before third-party packages are
|
|
25
|
+
* guaranteed to be present and is also imported from the worktree and
|
|
26
|
+
* runtime-deps preflight guards.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import fs from 'node:fs';
|
|
30
|
+
import path from 'node:path';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Detect the package manager from lockfile and manifest presence.
|
|
34
|
+
*
|
|
35
|
+
* Precedence: `pnpm-lock.yaml` > `yarn.lock` > `bun.lockb` >
|
|
36
|
+
* `package-lock.json` > `package.json` (npm without a lockfile yet) > `null`.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} root - Absolute directory to probe (consumer project root).
|
|
39
|
+
* @param {(p: string) => boolean} [exists=fs.existsSync] - Path existence probe.
|
|
40
|
+
* @returns {'pnpm'|'yarn'|'bun'|'npm'|null}
|
|
41
|
+
*/
|
|
42
|
+
export function detectPackageManager(root, exists = fs.existsSync) {
|
|
43
|
+
if (exists(path.join(root, 'pnpm-lock.yaml'))) return 'pnpm';
|
|
44
|
+
if (exists(path.join(root, 'yarn.lock'))) return 'yarn';
|
|
45
|
+
if (exists(path.join(root, 'bun.lockb'))) return 'bun';
|
|
46
|
+
if (exists(path.join(root, 'package-lock.json'))) return 'npm';
|
|
47
|
+
if (exists(path.join(root, 'package.json'))) return 'npm';
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Detect the package manager and whether the directory is a pnpm workspace
|
|
53
|
+
* root. The `workspaceRoot` flag is `true` only for pnpm when
|
|
54
|
+
* `pnpm-workspace.yaml` is present alongside the lockfile — the signal that
|
|
55
|
+
* `pnpm add` must carry `-w` to target the workspace-root manifest.
|
|
56
|
+
*
|
|
57
|
+
* Used by `lib/cli/update.js` which needs both pieces of information to
|
|
58
|
+
* construct the correct install command.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} root - Absolute directory to probe.
|
|
61
|
+
* @param {(p: string) => boolean} [exists=fs.existsSync] - Path existence probe.
|
|
62
|
+
* @returns {{ packageManager: 'pnpm'|'yarn'|'bun'|'npm', workspaceRoot: boolean }}
|
|
63
|
+
*/
|
|
64
|
+
export function detectPackageManagerWithWorkspace(
|
|
65
|
+
root,
|
|
66
|
+
exists = fs.existsSync,
|
|
67
|
+
) {
|
|
68
|
+
const pm = detectPackageManager(root, exists) ?? 'npm';
|
|
69
|
+
const workspaceRoot =
|
|
70
|
+
pm === 'pnpm' && exists(path.join(root, 'pnpm-workspace.yaml'));
|
|
71
|
+
return { packageManager: pm, workspaceRoot };
|
|
72
|
+
}
|
|
@@ -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
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
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 {
|