mindforge-cc 11.9.2 → 11.9.4
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/.agent/CLAUDE.md +37 -13
- package/.agent/hooks/mindforge-block-no-verify.js +61 -13
- package/.agent/hooks/mindforge-config-protection.js +82 -3
- package/.agent/hooks/mindforge-context-monitor.js +1 -1
- package/.agent/hooks/mindforge-workflow-guard.js +2 -2
- package/.agent/hooks/run-with-flags.js +190 -20
- package/.agent/mindforge/browse.md +2 -2
- package/.agent/mindforge/checkpoint.md +1 -1
- package/.agent/mindforge/harness-audit.md +1 -1
- package/.agent/mindforge/orch-add-feature.md +1 -1
- package/.agent/mindforge/orch-build-mvp.md +1 -1
- package/.agent/mindforge/orch-change-feature.md +1 -1
- package/.agent/mindforge/orch-fix-defect.md +1 -1
- package/.agent/mindforge/orch-refine-code.md +1 -1
- package/.agent/mindforge/qa.md +2 -2
- package/.claude/CLAUDE.md +37 -13
- package/.claude/commands/mindforge/browse.md +2 -2
- package/.claude/commands/mindforge/checkpoint.md +1 -1
- package/.claude/commands/mindforge/harness-audit.md +1 -1
- package/.claude/commands/mindforge/orch-add-feature.md +1 -1
- package/.claude/commands/mindforge/orch-build-mvp.md +1 -1
- package/.claude/commands/mindforge/orch-change-feature.md +1 -1
- package/.claude/commands/mindforge/orch-fix-defect.md +1 -1
- package/.claude/commands/mindforge/orch-refine-code.md +1 -1
- package/.claude/commands/mindforge/qa.md +2 -2
- package/.mindforge/MINDFORGE-SCHEMA.json +1 -1
- package/.mindforge/config.json +3 -3
- package/.mindforge/engine/autonomous/headless-adapter.md +9 -2
- package/.mindforge/engine/temporal-protocol.md +2 -2
- package/.mindforge/governance/change-classifier.md +20 -4
- package/.mindforge/skills/agent-architecture-audit/SKILL.md +2 -2
- package/.mindforge/skills/orch-pipeline/SKILL.md +4 -4
- package/CHANGELOG.md +357 -0
- package/MINDFORGE.md +13 -6
- package/README.md +49 -30
- package/RELEASENOTES.md +65 -2
- package/SECURITY.md +22 -3
- package/bin/autonomous/auto-runner.js +65 -2
- package/bin/change-classifier.js +151 -16
- package/bin/dashboard/api-router.js +18 -38
- package/bin/dashboard/frontend/app.js +429 -0
- package/bin/dashboard/frontend/index.html +13 -406
- package/bin/dashboard/metrics-aggregator.js +52 -23
- package/bin/dashboard/server.js +160 -1
- package/bin/dashboard/sse-bridge.js +11 -8
- package/bin/engine/sre-manager.js +1 -1
- package/bin/engine/temporal-cli.js +56 -6
- package/bin/engine/verification-runner.js +134 -17
- package/bin/engine/verify-cli.js +25 -7
- package/bin/governance/approval-record.js +147 -0
- package/bin/governance/approve.js +24 -8
- package/bin/governance/policy-engine.js +33 -3
- package/bin/governance/policy-gate-hardened.js +36 -1
- package/bin/governance/verify-approvals.js +175 -0
- package/bin/harness-audit.js +224 -10
- package/bin/hooks/instinct-capture-hook.js +12 -4
- package/bin/install.js +63 -3
- package/bin/installer/harness-adapter-compliance.js +339 -28
- package/bin/installer/hook-registration.js +547 -0
- package/bin/installer-core.js +481 -65
- package/bin/learning/instinct-cli.js +7 -0
- package/bin/memory/vector-hub.js +196 -13
- package/bin/migrations/0.6.0-to-1.0.0.js +30 -25
- package/bin/migrations/1.0.0-to-2.0.0.js +22 -23
- package/bin/mindforge-cli.js +67 -6
- package/bin/models/cost-tracker.js +104 -6
- package/bin/models/model-client.js +6 -1
- package/bin/revops/debt-monitor.js +57 -13
- package/bin/security/trust-gate-hook.js +50 -6
- package/bin/skill-validator.js +6 -1
- package/bin/skills-builder/skill-scorer.js +46 -6
- package/bin/updater/self-update.js +6 -1
- package/bin/updater/version-comparator.js +21 -1
- package/bin/utils/mindforge-version.js +99 -0
- package/bin/utils/redact-secrets.js +106 -0
- package/bin/validate-config.js +42 -2
- package/bin/wizard/setup-wizard.js +4 -1
- package/bin/wizard/theme.js +9 -1
- package/changelogs/index.json +11 -9
- package/changelogs/v11.9.3.md +204 -0
- package/changelogs/v11.9.4.md +155 -0
- package/docs/References/config-reference.md +5 -2
- package/docs/References/sdk-api.md +1 -1
- package/docs/Templates/Codebase/architecture.md +1 -1
- package/docs/commands-reference.md +4 -5
- package/docs/faq.md +25 -5
- package/docs/getting-started.md +10 -4
- package/docs/sdk-reference.md +15 -7
- package/docs/troubleshooting.md +65 -6
- package/docs/user-guide.md +14 -14
- package/examples/sdk-integration/README.md +1 -1
- package/package.json +8 -3
- package/subagents/.claude-plugin/marketplace.json +1 -1
- package/.mindforge/memory/sync-manifest.json +0 -6
- package/bin/dashboard/approval-handler.js +0 -136
package/bin/installer-core.js
CHANGED
|
@@ -11,7 +11,13 @@ const SessionMemoryLoader = require('./memory/session-memory-loader');
|
|
|
11
11
|
const Theme = require('./wizard/theme');
|
|
12
12
|
const c = Theme.colors;
|
|
13
13
|
|
|
14
|
-
const VERSION = require('../package.json').version
|
|
14
|
+
// A `const VERSION = require('../package.json').version` used to sit here. It was DEAD — declared,
|
|
15
|
+
// never read, never exported — and it was the only reason this module could fail to load at all.
|
|
16
|
+
// `require` of a missing path throws MODULE_NOT_FOUND, and in an install this file lands at
|
|
17
|
+
// <project>/bin/installer-core.js, so '../package.json' is the CONSUMER's manifest: present and
|
|
18
|
+
// wrong (their app's version), or absent and fatal. A dead read that can only ever crash is pure
|
|
19
|
+
// liability, so it is gone rather than rewired. Anything here that needs MindForge's version must
|
|
20
|
+
// call resolveMindforgeVersion() from ./utils/mindforge-version, which resolves by package NAME.
|
|
15
21
|
|
|
16
22
|
// ── Runtime configurations ────────────────────────────────────────────────────
|
|
17
23
|
const RUNTIMES = {
|
|
@@ -164,13 +170,108 @@ function getCommandDescription(content) {
|
|
|
164
170
|
return 'No description available';
|
|
165
171
|
}
|
|
166
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Where a command file from a given namespace must be written, and under what name.
|
|
175
|
+
*
|
|
176
|
+
* THE DEFECT THIS FIXES. Both command sources — `.agent/mindforge` and `.agent/forge` — were written
|
|
177
|
+
* into the SAME directory under their bare filenames. `.agent/forge` holds exactly three files and
|
|
178
|
+
* ALL THREE collide with mindforge commands, and forge was pushed second, so forge won. Measured on
|
|
179
|
+
* a real `--claude --local` install, the installed file against its two candidate sources:
|
|
180
|
+
*
|
|
181
|
+
* help.md 33 lines -> 11 (-22)
|
|
182
|
+
* init-project.md 170 lines -> 36 (-134)
|
|
183
|
+
* plan-phase.md 131 lines -> 34 (-97)
|
|
184
|
+
*
|
|
185
|
+
* 253 lines of the three flagship commands destroyed on every install, with no warning. The same bug
|
|
186
|
+
* inflated the reported count: `totalCount` summed both sources, so the installer announced 224
|
|
187
|
+
* commands while 221 landed — the overcount was exactly the three overwritten files.
|
|
188
|
+
*
|
|
189
|
+
* They are not duplicates, which is what makes the loss silent rather than harmless. They are a
|
|
190
|
+
* SEPARATE command family: forge's own body says "Show all available FORGE commands" and
|
|
191
|
+
* "List every .md file in `.claude/commands/forge/`" — a directory the installer never created. So
|
|
192
|
+
* `/mindforge:help` was answering with forge's text, which then pointed the model at a path that
|
|
193
|
+
* does not exist.
|
|
194
|
+
*
|
|
195
|
+
* THE FIX follows a pattern this file already contains. The cross-IDE mirror below has always
|
|
196
|
+
* written `path.join(cwd, '.claude', 'commands', source.namespace)` — per-namespace directories. The
|
|
197
|
+
* primary install simply never did the same.
|
|
198
|
+
*
|
|
199
|
+
* @param {string} cmdsDir the runtime's configured commands directory
|
|
200
|
+
* @param {string} runtime RUNTIMES key
|
|
201
|
+
* @param {string} namespace 'mindforge' | 'forge'
|
|
202
|
+
* @param {string} file basename, e.g. 'help.md'
|
|
203
|
+
* @returns {{dir: string, name: string}}
|
|
204
|
+
*/
|
|
205
|
+
function resolveCommandTarget(cmdsDir, runtime, namespace, file) {
|
|
206
|
+
// Antigravity flattens everything into one `workflows/` directory and has always disambiguated by
|
|
207
|
+
// prefixing the namespace. Left exactly as it was — it never had the collision.
|
|
208
|
+
if (runtime === 'antigravity') return { dir: cmdsDir, name: `${namespace}:${file}` };
|
|
209
|
+
|
|
210
|
+
// Runtimes whose commands directory is already per-family (leaf 'mindforge': claude, opencode,
|
|
211
|
+
// gemini, copilot) get a SIBLING directory per namespace. For claude that is
|
|
212
|
+
// .claude/commands/forge/ — exactly where forge's own help text says forge commands live, so the
|
|
213
|
+
// fix makes that text true rather than merely stopping the overwrite.
|
|
214
|
+
if (path.basename(cmdsDir) === 'mindforge') {
|
|
215
|
+
return namespace === 'mindforge'
|
|
216
|
+
? { dir: cmdsDir, name: file }
|
|
217
|
+
: { dir: path.join(path.dirname(cmdsDir), namespace), name: file };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Flat runtimes (cursor writes into `rules/`). Keep mindforge's filenames unchanged — renaming
|
|
221
|
+
// them would alter cursor's rule set for reasons unrelated to this defect — and prefix the rest.
|
|
222
|
+
return namespace === 'mindforge'
|
|
223
|
+
? { dir: cmdsDir, name: file }
|
|
224
|
+
: { dir: cmdsDir, name: `${namespace}:${file}` };
|
|
225
|
+
}
|
|
226
|
+
|
|
167
227
|
// ── File system utilities ─────────────────────────────────────────────────────
|
|
228
|
+
/**
|
|
229
|
+
* Refuse to write through a symlink.
|
|
230
|
+
*
|
|
231
|
+
* THE DEFECT, reproduced with a canary before this guard existed. `fs.writeFileSync` and
|
|
232
|
+
* `fs.copyFileSync` open the destination O_WRONLY|O_CREAT|O_TRUNC and FOLLOW symlinks, and every
|
|
233
|
+
* installer write funnels through the two primitives below. So a repository that commits its entry
|
|
234
|
+
* file as a symlink turned the documented install command into an arbitrary-file overwrite:
|
|
235
|
+
*
|
|
236
|
+
* $ ln -s <victim> <project>/CLAUDE.md # the repo carries this; git preserves symlinks
|
|
237
|
+
* $ npx mindforge-cc@latest --claude --local
|
|
238
|
+
* victim before: 24 bytes sha 2cfdbb20c25ced11
|
|
239
|
+
* victim after: 5646 bytes sha 05b78d05307b2350 <- overwritten
|
|
240
|
+
* <project>/CLAUDE.md.backup-<epoch> CONTAINS THE VICTIM CONTENT <- and disclosed
|
|
241
|
+
*
|
|
242
|
+
* Two separate harms in one step: the target is destroyed, and because safeCopyClaude reads the
|
|
243
|
+
* destination THROUGH the link before replacing it, the victim's previous contents are copied into
|
|
244
|
+
* the project's working tree as a backup file. Point the link at anything the installing user can
|
|
245
|
+
* write and both happen with their privileges.
|
|
246
|
+
*
|
|
247
|
+
* Refusing rather than unlinking is deliberate. Unlinking would silently change what the user's
|
|
248
|
+
* project looks like; refusing leaves their file untouched and tells them why. Writing through a
|
|
249
|
+
* symlink is not something an installer ever legitimately needs to do.
|
|
250
|
+
*
|
|
251
|
+
* SCOPE, stated rather than implied: this guards the destination FILE. A symlinked DIRECTORY in the
|
|
252
|
+
* destination path is a separate escape — mkdirSync/copyFileSync resolve it too — and is not covered
|
|
253
|
+
* here. Closing that needs a containment check against the install root, which behaves differently
|
|
254
|
+
* for --local (cwd-relative) and global installs, so it belongs in its own change.
|
|
255
|
+
*/
|
|
256
|
+
function assertNotSymlink(p) {
|
|
257
|
+
let st;
|
|
258
|
+
try { st = fs.lstatSync(p); } catch { return; } // absent is a normal, safe state
|
|
259
|
+
if (!st.isSymbolicLink()) return;
|
|
260
|
+
let target = '';
|
|
261
|
+
try { target = ` -> ${fs.readlinkSync(p)}`; } catch { target = ' -> <dangling>'; }
|
|
262
|
+
throw new Error(
|
|
263
|
+
`[installer] REFUSING to write through a symlink: ${p}${target}\n`
|
|
264
|
+
+ ' fs.writeFileSync/copyFileSync follow symlinks, so this would overwrite the target outside\n'
|
|
265
|
+
+ ' the project and could copy its contents into the working tree as a backup.\n'
|
|
266
|
+
+ ' Remove or replace the link, then re-run the installer.');
|
|
267
|
+
}
|
|
268
|
+
|
|
168
269
|
const fsu = {
|
|
169
270
|
exists: p => fs.existsSync(p),
|
|
170
271
|
read: p => fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : '',
|
|
171
|
-
write: (p, t) => { fsu.ensureDir(path.dirname(p)); fs.writeFileSync(p, t, 'utf8'); },
|
|
272
|
+
write: (p, t) => { assertNotSymlink(p); fsu.ensureDir(path.dirname(p)); fs.writeFileSync(p, t, 'utf8'); },
|
|
172
273
|
ensureDir: p => { if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true }); },
|
|
173
|
-
copy: (src, dst) => { fsu.ensureDir(path.dirname(dst)); fs.copyFileSync(src, dst); },
|
|
274
|
+
copy: (src, dst) => { assertNotSymlink(dst); fsu.ensureDir(path.dirname(dst)); fs.copyFileSync(src, dst); },
|
|
174
275
|
listFiles: p => fs.existsSync(p) ? fs.readdirSync(p) : [],
|
|
175
276
|
listFilesRecursive: (p, ext = '.md') => {
|
|
176
277
|
if (!fs.existsSync(p)) return [];
|
|
@@ -301,6 +402,26 @@ const SENSITIVE_EXCLUDE = [
|
|
|
301
402
|
'.git',
|
|
302
403
|
'.DS_Store',
|
|
303
404
|
'browser-daemon.log',
|
|
405
|
+
// The framework's OWN runtime state, which is not the consumer's and is not small.
|
|
406
|
+
//
|
|
407
|
+
// Measured on a `--claude --local` install whose source was this working tree: the target received
|
|
408
|
+
// celestial.db (11.2 MB, sha256 IDENTICAL to the source, so copied rather than created) plus its
|
|
409
|
+
// -wal and -shm sidecars and SEVEN orphaned `celestial.db.tmp.<pid>.async` exports at 11.1 MB each
|
|
410
|
+
// — about 89 MB of the developer's traces, skills and attestations landing in someone else's
|
|
411
|
+
// project. `browser-daemon.log` was already excluded here; the database beside it was not.
|
|
412
|
+
//
|
|
413
|
+
// SCOPE, stated honestly: `npm pack --dry-run` confirms neither celestial.db nor the daemon token
|
|
414
|
+
// ships in the tarball, so an ordinary `npx mindforge-cc` install has nothing to copy and was never
|
|
415
|
+
// exposed. This bites installs whose SOURCE is a working tree — a git-clone install, or local
|
|
416
|
+
// development. It is defence in depth rather than a live consumer leak, and it belongs here because
|
|
417
|
+
// `files[]` excluding it today is not a guarantee about tomorrow.
|
|
418
|
+
//
|
|
419
|
+
// One pattern for the database and every sidecar and orphan: .db, .db-wal, .db-shm,
|
|
420
|
+
// .db.tmp.<pid>.async, .db.conflict.<pid>.<len>.
|
|
421
|
+
/^celestial\.db($|[.-])/,
|
|
422
|
+
// A live capability token. Not a credential for a remote service, but still this machine's handle
|
|
423
|
+
// to a running daemon, and nothing the target project should receive.
|
|
424
|
+
/^\.browser-daemon-token$/,
|
|
304
425
|
/audit\.jsonl/i,
|
|
305
426
|
/handoff\.json/i,
|
|
306
427
|
/jira-sync\.json/i,
|
|
@@ -359,6 +480,12 @@ function resolveBaseDir(runtime, scope) {
|
|
|
359
480
|
function safeCopyClaude(src, dst, options = {}) {
|
|
360
481
|
const { force = false, verbose = false } = options;
|
|
361
482
|
|
|
483
|
+
// BEFORE reading. fsu.read() resolves the link, so checking here rather than relying on the guard
|
|
484
|
+
// inside fsu.copy() is what stops the DISCLOSURE half: with a symlinked destination the old order
|
|
485
|
+
// read the victim's contents and wrote them to `${dst}.backup-<epoch>` inside the project, and that
|
|
486
|
+
// backup path is a fresh regular file, so the primitive's guard would never have fired on it.
|
|
487
|
+
assertNotSymlink(dst);
|
|
488
|
+
|
|
362
489
|
if (fsu.exists(dst)) {
|
|
363
490
|
const existing = fsu.read(dst);
|
|
364
491
|
|
|
@@ -382,6 +509,29 @@ function safeCopyClaude(src, dst, options = {}) {
|
|
|
382
509
|
}
|
|
383
510
|
|
|
384
511
|
// ── Install verification ──────────────────────────────────────────────────────
|
|
512
|
+
/**
|
|
513
|
+
* Check that an install produced the files it promised.
|
|
514
|
+
*
|
|
515
|
+
* THIS WAS DEAD CODE. Declared here and called from nowhere, while install() printed
|
|
516
|
+
* "Install verified" unconditionally under a section header reading "4. Verify installation".
|
|
517
|
+
* Two tests kept it alive without ever running it — tests/production.test.js:86 and
|
|
518
|
+
* tests/install.test.js:179 both assert only that the SOURCE CONTAINS the string
|
|
519
|
+
* "verifyInstall". They would pass against an empty function body, and they are why the dead
|
|
520
|
+
* function survived: the suite guaranteed the appearance of verification.
|
|
521
|
+
*
|
|
522
|
+
* Its contract was also UNSATISFIABLE. It required docs/registry/COMMANDS.md and
|
|
523
|
+
* docs/registry/PERSONAS.md under the project root, but docs/registry/ ships ZERO files in the
|
|
524
|
+
* npm tarball, so those two could never exist in a consumer install. Measured against a clean
|
|
525
|
+
* `--claude --local`: 12 of the 14 required files present, those two absent. Wiring it as
|
|
526
|
+
* written would therefore have exited 1 on every successful install — which is presumably why
|
|
527
|
+
* nobody wired it. They are removed rather than "fixed" by copying the files, because requiring
|
|
528
|
+
* an artefact the package does not publish is a category error, not a copy gap.
|
|
529
|
+
*
|
|
530
|
+
* Returns a result instead of calling process.exit, so it is testable without spawning an
|
|
531
|
+
* installer. The caller owns the exit decision.
|
|
532
|
+
*
|
|
533
|
+
* @returns {{ok: boolean, missing: string[], checked: number}}
|
|
534
|
+
*/
|
|
385
535
|
function verifyInstall(baseDir, cmdsDir, runtime, scope) {
|
|
386
536
|
const cfg = RUNTIMES[runtime];
|
|
387
537
|
const pfx = runtime === 'antigravity' ? 'mindforge:' : '';
|
|
@@ -392,25 +542,42 @@ function verifyInstall(baseDir, cmdsDir, runtime, scope) {
|
|
|
392
542
|
path.join(cmdsDir, `${pfx}health.md`),
|
|
393
543
|
path.join(cmdsDir, `${pfx}execute-phase.md`),
|
|
394
544
|
path.join(cmdsDir, `${pfx}security-scan.md`),
|
|
395
|
-
// Sovereign Engine logic
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
545
|
+
// Sovereign Engine logic — LOCAL SCOPE ONLY, because a global install never writes it.
|
|
546
|
+
//
|
|
547
|
+
// These six were required unconditionally against process.cwd(), so every `--claude --global`
|
|
548
|
+
// install ended:
|
|
549
|
+
//
|
|
550
|
+
// ❌ Install verification failed — 6 of 12 required file(s) missing
|
|
551
|
+
// Retry: npx mindforge-cc@latest --claude --global --force
|
|
552
|
+
// exit 1
|
|
553
|
+
//
|
|
554
|
+
// Measured: a global install writes 389 files to $HOME/.claude and ZERO to bin/ anywhere —
|
|
555
|
+
// neither the project nor $HOME/.claude/bin. That is deliberate, not a gap: the block that copies
|
|
556
|
+
// sovereignEngines is gated `if (scope === 'local' && !selfInstall)` at :845. So verification was
|
|
557
|
+
// demanding artifacts of an operation the installer had correctly chosen not to perform, then
|
|
558
|
+
// advising a --force retry that cannot help — --force does not change which scope branch runs.
|
|
559
|
+
//
|
|
560
|
+
// The scope-conditional shape was already here, applied to the entry file on the first line of
|
|
561
|
+
// this array and to nothing else. Extending it is the smaller change; the alternative — making a
|
|
562
|
+
// global install carry bin/ — is a decision about what global scope MEANS and is deliberately not
|
|
563
|
+
// taken here. Flagged instead: a global install currently gives a user commands under
|
|
564
|
+
// ~/.claude/commands/mindforge/ with no Node runtime beside them, and whether that is coherent is
|
|
565
|
+
// a product question rather than a verification bug.
|
|
566
|
+
...(scope === 'local' ? [
|
|
567
|
+
path.join(process.cwd(), 'bin/governance/policy-engine.js'),
|
|
568
|
+
path.join(process.cwd(), 'bin/governance/quantum-crypto.js'),
|
|
569
|
+
path.join(process.cwd(), 'bin/autonomous/intent-harvester.js'),
|
|
570
|
+
path.join(process.cwd(), 'bin/memory/cli.js'),
|
|
571
|
+
path.join(process.cwd(), 'bin/models/cost-tracker.js'),
|
|
572
|
+
path.join(process.cwd(), 'bin/research/research-engine.js'),
|
|
573
|
+
] : []),
|
|
404
574
|
];
|
|
405
575
|
|
|
406
576
|
const missing = required.filter(f => !fsu.exists(f));
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
console.error(`\n Retry: npx mindforge-cc@latest --${runtime} --${scope} --force`);
|
|
412
|
-
process.exit(1);
|
|
413
|
-
}
|
|
577
|
+
// `checked` is reported by the caller ("Install verified (N required files present)"), so it has to
|
|
578
|
+
// reflect what was actually examined for this scope — 12 local, 6 global. Printing 12 after checking
|
|
579
|
+
// 6 would be the same class of false claim this function exists to catch.
|
|
580
|
+
return { ok: missing.length === 0, missing, checked: required.length };
|
|
414
581
|
}
|
|
415
582
|
|
|
416
583
|
// ── Install single runtime ────────────────────────────────────────────────────
|
|
@@ -425,13 +592,39 @@ async function install(runtime, scope, options = {}) {
|
|
|
425
592
|
const cfg = RUNTIMES[runtime];
|
|
426
593
|
const baseDir = resolveBaseDir(runtime, scope);
|
|
427
594
|
const cmdsDir = norm(path.join(baseDir, cfg.commandsSubdir));
|
|
428
|
-
|
|
595
|
+
// SCOPE MATTERS, and leaving it out was a regression this commit introduced and then had to fix.
|
|
596
|
+
//
|
|
597
|
+
// isSelfInstall() answers only "is the CURRENT DIRECTORY the MindForge repo". The guard's actual
|
|
598
|
+
// premise is narrower: "the files I am about to write ARE this repository's own tracked files".
|
|
599
|
+
// That holds for a LOCAL install, whose baseDir is `.claude/` inside the repo. It does NOT hold for
|
|
600
|
+
// a GLOBAL install, whose baseDir is `~/.claude/` — a directory that has nothing to do with the
|
|
601
|
+
// checkout you happen to be standing in.
|
|
602
|
+
//
|
|
603
|
+
// Measured on `--claude --global` run from a MindForge checkout, with HOME confined:
|
|
604
|
+
// develop exit 0, 225 files written to $HOME/.claude
|
|
605
|
+
// with cwd-only gate exit 1, 0 files, then "Retry: ... --force" — which also fails
|
|
606
|
+
// So gating on cwd alone broke global installs for every MindForge developer, and offered a retry
|
|
607
|
+
// that could not work. Anchoring on scope as well restores it while keeping the local-install
|
|
608
|
+
// protection that is the whole point.
|
|
609
|
+
//
|
|
610
|
+
// Every downstream gate reads this one binding, so the correction lands at all twelve sites at
|
|
611
|
+
// once rather than being re-derived (and re-forgotten) at each.
|
|
612
|
+
const selfInstall = isSelfInstall() && scope === 'local';
|
|
429
613
|
const targetDir = baseDir;
|
|
614
|
+
// REG-01 result, printed in the final summary. Declared here so the summary cannot reference an
|
|
615
|
+
// undefined binding when the registration block is skipped (global scope, self-install, etc.).
|
|
616
|
+
let hookRegistration = { status: 'not-attempted', reason: 'registration block not reached', registered: false };
|
|
430
617
|
|
|
431
618
|
Theme.printPrompt(`Runtime : ${c.cyan(runtime)}`);
|
|
432
619
|
Theme.printPrompt(`Scope : ${c.dim(scope)} → ${c.bold(targetDir)}`);
|
|
433
620
|
if (options.dryRun) Theme.printStatus('Mode : DRY RUN (no changes)', 'warn');
|
|
434
|
-
|
|
621
|
+
// Names what is skipped, because the previous wording was FALSE. It read "skipping framework file
|
|
622
|
+
// copy" while the code still overwrote the entry file and 149 tracked command files. A message
|
|
623
|
+
// describing work the code does not do is worse than no message — it is why nobody noticed.
|
|
624
|
+
if (selfInstall) {
|
|
625
|
+
Theme.printStatus(c.yellow('Self-install detected — leaving this repository\'s own tracked files alone'), 'warn');
|
|
626
|
+
Theme.printStatus(c.dim(` not written: ${cfg.entryFile}, commands, skills, hooks, personas, docs, subagents, memory`), 'info');
|
|
627
|
+
}
|
|
435
628
|
|
|
436
629
|
if (dryRun) {
|
|
437
630
|
console.log('\n Would install:');
|
|
@@ -493,12 +686,31 @@ async function install(runtime, scope, options = {}) {
|
|
|
493
686
|
// ✨ RUNTIME ADAPTATION: Generate specific content for this runtime
|
|
494
687
|
const adaptedContent = generateEntryContent(runtime, content);
|
|
495
688
|
|
|
496
|
-
// Keep legacy location based on runtime config
|
|
689
|
+
// Keep legacy location based on runtime config.
|
|
690
|
+
//
|
|
691
|
+
// THE LEAK. This staging file was written and never removed — no unlink, no finally, no exit
|
|
692
|
+
// handler. Every install left one behind, forever. Measured on this machine: 2,711 orphaned
|
|
693
|
+
// `/tmp/CLAUDE.md-<ms>.md` files (~21 MB), and 905 more accumulated within a day of clearing
|
|
694
|
+
// them. Unconditional, unlike the .mindforge excludes below: it fires on an ordinary
|
|
695
|
+
// `npx mindforge-cc` install too, because os.tmpdir() has nothing to do with the source tree.
|
|
696
|
+
//
|
|
697
|
+
// Removed in a `finally` rather than after the last write, so a throw from any of the three
|
|
698
|
+
// safeCopyClaude calls cannot skip it — a leak on the failure path is how the original one
|
|
699
|
+
// survived review.
|
|
497
700
|
const tempEntry = path.join(os.tmpdir(), `${cfg.entryFile}-${Date.now()}.md`);
|
|
498
701
|
fsu.write(tempEntry, adaptedContent);
|
|
499
|
-
|
|
702
|
+
try {
|
|
703
|
+
|
|
500
704
|
const targetPath = path.join(baseDir, cfg.entryFile);
|
|
501
|
-
|
|
705
|
+
// GATED ON !selfInstall, which it was not. The guard existed and stopped one line short: the root
|
|
706
|
+
// mirror below has always been gated, this write never was. In MindForge's own repository
|
|
707
|
+
// `.claude/CLAUDE.md` is TRACKED and not gitignored (226 files under .claude/ are tracked), so a
|
|
708
|
+
// self-install overwrote a committed file. No backup was taken either, and for a reason worth
|
|
709
|
+
// naming: safeCopyClaude only backs up when the existing content does NOT contain "MindForge" —
|
|
710
|
+
// and the repo's own entry file does, so it took the silent-replace path every time.
|
|
711
|
+
if (!selfInstall) {
|
|
712
|
+
safeCopyClaude(tempEntry, targetPath, { force, verbose });
|
|
713
|
+
}
|
|
502
714
|
|
|
503
715
|
// ✨ STANDARD: Inject into project root and IDE-specific rules files
|
|
504
716
|
if (scope === 'local' && !selfInstall) {
|
|
@@ -515,9 +727,19 @@ async function install(runtime, scope, options = {}) {
|
|
|
515
727
|
} else {
|
|
516
728
|
Theme.printResolved(`${c.bold('CLAUDE.md')} (Mirrored to project root)`);
|
|
517
729
|
}
|
|
518
|
-
} else {
|
|
730
|
+
} else if (!selfInstall) {
|
|
731
|
+
// `!selfInstall` added alongside the write gate above. This branch printed the entry file name
|
|
732
|
+
// as a resolved artifact whenever the root mirror was skipped — including on a self-install,
|
|
733
|
+
// where the write no longer happens. Gating the write and leaving the print would have replaced
|
|
734
|
+
// one false claim with another, which is the failure this whole change is about.
|
|
519
735
|
Theme.printResolved(c.bold(cfg.entryFile));
|
|
520
736
|
}
|
|
737
|
+
|
|
738
|
+
} finally {
|
|
739
|
+
// Best-effort: a staging file we cannot remove is not worth failing an otherwise good install
|
|
740
|
+
// over, and the next run writes a differently-named one regardless.
|
|
741
|
+
try { fs.rmSync(tempEntry, { force: true }); } catch { /* nothing further to do */ }
|
|
742
|
+
}
|
|
521
743
|
}
|
|
522
744
|
|
|
523
745
|
// ── 2. Install commands ─────────────────────────────────────────────────────
|
|
@@ -526,29 +748,39 @@ async function install(runtime, scope, options = {}) {
|
|
|
526
748
|
{ src: src('.agent', 'forge'), namespace: 'forge' }
|
|
527
749
|
];
|
|
528
750
|
|
|
529
|
-
if (runtime === 'claude') {
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
751
|
+
// A `if (runtime === 'claude') { cmdSources.length = 0; ...push the identical two entries... }`
|
|
752
|
+
// block used to sit here. It was a NO-OP — it cleared the array and pushed back exactly what the
|
|
753
|
+
// initializer above already contains. Its comment described a historical change (reading from
|
|
754
|
+
// .agent/mindforge rather than the gitignored .claude/commands/mindforge) that the initializer
|
|
755
|
+
// already reflects. Removed, because a special case that does nothing implies claude is handled
|
|
756
|
+
// differently here when it is not.
|
|
757
|
+
|
|
758
|
+
// GATED ON !selfInstall for the same reason as the entry file above. Measured on a self-install into
|
|
759
|
+
// a clone of this repository: 149 TRACKED files under .claude/commands/ overwritten, zero backups,
|
|
760
|
+
// while the installer printed "Self-install detected — skipping framework file copy". It was not
|
|
761
|
+
// skipping; it said so and then copied.
|
|
762
|
+
//
|
|
763
|
+
// Skipping is correct rather than merely safe. In this repository `.agent/mindforge` and
|
|
764
|
+
// `.claude/commands/mindforge` are BOTH committed and legitimately differ (an audit measured 146
|
|
765
|
+
// entries differing between the two tracked copies, mostly frontmatter quoting). Copying does not
|
|
766
|
+
// "sync" them, it silently picks one side and destroys the other's committed state — inside a git
|
|
767
|
+
// working tree, with no prompt, under a message saying it did nothing.
|
|
768
|
+
//
|
|
769
|
+
// verifyInstall still passes afterwards: every file it requires under cmdsDir (help.md,
|
|
770
|
+
// init-project.md, health.md, execute-phase.md, security-scan.md) is tracked, so the files it checks
|
|
771
|
+
// are present from git rather than from this copy. Verified, not assumed.
|
|
539
772
|
let totalCount = 0;
|
|
540
|
-
cmdSources.forEach(source => {
|
|
773
|
+
(selfInstall ? [] : cmdSources).forEach(source => {
|
|
541
774
|
if (!fsu.exists(source.src)) return;
|
|
542
775
|
|
|
543
776
|
const files = fsu.listFiles(source.src).filter(f => f.endsWith('.md'));
|
|
544
777
|
totalCount += files.length;
|
|
545
|
-
fsu.ensureDir(cmdsDir);
|
|
546
778
|
|
|
547
779
|
files.forEach(f => {
|
|
548
|
-
|
|
549
|
-
|
|
780
|
+
const { dir: destDir, name: targetName } = resolveCommandTarget(cmdsDir, runtime, source.namespace, f);
|
|
781
|
+
fsu.ensureDir(destDir);
|
|
550
782
|
const srcPath = path.join(source.src, f);
|
|
551
|
-
const dstPath = path.join(
|
|
783
|
+
const dstPath = path.join(destDir, targetName);
|
|
552
784
|
|
|
553
785
|
if (runtime === 'antigravity') {
|
|
554
786
|
const content = fsu.read(srcPath);
|
|
@@ -607,6 +839,16 @@ async function install(runtime, scope, options = {}) {
|
|
|
607
839
|
Theme.printResolved(`${c.bold(asset.label.padEnd(12))} (Enterprise sync)`);
|
|
608
840
|
}
|
|
609
841
|
});
|
|
842
|
+
|
|
843
|
+
// ── 2.1b REG-01: register the hooks we just copied ────────────────────────
|
|
844
|
+
// Measured before this landed: 0 of 6 harnesses wrote any settings.json, and this file plus
|
|
845
|
+
// bin/install.js contained ZERO references to settings.json, PreToolUse or a hook dispatcher.
|
|
846
|
+
// 11 hook scripts landed and none of them could ever fire. register() is deliberately narrow —
|
|
847
|
+
// claude + local + non-Windows only — and returns a machine-readable status for every other
|
|
848
|
+
// case rather than writing a config it cannot verify. It EXECUTES all 8 emitted commands before
|
|
849
|
+
// keeping the file, and rolls back if any deny-class hook fails to deny.
|
|
850
|
+
hookRegistration = require('./installer/hook-registration')
|
|
851
|
+
.register({ projectRoot: process.cwd(), repoRoot: SOURCE_ROOT, runtime, scope, selfInstall, dryRun });
|
|
610
852
|
}
|
|
611
853
|
|
|
612
854
|
// ── 2.2 Install Subagents (native Claude-Code agents, both scopes) ──────────
|
|
@@ -630,14 +872,13 @@ async function install(runtime, scope, options = {}) {
|
|
|
630
872
|
const forgeSrc = src('.mindforge');
|
|
631
873
|
const forgeDst = path.join(process.cwd(), '.mindforge');
|
|
632
874
|
if (fsu.exists(forgeSrc)) {
|
|
633
|
-
//
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
875
|
+
// An 18-entry `standardFrameworkFolders` list was declared here and never referenced. The
|
|
876
|
+
// non-minimal branch below copies the whole of .mindforge/ with copyDir, so the list
|
|
877
|
+
// described a selection that no code performed — deleting it changes nothing. It was the
|
|
878
|
+
// second dead copy-list in this file; the other was `coreEngines`, which named four
|
|
879
|
+
// bin/sre/ paths nothing installed. tests/install-module-load.test.js now fails on any
|
|
880
|
+
// array declared and never used here, because a copy list nothing reads is
|
|
881
|
+
// indistinguishable from files that are not installed.
|
|
641
882
|
if (minimal) {
|
|
642
883
|
const minimalEntries = new Set([
|
|
643
884
|
'MINDFORGE-SCHEMA.json',
|
|
@@ -724,18 +965,38 @@ async function install(runtime, scope, options = {}) {
|
|
|
724
965
|
Theme.printResolved(`${c.bold('WALKTHROUGH.md')} (updated)`);
|
|
725
966
|
}
|
|
726
967
|
|
|
727
|
-
//
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
968
|
+
// Engine subtrees copied into the consumer project.
|
|
969
|
+
//
|
|
970
|
+
// The first eleven entries are the feature engines. The last four are their DEPENDENCIES,
|
|
971
|
+
// and their absence was breaking the install: measured on a clean `--claude --local`, 16 of
|
|
972
|
+
// 119 installed modules failed to load with MODULE_NOT_FOUND, across seven subtrees —
|
|
973
|
+
// bin/autonomous/audit-writer.js -> ../utils/file-lock (the audit-chain writer)
|
|
974
|
+
// bin/autonomous/auto-runner.js -> ../utils/file-lock
|
|
975
|
+
// bin/autonomous/state-manager.js -> ../utils/file-io
|
|
976
|
+
// bin/governance/policy-engine.js -> ../utils/file-io
|
|
977
|
+
// bin/memory/knowledge-graph.js -> ../utils/file-lock
|
|
978
|
+
// bin/models/model-router.js -> ../utils/mindforge-params
|
|
979
|
+
// bin/engine/nexus-tracer.js -> ../utils/index
|
|
980
|
+
// ... and nine more
|
|
981
|
+
// bin/utils/ alone accounts for all sixteen. revops/, review/ and migrations/ are each
|
|
982
|
+
// required by an installed engine module too (engine/remediation-engine.js ->
|
|
983
|
+
// ../revops/remediation-queue, and so on).
|
|
984
|
+
//
|
|
985
|
+
// tests/install-module-load.test.js performs a real install and requires every installed
|
|
986
|
+
// module, so this list cannot silently drift again: adding an engine whose dependency is
|
|
987
|
+
// absent fails there rather than at a consumer's first run.
|
|
736
988
|
const sovereignEngines = [
|
|
737
|
-
'governance', 'autonomous', 'memory', 'models', 'research',
|
|
738
|
-
'wizard', 'updater', 'dashboard', 'browser', 'skills-builder', 'engine'
|
|
989
|
+
'governance', 'autonomous', 'memory', 'models', 'research',
|
|
990
|
+
'wizard', 'updater', 'dashboard', 'browser', 'skills-builder', 'engine',
|
|
991
|
+
// dependencies of the above — not features
|
|
992
|
+
'utils', 'revops', 'review', 'migrations',
|
|
993
|
+
// Required by bin/mindforge-cli.js:171 for the `workflow` verb. Added in the same change that
|
|
994
|
+
// started shipping the CLI: the router's require is lazy, so the CLI still LOADED without it,
|
|
995
|
+
// but `mindforge workflow list` exited 1 with MODULE_NOT_FOUND. Caught by
|
|
996
|
+
// tests/install-module-load.test.js's require-resolution scan the moment the CLI began landing
|
|
997
|
+
// — the entry point and its dispatch targets are one unit. One file, 4 KB, and its own requires
|
|
998
|
+
// are `fs` and `path` only.
|
|
999
|
+
'workflows'
|
|
739
1000
|
];
|
|
740
1001
|
sovereignEngines.forEach(engine => {
|
|
741
1002
|
const srcDir = src('bin', engine);
|
|
@@ -746,6 +1007,88 @@ async function install(runtime, scope, options = {}) {
|
|
|
746
1007
|
}
|
|
747
1008
|
});
|
|
748
1009
|
|
|
1010
|
+
// Individual top-level bin/ files an installed module requires. hindsight-injector is
|
|
1011
|
+
// required by both bin/dashboard/temporal-api.js and bin/engine/temporal-cli.js, which do
|
|
1012
|
+
// install, so without it those two fail to load.
|
|
1013
|
+
//
|
|
1014
|
+
// This replaces a `coreEngines` array that was declared here and NEVER REFERENCED — six
|
|
1015
|
+
// paths, four of them under bin/sre/, copied by nothing. bin/sre/ stays uninstalled
|
|
1016
|
+
// deliberately: no installed module requires it, so wiring the dead array would have shipped
|
|
1017
|
+
// files nothing loads rather than fixing anything.
|
|
1018
|
+
// bin/mindforge-cli.js is here, not behind --with-utils, because it is the ENTRY POINT the
|
|
1019
|
+
// documentation tells users to run. Measured on a default `--claude --local --skip-wizard`
|
|
1020
|
+
// install before this change: 152 bin/**/*.js landed across 15 subdirectories, but only ONE
|
|
1021
|
+
// top-level file, and `find . -name mindforge-cli.js` returned nothing. So
|
|
1022
|
+
// docs/getting-started.md's promise that "the `mindforge` CLI command is available for runtime
|
|
1023
|
+
// operations" was false on the documented path, and every fix to that CLI was invisible to
|
|
1024
|
+
// anyone who followed the docs.
|
|
1025
|
+
//
|
|
1026
|
+
// Shipping the single file is sufficient, verified rather than assumed: its only load-time
|
|
1027
|
+
// requires are `child_process` and `path`, both Node builtins, and the subdirectories it
|
|
1028
|
+
// dispatches into (bin/utils 11 files, bin/engine 23, bin/wizard 4) already land via
|
|
1029
|
+
// sovereignEngines above.
|
|
1030
|
+
//
|
|
1031
|
+
// The rest of bin/ stays behind --with-utils. This is the entry point, not a bulk copy.
|
|
1032
|
+
// EVERY top-level bin/*.js the router can dispatch to, not just the entry point.
|
|
1033
|
+
//
|
|
1034
|
+
// `sovereignEngines` copies bin/ SUBDIRECTORIES, so the 13 nested scripts in the COMMANDS table
|
|
1035
|
+
// arrive fine. The 6 scripts that live directly in bin/ had no carrier beyond this list, which
|
|
1036
|
+
// held two entries. Measured on the real npx shape — `npm pack`, extract into
|
|
1037
|
+
// node_modules/mindforge-cc, run its installer, then invoke each verb in the installed project —
|
|
1038
|
+
// 11 of 27 routed verbs died in Node's module loader:
|
|
1039
|
+
//
|
|
1040
|
+
// security-scan Cannot find module '<proj>/bin/validate-config.js'
|
|
1041
|
+
// health Cannot find module '<proj>/bin/installer-core.js'
|
|
1042
|
+
// classify Cannot find module '<proj>/bin/change-classifier.js'
|
|
1043
|
+
// validate-skill Cannot find module '<proj>/bin/skill-validator.js'
|
|
1044
|
+
// install-skill / register-skill / audit-skill bin/skill-registry.js
|
|
1045
|
+
// spawn / identity / subagent bin/spawn-agent.js
|
|
1046
|
+
// test-memory tests/memory.test.js
|
|
1047
|
+
//
|
|
1048
|
+
// Two of those matter more than their count. `security-scan` is the verb the protocol mandates
|
|
1049
|
+
// PRE-COMMIT for any Auth/Payment/PII change, and `health` is step 1 of "Verify install" in
|
|
1050
|
+
// docs/getting-started.md:110 — so the documented first thing a new user runs exited non-zero
|
|
1051
|
+
// with a stack trace.
|
|
1052
|
+
//
|
|
1053
|
+
// WHY THIS DID NOT SHOW UP EARLIER. tests/install-module-load.test.js checks that every internal
|
|
1054
|
+
// require inside an INSTALLED module resolves, and it passes: the copied files' own dependencies
|
|
1055
|
+
// are complete. The gap was one level up — files the ROUTER references that were never copied at
|
|
1056
|
+
// all, so there was no installed module whose requires could be checked. And installing from a
|
|
1057
|
+
// working tree hides it entirely, because `src()` then points at the full checkout; only the
|
|
1058
|
+
// packed-tarball shape reproduces it. That is why the new assertion in
|
|
1059
|
+
// tests/install-module-load.test.js drives the expectation off the COMMANDS table instead of a
|
|
1060
|
+
// hardcoded list — a route added without a carrier here will fail immediately.
|
|
1061
|
+
//
|
|
1062
|
+
// tests/memory.test.js is NOT fixable from here: `files[]` excludes tests/, so it is not in the
|
|
1063
|
+
// tarball and the installer has nothing to copy. That verb has to be removed from the router,
|
|
1064
|
+
// which bin/mindforge-cli.js owns.
|
|
1065
|
+
const coreFiles = [
|
|
1066
|
+
'bin/hindsight-injector.js',
|
|
1067
|
+
'bin/mindforge-cli.js',
|
|
1068
|
+
'bin/validate-config.js',
|
|
1069
|
+
'bin/installer-core.js',
|
|
1070
|
+
'bin/change-classifier.js',
|
|
1071
|
+
'bin/skill-validator.js',
|
|
1072
|
+
'bin/skill-registry.js',
|
|
1073
|
+
'bin/spawn-agent.js',
|
|
1074
|
+
// installer-core.js requires ./installer/hook-registration at :825, so installing the former
|
|
1075
|
+
// without this one trades a missing verb for a module-loader crash inside it. Caught by
|
|
1076
|
+
// tests/install-module-load.test.js the moment installer-core.js was added here, which is
|
|
1077
|
+
// exactly what that test is for. Named individually rather than adding 'installer' to
|
|
1078
|
+
// sovereignEngines: that directory also holds harness-adapter-compliance.js and the
|
|
1079
|
+
// install-manifests/install-state pair, which are build- and CI-side and have no business in a
|
|
1080
|
+
// consumer project.
|
|
1081
|
+
'bin/installer/hook-registration.js',
|
|
1082
|
+
];
|
|
1083
|
+
coreFiles.forEach(rel => {
|
|
1084
|
+
const srcFile = src(...rel.split('/'));
|
|
1085
|
+
const dstFile = path.join(process.cwd(), rel);
|
|
1086
|
+
if (fsu.exists(srcFile)) {
|
|
1087
|
+
fsu.ensureDir(path.dirname(dstFile));
|
|
1088
|
+
fsu.copy(srcFile, dstFile);
|
|
1089
|
+
}
|
|
1090
|
+
});
|
|
1091
|
+
|
|
749
1092
|
// ✨ SOVEREIGN INITIALIZATION: report actual security posture honestly.
|
|
750
1093
|
// The PQAS minter is gated SOLELY behind experimental.pqc_demo (see
|
|
751
1094
|
// bin/governance/quantum-crypto.js: getProvider/_assertPqcDemoEnabled). When
|
|
@@ -778,7 +1121,47 @@ async function install(runtime, scope, options = {}) {
|
|
|
778
1121
|
}
|
|
779
1122
|
|
|
780
1123
|
// ── 4. Verify installation ──────────────────────────────────────────────────
|
|
781
|
-
|
|
1124
|
+
// This line used to print "Install verified" unconditionally, directly under this header,
|
|
1125
|
+
// while verifyInstall() sat unreferenced 400 lines above. The claim is now earned: the check
|
|
1126
|
+
// runs, names what is missing, and exits non-zero rather than reporting success.
|
|
1127
|
+
//
|
|
1128
|
+
// Not reachable on a dry run — that path returns earlier, so nothing is verified against a
|
|
1129
|
+
// tree nothing was written to.
|
|
1130
|
+
const verification = verifyInstall(baseDir, cmdsDir, runtime, scope);
|
|
1131
|
+
if (!verification.ok) {
|
|
1132
|
+
console.error(`\n ❌ Install verification failed — ${verification.missing.length} of ` +
|
|
1133
|
+
`${verification.checked} required file(s) missing:`);
|
|
1134
|
+
verification.missing.forEach(f => console.error(` ${f}`));
|
|
1135
|
+
console.error(`\n Retry: npx mindforge-cc@latest --${runtime} --${scope} --force`);
|
|
1136
|
+
process.exit(1);
|
|
1137
|
+
}
|
|
1138
|
+
Theme.printResolved(c.bold(`Install verified (${verification.checked} required files present)`));
|
|
1139
|
+
|
|
1140
|
+
// ── 4b. REG-01 hook-registration status ─────────────────────────────────────
|
|
1141
|
+
// Printed ALWAYS, in one machine-readable line, including when nothing was registered. A silent
|
|
1142
|
+
// skip is how "0 of 6 harnesses register a hook" went unnoticed for the product's whole life: the
|
|
1143
|
+
// installer exited 0 with a success banner and never mentioned that the gates it had just copied
|
|
1144
|
+
// were inert. Whatever the outcome, the operator is told which it was and why.
|
|
1145
|
+
if (hookRegistration.registered) {
|
|
1146
|
+
Theme.printResolved(c.bold(`Hooks registered: ${hookRegistration.reason}`));
|
|
1147
|
+
Theme.printStatus(c.yellow('Restart your harness — Claude Code snapshots hooks at session start, '
|
|
1148
|
+
+ 'so the gates are not live in an already-open session.'), 'warn');
|
|
1149
|
+
if (hookRegistration.backup) {
|
|
1150
|
+
Theme.printStatus(c.dim(`Previous settings backed up to ${hookRegistration.backup}`), 'info');
|
|
1151
|
+
}
|
|
1152
|
+
} else {
|
|
1153
|
+
Theme.printStatus(c.yellow(`Hooks NOT registered (${hookRegistration.status}): ${hookRegistration.reason}`), 'warn');
|
|
1154
|
+
Theme.printStatus(c.dim('The hook scripts are installed but nothing invokes them, so no tool call '
|
|
1155
|
+
+ 'is gated. This is stated rather than implied — see "Hooks are installed but nothing is '
|
|
1156
|
+
+ 'blocked" in docs/troubleshooting.md.'), 'info');
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// Non-fatal advisories. Printed for BOTH outcomes, because the case that produces one today —
|
|
1160
|
+
// an ancestor project with its own settings.json — used to CANCEL registration outright, and the
|
|
1161
|
+
// whole point of demoting it to a warning is that the operator hears it without losing the gates.
|
|
1162
|
+
for (const w of hookRegistration.warnings || []) {
|
|
1163
|
+
Theme.printStatus(c.yellow(w), 'warn');
|
|
1164
|
+
}
|
|
782
1165
|
}
|
|
783
1166
|
|
|
784
1167
|
// ── Uninstall ─────────────────────────────────────────────────────────────────
|
|
@@ -922,11 +1305,31 @@ async function run(args) {
|
|
|
922
1305
|
const options = { dryRun, force, verbose, withUtils, minimal };
|
|
923
1306
|
|
|
924
1307
|
// Get package.json for version
|
|
925
|
-
|
|
1308
|
+
// MINDFORGE'S VERSION, NOT THE HOST PROJECT'S — and it must not crash when there is no manifest.
|
|
1309
|
+
//
|
|
1310
|
+
// This read was `JSON.parse(fsu.read(path.join(SOURCE_ROOT, 'package.json')))`, and SOURCE_ROOT is
|
|
1311
|
+
// path.resolve(__dirname, '..'). In an install that lands at <project>/bin/, so it read the CONSUMER's
|
|
1312
|
+
// package.json. Measured on the published 11.9.3 tarball: in a project declaring version 0.4.2,
|
|
1313
|
+
// `mindforge health` printed the banner `RELEASE v0.4.2` and then, 26 lines later, `Current : v11.9.3`
|
|
1314
|
+
// — one command, two contradictory versions, exit 0. In a project with NO package.json the whole
|
|
1315
|
+
// command was `Unexpected end of JSON input`, exit 1, no report at all: fsu.read returns empty for a
|
|
1316
|
+
// missing file and JSON.parse throws.
|
|
1317
|
+
//
|
|
1318
|
+
// This is the same defect 11.9.3 fixed for `--version`, in the one verb whose advertised job is
|
|
1319
|
+
// "verify project health and installation integrity". bin/utils/mindforge-version.js already resolves
|
|
1320
|
+
// correctly — by package NAME, which is what distinguishes our manifest from a consumer's — and it
|
|
1321
|
+
// shipped in the same release. It simply was not used here.
|
|
1322
|
+
//
|
|
1323
|
+
// It THROWS rather than guessing, which is right for a version check and wrong for a banner, so the
|
|
1324
|
+
// banner degrades to 'unknown' instead of taking the whole health report down with it.
|
|
1325
|
+
let bannerVersion = 'unknown';
|
|
1326
|
+
try {
|
|
1327
|
+
bannerVersion = require('./utils/mindforge-version').resolveMindforgeVersion(process.cwd()).version;
|
|
1328
|
+
} catch { /* a banner must never be the reason health cannot run */ }
|
|
926
1329
|
|
|
927
1330
|
// Print header and brand manifest
|
|
928
1331
|
// Print header and brand manifest
|
|
929
|
-
Theme.printHeader(
|
|
1332
|
+
Theme.printHeader(bannerVersion);
|
|
930
1333
|
Theme.printBrandManifest();
|
|
931
1334
|
// Check for updates only
|
|
932
1335
|
if (isCheck) {
|
|
@@ -944,14 +1347,27 @@ async function run(args) {
|
|
|
944
1347
|
}
|
|
945
1348
|
|
|
946
1349
|
if (!isUninstall) {
|
|
947
|
-
|
|
948
|
-
|
|
1350
|
+
// collectManifestStats() counts the SOURCE tree, not what was written. For a normal install those
|
|
1351
|
+
// coincide, so the panel is accidentally accurate. For a self-install nothing is copied, and the
|
|
1352
|
+
// panel announced "ACTIONS 221 — Total autonomous commands deployed" and
|
|
1353
|
+
// "Skill Packs (123 verified)" for a run that deployed and verified nothing: a summary of the
|
|
1354
|
+
// repository presenting itself as an installation report. Gating it is the honest minimum. Making
|
|
1355
|
+
// the panel report MEASURED counts on every path is a larger change and is deliberately not
|
|
1356
|
+
// attempted here — it would need the install to return what it wrote.
|
|
1357
|
+
if (isSelfInstall()) {
|
|
1358
|
+
Theme.printResolved(c.bold('Self-install complete — no framework files were written'));
|
|
1359
|
+
Theme.printStatus(c.dim('This repository IS the framework: its committed .claude/ and .agent/ '
|
|
1360
|
+
+ 'trees are the source, so there was nothing to deploy.'), 'info');
|
|
1361
|
+
} else {
|
|
1362
|
+
const stats = collectManifestStats();
|
|
1363
|
+
Theme.printSuccessV2(runtime, scope, stats);
|
|
1364
|
+
}
|
|
949
1365
|
} else {
|
|
950
1366
|
Theme.printResolved(c.bold('MindForge uninstalled'));
|
|
951
1367
|
}
|
|
952
1368
|
}
|
|
953
1369
|
|
|
954
|
-
module.exports = { run, install, uninstall, RUNTIMES, generateEntryContent, SENSITIVE_EXCLUDE, MINDFORGE_DEV_EXCLUDE };
|
|
1370
|
+
module.exports = { run, install, uninstall, verifyInstall, RUNTIMES, generateEntryContent, SENSITIVE_EXCLUDE, MINDFORGE_DEV_EXCLUDE };
|
|
955
1371
|
|
|
956
1372
|
if (require.main === module) {
|
|
957
1373
|
const args = process.argv.slice(2);
|