mindforge-cc 11.9.1 → 11.9.3
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/consult.md +1 -1
- package/.agent/mindforge/cost-report.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/consult.md +1 -1
- package/.claude/commands/mindforge/cost-report.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 +126 -13
- package/.mindforge/config.json +4 -4
- package/.mindforge/engine/autonomous/headless-adapter.md +2 -2
- package/.mindforge/engine/cost-tracking/router.md +1 -1
- package/.mindforge/engine/cost-tracking/token-ledger.md +21 -24
- package/.mindforge/engine/temporal-protocol.md +2 -2
- package/.mindforge/governance/change-classifier.md +20 -4
- package/.mindforge/memory/sync-manifest.json +1 -1
- package/.mindforge/metrics/METRICS-SCHEMA.md +13 -4
- package/.mindforge/personas/cost-optimizer.md +2 -2
- package/.mindforge/personas/multi-model-bridge.md +1 -1
- package/.mindforge/skills/agent-architecture-audit/SKILL.md +2 -2
- package/.mindforge/skills/cost-aware-routing/SKILL.md +3 -3
- package/.mindforge/skills/multi-llm-consult/SKILL.md +2 -2
- package/.mindforge/skills/orch-pipeline/SKILL.md +4 -4
- package/CHANGELOG.md +402 -0
- package/MINDFORGE.md +13 -6
- package/README.md +51 -2
- package/RELEASENOTES.md +55 -2
- package/SECURITY.md +22 -3
- package/bin/autonomous/audit-writer.js +48 -33
- package/bin/autonomous/auto-runner.js +65 -2
- package/bin/change-classifier.js +151 -16
- package/bin/dashboard/api-router.js +28 -47
- package/bin/dashboard/error-response.js +44 -0
- package/bin/dashboard/frontend/app.js +429 -0
- package/bin/dashboard/frontend/index.html +14 -390
- package/bin/dashboard/metrics-aggregator.js +75 -30
- package/bin/dashboard/revops-api.js +12 -2
- package/bin/dashboard/server.js +245 -6
- package/bin/dashboard/sse-bridge.js +11 -8
- package/bin/dashboard/temporal-api.js +11 -5
- package/bin/engine/remediation-engine.js +12 -1
- package/bin/engine/sre-manager.js +1 -1
- package/bin/engine/temporal-cli.js +56 -6
- package/bin/engine/temporal-hub.js +41 -9
- package/bin/engine/verification-runner.js +134 -17
- package/bin/engine/verify-cli.js +25 -7
- package/bin/eval/eval-harness.js +212 -1
- package/bin/eval/golden-set-retrieval.json +9 -0
- package/bin/governance/approval-record.js +147 -0
- package/bin/governance/approve.js +12 -7
- package/bin/governance/policy-engine.js +41 -3
- package/bin/governance/policy-gate-hardened.js +36 -1
- package/bin/governance/verify-approvals.js +163 -0
- package/bin/harness-audit.js +224 -10
- package/bin/hindsight-injector.js +8 -2
- package/bin/hooks/instinct-capture-hook.js +19 -5
- package/bin/install.js +63 -3
- package/bin/installer/harness-adapter-compliance.js +339 -28
- package/bin/installer/hook-registration.js +504 -0
- package/bin/installer-core.js +451 -63
- package/bin/learning/instinct-cli.js +14 -24
- package/bin/memory/knowledge-capture.js +23 -3
- package/bin/memory/knowledge-graph.js +70 -31
- package/bin/memory/vector-hub.js +500 -44
- 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 +110 -17
- package/bin/models/cost-tracker.js +126 -29
- package/bin/models/model-client.js +6 -1
- package/bin/models/model-router.js +28 -7
- package/bin/models/usage-record.js +71 -0
- 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/file-lock.js +106 -0
- package/bin/utils/mindforge-params.js +124 -0
- package/bin/utils/mindforge-version.js +99 -0
- package/bin/utils/redact-secrets.js +106 -0
- package/bin/validate-config.js +75 -17
- 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.2.md +209 -0
- package/changelogs/v11.9.3.md +195 -0
- package/docs/References/config-reference.md +76 -14
- 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 +3 -3
- package/docs/sdk-reference.md +15 -7
- package/docs/troubleshooting.md +10 -6
- package/docs/user-guide.md +14 -14
- package/examples/sdk-integration/README.md +1 -1
- package/package.json +10 -4
- package/subagents/.claude-plugin/marketplace.json +1 -1
- package/bin/dashboard/approval-handler.js +0 -136
|
@@ -3,18 +3,19 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Changes:
|
|
5
5
|
* 1. HANDOFF.json: add `plugin_api_version` field
|
|
6
|
-
* 2. AUDIT.jsonl:
|
|
6
|
+
* 2. AUDIT.jsonl: append a migration record — existing entries are NEVER rewritten (see step 2)
|
|
7
7
|
* 3. MINDFORGE.md: convert VERIFY_PASS_RATE_WARNING_THRESHOLD if in old 0-100 format
|
|
8
8
|
* 4. STATE.md: add v1.0.0 compatibility note if it doesn't already have one
|
|
9
9
|
*/
|
|
10
10
|
'use strict';
|
|
11
11
|
|
|
12
12
|
const fs = require('fs');
|
|
13
|
+
const { appendAuditEntrySync } = require('../autonomous/audit-writer');
|
|
13
14
|
|
|
14
15
|
module.exports = {
|
|
15
16
|
fromVersion: '0.6.0',
|
|
16
17
|
toVersion: '1.0.0',
|
|
17
|
-
description: 'Add plugin_api_version;
|
|
18
|
+
description: 'Add plugin_api_version; record the migration in the audit log; normalise MINDFORGE.md thresholds',
|
|
18
19
|
|
|
19
20
|
async run(paths) {
|
|
20
21
|
// ── 1. HANDOFF.json ───────────────────────────────────────────────────────
|
|
@@ -36,31 +37,35 @@ module.exports = {
|
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
// ── 2. AUDIT.jsonl ────────────────────────────────────────────────────────
|
|
40
|
+
//
|
|
41
|
+
// APPEND-ONLY. This step used to rewrite every entry to backfill `session_id`, which BROKE the
|
|
42
|
+
// hash chain and then reported success. Measured on a 50-entry chain written by the real writer:
|
|
43
|
+
//
|
|
44
|
+
// before -> audit chain valid: 50 entries exit 0
|
|
45
|
+
// after -> audit chain BROKEN at entry 0: hash mismatch (entry mutated) exit 1
|
|
46
|
+
//
|
|
47
|
+
// 50 of 50 entries mutated, integrity destroyed at the very first entry, and the migration printed
|
|
48
|
+
// "backfilled session_id in 50 of 50 entries" and carried on to report "All migrations complete".
|
|
49
|
+
// bin/governance/audit-hash.js hashes {...entry, previous_hash} with JSON.stringify, so ANY added
|
|
50
|
+
// key changes the material — a back-linked log cannot be edited in place, only appended to. No file
|
|
51
|
+
// in this directory referenced the canonical hasher.
|
|
52
|
+
//
|
|
53
|
+
// AND THE BACKFILL BOUGHT NOTHING. The only consumer of `session_id` on an audit entry is
|
|
54
|
+
// bin/dashboard/metrics-aggregator.js:253,286, which reads
|
|
55
|
+
// `entry.authored_by || entry.session_id || 'unknown'`. So the rewrite swapped the placeholder
|
|
56
|
+
// 'unknown' for the placeholder 'migrated-from-pre-1.0' — no consumer distinguishes them — at the
|
|
57
|
+
// cost of every integrity guarantee in the file. Deleting it loses nothing.
|
|
58
|
+
//
|
|
59
|
+
// What IS worth recording is that a migration touched this project, so one entry is APPENDED
|
|
60
|
+
// through the canonical writer. An append extends the chain instead of invalidating it.
|
|
39
61
|
if (fs.existsSync(paths.audit)) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
try {
|
|
46
|
-
const entry = JSON.parse(line);
|
|
47
|
-
if (!entry.session_id) {
|
|
48
|
-
entry.session_id = 'migrated-from-pre-1.0';
|
|
49
|
-
modified++;
|
|
50
|
-
return JSON.stringify(entry);
|
|
51
|
-
}
|
|
52
|
-
return line;
|
|
53
|
-
} catch {
|
|
54
|
-
return line; // Preserve unparseable lines exactly as-is (quarantine pattern)
|
|
55
|
-
}
|
|
62
|
+
appendAuditEntrySync(paths.audit, {
|
|
63
|
+
event: 'schema_migrated',
|
|
64
|
+
target_id: 'AUDIT.jsonl',
|
|
65
|
+
description: 'schema 0.6.0 -> 1.0.0; existing entries left byte-identical (append-only log)',
|
|
66
|
+
agent: 'migrate',
|
|
56
67
|
});
|
|
57
|
-
|
|
58
|
-
if (modified > 0) {
|
|
59
|
-
fs.writeFileSync(paths.audit, updated.join('\n') + '\n');
|
|
60
|
-
console.log(` • AUDIT.jsonl: backfilled session_id in ${modified} of ${lines.length} entries`);
|
|
61
|
-
} else {
|
|
62
|
-
console.log(' • AUDIT.jsonl: all entries already have session_id');
|
|
63
|
-
}
|
|
68
|
+
console.log(' • AUDIT.jsonl: recorded the migration as a new entry; existing entries untouched');
|
|
64
69
|
}
|
|
65
70
|
|
|
66
71
|
// ── 3. MINDFORGE.md ───────────────────────────────────────────────────────
|
|
@@ -11,11 +11,12 @@
|
|
|
11
11
|
|
|
12
12
|
const fs = require('fs');
|
|
13
13
|
const path = require('path');
|
|
14
|
+
const { appendAuditEntrySync } = require('../autonomous/audit-writer');
|
|
14
15
|
|
|
15
16
|
module.exports = {
|
|
16
17
|
fromVersion: '1.0.0',
|
|
17
18
|
toVersion: '2.0.0',
|
|
18
|
-
description: 'Additive schema upgrade:
|
|
19
|
+
description: 'Additive schema upgrade: record the migration in the audit log; model_group in tokens',
|
|
19
20
|
|
|
20
21
|
async run(paths) {
|
|
21
22
|
const backupDir = path.join(path.dirname(paths.handoff), '.backups', `v1-to-v2-${Date.now()}`);
|
|
@@ -65,29 +66,27 @@ module.exports = {
|
|
|
65
66
|
});
|
|
66
67
|
|
|
67
68
|
// ── 2. AUDIT.jsonl ────────────────────────────────────────────────────────
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
69
|
+
//
|
|
70
|
+
// APPEND-ONLY, for the same reason as 0.6.0-to-1.0.0.js step 2: this rewrote every entry to add
|
|
71
|
+
// `runtime` and `agent_id`, and bin/governance/audit-hash.js hashes {...entry, previous_hash}, so
|
|
72
|
+
// any added key changes the hash material and the chain breaks at the first entry while the
|
|
73
|
+
// migration reports success.
|
|
74
|
+
//
|
|
75
|
+
// Worse than its sibling, because here the backfill had NO consumer at all: `git grep agent_id`
|
|
76
|
+
// and `git grep model_group` outside bin/migrations/ return zero readers in bin/. The chain was
|
|
77
|
+
// being destroyed to populate fields nothing reads.
|
|
78
|
+
//
|
|
79
|
+
// Note safeMigrate() cannot express this — it takes raw content and returns replacement content,
|
|
80
|
+
// which is a rewrite by construction. An append goes through the canonical writer instead.
|
|
81
|
+
if (fs.existsSync(paths.audit)) {
|
|
82
|
+
appendAuditEntrySync(paths.audit, {
|
|
83
|
+
event: 'schema_migrated',
|
|
84
|
+
target_id: 'AUDIT.jsonl',
|
|
85
|
+
description: 'schema 1.0.0 -> 2.0.0; existing entries left byte-identical (append-only log)',
|
|
86
|
+
agent: 'migrate',
|
|
87
87
|
});
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
});
|
|
88
|
+
console.log(' • AUDIT.jsonl: recorded the migration as a new entry; existing entries untouched');
|
|
89
|
+
}
|
|
91
90
|
|
|
92
91
|
// ── 3. token-usage.jsonl ──────────────────────────────────────────────────
|
|
93
92
|
const tokensFile = path.join(path.dirname(paths.handoff), 'token-usage.jsonl');
|
package/bin/mindforge-cli.js
CHANGED
|
@@ -25,8 +25,10 @@ const ROOT = path.resolve(__dirname, '..');
|
|
|
25
25
|
const COMMANDS = {
|
|
26
26
|
'security-scan': {
|
|
27
27
|
script: 'bin/validate-config.js',
|
|
28
|
-
description: 'Validate configuration and run security checks'
|
|
29
|
-
defaultArgs:
|
|
28
|
+
description: 'Validate configuration and run security checks'
|
|
29
|
+
// No defaultArgs: validate-config.js already defaults to MINDFORGE.md
|
|
30
|
+
// (bin/validate-config.js:13). Declaring it here would prepend a positional
|
|
31
|
+
// that shadows a user-supplied config path.
|
|
30
32
|
},
|
|
31
33
|
'health': {
|
|
32
34
|
script: 'bin/installer-core.js',
|
|
@@ -57,20 +59,31 @@ const COMMANDS = {
|
|
|
57
59
|
script: 'bin/skill-validator.js',
|
|
58
60
|
description: 'Run Level 1 & 2 validation on a SKILL.md file'
|
|
59
61
|
},
|
|
62
|
+
// NOTE: install-skill / register-skill / audit-skill deliberately carry NO
|
|
63
|
+
// defaultArgs. Supplying their subcommand token here reaches skill-registry's
|
|
64
|
+
// write paths, which perform no existence or validation checks:
|
|
65
|
+
// - audit-skill <name> <ver> <tier> appended a hash-chained
|
|
66
|
+
// {event:'skill_installed', validation_passed:true} entry for a skill that
|
|
67
|
+
// does not exist, and exited 0. For a product whose central claim is a
|
|
68
|
+
// tamper-evident audit chain, a CLI that mints authentic-looking false
|
|
69
|
+
// entries on request is worse than one that refuses.
|
|
70
|
+
// - register-skill <name> <ver> 1 wrote a malformed 5-column row above the
|
|
71
|
+
// table header of .mindforge/org/skills/MANIFEST.md, which ships in the
|
|
72
|
+
// tarball, and exited 0.
|
|
73
|
+
// Without defaultArgs these refuse with "Invalid or missing action" (exit 1),
|
|
74
|
+
// which is the pre-11.9.2 behaviour. Re-enable only once skill-registry gates
|
|
75
|
+
// on skill existence and fixes its table-separator match.
|
|
60
76
|
'install-skill': {
|
|
61
77
|
script: 'bin/skill-registry.js',
|
|
62
|
-
description: 'Install a skill to the correct tier folder (
|
|
63
|
-
defaultArgs: ['install']
|
|
78
|
+
description: 'Install a skill to the correct tier folder (pass the "install" action explicitly)'
|
|
64
79
|
},
|
|
65
80
|
'register-skill': {
|
|
66
81
|
script: 'bin/skill-registry.js',
|
|
67
|
-
description: 'Register a skill in MANIFEST.md'
|
|
68
|
-
defaultArgs: ['register']
|
|
82
|
+
description: 'Register a skill in MANIFEST.md (pass the "register" action explicitly)'
|
|
69
83
|
},
|
|
70
84
|
'audit-skill': {
|
|
71
85
|
script: 'bin/skill-registry.js',
|
|
72
|
-
description: 'Record skill life cycle events in audit log'
|
|
73
|
-
defaultArgs: ['audit']
|
|
86
|
+
description: 'Record skill life cycle events in audit log (pass the "audit" action explicitly)'
|
|
74
87
|
},
|
|
75
88
|
'remember': {
|
|
76
89
|
script: 'bin/memory/cli.js',
|
|
@@ -98,6 +111,15 @@ const COMMANDS = {
|
|
|
98
111
|
description: 'Invoke a specialized identity from /agents/',
|
|
99
112
|
defaultArgs: ['identity']
|
|
100
113
|
},
|
|
114
|
+
// spawn-agent.js reads MODE from ARGS[0] and supports spawn|identity|subagent.
|
|
115
|
+
// 'subagent' had no router entry, so it was reached as `mindforge spawn subagent
|
|
116
|
+
// <name>` — which prepending 'spawn' now shadows (MODE='spawn', TARGET='subagent').
|
|
117
|
+
// Promoting it to a first-class command keeps the only implemented mode reachable.
|
|
118
|
+
'subagent': {
|
|
119
|
+
script: 'bin/spawn-agent.js',
|
|
120
|
+
description: 'Invoke a subagent definition from /subagents/',
|
|
121
|
+
defaultArgs: ['subagent']
|
|
122
|
+
},
|
|
101
123
|
'temporal': {
|
|
102
124
|
script: 'bin/engine/temporal-cli.js',
|
|
103
125
|
description: 'Manage time-travel debugging and state history'
|
|
@@ -129,10 +151,14 @@ const COMMANDS = {
|
|
|
129
151
|
script: 'bin/engine/learning-manager.js',
|
|
130
152
|
description: 'Consult or initialize the project agentic learning memory'
|
|
131
153
|
},
|
|
154
|
+
// No defaultArgs: 'record' reaches a writer that resolves its target relative to
|
|
155
|
+
// the spawn cwd, which is still ROOT (deferred to v12). In a consumer install that
|
|
156
|
+
// writes inside node_modules/mindforge-cc/ and is destroyed by the next npm ci.
|
|
157
|
+
// Activating a writer while its path resolution is known-wrong is worse than
|
|
158
|
+
// leaving it inert; re-enable together with the cwd fix.
|
|
132
159
|
'record-learning': {
|
|
133
160
|
script: 'bin/engine/learning-manager.js',
|
|
134
|
-
description: 'Append a new Learning Entry to the Evolution Log'
|
|
135
|
-
defaultArgs: ['record']
|
|
161
|
+
description: 'Append a new Learning Entry to the Evolution Log (pass the "record" action explicitly)'
|
|
136
162
|
},
|
|
137
163
|
'verify': {
|
|
138
164
|
script: 'bin/engine/verify-cli.js',
|
|
@@ -141,6 +167,17 @@ const COMMANDS = {
|
|
|
141
167
|
};
|
|
142
168
|
|
|
143
169
|
// ── Workflow subcommand (non-script, handled inline) ─────────────────────────
|
|
170
|
+
//
|
|
171
|
+
// Routable but NOT a COMMANDS key, because it dispatches to a module rather than spawning a script.
|
|
172
|
+
// That made the CLI lie about itself: `workflow` is the most-documented verb in the project (126
|
|
173
|
+
// references across the docs) and it works, yet it appeared in neither `--help` nor the
|
|
174
|
+
// "Available commands" list, so a user who mistyped it was told it does not exist. Declared here, next
|
|
175
|
+
// to the handler that makes it real, and consumed by both self-report sites below — one source, so the
|
|
176
|
+
// two cannot drift.
|
|
177
|
+
const INLINE_COMMANDS = ['workflow'];
|
|
178
|
+
/** Every verb the router will actually dispatch: table-driven plus inline. */
|
|
179
|
+
const ROUTABLE = [...Object.keys(COMMANDS), ...INLINE_COMMANDS].sort();
|
|
180
|
+
|
|
144
181
|
if (COMMAND === 'workflow') {
|
|
145
182
|
const workflowRunner = require('./workflows/workflow-runner');
|
|
146
183
|
workflowRunner.run(COMMAND_ARGS[0], COMMAND_ARGS.slice(1));
|
|
@@ -148,8 +185,18 @@ if (COMMAND === 'workflow') {
|
|
|
148
185
|
}
|
|
149
186
|
|
|
150
187
|
if (ARGS.includes('--version') || ARGS.includes('-V')) {
|
|
151
|
-
|
|
152
|
-
|
|
188
|
+
// Resolve by package NAME. In an install this file lands at <project>/bin/mindforge-cli.js, so
|
|
189
|
+
// '../package.json' is the CONSUMER's manifest and `--version` confidently printed THEIR app's
|
|
190
|
+
// version as MindForge's — measured: 1.0.0 for a host app at 1.0.0, while MindForge was 11.9.2.
|
|
191
|
+
// Exit non-zero on an unresolvable version rather than guessing: the whole point of this command
|
|
192
|
+
// is to be trusted, and a plausible wrong answer is worse than an honest failure.
|
|
193
|
+
try {
|
|
194
|
+
console.log(require('./utils/mindforge-version').resolveMindforgeVersion().version);
|
|
195
|
+
process.exit(0);
|
|
196
|
+
} catch (err) {
|
|
197
|
+
console.error(err.message);
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
153
200
|
}
|
|
154
201
|
|
|
155
202
|
if (!COMMAND || ARGS.includes('--help') || ARGS.includes('-h')) {
|
|
@@ -161,8 +208,10 @@ const target = COMMANDS[COMMAND];
|
|
|
161
208
|
if (!target) {
|
|
162
209
|
console.error(`Unknown command: ${COMMAND}`);
|
|
163
210
|
|
|
164
|
-
// Suggest similar commands using Levenshtein distance
|
|
165
|
-
|
|
211
|
+
// Suggest similar commands using Levenshtein distance. ROUTABLE, not Object.keys(COMMANDS): a
|
|
212
|
+
// near-miss on `workflow` (`worklow`, `wokflow`) previously produced no suggestion at all, because
|
|
213
|
+
// the only verb it resembles was absent from the pool.
|
|
214
|
+
const suggestions = ROUTABLE
|
|
166
215
|
.map(cmd => ({ cmd, dist: levenshtein(COMMAND, cmd) }))
|
|
167
216
|
.filter(s => s.dist <= 3)
|
|
168
217
|
.sort((a, b) => a.dist - b.dist)
|
|
@@ -171,18 +220,58 @@ if (!target) {
|
|
|
171
220
|
if (suggestions.length > 0) {
|
|
172
221
|
console.error(`\nDid you mean: ${suggestions.map(s => s.cmd).join(', ')}?`);
|
|
173
222
|
} else {
|
|
174
|
-
console.error('Available commands: ' +
|
|
223
|
+
console.error('Available commands: ' + ROUTABLE.join(', '));
|
|
175
224
|
}
|
|
176
225
|
process.exit(1);
|
|
177
226
|
}
|
|
178
227
|
|
|
179
228
|
const scriptPath = path.join(ROOT, target.script);
|
|
180
|
-
|
|
229
|
+
|
|
230
|
+
// defaultArgs are PREPENDED, never replaced. They carry the subcommand token or
|
|
231
|
+
// mode flag the child script requires (e.g. 'inject' for hindsight, '--check' for
|
|
232
|
+
// health), so dropping them when the user supplies an argument silently changes
|
|
233
|
+
// which code path runs — `health --force` used to lose '--check' and fall through
|
|
234
|
+
// to installer-core's install() path.
|
|
235
|
+
const finalArgs = [...(target.defaultArgs || []), ...COMMAND_ARGS];
|
|
181
236
|
|
|
182
237
|
console.log(`🚀 Executing: ${COMMAND} (${target.description})`);
|
|
183
238
|
|
|
239
|
+
// The child runs in the USER'S project, not in MindForge's install directory.
|
|
240
|
+
//
|
|
241
|
+
// This was `cwd: ROOT`, which made every routed command operate on the framework's own tree
|
|
242
|
+
// instead of the caller's. The worst case was `security-scan`, the command the protocol mandates
|
|
243
|
+
// pre-commit for any Auth/Payment/PII change: measured against a fixture project whose
|
|
244
|
+
// MINDFORGE.md declared 4 settings including `[MIN_SOUL_SCORE] = 99` (schema maximum is 10) and
|
|
245
|
+
// `[COST_HARD_LIMIT_USD] = not-a-number`, it printed
|
|
246
|
+
//
|
|
247
|
+
// ✅ MINDFORGE.md valid — 43 settings configured
|
|
248
|
+
//
|
|
249
|
+
// and exited 0. 43 is MindForge's OWN setting count. Two different fixture configs produced
|
|
250
|
+
// byte-identical output, which is the proof it read neither: a security gate that cannot fail,
|
|
251
|
+
// because it never sees the input it claims to check.
|
|
252
|
+
//
|
|
253
|
+
// MEASURED BLAST RADIUS before changing it — all 27 routed commands run under both values against
|
|
254
|
+
// the same fixture. 7 differ, every one of them moving from the vendor's tree to the caller's, and
|
|
255
|
+
// NONE regressed from success to failure:
|
|
256
|
+
//
|
|
257
|
+
// security-scan validated MindForge's config -> reads the caller's
|
|
258
|
+
// classify TIER=2 from MindForge's git diff -> diffs the caller's repo
|
|
259
|
+
// pr-review, cross-review loaded MindForge's ConfigManager -> looks in the caller's project
|
|
260
|
+
// learning, record-learning reported MindForge's state -> reports the caller's
|
|
261
|
+
// test-memory same ConfigManager shift
|
|
262
|
+
//
|
|
263
|
+
// `classify` deserves its own note: for every consumer it was classifying MindForge's changes.
|
|
264
|
+
//
|
|
265
|
+
// CI is unaffected, checked rather than assumed. control-plane.yml:80 runs `security-scan` as the
|
|
266
|
+
// required ⚖️ Governance Enforcement check and mindforge-ci.yml:46 runs validate-config.js
|
|
267
|
+
// directly; both execute with the repo root as cwd, so ROOT and process.cwd() are the same path
|
|
268
|
+
// there and behaviour is byte-identical.
|
|
269
|
+
//
|
|
270
|
+
// A script that needs the FRAMEWORK's own assets must resolve them from __dirname, which is
|
|
271
|
+
// independent of cwd — see the SCHEMA_PATH note in bin/validate-config.js. Anchoring vendor assets
|
|
272
|
+
// to the process's working directory is what coupled these two unrelated things in the first place.
|
|
184
273
|
const result = spawnSync('node', [scriptPath, ...finalArgs], {
|
|
185
|
-
cwd:
|
|
274
|
+
cwd: process.cwd(),
|
|
186
275
|
stdio: 'inherit',
|
|
187
276
|
env: { ...process.env, MINDFORGE_CLI: 'true' }
|
|
188
277
|
});
|
|
@@ -220,8 +309,12 @@ function printUsage() {
|
|
|
220
309
|
for (const [name, cfg] of Object.entries(COMMANDS)) {
|
|
221
310
|
console.log(` ${name.padEnd(15)} ${cfg.description}`);
|
|
222
311
|
}
|
|
312
|
+
// Listed separately because it dispatches to a module rather than spawning a script, so it has no
|
|
313
|
+
// COMMANDS entry to carry a description. Omitting it made --help contradict the router.
|
|
314
|
+
console.log(` ${'workflow'.padEnd(15)} Run a registered dynamic workflow (see \`workflow list\`)`);
|
|
223
315
|
console.log('\nExamples:');
|
|
224
316
|
console.log(' node bin/mindforge-cli.js security-scan');
|
|
225
317
|
console.log(' node bin/mindforge-cli.js headless --phase 1');
|
|
318
|
+
console.log(' node bin/mindforge-cli.js workflow list');
|
|
226
319
|
console.log('\n');
|
|
227
320
|
}
|
|
@@ -4,32 +4,34 @@
|
|
|
4
4
|
'use strict';
|
|
5
5
|
|
|
6
6
|
const fs = require('fs');
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
const METRICS_DIR = path.join(process.cwd(), '.mindforge', 'metrics');
|
|
10
|
-
const USAGE_LOG = path.join(METRICS_DIR, 'token-usage.jsonl');
|
|
7
|
+
const { ledgerPath, ledgerDir, buildRecord, entryCost, entryDay } = require('./usage-record');
|
|
11
8
|
|
|
9
|
+
// Paths are resolved lazily (see usage-record.js) so the suite can exercise the
|
|
10
|
+
// ledger inside a temp cwd instead of appending to the developer's real ledger.
|
|
12
11
|
function ensureDir() {
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
const dir = ledgerDir();
|
|
13
|
+
if (!fs.existsSync(dir)) {
|
|
14
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
let _dailyCache = { value: 0, computed_at: 0 };
|
|
19
19
|
|
|
20
20
|
function getTodaySpend() {
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
const usageLog = ledgerPath();
|
|
22
|
+
if (!fs.existsSync(usageLog)) return 0;
|
|
23
|
+
|
|
23
24
|
const today = new Date().toISOString().slice(0, 10);
|
|
24
|
-
const content = fs.readFileSync(
|
|
25
|
+
const content = fs.readFileSync(usageLog, 'utf8');
|
|
25
26
|
const lines = content.trim().split('\n');
|
|
26
|
-
|
|
27
|
+
|
|
27
28
|
let total = 0;
|
|
28
29
|
for (const line of lines) {
|
|
30
|
+
if (!line) continue;
|
|
29
31
|
try {
|
|
30
32
|
const entry = JSON.parse(line);
|
|
31
|
-
if (entry
|
|
32
|
-
total += entry
|
|
33
|
+
if (entryDay(entry) === today) {
|
|
34
|
+
total += entryCost(entry);
|
|
33
35
|
}
|
|
34
36
|
} catch (e) {
|
|
35
37
|
process.stderr.write('[cost-tracker] Skipped malformed entry\n');
|
|
@@ -47,42 +49,137 @@ function getTodaySpendCached() {
|
|
|
47
49
|
return _dailyCache.value;
|
|
48
50
|
}
|
|
49
51
|
|
|
52
|
+
// COST-02 — the $25/day cap declared at MINDFORGE.md:54 was inert from the day it
|
|
53
|
+
// was written. v11.9.2 read `settings.MODEL_COST_HARD_LIMIT_USD`; the registry
|
|
54
|
+
// declares `[COST_HARD_LIMIT_USD]`. The MODEL_-prefixed name appears in exactly one
|
|
55
|
+
// shipped file (.mindforge/MINDFORGE-V2-SCHEMA.json:58) and that file has no code
|
|
56
|
+
// reader, so the lookup was always undefined -> parseFloat('0.0') -> 0 -> the
|
|
57
|
+
// `hardLimit <= 0` guard returned before any spend was compared. Canonical registry
|
|
58
|
+
// key first; the MODEL_-prefixed name is still read second so anyone who copied it
|
|
59
|
+
// out of the V2 schema keeps the working cap they have instead of silently losing it.
|
|
60
|
+
const HARD_LIMIT_KEYS = ['COST_HARD_LIMIT_USD', 'MODEL_COST_HARD_LIMIT_USD'];
|
|
61
|
+
const WARN_LIMIT_KEYS = ['COST_WARN_USD', 'MODEL_COST_WARN_USD'];
|
|
62
|
+
|
|
63
|
+
/** First non-empty key from `keys`, canonical-first. Returns null when none is set. */
|
|
64
|
+
function findThreshold(settings, keys) {
|
|
65
|
+
for (const key of keys) {
|
|
66
|
+
const raw = settings[key];
|
|
67
|
+
if (raw !== undefined && raw !== null && String(raw).trim() !== '') {
|
|
68
|
+
return { key, raw: String(raw) };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Classify a registry cost threshold. Returns a new object, never mutates input:
|
|
76
|
+
* { state: 'unset' } key absent/empty -> caller fails OPEN
|
|
77
|
+
* { state: 'disabled', key, value:0 } explicit 0 -> caller fails OPEN
|
|
78
|
+
* { state: 'armed', key, value } finite positive USD -> caller enforces
|
|
79
|
+
* { state: 'invalid', key, raw } unreadable or negative -> caller fails CLOSED
|
|
80
|
+
*
|
|
81
|
+
* parseFloat after stripping a leading `$` and thousands separators — not Number() —
|
|
82
|
+
* because `= $25.00` and `= 25.00 USD` are shapes a human types into MINDFORGE.md and
|
|
83
|
+
* both plainly mean 25; Number() would call them invalid and refuse every model call.
|
|
84
|
+
* Only a value with no leading number at all is invalid. Non-finite is invalid on
|
|
85
|
+
* purpose: `Infinity` compares false against every projection, i.e. it is not a cap.
|
|
86
|
+
*/
|
|
87
|
+
function classifyThreshold(found) {
|
|
88
|
+
if (!found) return { state: 'unset' };
|
|
89
|
+
const value = parseFloat(found.raw.trim());
|
|
90
|
+
if (!Number.isFinite(value) || value < 0) return { state: 'invalid', key: found.key, raw: found.raw };
|
|
91
|
+
if (value === 0) return { state: 'disabled', key: found.key, value: 0 };
|
|
92
|
+
return { state: 'armed', key: found.key, value };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// [COST_WARN_USD] had no reader anywhere in bin/ before COST-02. At most one line per
|
|
96
|
+
// UTC day per threshold, so an armed warning does not append to stderr on every call.
|
|
97
|
+
// Replaced as a whole object rather than mutated in place.
|
|
98
|
+
let _warnState = { day: '', threshold: 0 };
|
|
99
|
+
|
|
100
|
+
/** Soft threshold. Must never throw — a warning that blocks is a second hard cap. */
|
|
101
|
+
function warnIfCrossed(settings, projected, hardLimit) {
|
|
102
|
+
const warn = classifyThreshold(findThreshold(settings, WARN_LIMIT_KEYS));
|
|
103
|
+
if (warn.state !== 'armed') return;
|
|
104
|
+
// Suppress only when an ARMED hard cap sits at or below the warn value — there the
|
|
105
|
+
// throw preempts this line anyway. With no hard cap, nothing preempts it, and an
|
|
106
|
+
// upgraded install without [COST_HARD_LIMIT_USD] is the common case
|
|
107
|
+
// (installer-core.js:706 never rewrites an existing MINDFORGE.md) — precisely the
|
|
108
|
+
// install that most needs a spend warning.
|
|
109
|
+
if (hardLimit !== null && warn.value >= hardLimit) return;
|
|
110
|
+
if (projected < warn.value) return;
|
|
111
|
+
|
|
112
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
113
|
+
if (_warnState.day === today && _warnState.threshold === warn.value) return;
|
|
114
|
+
_warnState = { day: today, threshold: warn.value };
|
|
115
|
+
const cap = hardLimit === null ? 'no hard cap set' : `hard cap ${hardLimit}`;
|
|
116
|
+
process.stderr.write(
|
|
117
|
+
`[cost-tracker] Projected daily spend ${projected.toFixed(4)} crossed [${warn.key}] = ${warn.value} (${cap})\n`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
50
121
|
async function preflight(estimatedCost = 0) {
|
|
51
122
|
const settings = require('./model-router').getAllSettings();
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
123
|
+
const limit = classifyThreshold(findThreshold(settings, HARD_LIMIT_KEYS));
|
|
124
|
+
|
|
125
|
+
// Fail CLOSED on a limit that is present but unreadable: a cap nobody can parse is
|
|
126
|
+
// not a cap. Not a new surprise either — bin/validate-config.js already rejects this
|
|
127
|
+
// exact config with exit 1, because COST_HARD_LIMIT_USD is typed "number" at
|
|
128
|
+
// .mindforge/MINDFORGE-SCHEMA.json:87. The code is distinct from COST_LIMIT_REACHED
|
|
129
|
+
// so a caller can tell a spend stop from a config fault; bin/models/model-client.js
|
|
130
|
+
// re-throws both, which is the only reason this throw is not swallowed.
|
|
131
|
+
if (limit.state === 'invalid') {
|
|
132
|
+
throw Object.assign(
|
|
133
|
+
new Error(`[${limit.key}] = "${limit.raw}" is not a USD amount — the daily cost cap cannot be evaluated. Set a number in MINDFORGE.md (0 disables the cap).`),
|
|
134
|
+
{ code: 'COST_LIMIT_MISCONFIGURED', key: limit.key, raw: limit.raw }
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Fail OPEN when the key is absent or explicitly 0. DELIBERATE — do not invert it.
|
|
139
|
+
// docs/research/2026-08-v12-upgrade-report.md:85 recommends making an unset limit a
|
|
140
|
+
// config error, but bin/installer-core.js:706 writes MINDFORGE.md only when it does
|
|
141
|
+
// not already exist, so every install upgraded from a registry predating this key
|
|
142
|
+
// would start refusing every model call. The shipped schema agrees the key is
|
|
143
|
+
// optional: it sits in `recommended`, not `required` (.mindforge/MINDFORGE-SCHEMA.json
|
|
144
|
+
// :13-17), and bin/validate-config.js:48-49 only warns and exits 0 when it is absent.
|
|
145
|
+
if (limit.state !== 'armed') {
|
|
146
|
+
// The soft threshold must NOT depend on the hard cap. Guarded on the warn key so
|
|
147
|
+
// the no-cost-config fast path stays a pure early return and never reads the ledger.
|
|
148
|
+
if (classifyThreshold(findThreshold(settings, WARN_LIMIT_KEYS)).state === 'armed') {
|
|
149
|
+
warnIfCrossed(settings, getTodaySpendCached() + estimatedCost, null);
|
|
150
|
+
}
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
55
153
|
|
|
56
154
|
const todaySpend = getTodaySpendCached();
|
|
57
155
|
const projected = todaySpend + estimatedCost;
|
|
58
156
|
|
|
59
|
-
if (projected >=
|
|
157
|
+
if (projected >= limit.value) {
|
|
60
158
|
throw Object.assign(
|
|
61
|
-
new Error(`Daily cost limit $${
|
|
62
|
-
{ code: 'COST_LIMIT_REACHED', spend: todaySpend, limit:
|
|
159
|
+
new Error(`Daily cost limit $${limit.value} reached (Today: $${todaySpend.toFixed(4)})`),
|
|
160
|
+
{ code: 'COST_LIMIT_REACHED', spend: todaySpend, limit: limit.value }
|
|
63
161
|
);
|
|
64
162
|
}
|
|
163
|
+
|
|
164
|
+
warnIfCrossed(settings, projected, limit.value);
|
|
65
165
|
}
|
|
66
166
|
|
|
67
167
|
async function record(entry) {
|
|
68
168
|
ensureDir();
|
|
69
|
-
const enriched =
|
|
70
|
-
|
|
71
|
-
date: new Date().toISOString().slice(0, 10),
|
|
72
|
-
timestamp: new Date().toISOString()
|
|
73
|
-
};
|
|
74
|
-
fs.appendFileSync(USAGE_LOG, JSON.stringify(enriched) + '\n');
|
|
169
|
+
const enriched = buildRecord(entry);
|
|
170
|
+
fs.appendFileSync(ledgerPath(), JSON.stringify(enriched) + '\n');
|
|
75
171
|
_dailyCache.computed_at = 0; // Invalidate cache
|
|
76
172
|
}
|
|
77
173
|
|
|
78
174
|
function getSummary(params = { days: 7 }) {
|
|
79
|
-
|
|
80
|
-
|
|
175
|
+
const usageLog = ledgerPath();
|
|
176
|
+
if (!fs.existsSync(usageLog)) return { total_usd: 0, by_model: {} };
|
|
177
|
+
|
|
81
178
|
const cutoffDate = new Date();
|
|
82
179
|
cutoffDate.setDate(cutoffDate.getDate() - params.days);
|
|
83
180
|
const cutoffStr = cutoffDate.toISOString().slice(0, 10);
|
|
84
181
|
|
|
85
|
-
const content = fs.readFileSync(
|
|
182
|
+
const content = fs.readFileSync(usageLog, 'utf8');
|
|
86
183
|
const lines = content.trim().split('\n');
|
|
87
184
|
|
|
88
185
|
const result = {
|
|
@@ -95,8 +192,8 @@ function getSummary(params = { days: 7 }) {
|
|
|
95
192
|
for (const line of lines) {
|
|
96
193
|
try {
|
|
97
194
|
const entry = JSON.parse(line);
|
|
98
|
-
if (entry
|
|
99
|
-
const cost = entry
|
|
195
|
+
if (entryDay(entry) >= cutoffStr) {
|
|
196
|
+
const cost = entryCost(entry);
|
|
100
197
|
result.total_usd += cost;
|
|
101
198
|
result.calls++;
|
|
102
199
|
|
|
@@ -41,10 +41,15 @@ class ModelClient {
|
|
|
41
41
|
let modelId = routing.model;
|
|
42
42
|
|
|
43
43
|
// 2. Pre-flight cost check
|
|
44
|
+
// Both COST_ codes must propagate. This catch re-threw only COST_LIMIT_REACHED,
|
|
45
|
+
// so COST-02's fail-closed config fault would have been discarded here and the
|
|
46
|
+
// call would have proceeded uncapped — an interlock reporting success while doing
|
|
47
|
+
// nothing. Anything else still falls through on purpose: an unreadable ledger is
|
|
48
|
+
// a broken meter, and a broken meter must not block work.
|
|
44
49
|
try {
|
|
45
50
|
await CostTracker.preflight(0.05); // Conservative estimate
|
|
46
51
|
} catch (e) {
|
|
47
|
-
if (e.code === 'COST_LIMIT_REACHED') throw e;
|
|
52
|
+
if (e.code === 'COST_LIMIT_REACHED' || e.code === 'COST_LIMIT_MISCONFIGURED') throw e;
|
|
48
53
|
}
|
|
49
54
|
|
|
50
55
|
// 3. Execute with fallbacks
|
|
@@ -7,11 +7,15 @@
|
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const path = require('path');
|
|
9
9
|
|
|
10
|
+
// The one bracket-aware MINDFORGE.md reader (see bin/utils/mindforge-params.js).
|
|
11
|
+
const { readParams } = require('../utils/mindforge-params');
|
|
12
|
+
|
|
10
13
|
// v9: Model topology aligned to Claude 4.x family (2026-04)
|
|
11
14
|
const DEFAULTS = {
|
|
12
15
|
PLANNER_MODEL: 'claude-opus-4-7',
|
|
13
16
|
EXECUTOR_MODEL: 'claude-sonnet-4-6',
|
|
14
17
|
REVIEWER_MODEL: 'claude-sonnet-4-6',
|
|
18
|
+
VERIFIER_MODEL: 'claude-sonnet-4-6',
|
|
15
19
|
SECURITY_MODEL: 'claude-opus-4-7',
|
|
16
20
|
RESEARCH_MODEL: 'gemini-2.5-pro',
|
|
17
21
|
QA_MODEL: 'claude-sonnet-4-6',
|
|
@@ -40,14 +44,31 @@ let _settingsMtime = 0;
|
|
|
40
44
|
const CACHE_CHECK_INTERVAL_MS = 60000;
|
|
41
45
|
let _lastCacheCheck = 0;
|
|
42
46
|
|
|
47
|
+
// MINDFORGE.md declares the model topology with SHORT persona keys ([PLANNER]);
|
|
48
|
+
// the router's canonical setting keys are the *_MODEL names in DEFAULTS above,
|
|
49
|
+
// which is what every getAllSettings() consumer reads. Map short -> canonical
|
|
50
|
+
// and keep BOTH in the returned object so nothing reading *_MODEL breaks.
|
|
51
|
+
const KEY_ALIASES = {
|
|
52
|
+
PLANNER: 'PLANNER_MODEL',
|
|
53
|
+
EXECUTOR: 'EXECUTOR_MODEL',
|
|
54
|
+
REVIEWER: 'REVIEWER_MODEL',
|
|
55
|
+
VERIFIER: 'VERIFIER_MODEL',
|
|
56
|
+
SECURITY: 'SECURITY_MODEL',
|
|
57
|
+
RESEARCH: 'RESEARCH_MODEL',
|
|
58
|
+
QA: 'QA_MODEL',
|
|
59
|
+
DEBUG: 'DEBUG_MODEL',
|
|
60
|
+
QUICK: 'QUICK_MODEL',
|
|
61
|
+
};
|
|
62
|
+
|
|
43
63
|
function parseSettings(filePath) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
64
|
+
// v11.9.2 matched /^([A-Z0-9_]+)=(.*)$/ here, which matches ZERO lines of a
|
|
65
|
+
// bracketed MINDFORGE.md — routing silently always used DEFAULTS.
|
|
66
|
+
const raw = readParams(filePath);
|
|
67
|
+
const settings = { ...DEFAULTS, ...raw };
|
|
68
|
+
// An explicit [PLANNER_MODEL] always wins over the short [PLANNER] form.
|
|
69
|
+
for (const [shortKey, canonicalKey] of Object.entries(KEY_ALIASES)) {
|
|
70
|
+
if (raw[shortKey] !== undefined && raw[canonicalKey] === undefined) {
|
|
71
|
+
settings[canonicalKey] = raw[shortKey];
|
|
51
72
|
}
|
|
52
73
|
}
|
|
53
74
|
return settings;
|