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.
Files changed (98) hide show
  1. package/.agent/CLAUDE.md +13 -0
  2. package/.agent/hooks/lib/hook-flags.js +78 -0
  3. package/.agent/hooks/lib/pretooluse-visible-output.js +46 -0
  4. package/.agent/hooks/mindforge-block-no-verify.js +552 -0
  5. package/.agent/hooks/mindforge-config-protection.js +144 -0
  6. package/.agent/hooks/run-with-flags.js +207 -0
  7. package/.agent/mindforge/checkpoint.md +76 -0
  8. package/.agent/mindforge/harness-audit.md +59 -0
  9. package/.agent/mindforge/instinct.md +46 -0
  10. package/.agent/mindforge/orch-add-feature.md +43 -0
  11. package/.agent/mindforge/orch-build-mvp.md +48 -0
  12. package/.agent/mindforge/orch-change-feature.md +45 -0
  13. package/.agent/mindforge/orch-fix-defect.md +43 -0
  14. package/.agent/mindforge/orch-refine-code.md +43 -0
  15. package/.claude/CLAUDE.md +13 -0
  16. package/.claude/commands/mindforge/checkpoint.md +76 -0
  17. package/.claude/commands/mindforge/execute-phase.md +47 -6
  18. package/.claude/commands/mindforge/harness-audit.md +59 -0
  19. package/.claude/commands/mindforge/instinct.md +46 -0
  20. package/.claude/commands/mindforge/orch-add-feature.md +43 -0
  21. package/.claude/commands/mindforge/orch-build-mvp.md +48 -0
  22. package/.claude/commands/mindforge/orch-change-feature.md +45 -0
  23. package/.claude/commands/mindforge/orch-fix-defect.md +43 -0
  24. package/.claude/commands/mindforge/orch-refine-code.md +43 -0
  25. package/.claude/commands/mindforge/plan-write.md +11 -0
  26. package/.claude/commands/mindforge/product-spec.md +76 -0
  27. package/.mindforge/config.json +2 -2
  28. package/.mindforge/engine/instincts/instinct-schema.md +17 -9
  29. package/.mindforge/imported-agents.jsonl +10 -0
  30. package/.mindforge/manifests/install-components.json +36 -0
  31. package/.mindforge/manifests/install-modules.json +193 -0
  32. package/.mindforge/manifests/install-profiles.json +57 -0
  33. package/.mindforge/memory/sync-manifest.json +1 -1
  34. package/.mindforge/personas/gan-evaluator.md +226 -0
  35. package/.mindforge/personas/gan-generator.md +151 -0
  36. package/.mindforge/personas/gan-planner.md +118 -0
  37. package/.mindforge/personas/harness-optimizer.md +55 -0
  38. package/.mindforge/personas/loop-operator.md +58 -0
  39. package/.mindforge/schemas/hooks.schema.json +199 -0
  40. package/.mindforge/schemas/install-modules.schema.json +44 -0
  41. package/.mindforge/schemas/install-state.schema.json +95 -0
  42. package/.mindforge/schemas/plugin.schema.json +75 -0
  43. package/.mindforge/schemas/provenance.schema.json +31 -0
  44. package/.mindforge/skills/agent-architecture-audit/SKILL.md +272 -0
  45. package/.mindforge/skills/continuous-learning/SKILL.md +16 -0
  46. package/.mindforge/skills/orch-pipeline/SKILL.md +284 -0
  47. package/.mindforge/skills/writing-plans/SKILL.md +76 -0
  48. package/CHANGELOG.md +120 -0
  49. package/MINDFORGE.md +3 -3
  50. package/README.md +0 -1
  51. package/RELEASENOTES.md +131 -0
  52. package/SECURITY.md +16 -0
  53. package/bin/autonomous/auto-runner.js +46 -5
  54. package/bin/autonomous/handoff-schema.js +114 -0
  55. package/bin/autonomous/session-guardian.sh +138 -0
  56. package/bin/autonomous/supervisor.js +98 -0
  57. package/bin/change-classifier.js +19 -5
  58. package/bin/dashboard/api-router.js +10 -1
  59. package/bin/governance/approve.js +65 -28
  60. package/bin/governance/config-manager.js +3 -1
  61. package/bin/governance/rbac-manager.js +14 -6
  62. package/bin/harness-audit.js +520 -0
  63. package/bin/hooks/instinct-capture-hook.js +16 -1
  64. package/bin/hooks/lib/detect-project.js +72 -0
  65. package/bin/installer/harness-adapter-compliance.js +321 -0
  66. package/bin/installer/install-manifests.js +200 -0
  67. package/bin/installer/install-state.js +243 -0
  68. package/bin/installer-core.js +1 -1
  69. package/bin/learning/instinct-cli.js +359 -0
  70. package/bin/learning/lib/ssrf-guard.js +252 -0
  71. package/bin/memory/eis-client.js +31 -10
  72. package/bin/memory/federated-sync.js +11 -2
  73. package/bin/memory/knowledge-capture.js +10 -1
  74. package/bin/memory/pillar-health-tracker.js +9 -1
  75. package/bin/models/llm-errors.js +79 -0
  76. package/bin/models/model-client.js +39 -4
  77. package/bin/models/ollama-provider.js +115 -0
  78. package/bin/models/openai-provider.js +40 -9
  79. package/bin/models/profiles-loader.js +147 -0
  80. package/bin/models/provider-registry.js +59 -0
  81. package/bin/review/ads-engine.js +2 -2
  82. package/bin/revops/market-evaluator.js +23 -2
  83. package/bin/revops/router-steering-v2.js +17 -2
  84. package/bin/security/trust-boundaries.js +20 -3
  85. package/bin/utils/readiness-gate.js +169 -0
  86. package/bin/worktree/engine.js +497 -0
  87. package/package.json +8 -2
  88. package/subagents/categories/04-quality-security/.claude-plugin/plugin.json +10 -0
  89. package/subagents/categories/04-quality-security/go-build-resolver.md +105 -0
  90. package/subagents/categories/04-quality-security/go-reviewer.md +87 -0
  91. package/subagents/categories/04-quality-security/python-reviewer.md +109 -0
  92. package/subagents/categories/04-quality-security/react-build-resolver.md +215 -0
  93. package/subagents/categories/04-quality-security/react-reviewer.md +167 -0
  94. package/subagents/categories/04-quality-security/rust-build-resolver.md +159 -0
  95. package/subagents/categories/04-quality-security/rust-reviewer.md +105 -0
  96. package/subagents/categories/04-quality-security/silent-failure-hunter.md +67 -0
  97. package/subagents/categories/04-quality-security/type-design-analyzer.md +58 -0
  98. 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
- constructor(apiKey) {
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 = https.request({
28
- hostname: 'api.openai.com',
29
- path: '/v1/chat/completions',
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(Object.assign(new Error(json.error?.message || 'OpenAI API error'), { status: res.statusCode }));
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 = https.request({
93
- hostname: 'api.openai.com',
94
- path: '/v1/chat/completions',
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 };
@@ -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 { v4: uuidv4 } = require('uuid');
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 = uuidv4();
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 gold = this.marketRegistry['claude-3-5-sonnet'];
54
- return { model_id: 'claude-3-5-sonnet', ...gold };
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
- console.log(`[AgRevOps] Steered Span ${spanId} to ${selection.selected_model} (MIR: ${mir})`);
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, r"m, r\m and
83
- * rm${IFS}-rf${IFS}/ back into plain `rm -rf /` so the existing patterns fire.
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(/\s+/g, ' '); // collapse whitespace runs
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
+ }