mindforge-cc 11.4.0 → 11.5.1
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 +13 -0
- package/.agent/hooks/lib/hook-flags.js +78 -0
- package/.agent/hooks/lib/pretooluse-visible-output.js +46 -0
- package/.agent/hooks/mindforge-block-no-verify.js +552 -0
- package/.agent/hooks/mindforge-config-protection.js +144 -0
- package/.agent/hooks/run-with-flags.js +207 -0
- package/.agent/mindforge/checkpoint.md +76 -0
- package/.agent/mindforge/harness-audit.md +59 -0
- package/.agent/mindforge/instinct.md +46 -0
- package/.agent/mindforge/orch-add-feature.md +43 -0
- package/.agent/mindforge/orch-build-mvp.md +48 -0
- package/.agent/mindforge/orch-change-feature.md +45 -0
- package/.agent/mindforge/orch-fix-defect.md +43 -0
- package/.agent/mindforge/orch-refine-code.md +43 -0
- package/.claude/CLAUDE.md +13 -0
- package/.claude/commands/mindforge/checkpoint.md +76 -0
- package/.claude/commands/mindforge/execute-phase.md +47 -6
- package/.claude/commands/mindforge/harness-audit.md +59 -0
- package/.claude/commands/mindforge/instinct.md +46 -0
- package/.claude/commands/mindforge/orch-add-feature.md +43 -0
- package/.claude/commands/mindforge/orch-build-mvp.md +48 -0
- package/.claude/commands/mindforge/orch-change-feature.md +45 -0
- package/.claude/commands/mindforge/orch-fix-defect.md +43 -0
- package/.claude/commands/mindforge/orch-refine-code.md +43 -0
- package/.claude/commands/mindforge/plan-write.md +11 -0
- package/.claude/commands/mindforge/product-spec.md +76 -0
- package/.mindforge/config.json +2 -2
- package/.mindforge/engine/instincts/instinct-schema.md +17 -9
- package/.mindforge/imported-agents.jsonl +10 -0
- package/.mindforge/manifests/install-components.json +36 -0
- package/.mindforge/manifests/install-modules.json +193 -0
- package/.mindforge/manifests/install-profiles.json +57 -0
- package/.mindforge/memory/sync-manifest.json +1 -1
- package/.mindforge/personas/gan-evaluator.md +226 -0
- package/.mindforge/personas/gan-generator.md +151 -0
- package/.mindforge/personas/gan-planner.md +118 -0
- package/.mindforge/personas/harness-optimizer.md +55 -0
- package/.mindforge/personas/loop-operator.md +58 -0
- package/.mindforge/schemas/hooks.schema.json +199 -0
- package/.mindforge/schemas/install-modules.schema.json +44 -0
- package/.mindforge/schemas/install-state.schema.json +95 -0
- package/.mindforge/schemas/plugin.schema.json +75 -0
- package/.mindforge/schemas/provenance.schema.json +31 -0
- package/.mindforge/skills/agent-architecture-audit/SKILL.md +272 -0
- package/.mindforge/skills/continuous-learning/SKILL.md +16 -0
- package/.mindforge/skills/orch-pipeline/SKILL.md +284 -0
- package/.mindforge/skills/writing-plans/SKILL.md +76 -0
- package/CHANGELOG.md +120 -0
- package/MINDFORGE.md +3 -3
- package/README.md +0 -1
- package/RELEASENOTES.md +131 -0
- package/SECURITY.md +16 -0
- package/bin/autonomous/auto-runner.js +46 -5
- package/bin/autonomous/handoff-schema.js +114 -0
- package/bin/autonomous/session-guardian.sh +138 -0
- package/bin/autonomous/supervisor.js +98 -0
- package/bin/change-classifier.js +19 -5
- package/bin/dashboard/api-router.js +10 -1
- package/bin/governance/approve.js +65 -28
- package/bin/governance/config-manager.js +3 -1
- package/bin/governance/rbac-manager.js +14 -6
- package/bin/harness-audit.js +520 -0
- package/bin/hooks/instinct-capture-hook.js +16 -1
- package/bin/hooks/lib/detect-project.js +72 -0
- package/bin/installer/harness-adapter-compliance.js +321 -0
- package/bin/installer/install-manifests.js +200 -0
- package/bin/installer/install-state.js +243 -0
- package/bin/installer-core.js +1 -1
- package/bin/learning/instinct-cli.js +359 -0
- package/bin/learning/lib/ssrf-guard.js +252 -0
- package/bin/memory/eis-client.js +31 -10
- package/bin/memory/federated-sync.js +11 -2
- package/bin/memory/knowledge-capture.js +10 -1
- package/bin/memory/pillar-health-tracker.js +9 -1
- package/bin/models/llm-errors.js +79 -0
- package/bin/models/model-client.js +39 -4
- package/bin/models/ollama-provider.js +115 -0
- package/bin/models/openai-provider.js +40 -9
- package/bin/models/profiles-loader.js +147 -0
- package/bin/models/provider-registry.js +59 -0
- package/bin/review/ads-engine.js +2 -2
- package/bin/revops/market-evaluator.js +23 -2
- package/bin/revops/router-steering-v2.js +17 -2
- package/bin/security/trust-boundaries.js +20 -3
- package/bin/utils/readiness-gate.js +169 -0
- package/bin/worktree/engine.js +497 -0
- package/package.json +8 -2
- package/subagents/categories/04-quality-security/.claude-plugin/plugin.json +10 -0
- package/subagents/categories/04-quality-security/go-build-resolver.md +105 -0
- package/subagents/categories/04-quality-security/go-reviewer.md +87 -0
- package/subagents/categories/04-quality-security/python-reviewer.md +109 -0
- package/subagents/categories/04-quality-security/react-build-resolver.md +215 -0
- package/subagents/categories/04-quality-security/react-reviewer.md +167 -0
- package/subagents/categories/04-quality-security/rust-build-resolver.md +159 -0
- package/subagents/categories/04-quality-security/rust-reviewer.md +105 -0
- package/subagents/categories/04-quality-security/silent-failure-hunter.md +67 -0
- package/subagents/categories/04-quality-security/type-design-analyzer.md +58 -0
- package/subagents/categories/04-quality-security/typescript-reviewer.md +126 -0
|
@@ -1,13 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MindForge v2 — OpenAI Provider
|
|
2
|
+
* MindForge v2 — OpenAI-compatible Provider
|
|
3
|
+
*
|
|
4
|
+
* Wave 3 (3.6): parameterized for an arbitrary OpenAI-compatible base URL, so a
|
|
5
|
+
* single class backs OpenAI + Azure OpenAI + Together / Groq / OpenRouter / vLLM.
|
|
6
|
+
* baseUrl/apiKeyEnv are driven from an optional `base_url` field per model in
|
|
7
|
+
* revops.market_registry; default stays api.openai.com so existing behavior is
|
|
8
|
+
* unchanged. Errors are classified via the typed llm-errors taxonomy.
|
|
3
9
|
*/
|
|
4
10
|
'use strict';
|
|
5
11
|
|
|
6
12
|
const https = require('https');
|
|
13
|
+
const http = require('http');
|
|
14
|
+
const { URL } = require('url');
|
|
15
|
+
const { classifyError } = require('./llm-errors');
|
|
7
16
|
|
|
8
17
|
class OpenAIProvider {
|
|
9
|
-
|
|
18
|
+
/**
|
|
19
|
+
* @param {string} apiKey
|
|
20
|
+
* @param {object} [opts]
|
|
21
|
+
* @param {string} [opts.baseUrl] e.g. "https://api.groq.com/openai/v1". Defaults
|
|
22
|
+
* to OpenAI. Accepts with or without a trailing /v1.
|
|
23
|
+
*/
|
|
24
|
+
constructor(apiKey, opts = {}) {
|
|
10
25
|
this.apiKey = apiKey;
|
|
26
|
+
this.baseUrl = opts.baseUrl || process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
_endpoint(pathSuffix) {
|
|
30
|
+
const base = this.baseUrl.replace(/\/+$/, '');
|
|
31
|
+
// Allow base with or without /v1; chat path is appended after the base.
|
|
32
|
+
const full = /\/v\d+$/.test(base) ? `${base}${pathSuffix}` : `${base}/v1${pathSuffix}`;
|
|
33
|
+
return new URL(full);
|
|
11
34
|
}
|
|
12
35
|
|
|
13
36
|
async complete(params) {
|
|
@@ -23,10 +46,14 @@ class OpenAIProvider {
|
|
|
23
46
|
temperature,
|
|
24
47
|
});
|
|
25
48
|
|
|
49
|
+
const url = this._endpoint('/chat/completions');
|
|
50
|
+
const transport = url.protocol === 'http:' ? http : https;
|
|
51
|
+
|
|
26
52
|
return new Promise((resolve, reject) => {
|
|
27
|
-
const req =
|
|
28
|
-
hostname:
|
|
29
|
-
|
|
53
|
+
const req = transport.request({
|
|
54
|
+
hostname: url.hostname,
|
|
55
|
+
port: url.port || undefined,
|
|
56
|
+
path: url.pathname,
|
|
30
57
|
method: 'POST',
|
|
31
58
|
headers: {
|
|
32
59
|
'Content-Type': 'application/json',
|
|
@@ -41,7 +68,7 @@ class OpenAIProvider {
|
|
|
41
68
|
try {
|
|
42
69
|
const json = JSON.parse(body);
|
|
43
70
|
if (res.statusCode !== 200) {
|
|
44
|
-
return reject(
|
|
71
|
+
return reject(classifyError(json.error?.message || 'OpenAI API error', { provider: 'openai', status: res.statusCode }));
|
|
45
72
|
}
|
|
46
73
|
|
|
47
74
|
const inputTokens = json.usage.prompt_tokens;
|
|
@@ -88,10 +115,14 @@ class OpenAIProvider {
|
|
|
88
115
|
stream: true,
|
|
89
116
|
});
|
|
90
117
|
|
|
118
|
+
const url = this._endpoint('/chat/completions');
|
|
119
|
+
const transport = url.protocol === 'http:' ? http : https;
|
|
120
|
+
|
|
91
121
|
return new Promise((resolve, reject) => {
|
|
92
|
-
const req =
|
|
93
|
-
hostname:
|
|
94
|
-
|
|
122
|
+
const req = transport.request({
|
|
123
|
+
hostname: url.hostname,
|
|
124
|
+
port: url.port || undefined,
|
|
125
|
+
path: url.pathname,
|
|
95
126
|
method: 'POST',
|
|
96
127
|
headers: {
|
|
97
128
|
'Content-Type': 'application/json',
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MindForge — Layered agent-profile + orchestration-template resolver.
|
|
5
|
+
*
|
|
6
|
+
* Ports the two pure algorithms from ECC's ecc2/src/config/mod.rs as JSON (no
|
|
7
|
+
* TOML dep): reusable agent profiles with single-inheritance, and named
|
|
8
|
+
* multi-step orchestration templates with {{var}} interpolation. Lets MindForge
|
|
9
|
+
* declare reusable reviewer/security profiles and named swarm templates that
|
|
10
|
+
* feed model-router / model-broker / the swarm-execution skill.
|
|
11
|
+
*
|
|
12
|
+
* resolveAgentProfile(profiles, name)
|
|
13
|
+
* - single-inheritance via `inherits`, with cycle detection (throws on cycle)
|
|
14
|
+
* - tool/dir lists merge-unique; scalars override; append_system_prompt
|
|
15
|
+
* concatenates parent\n\nchild (ECC semantics).
|
|
16
|
+
* resolveOrchestrationTemplate(templates, profiles, name, vars)
|
|
17
|
+
* - {{var}} interpolation that FAILS LOUD listing every missing variable
|
|
18
|
+
* - validates each referenced profile resolves.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const PLACEHOLDER = /\{\{\s*([A-Za-z0-9_-]+)\s*\}\}/g;
|
|
22
|
+
|
|
23
|
+
function mergeUnique(base, additions) {
|
|
24
|
+
const out = base.slice();
|
|
25
|
+
for (const v of (additions || [])) {
|
|
26
|
+
if (!out.includes(v)) out.push(v);
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolve an agent profile by name against a { name: profileConfig } map.
|
|
33
|
+
* Each profileConfig may have: inherits, agent, model, allowedTools[],
|
|
34
|
+
* disallowedTools[], permissionMode, addDirs[], maxBudgetUsd, tokenBudget,
|
|
35
|
+
* appendSystemPrompt.
|
|
36
|
+
*/
|
|
37
|
+
function resolveAgentProfile(profiles, name, _chain) {
|
|
38
|
+
const chain = _chain || [];
|
|
39
|
+
if (chain.includes(name)) {
|
|
40
|
+
throw new Error(`agent profile inheritance cycle: ${[...chain, name].join(' -> ')}`);
|
|
41
|
+
}
|
|
42
|
+
const profile = profiles && profiles[name];
|
|
43
|
+
if (!profile) {
|
|
44
|
+
throw new Error(`Unknown agent profile: ${name}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
chain.push(name);
|
|
48
|
+
const resolved = profile.inherits
|
|
49
|
+
? resolveAgentProfile(profiles, profile.inherits, chain)
|
|
50
|
+
: { profileName: '', allowedTools: [], disallowedTools: [], addDirs: [], appendSystemPrompt: null };
|
|
51
|
+
chain.pop();
|
|
52
|
+
|
|
53
|
+
return applyProfile(resolved, name, profile);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function applyProfile(resolved, name, config) {
|
|
57
|
+
const next = Object.assign({}, resolved, { profileName: name });
|
|
58
|
+
if (config.agent != null) next.agent = config.agent;
|
|
59
|
+
if (config.model != null) next.model = config.model;
|
|
60
|
+
next.allowedTools = mergeUnique(resolved.allowedTools || [], config.allowedTools);
|
|
61
|
+
next.disallowedTools = mergeUnique(resolved.disallowedTools || [], config.disallowedTools);
|
|
62
|
+
if (config.permissionMode != null) next.permissionMode = config.permissionMode;
|
|
63
|
+
next.addDirs = mergeUnique(resolved.addDirs || [], config.addDirs);
|
|
64
|
+
if (config.maxBudgetUsd != null) next.maxBudgetUsd = config.maxBudgetUsd;
|
|
65
|
+
if (config.tokenBudget != null) next.tokenBudget = config.tokenBudget;
|
|
66
|
+
|
|
67
|
+
const parentPrompt = resolved.appendSystemPrompt || null;
|
|
68
|
+
const childPrompt = config.appendSystemPrompt || null;
|
|
69
|
+
next.appendSystemPrompt =
|
|
70
|
+
parentPrompt && childPrompt ? `${parentPrompt}\n\n${childPrompt}`
|
|
71
|
+
: parentPrompt || childPrompt || null;
|
|
72
|
+
|
|
73
|
+
return next;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Interpolate {{var}} placeholders. Collects ALL missing keys and throws a
|
|
78
|
+
* single error listing them (fail-loud), matching ECC's behavior.
|
|
79
|
+
*/
|
|
80
|
+
function interpolateRequired(value, vars) {
|
|
81
|
+
const missing = new Set();
|
|
82
|
+
const rendered = String(value).replace(PLACEHOLDER, (_m, key) => {
|
|
83
|
+
if (vars && Object.prototype.hasOwnProperty.call(vars, key)) return String(vars[key]);
|
|
84
|
+
missing.add(key);
|
|
85
|
+
return '';
|
|
86
|
+
});
|
|
87
|
+
if (missing.size > 0) {
|
|
88
|
+
throw new Error(`missing orchestration template variable(s): ${[...missing].sort().join(', ')}`);
|
|
89
|
+
}
|
|
90
|
+
return rendered;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function interpolateOptional(value, vars) {
|
|
94
|
+
if (value == null) return null;
|
|
95
|
+
const rendered = interpolateRequired(value, vars).trim();
|
|
96
|
+
return rendered === '' ? null : rendered;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Resolve a named orchestration template into concrete steps.
|
|
101
|
+
* templates: { name: { description?, project?, taskGroup?, agent?, profile?,
|
|
102
|
+
* worktree?, steps: [{ name?, task, agent?, profile?, worktree?, project?, taskGroup? }] } }
|
|
103
|
+
*/
|
|
104
|
+
function resolveOrchestrationTemplate(templates, profiles, name, vars = {}) {
|
|
105
|
+
const template = templates && templates[name];
|
|
106
|
+
if (!template) throw new Error(`Unknown orchestration template: ${name}`);
|
|
107
|
+
if (!Array.isArray(template.steps) || template.steps.length === 0) {
|
|
108
|
+
throw new Error(`orchestration template ${name} has no steps`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const description = interpolateOptional(template.description, vars);
|
|
112
|
+
const project = interpolateOptional(template.project, vars);
|
|
113
|
+
const taskGroup = interpolateOptional(template.taskGroup, vars);
|
|
114
|
+
const defaultAgent = interpolateOptional(template.agent, vars);
|
|
115
|
+
const defaultProfile = interpolateOptional(template.profile, vars);
|
|
116
|
+
if (defaultProfile) resolveAgentProfile(profiles, defaultProfile); // validate
|
|
117
|
+
|
|
118
|
+
const steps = template.steps.map((step, index) => {
|
|
119
|
+
let task;
|
|
120
|
+
try {
|
|
121
|
+
task = interpolateRequired(step.task, vars);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
throw new Error(`resolve task for orchestration template ${name} step ${index + 1}: ${err.message}`);
|
|
124
|
+
}
|
|
125
|
+
const stepName = interpolateOptional(step.name, vars) || `step ${index + 1}`;
|
|
126
|
+
const agent = interpolateOptional(step.agent != null ? step.agent : defaultAgent, vars);
|
|
127
|
+
const profile = interpolateOptional(step.profile != null ? step.profile : defaultProfile, vars);
|
|
128
|
+
if (profile) resolveAgentProfile(profiles, profile); // validate
|
|
129
|
+
|
|
130
|
+
const worktree = step.worktree != null ? step.worktree
|
|
131
|
+
: (template.worktree != null ? template.worktree : false);
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
name: stepName,
|
|
135
|
+
task,
|
|
136
|
+
agent,
|
|
137
|
+
profile,
|
|
138
|
+
worktree,
|
|
139
|
+
project: interpolateOptional(step.project != null ? step.project : project, vars),
|
|
140
|
+
taskGroup: interpolateOptional(step.taskGroup != null ? step.taskGroup : taskGroup, vars),
|
|
141
|
+
};
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return { templateName: name, description, project, taskGroup, steps };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = { resolveAgentProfile, resolveOrchestrationTemplate, interpolateRequired };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MindForge — Provider Registry / Resolver seam.
|
|
3
|
+
*
|
|
4
|
+
* Adapted from ECC (src/llm/providers/resolver.py) as a pure internal seam.
|
|
5
|
+
* model-client.js consults resolveProvider(modelId) BEFORE its prefix-matching
|
|
6
|
+
* fallback, so providers are pluggable (e.g. add a custom OpenAI-compatible
|
|
7
|
+
* endpoint, or force a sovereign local provider) without editing _getProvider.
|
|
8
|
+
*
|
|
9
|
+
* Sovereignty / cost-capping / offline-test override:
|
|
10
|
+
* MINDFORGE_LLM_PROVIDER=ollama forces every resolve to the named provider.
|
|
11
|
+
*
|
|
12
|
+
* Does NOT adopt ECC's .llm.env file convention — that would violate MindForge's
|
|
13
|
+
* single-source-of-truth (MINDFORGE.md carries *_MODEL, .mindforge/config.json
|
|
14
|
+
* carries cost_routing). Registration is in-process only.
|
|
15
|
+
*/
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
// name -> factory(modelId) => provider instance (or null if unavailable)
|
|
19
|
+
const _registry = new Map();
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Register a provider factory under a name. The factory receives the modelId and
|
|
23
|
+
* returns a provider instance or null (e.g. when its API key/base URL is unset).
|
|
24
|
+
*/
|
|
25
|
+
function registerProvider(name, factory) {
|
|
26
|
+
if (typeof name !== 'string' || !name) throw new Error('provider name must be a non-empty string');
|
|
27
|
+
if (typeof factory !== 'function') throw new Error('provider factory must be a function');
|
|
28
|
+
_registry.set(name.toLowerCase(), factory);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function hasProvider(name) {
|
|
32
|
+
return _registry.has(String(name || '').toLowerCase());
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Resolve a provider for a modelId. Honors the MINDFORGE_LLM_PROVIDER override
|
|
37
|
+
* first (sovereignty/offline), then any explicitly registered factory. Returns
|
|
38
|
+
* null if nothing matches — the caller's built-in prefix fallback then runs.
|
|
39
|
+
*/
|
|
40
|
+
function resolveProvider(modelId) {
|
|
41
|
+
const override = String(process.env.MINDFORGE_LLM_PROVIDER || '').trim().toLowerCase();
|
|
42
|
+
if (override && _registry.has(override)) {
|
|
43
|
+
return _registry.get(override)(modelId);
|
|
44
|
+
}
|
|
45
|
+
// No name-based match here by default; model-client owns the prefix routing.
|
|
46
|
+
// Registered factories are consulted by explicit name only (via the override
|
|
47
|
+
// or a caller that knows the provider name). This keeps the seam additive.
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function listProviders() {
|
|
52
|
+
return [..._registry.keys()];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function _reset() {
|
|
56
|
+
_registry.clear();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { registerProvider, resolveProvider, hasProvider, listProviders, _reset };
|
package/bin/review/ads-engine.js
CHANGED
|
@@ -8,7 +8,7 @@ const fs = require('fs');
|
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const ModelClient = require('../models/model-client');
|
|
10
10
|
const { calculateSoulScore, parseMetrics, synthesizeADSPlan } = require('./ads-synthesizer');
|
|
11
|
-
const
|
|
11
|
+
const crypto = require('crypto');
|
|
12
12
|
|
|
13
13
|
async function runADSSynthesis(params) {
|
|
14
14
|
const {
|
|
@@ -89,7 +89,7 @@ Finalize the PLAN.md. Include the [ADS_VERDICT]: [MERGED|BLUE|RED] (Score: X.XXX
|
|
|
89
89
|
process.stdout.write('done.\n');
|
|
90
90
|
|
|
91
91
|
// Finalize outputs
|
|
92
|
-
const adsUuid =
|
|
92
|
+
const adsUuid = crypto.randomUUID();
|
|
93
93
|
const adrDir = path.join(process.cwd(), '.planning', 'decisions');
|
|
94
94
|
if (!fs.existsSync(adrDir)) fs.mkdirSync(adrDir, { recursive: true });
|
|
95
95
|
|
|
@@ -48,10 +48,31 @@ class MarketEvaluator {
|
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
50
|
* Intelligence fallback for mission-critical tasks.
|
|
51
|
+
*
|
|
52
|
+
* Resolves a model that ACTUALLY exists in the market registry (with real
|
|
53
|
+
* cost/benchmark fields), so downstream cost accounting can never silently
|
|
54
|
+
* zero out. Order: configured premium_fallback_model -> highest-benchmark
|
|
55
|
+
* registry entry. FAILS CLOSED (throws) if the registry is empty rather than
|
|
56
|
+
* returning a phantom costless model — a missing cost must never let the
|
|
57
|
+
* AgRevOps hard-limit escalation become a no-op.
|
|
58
|
+
*
|
|
59
|
+
* (Previously hardcoded 'claude-3-5-sonnet', which is ABSENT from the registry
|
|
60
|
+
* — `gold` was undefined and this returned a model with no cost fields.)
|
|
51
61
|
*/
|
|
52
62
|
getPremiumProvider() {
|
|
53
|
-
const
|
|
54
|
-
|
|
63
|
+
const preferred = configManager.get('revops.premium_fallback_model', null);
|
|
64
|
+
if (preferred && this.marketRegistry[preferred]) {
|
|
65
|
+
return { model_id: preferred, ...this.marketRegistry[preferred] };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Fall back to the highest-benchmark registry entry.
|
|
69
|
+
const ranked = Object.entries(this.marketRegistry)
|
|
70
|
+
.sort((a, b) => (b[1].benchmark || 0) - (a[1].benchmark || 0));
|
|
71
|
+
if (ranked.length === 0) {
|
|
72
|
+
throw new Error('[market-evaluator] no models in revops.market_registry — cannot resolve a premium provider (failing closed)');
|
|
73
|
+
}
|
|
74
|
+
const [id, data] = ranked[0];
|
|
75
|
+
return { model_id: id, ...data };
|
|
55
76
|
}
|
|
56
77
|
|
|
57
78
|
/**
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
'use strict';
|
|
9
9
|
|
|
10
10
|
const marketEvaluator = require('./market-evaluator');
|
|
11
|
+
const configManager = require('../governance/config-manager');
|
|
11
12
|
|
|
12
13
|
class RouterSteering {
|
|
13
14
|
constructor() {
|
|
@@ -16,6 +17,16 @@ class RouterSteering {
|
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Steers a reasoning task to the optimal model.
|
|
20
|
+
*
|
|
21
|
+
* SHADOW MODE (cost_routing.shadow_mode, default true): the latch that gates
|
|
22
|
+
* whether arbitrage steering is AUTHORITATIVE. When shadow_mode is on, we
|
|
23
|
+
* still COMPUTE and record the recommendation (for observation/telemetry) but
|
|
24
|
+
* mark the selection `shadow:true` / `authoritative:false` so callers do NOT
|
|
25
|
+
* act on the model switch — the steerer observes without changing routing.
|
|
26
|
+
* Only when shadow_mode is explicitly false does it return an authoritative
|
|
27
|
+
* selection. Previously this config was never read and steering was always
|
|
28
|
+
* live regardless of the latch (Wave 6 governance-theater fix).
|
|
29
|
+
*
|
|
19
30
|
* @param {string} spanId - From Nexus Tracer
|
|
20
31
|
* @param {string} taskDescription - Natural language task context
|
|
21
32
|
* @param {Object} preferences - Manual overrides (optional)
|
|
@@ -23,6 +34,7 @@ class RouterSteering {
|
|
|
23
34
|
async steer(spanId, taskDescription, preferences = {}) {
|
|
24
35
|
const mir = preferences.mir || this._calculateMIR(taskDescription);
|
|
25
36
|
const recommendation = marketEvaluator.getBestProvider(mir);
|
|
37
|
+
const shadowMode = configManager.get('cost_routing.shadow_mode', true) === true;
|
|
26
38
|
|
|
27
39
|
const selection = {
|
|
28
40
|
span_id: spanId,
|
|
@@ -30,11 +42,14 @@ class RouterSteering {
|
|
|
30
42
|
selected_model: recommendation.model_id,
|
|
31
43
|
provider: recommendation.provider,
|
|
32
44
|
estimated_arbitrage_savings: marketEvaluator.calculateArbitrageSavings(recommendation.model_id),
|
|
45
|
+
shadow: shadowMode,
|
|
46
|
+
authoritative: !shadowMode,
|
|
33
47
|
timestamp: new Date().toISOString()
|
|
34
48
|
};
|
|
35
49
|
|
|
36
|
-
|
|
37
|
-
|
|
50
|
+
const mode = shadowMode ? 'SHADOW (observe-only)' : 'LIVE';
|
|
51
|
+
console.log(`[AgRevOps] ${mode}: Span ${spanId} -> ${selection.selected_model} (MIR: ${mir})`);
|
|
52
|
+
|
|
38
53
|
this.history.push(selection);
|
|
39
54
|
return selection;
|
|
40
55
|
}
|
|
@@ -79,8 +79,18 @@ const IFS_TOKEN = /\$\{IFS\}|\$IFS/g;
|
|
|
79
79
|
/**
|
|
80
80
|
* De-obfuscates shell metacharacter tricks WITHOUT emulating a real shell.
|
|
81
81
|
* Strips quotes (' ") and backslash escapes, collapses ${IFS}/$IFS to a space,
|
|
82
|
-
* then collapses runs of whitespace. This turns r''m,
|
|
83
|
-
* rm${IFS}-rf${IFS}/
|
|
82
|
+
* removes bare `#` tokens, then collapses runs of whitespace. This turns r''m,
|
|
83
|
+
* r"m, r\m, rm${IFS}-rf${IFS}/ and the quoted-hash evasion rm "#" -rf / back
|
|
84
|
+
* into plain `rm -rf /` so the existing patterns fire.
|
|
85
|
+
*
|
|
86
|
+
* The `#` step closes a real bypass (audit, Wave 6): quote-stripping turns the
|
|
87
|
+
* DESTRUCTIVE `rm "#" -rf /` (in bash, "#" is a literal arg, so rm -rf / runs)
|
|
88
|
+
* into `rm # -rf /`, where the bare `#` sat between `rm` and its flags and broke
|
|
89
|
+
* the regex. We drop standalone `#` tokens (a `#` delimited by whitespace/start/
|
|
90
|
+
* end) so the de-obfuscated form collapses to `rm -rf /` and matches. We do NOT
|
|
91
|
+
* strip `#`-to-end-of-line (that would swallow flags after a genuine comment and
|
|
92
|
+
* could MASK a destructive prefix); only the lone token is removed.
|
|
93
|
+
*
|
|
84
94
|
* Intentionally conservative: it removes characters rather than interpreting
|
|
85
95
|
* them, which can only make a string MORE likely to match (fail-toward-block).
|
|
86
96
|
*/
|
|
@@ -89,7 +99,9 @@ function normalizeShell(input) {
|
|
|
89
99
|
.split(NUL).join('') // shells ignore NUL; never let it split a token
|
|
90
100
|
.replace(IFS_TOKEN, ' ') // ${IFS}/$IFS -> space
|
|
91
101
|
.replace(/[\\'"]/g, '') // drop backslash escapes and quote chars
|
|
92
|
-
.replace(
|
|
102
|
+
.replace(/(^|\s)#(?=\s|$)/g, '$1') // drop bare `#` tokens (post-unquote evasion)
|
|
103
|
+
.replace(/\s+/g, ' ') // collapse whitespace runs
|
|
104
|
+
.trim();
|
|
93
105
|
}
|
|
94
106
|
|
|
95
107
|
/**
|
|
@@ -112,6 +124,11 @@ function isHighImpact(command) {
|
|
|
112
124
|
/git\s+reset\s+--hard/i,
|
|
113
125
|
/delete\s+from/i,
|
|
114
126
|
/truncate\s+table/i,
|
|
127
|
+
// Unix `truncate -s <size> <path>` zeroes/shrinks a file in place — a
|
|
128
|
+
// destructive data-loss op the SQL-only `truncate table` pattern above
|
|
129
|
+
// misses. Match the size flag (-s, -s0, --size) so `truncate -s 0 <path>`
|
|
130
|
+
// is gated; benign words ("truncated output") and the SQL form are not.
|
|
131
|
+
/\btruncate\s+(-{1,2}s\w*|--size)\b/i,
|
|
115
132
|
/\bmkfs(\.\w+)?\s+\/dev\//i,
|
|
116
133
|
// #11: any dd write target, not just /dev/ (dd if=... of=important.db).
|
|
117
134
|
// Original /dev/-only check is a subset of this, so it stays covered.
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* MindForge — Reusable deterministic readiness-gate engine.
|
|
6
|
+
*
|
|
7
|
+
* Adapted from ECC's observability-readiness.js engine SHAPE (buildReport /
|
|
8
|
+
* renderText / {pass, fix, points, top_actions} / exit 0|1). This is the shared
|
|
9
|
+
* helper that bin/harness-audit.js's pattern and a release-readiness check-set
|
|
10
|
+
* both reuse, instead of shipping two near-identical engines.
|
|
11
|
+
*
|
|
12
|
+
* A "check set" is an array of { id, label, points, pass:boolean, fix:string }.
|
|
13
|
+
* buildReport scores them; renderText prints; runGate exits 0 (pass) or 1 (fail)
|
|
14
|
+
* so CI can block on it.
|
|
15
|
+
*
|
|
16
|
+
* Also ships a MindForge RELEASE-READINESS check set: version parity across
|
|
17
|
+
* package.json / MINDFORGE.md / config.json, RELEASENOTES + CHANGELOG presence,
|
|
18
|
+
* and required .planning artifacts.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
|
|
24
|
+
const RUBRIC_VERSION = '2026-06-10';
|
|
25
|
+
|
|
26
|
+
function fileExists(root, rel) {
|
|
27
|
+
return fs.existsSync(path.join(root, rel));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function safeRead(root, rel) {
|
|
31
|
+
try { return fs.readFileSync(path.join(root, rel), 'utf8'); } catch { return ''; }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function safeJson(text) {
|
|
35
|
+
try { return JSON.parse(text); } catch { return null; }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Score a check set into the standard report contract.
|
|
40
|
+
*/
|
|
41
|
+
function buildReport(name, checks) {
|
|
42
|
+
const maxScore = checks.reduce((s, c) => s + c.points, 0);
|
|
43
|
+
const score = checks.filter(c => c.pass).reduce((s, c) => s + c.points, 0);
|
|
44
|
+
const failed = checks.filter(c => !c.pass);
|
|
45
|
+
const topActions = failed
|
|
46
|
+
.slice()
|
|
47
|
+
.sort((a, b) => b.points - a.points)
|
|
48
|
+
.slice(0, 3)
|
|
49
|
+
.map(c => ({ action: c.fix, id: c.id, points: c.points }));
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
gate: name,
|
|
53
|
+
deterministic: true,
|
|
54
|
+
rubric_version: RUBRIC_VERSION,
|
|
55
|
+
score,
|
|
56
|
+
max_score: maxScore,
|
|
57
|
+
pass: failed.length === 0,
|
|
58
|
+
checks: checks.map(c => ({ id: c.id, label: c.label, points: c.points, pass: c.pass })),
|
|
59
|
+
top_actions: topActions,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function renderText(report) {
|
|
64
|
+
const lines = [`${report.gate} readiness: ${report.score}/${report.max_score} ${report.pass ? '(PASS)' : '(FAIL)'}`, ''];
|
|
65
|
+
for (const c of report.checks) {
|
|
66
|
+
lines.push(` ${c.pass ? 'PASS' : 'FAIL'} [${c.points}] ${c.label}`);
|
|
67
|
+
}
|
|
68
|
+
if (report.top_actions.length) {
|
|
69
|
+
lines.push('', 'Top actions:');
|
|
70
|
+
report.top_actions.forEach((a, i) => lines.push(` ${i + 1}) ${a.action} (${a.id})`));
|
|
71
|
+
}
|
|
72
|
+
return lines.join('\n');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* MindForge release-readiness check set. CI can block a release on this.
|
|
77
|
+
*/
|
|
78
|
+
function releaseReadinessChecks(root) {
|
|
79
|
+
const pkg = safeJson(safeRead(root, 'package.json')) || {};
|
|
80
|
+
const mindforgeMd = safeRead(root, 'MINDFORGE.md');
|
|
81
|
+
const config = safeJson(safeRead(root, '.mindforge/config.json')) || {};
|
|
82
|
+
|
|
83
|
+
const pkgVersion = pkg.version || '';
|
|
84
|
+
const mdVersionMatch = mindforgeMd.match(/\[VERSION\]\s*=\s*([0-9]+\.[0-9]+\.[0-9]+)/);
|
|
85
|
+
const mdVersion = mdVersionMatch ? mdVersionMatch[1] : '';
|
|
86
|
+
const configVersion = config.version || '';
|
|
87
|
+
|
|
88
|
+
return [
|
|
89
|
+
{
|
|
90
|
+
id: 'version-parity-md',
|
|
91
|
+
label: 'package.json version matches MINDFORGE.md [VERSION]',
|
|
92
|
+
points: 3,
|
|
93
|
+
pass: Boolean(pkgVersion) && pkgVersion === mdVersion,
|
|
94
|
+
fix: `Align versions: package.json=${pkgVersion || '?'} vs MINDFORGE.md=${mdVersion || '?'}`,
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: 'version-parity-config',
|
|
98
|
+
label: 'package.json version matches .mindforge/config.json version',
|
|
99
|
+
points: 3,
|
|
100
|
+
pass: Boolean(pkgVersion) && pkgVersion === configVersion,
|
|
101
|
+
fix: `Align versions: package.json=${pkgVersion || '?'} vs config.json=${configVersion || '?'}`,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: 'releasenotes',
|
|
105
|
+
label: 'RELEASENOTES.md present',
|
|
106
|
+
points: 2,
|
|
107
|
+
pass: fileExists(root, 'RELEASENOTES.md'),
|
|
108
|
+
fix: 'Add RELEASENOTES.md for this release.',
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: 'changelog',
|
|
112
|
+
label: 'CHANGELOG.md present',
|
|
113
|
+
points: 2,
|
|
114
|
+
pass: fileExists(root, 'CHANGELOG.md'),
|
|
115
|
+
fix: 'Add/update CHANGELOG.md.',
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: 'security-policy',
|
|
119
|
+
label: 'SECURITY.md + agentic threat model present',
|
|
120
|
+
points: 2,
|
|
121
|
+
pass: fileExists(root, 'SECURITY.md') && fileExists(root, 'MINDFORGE-AGENTIC-SECURITY.md'),
|
|
122
|
+
fix: 'Ensure SECURITY.md and MINDFORGE-AGENTIC-SECURITY.md exist.',
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
id: 'test-entrypoint',
|
|
126
|
+
label: 'test runner present (tests/run-all.js or npm test)',
|
|
127
|
+
points: 2,
|
|
128
|
+
pass: fileExists(root, 'tests/run-all.js') || typeof pkg.scripts?.test === 'string',
|
|
129
|
+
fix: 'Add tests/run-all.js or a package.json test script.',
|
|
130
|
+
},
|
|
131
|
+
];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const GATES = {
|
|
135
|
+
release: releaseReadinessChecks,
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
function runGate(name, root) {
|
|
139
|
+
const builder = GATES[name];
|
|
140
|
+
if (!builder) throw new Error(`Unknown gate: ${name}. Available: ${Object.keys(GATES).join(', ')}`);
|
|
141
|
+
return buildReport(name, builder(root));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
module.exports = { buildReport, renderText, runGate, releaseReadinessChecks, fileExists, safeRead, safeJson, RUBRIC_VERSION, GATES };
|
|
145
|
+
|
|
146
|
+
// CLI: node bin/utils/readiness-gate.js [gate] [--format json] [--root dir]
|
|
147
|
+
if (require.main === module) {
|
|
148
|
+
const args = process.argv.slice(2);
|
|
149
|
+
let gate = 'release';
|
|
150
|
+
let format = 'text';
|
|
151
|
+
let root = process.cwd();
|
|
152
|
+
for (let i = 0; i < args.length; i++) {
|
|
153
|
+
const a = args[i];
|
|
154
|
+
if (a === '--format') { format = (args[++i] || 'text').toLowerCase(); continue; }
|
|
155
|
+
if (a.startsWith('--format=')) { format = a.split('=')[1].toLowerCase(); continue; }
|
|
156
|
+
if (a === '--root') { root = path.resolve(args[++i] || process.cwd()); continue; }
|
|
157
|
+
if (a.startsWith('--root=')) { root = path.resolve(a.split('=')[1]); continue; }
|
|
158
|
+
if (!a.startsWith('-')) { gate = a; continue; }
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
const report = runGate(gate, root);
|
|
162
|
+
if (format === 'json') process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
163
|
+
else process.stdout.write(renderText(report) + '\n');
|
|
164
|
+
process.exit(report.pass ? 0 : 1);
|
|
165
|
+
} catch (err) {
|
|
166
|
+
process.stderr.write(`Error: ${err.message}\n`);
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
}
|