openclaw-node-harness 2.0.3 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +646 -3
- package/bin/hyperagent.mjs +419 -0
- package/bin/mesh-agent.js +603 -81
- package/bin/mesh-bridge.js +340 -11
- package/bin/mesh-deploy-listener.js +119 -97
- package/bin/mesh-deploy.js +8 -0
- package/bin/mesh-task-daemon.js +1005 -40
- package/bin/mesh.js +423 -6
- package/config/claude-settings.json +95 -0
- package/config/daemon.json.template +2 -1
- package/config/git-hooks/pre-commit +13 -0
- package/config/git-hooks/pre-push +12 -0
- package/config/harness-rules.json +174 -0
- package/config/plan-templates/team-bugfix.yaml +52 -0
- package/config/plan-templates/team-deploy.yaml +50 -0
- package/config/plan-templates/team-feature.yaml +71 -0
- package/config/roles/qa-engineer.yaml +36 -0
- package/config/roles/solidity-dev.yaml +51 -0
- package/config/roles/tech-architect.yaml +36 -0
- package/config/rules/framework/solidity.md +22 -0
- package/config/rules/framework/typescript.md +21 -0
- package/config/rules/framework/unity.md +21 -0
- package/config/rules/universal/design-docs.md +18 -0
- package/config/rules/universal/git-hygiene.md +18 -0
- package/config/rules/universal/security.md +19 -0
- package/config/rules/universal/test-standards.md +19 -0
- package/identity/DELEGATION.md +6 -6
- package/install.sh +300 -8
- package/lib/circling-parser.js +119 -0
- package/lib/hyperagent-store.mjs +652 -0
- package/lib/kanban-io.js +59 -10
- package/lib/mcp-knowledge/bench.mjs +118 -0
- package/lib/mcp-knowledge/core.mjs +528 -0
- package/lib/mcp-knowledge/package.json +25 -0
- package/lib/mcp-knowledge/server.mjs +245 -0
- package/lib/mcp-knowledge/test.mjs +802 -0
- package/lib/memory-budget.mjs +261 -0
- package/lib/mesh-collab.js +354 -4
- package/lib/mesh-harness.js +427 -0
- package/lib/mesh-plans.js +13 -5
- package/lib/mesh-registry.js +11 -2
- package/lib/mesh-tasks.js +67 -0
- package/lib/plan-templates.js +226 -0
- package/lib/pre-compression-flush.mjs +320 -0
- package/lib/role-loader.js +292 -0
- package/lib/rule-loader.js +358 -0
- package/lib/session-store.mjs +458 -0
- package/lib/transcript-parser.mjs +292 -0
- package/mission-control/drizzle/soul_schema_update.sql +29 -0
- package/mission-control/drizzle.config.ts +1 -4
- package/mission-control/package-lock.json +1571 -83
- package/mission-control/package.json +6 -2
- package/mission-control/scripts/gen-chronology.js +3 -3
- package/mission-control/scripts/import-pipeline-v2.js +0 -16
- package/mission-control/scripts/import-pipeline.js +0 -15
- package/mission-control/src/app/api/cowork/clusters/[id]/members/route.ts +117 -0
- package/mission-control/src/app/api/cowork/clusters/[id]/route.ts +84 -0
- package/mission-control/src/app/api/cowork/clusters/route.ts +141 -0
- package/mission-control/src/app/api/cowork/dispatch/route.ts +128 -0
- package/mission-control/src/app/api/cowork/events/route.ts +65 -0
- package/mission-control/src/app/api/cowork/intervene/route.ts +259 -0
- package/mission-control/src/app/api/cowork/sessions/[id]/route.ts +37 -0
- package/mission-control/src/app/api/cowork/sessions/route.ts +64 -0
- package/mission-control/src/app/api/diagnostics/route.ts +97 -0
- package/mission-control/src/app/api/diagnostics/test-runner/route.ts +990 -0
- package/mission-control/src/app/api/mesh/events/route.ts +95 -19
- package/mission-control/src/app/api/mesh/identity/route.ts +11 -0
- package/mission-control/src/app/api/mesh/tasks/[id]/route.ts +92 -0
- package/mission-control/src/app/api/mesh/tasks/route.ts +91 -0
- package/mission-control/src/app/api/tasks/[id]/handoff/route.ts +1 -1
- package/mission-control/src/app/api/tasks/[id]/route.ts +90 -4
- package/mission-control/src/app/api/tasks/route.ts +21 -30
- package/mission-control/src/app/cowork/page.tsx +261 -0
- package/mission-control/src/app/diagnostics/page.tsx +385 -0
- package/mission-control/src/app/graph/page.tsx +26 -0
- package/mission-control/src/app/memory/page.tsx +1 -1
- package/mission-control/src/app/obsidian/page.tsx +36 -6
- package/mission-control/src/app/roadmap/page.tsx +24 -0
- package/mission-control/src/app/souls/page.tsx +2 -2
- package/mission-control/src/components/board/execution-config.tsx +431 -0
- package/mission-control/src/components/board/kanban-board.tsx +75 -9
- package/mission-control/src/components/board/kanban-column.tsx +135 -19
- package/mission-control/src/components/board/task-card.tsx +55 -2
- package/mission-control/src/components/board/unified-task-dialog.tsx +82 -4
- package/mission-control/src/components/cowork/cluster-card.tsx +176 -0
- package/mission-control/src/components/cowork/create-cluster-dialog.tsx +251 -0
- package/mission-control/src/components/cowork/dispatch-form.tsx +423 -0
- package/mission-control/src/components/cowork/role-picker.tsx +102 -0
- package/mission-control/src/components/cowork/session-card.tsx +284 -0
- package/mission-control/src/components/layout/sidebar.tsx +39 -2
- package/mission-control/src/lib/__tests__/daily-log.test.ts +82 -0
- package/mission-control/src/lib/__tests__/memory-md.test.ts +87 -0
- package/mission-control/src/lib/__tests__/mesh-kv-sync.test.ts +465 -0
- package/mission-control/src/lib/__tests__/mocks/mock-kv.ts +131 -0
- package/mission-control/src/lib/__tests__/status-kanban.test.ts +46 -0
- package/mission-control/src/lib/__tests__/task-markdown.test.ts +188 -0
- package/mission-control/src/lib/__tests__/wikilinks.test.ts +175 -0
- package/mission-control/src/lib/config.ts +58 -0
- package/mission-control/src/lib/db/index.ts +69 -0
- package/mission-control/src/lib/db/schema.ts +61 -3
- package/mission-control/src/lib/hooks.ts +309 -0
- package/mission-control/src/lib/memory/entities.ts +3 -2
- package/mission-control/src/lib/nats.ts +66 -1
- package/mission-control/src/lib/parsers/task-markdown.ts +52 -2
- package/mission-control/src/lib/parsers/transcript.ts +4 -4
- package/mission-control/src/lib/scheduler.ts +12 -11
- package/mission-control/src/lib/sync/mesh-kv.ts +279 -0
- package/mission-control/src/lib/sync/tasks.ts +23 -1
- package/mission-control/src/lib/task-id.ts +32 -0
- package/mission-control/src/lib/tts/index.ts +33 -9
- package/mission-control/tsconfig.json +2 -1
- package/mission-control/vitest.config.ts +14 -0
- package/package.json +15 -2
- package/services/service-manifest.json +1 -1
- package/skills/cc-godmode/references/agents.md +8 -8
- package/workspace-bin/memory-daemon.mjs +199 -5
- package/workspace-bin/session-search.mjs +204 -0
- package/workspace-bin/web-fetch.mjs +65 -0
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mesh-harness.js — Mechanical enforcement layer for mesh tasks.
|
|
3
|
+
*
|
|
4
|
+
* Two harnesses exist:
|
|
5
|
+
* LOCAL — prompt injection only (companion-bridge consumes these)
|
|
6
|
+
* MESH — mechanical enforcement at pre/post execution stages
|
|
7
|
+
*
|
|
8
|
+
* This module implements the mesh side. Each rule has a mesh_enforcement type:
|
|
9
|
+
* - "scope_check" → post-execution: revert files outside task.scope
|
|
10
|
+
* - "post_scan" → post-execution: scan LLM stdout for error patterns
|
|
11
|
+
* - "post_validate" → post-commit: run validation command
|
|
12
|
+
* - "pre_commit_scan" → pre-commit: scan staged diff for patterns
|
|
13
|
+
* - "output_block" → post-execution: scan output for blocked patterns
|
|
14
|
+
* - "metric_required" → post-execution: flag metric-less completions
|
|
15
|
+
* - "pre_check" → pre-execution: verify service health
|
|
16
|
+
*
|
|
17
|
+
* All enforcement is LLM-agnostic — it operates on filesystem state and
|
|
18
|
+
* process output, not on prompt compliance.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const { execSync } = require('child_process');
|
|
24
|
+
const { globMatch } = require('./rule-loader');
|
|
25
|
+
|
|
26
|
+
// ── Rule Loading ─────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Load harness rules filtered by scope.
|
|
30
|
+
* @param {string} rulesPath — path to harness-rules.json
|
|
31
|
+
* @param {string} scope — "local" or "mesh"
|
|
32
|
+
* @returns {object[]} — active rules for this scope
|
|
33
|
+
*/
|
|
34
|
+
function loadHarnessRules(rulesPath, scope) {
|
|
35
|
+
if (!fs.existsSync(rulesPath)) return [];
|
|
36
|
+
try {
|
|
37
|
+
const rules = JSON.parse(fs.readFileSync(rulesPath, 'utf-8'));
|
|
38
|
+
return rules.filter(r =>
|
|
39
|
+
r.active !== false &&
|
|
40
|
+
Array.isArray(r.scope) &&
|
|
41
|
+
r.scope.includes(scope)
|
|
42
|
+
);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
console.error(`[mesh-harness] Failed to load ${rulesPath}: ${err.message}`);
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get rules by mesh_enforcement type.
|
|
51
|
+
*/
|
|
52
|
+
function rulesByEnforcement(rules, type) {
|
|
53
|
+
return rules.filter(r => r.mesh_enforcement === type);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── Enforcement: Scope Check ─────────────────────────
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Revert any files changed outside task.scope in a worktree.
|
|
60
|
+
* Returns { violations: string[], reverted: string[] }.
|
|
61
|
+
*/
|
|
62
|
+
function enforceScopeCheck(worktreePath, taskScope) {
|
|
63
|
+
if (!worktreePath || !taskScope || taskScope.length === 0) {
|
|
64
|
+
return { violations: [], reverted: [] };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const violations = [];
|
|
68
|
+
const reverted = [];
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const changed = execSync('git diff --name-only HEAD', {
|
|
72
|
+
cwd: worktreePath, timeout: 10000, encoding: 'utf-8',
|
|
73
|
+
}).trim();
|
|
74
|
+
|
|
75
|
+
// Also check untracked files
|
|
76
|
+
const untracked = execSync('git ls-files --others --exclude-standard', {
|
|
77
|
+
cwd: worktreePath, timeout: 10000, encoding: 'utf-8',
|
|
78
|
+
}).trim();
|
|
79
|
+
|
|
80
|
+
const allFiles = [...new Set([
|
|
81
|
+
...(changed ? changed.split('\n') : []),
|
|
82
|
+
...(untracked ? untracked.split('\n') : []),
|
|
83
|
+
])].filter(Boolean);
|
|
84
|
+
|
|
85
|
+
for (const file of allFiles) {
|
|
86
|
+
const inScope = taskScope.some(pattern => globMatch(pattern, file));
|
|
87
|
+
if (!inScope) {
|
|
88
|
+
violations.push(file);
|
|
89
|
+
try {
|
|
90
|
+
// Revert tracked files
|
|
91
|
+
execSync(`git checkout HEAD -- "${file}"`, {
|
|
92
|
+
cwd: worktreePath, timeout: 5000, stdio: 'pipe',
|
|
93
|
+
});
|
|
94
|
+
reverted.push(file);
|
|
95
|
+
} catch {
|
|
96
|
+
// Untracked file — remove it
|
|
97
|
+
try {
|
|
98
|
+
fs.unlinkSync(path.join(worktreePath, file));
|
|
99
|
+
reverted.push(file);
|
|
100
|
+
} catch { /* best effort */ }
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
} catch (err) {
|
|
105
|
+
console.error(`[mesh-harness] Scope check error: ${err.message}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { violations, reverted };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── Enforcement: Post-Execution Scan ─────────────────
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Scan LLM output for error patterns that suggest silent failure.
|
|
115
|
+
* Returns { suspicious: boolean, matches: string[] }.
|
|
116
|
+
*/
|
|
117
|
+
function postExecutionScan(llmOutput, scanPatterns) {
|
|
118
|
+
if (!llmOutput || !scanPatterns || scanPatterns.length === 0) {
|
|
119
|
+
return { suspicious: false, matches: [] };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const matches = [];
|
|
123
|
+
const lines = llmOutput.split('\n');
|
|
124
|
+
|
|
125
|
+
for (const line of lines) {
|
|
126
|
+
for (const pattern of scanPatterns) {
|
|
127
|
+
if (line.includes(pattern)) {
|
|
128
|
+
matches.push(line.trim().slice(0, 200));
|
|
129
|
+
break; // one match per line is enough
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Heuristic: if >20% of output lines contain error patterns, it's suspicious
|
|
135
|
+
// Also flag if any FAIL/PANIC/Traceback appears (high-confidence error signals)
|
|
136
|
+
const highConfidence = ['FAIL', 'PANIC', 'Traceback', 'FATAL'];
|
|
137
|
+
const hasHighConfidence = matches.some(m =>
|
|
138
|
+
highConfidence.some(hc => m.includes(hc))
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
suspicious: hasHighConfidence || matches.length > 3,
|
|
143
|
+
matches: matches.slice(0, 10), // cap at 10 matches
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── Enforcement: Output Block ────────────────────────
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Scan LLM output for blocked patterns (destructive commands, etc.).
|
|
151
|
+
* Returns { blocked: boolean, violations: { ruleId, match }[] }.
|
|
152
|
+
*/
|
|
153
|
+
function scanOutputForBlocks(llmOutput, blockRules) {
|
|
154
|
+
if (!llmOutput) return { blocked: false, violations: [] };
|
|
155
|
+
|
|
156
|
+
const violations = [];
|
|
157
|
+
|
|
158
|
+
for (const rule of blockRules) {
|
|
159
|
+
if (!rule.pattern) continue;
|
|
160
|
+
try {
|
|
161
|
+
const regex = new RegExp(rule.pattern, 'gm');
|
|
162
|
+
const matches = llmOutput.match(regex);
|
|
163
|
+
if (matches) {
|
|
164
|
+
violations.push({
|
|
165
|
+
ruleId: rule.id,
|
|
166
|
+
pattern: rule.pattern,
|
|
167
|
+
matches: matches.slice(0, 5),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
} catch { /* skip invalid regex */ }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
blocked: violations.length > 0,
|
|
175
|
+
violations,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── Enforcement: Pre-Commit Scan ─────────────────────
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Scan staged diff for secrets before committing.
|
|
183
|
+
* Returns { blocked: boolean, findings: string[] }.
|
|
184
|
+
*/
|
|
185
|
+
function preCommitSecretScan(worktreePath) {
|
|
186
|
+
if (!worktreePath) return { blocked: false, findings: [] };
|
|
187
|
+
|
|
188
|
+
const findings = [];
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
// Check if gitleaks is available
|
|
192
|
+
try {
|
|
193
|
+
const result = execSync('gitleaks detect --staged --no-banner 2>&1', {
|
|
194
|
+
cwd: worktreePath, timeout: 30000, encoding: 'utf-8',
|
|
195
|
+
});
|
|
196
|
+
if (/leaks?\s+found|secret|token/i.test(result)) {
|
|
197
|
+
findings.push(`gitleaks: ${result.trim().slice(0, 200)}`);
|
|
198
|
+
}
|
|
199
|
+
} catch (glErr) {
|
|
200
|
+
// gitleaks not available or found leaks (exit code 1)
|
|
201
|
+
if (glErr.stdout && /leaks?\s+found/i.test(glErr.stdout)) {
|
|
202
|
+
findings.push(`gitleaks: ${glErr.stdout.trim().slice(0, 200)}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Fallback: regex scan on staged diff
|
|
207
|
+
if (findings.length === 0) {
|
|
208
|
+
const diff = execSync('git diff --cached -U0 2>/dev/null', {
|
|
209
|
+
cwd: worktreePath, timeout: 10000, encoding: 'utf-8',
|
|
210
|
+
});
|
|
211
|
+
const secretPatterns = [
|
|
212
|
+
/^\+.*sk-[a-zA-Z0-9]{20,}/m,
|
|
213
|
+
/^\+.*AKIA[A-Z0-9]{16}/m,
|
|
214
|
+
/^\+.*password\s*=\s*["'][^"']+["']/im,
|
|
215
|
+
/^\+.*api_key\s*=\s*["'][^"']+["']/im,
|
|
216
|
+
/^\+.*secret\s*=\s*["'][^"']+["']/im,
|
|
217
|
+
];
|
|
218
|
+
for (const pat of secretPatterns) {
|
|
219
|
+
const match = diff.match(pat);
|
|
220
|
+
if (match) {
|
|
221
|
+
findings.push(`regex: ${match[0].trim().slice(0, 100)}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
} catch (err) {
|
|
226
|
+
console.error(`[mesh-harness] Pre-commit scan error: ${err.message}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
blocked: findings.length > 0,
|
|
231
|
+
findings,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ── Enforcement: Post-Commit Validation ──────────────
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Run a validation command after commit (e.g., conventional commit check).
|
|
239
|
+
* Returns { passed: boolean, output: string }.
|
|
240
|
+
*/
|
|
241
|
+
function postCommitValidate(worktreePath, command) {
|
|
242
|
+
if (!worktreePath || !command) return { passed: true, output: '' };
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
const output = execSync(command, {
|
|
246
|
+
cwd: worktreePath, timeout: 10000, encoding: 'utf-8', stdio: 'pipe',
|
|
247
|
+
});
|
|
248
|
+
return { passed: true, output: output.trim() };
|
|
249
|
+
} catch (err) {
|
|
250
|
+
return {
|
|
251
|
+
passed: false,
|
|
252
|
+
output: (err.stdout || err.stderr || err.message || '').trim().slice(0, 500),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ── Composite: Run All Mesh Enforcement ──────────────
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Run the full mesh harness enforcement suite for a completed task.
|
|
261
|
+
* Called after LLM exits, before commitAndMergeWorktree.
|
|
262
|
+
*
|
|
263
|
+
* @param {object} opts
|
|
264
|
+
* @param {object[]} opts.rules — loaded mesh harness rules
|
|
265
|
+
* @param {string} opts.worktreePath — task worktree
|
|
266
|
+
* @param {string[]} opts.taskScope — task.scope glob patterns
|
|
267
|
+
* @param {string} opts.llmOutput — LLM stdout
|
|
268
|
+
* @param {boolean} opts.hasMetric — whether task has a metric
|
|
269
|
+
* @param {function} opts.log — logging function
|
|
270
|
+
* @returns {object} — { pass, violations, warnings }
|
|
271
|
+
*/
|
|
272
|
+
function runMeshHarness(opts) {
|
|
273
|
+
const { rules, worktreePath, taskScope, llmOutput, hasMetric, log, role } = opts;
|
|
274
|
+
const violations = [];
|
|
275
|
+
const warnings = [];
|
|
276
|
+
|
|
277
|
+
// 1. Scope enforcement
|
|
278
|
+
const scopeRules = rulesByEnforcement(rules, 'scope_check');
|
|
279
|
+
if (scopeRules.length > 0 && taskScope && taskScope.length > 0) {
|
|
280
|
+
const result = enforceScopeCheck(worktreePath, taskScope);
|
|
281
|
+
if (result.violations.length > 0) {
|
|
282
|
+
const msg = `SCOPE VIOLATION: ${result.violations.length} file(s) outside scope reverted: ${result.reverted.join(', ')}`;
|
|
283
|
+
violations.push({ rule: 'scope-enforcement', message: msg, files: result.violations });
|
|
284
|
+
log(`[HARNESS] ${msg}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// 1.5. Forbidden pattern check (from role profile, runs on worktree files)
|
|
289
|
+
if (role && role.forbidden_patterns && worktreePath) {
|
|
290
|
+
const { checkForbiddenPatterns } = require('./role-loader');
|
|
291
|
+
// Get list of changed files in worktree (post-scope-revert)
|
|
292
|
+
try {
|
|
293
|
+
const { execSync } = require('child_process');
|
|
294
|
+
const changed = execSync('git diff --name-only HEAD', {
|
|
295
|
+
cwd: worktreePath, timeout: 10000, encoding: 'utf-8',
|
|
296
|
+
}).trim();
|
|
297
|
+
const untracked = execSync('git ls-files --others --exclude-standard', {
|
|
298
|
+
cwd: worktreePath, timeout: 10000, encoding: 'utf-8',
|
|
299
|
+
}).trim();
|
|
300
|
+
const outputFiles = [...new Set([
|
|
301
|
+
...(changed ? changed.split('\n') : []),
|
|
302
|
+
...(untracked ? untracked.split('\n') : []),
|
|
303
|
+
])].filter(Boolean);
|
|
304
|
+
|
|
305
|
+
if (outputFiles.length > 0) {
|
|
306
|
+
const fpResult = checkForbiddenPatterns(role, outputFiles, worktreePath);
|
|
307
|
+
if (!fpResult.passed) {
|
|
308
|
+
for (const v of fpResult.violations) {
|
|
309
|
+
const msg = `FORBIDDEN PATTERN: "${v.description}" in ${v.file} (matched: ${v.match})`;
|
|
310
|
+
violations.push({ rule: `forbidden:${v.pattern}`, message: msg, file: v.file });
|
|
311
|
+
log(`[HARNESS] ${msg}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
} catch (err) {
|
|
316
|
+
log(`[HARNESS] Forbidden pattern check error: ${err.message}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// 2. Output block scan
|
|
321
|
+
const blockRules = rulesByEnforcement(rules, 'output_block');
|
|
322
|
+
if (blockRules.length > 0) {
|
|
323
|
+
const result = scanOutputForBlocks(llmOutput, blockRules);
|
|
324
|
+
if (result.blocked) {
|
|
325
|
+
for (const v of result.violations) {
|
|
326
|
+
const msg = `OUTPUT BLOCK: rule "${v.ruleId}" matched pattern /${v.pattern}/ (${v.matches.length} occurrence(s))`;
|
|
327
|
+
violations.push({ rule: v.ruleId, message: msg });
|
|
328
|
+
log(`[HARNESS] ${msg}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// 3. Post-execution error scan (for metric-less tasks only)
|
|
334
|
+
const scanRules = rulesByEnforcement(rules, 'post_scan');
|
|
335
|
+
if (scanRules.length > 0 && !hasMetric) {
|
|
336
|
+
for (const rule of scanRules) {
|
|
337
|
+
const result = postExecutionScan(llmOutput, rule.mesh_scan_patterns);
|
|
338
|
+
if (result.suspicious) {
|
|
339
|
+
const msg = `SUSPICIOUS OUTPUT: ${result.matches.length} error-like patterns found in output (no metric to verify)`;
|
|
340
|
+
warnings.push({ rule: rule.id, message: msg, matches: result.matches });
|
|
341
|
+
log(`[HARNESS] ${msg}`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// 4. Metric-required flag
|
|
347
|
+
const metricRules = rulesByEnforcement(rules, 'metric_required');
|
|
348
|
+
if (metricRules.length > 0 && !hasMetric) {
|
|
349
|
+
warnings.push({
|
|
350
|
+
rule: 'build-before-done',
|
|
351
|
+
message: 'Task completed without metric — no mechanical verification of success',
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// 5. Pre-commit secret scan
|
|
356
|
+
const secretRules = rulesByEnforcement(rules, 'pre_commit_scan');
|
|
357
|
+
if (secretRules.length > 0 && worktreePath) {
|
|
358
|
+
const result = preCommitSecretScan(worktreePath);
|
|
359
|
+
if (result.blocked) {
|
|
360
|
+
const msg = `SECRET DETECTED: ${result.findings.join('; ')}`;
|
|
361
|
+
violations.push({ rule: 'no-hardcoded-secrets', message: msg, findings: result.findings });
|
|
362
|
+
log(`[HARNESS] ${msg}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const pass = violations.length === 0;
|
|
367
|
+
|
|
368
|
+
return { pass, violations, warnings };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Run post-commit validation checks.
|
|
373
|
+
* Called after commitAndMergeWorktree, before reporting completion.
|
|
374
|
+
*
|
|
375
|
+
* @param {object[]} rules — loaded mesh harness rules
|
|
376
|
+
* @param {string} worktreePath
|
|
377
|
+
* @param {function} log
|
|
378
|
+
* @returns {object[]} — array of { rule, passed, output } for each validation
|
|
379
|
+
*/
|
|
380
|
+
function runPostCommitValidation(rules, worktreePath, log) {
|
|
381
|
+
const results = [];
|
|
382
|
+
const validateRules = rulesByEnforcement(rules, 'post_validate');
|
|
383
|
+
|
|
384
|
+
for (const rule of validateRules) {
|
|
385
|
+
if (!rule.mesh_validate_command) continue;
|
|
386
|
+
const result = postCommitValidate(worktreePath, rule.mesh_validate_command);
|
|
387
|
+
results.push({
|
|
388
|
+
rule: rule.id,
|
|
389
|
+
passed: result.passed,
|
|
390
|
+
output: result.output,
|
|
391
|
+
});
|
|
392
|
+
if (!result.passed) {
|
|
393
|
+
log(`[HARNESS] POST-COMMIT FAIL: ${rule.id} — ${result.output}`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return results;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Get inject-type rules for prompt injection (soft enforcement layer).
|
|
402
|
+
* These are injected into the LLM prompt in addition to mechanical enforcement.
|
|
403
|
+
* LLM-agnostic: returns markdown text that any LLM can consume.
|
|
404
|
+
*/
|
|
405
|
+
function formatHarnessForPrompt(rules) {
|
|
406
|
+
const injectRules = rules.filter(r => r.type === 'inject' && r.content);
|
|
407
|
+
if (injectRules.length === 0) return '';
|
|
408
|
+
|
|
409
|
+
const parts = ['## Harness Rules', ''];
|
|
410
|
+
for (const rule of injectRules) {
|
|
411
|
+
parts.push(rule.content);
|
|
412
|
+
}
|
|
413
|
+
return parts.join('\n');
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
module.exports = {
|
|
417
|
+
loadHarnessRules,
|
|
418
|
+
rulesByEnforcement,
|
|
419
|
+
enforceScopeCheck,
|
|
420
|
+
postExecutionScan,
|
|
421
|
+
scanOutputForBlocks,
|
|
422
|
+
preCommitSecretScan,
|
|
423
|
+
postCommitValidate,
|
|
424
|
+
runMeshHarness,
|
|
425
|
+
runPostCommitValidation,
|
|
426
|
+
formatHarnessForPrompt,
|
|
427
|
+
};
|
package/lib/mesh-plans.js
CHANGED
|
@@ -27,9 +27,10 @@ const PLAN_STATUS = {
|
|
|
27
27
|
// ── Subtask Statuses ───────────────────────────────
|
|
28
28
|
|
|
29
29
|
const SUBTASK_STATUS = {
|
|
30
|
-
PENDING: 'pending',
|
|
31
|
-
QUEUED: 'queued',
|
|
32
|
-
RUNNING: 'running',
|
|
30
|
+
PENDING: 'pending', // not yet dispatched (waiting for wave)
|
|
31
|
+
QUEUED: 'queued', // dispatched to queue
|
|
32
|
+
RUNNING: 'running', // actively being worked
|
|
33
|
+
PENDING_REVIEW: 'pending_review', // work done, awaiting human approval
|
|
33
34
|
COMPLETED: 'completed',
|
|
34
35
|
FAILED: 'failed',
|
|
35
36
|
BLOCKED: 'blocked',
|
|
@@ -83,6 +84,7 @@ function createPlan({
|
|
|
83
84
|
planner = 'daedalus',
|
|
84
85
|
planner_soul = null,
|
|
85
86
|
requires_approval = true,
|
|
87
|
+
failure_policy = 'continue_best_effort', // 'continue_best_effort' | 'abort_on_first_fail' | 'abort_on_critical_fail'
|
|
86
88
|
subtasks = [],
|
|
87
89
|
}) {
|
|
88
90
|
const planId = `PLAN-${parent_task_id}-${Date.now()}`;
|
|
@@ -109,6 +111,8 @@ function createPlan({
|
|
|
109
111
|
scope: st.scope || [],
|
|
110
112
|
success_criteria: st.success_criteria || [],
|
|
111
113
|
|
|
114
|
+
critical: st.critical || false, // critical subtask failure can abort plan (abort_on_critical_fail policy)
|
|
115
|
+
|
|
112
116
|
depends_on: st.depends_on || [],
|
|
113
117
|
wave: 0, // computed below
|
|
114
118
|
|
|
@@ -142,6 +146,7 @@ function createPlan({
|
|
|
142
146
|
total_budget_minutes: totalBudget,
|
|
143
147
|
estimated_waves: maxWave + 1,
|
|
144
148
|
|
|
149
|
+
failure_policy,
|
|
145
150
|
requires_approval,
|
|
146
151
|
approved_by: null,
|
|
147
152
|
approved_at: null,
|
|
@@ -314,12 +319,15 @@ function routeDelegation(subtask) {
|
|
|
314
319
|
|
|
315
320
|
/**
|
|
316
321
|
* Auto-route all subtasks in a plan that don't already have delegation set.
|
|
317
|
-
* Mutates subtasks in place.
|
|
322
|
+
* Mutates subtasks in place. Each routing decision is logged to the subtask's
|
|
323
|
+
* delegation.reason field for inspection via `mesh plan show`.
|
|
318
324
|
*/
|
|
319
|
-
function autoRoutePlan(plan) {
|
|
325
|
+
function autoRoutePlan(plan, { log } = {}) {
|
|
326
|
+
const logger = log || (() => {});
|
|
320
327
|
for (const st of plan.subtasks) {
|
|
321
328
|
if (!st.delegation || !st.delegation.mode || st.delegation.mode === 'auto') {
|
|
322
329
|
st.delegation = routeDelegation(st);
|
|
330
|
+
logger(`AUTO-ROUTE ${st.subtask_id} → ${st.delegation.mode}: ${st.delegation.reason}`);
|
|
323
331
|
}
|
|
324
332
|
}
|
|
325
333
|
return plan;
|
package/lib/mesh-registry.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* mesh-registry.js — NATS KV tool registry for OpenClaw mesh.
|
|
3
3
|
*
|
|
4
|
+
* STATUS: UNUSED — fully implemented but no callers exist yet. Kept for
|
|
5
|
+
* future tool-mesh integration. Review before adopting; remove if still
|
|
6
|
+
* uncalled by next major release.
|
|
7
|
+
*
|
|
4
8
|
* Shared library for:
|
|
5
9
|
* - Registering tools in MESH_TOOLS KV bucket
|
|
6
10
|
* - Heartbeat refresh (keeps tools alive via TTL)
|
|
@@ -36,7 +40,9 @@ class MeshRegistry {
|
|
|
36
40
|
|
|
37
41
|
async init() {
|
|
38
42
|
const js = this.nc.jetstream();
|
|
39
|
-
|
|
43
|
+
// TTL: entries auto-expire after 120s if not refreshed by heartbeat (60s interval).
|
|
44
|
+
// Prevents stale entries from crashed services that never called shutdown().
|
|
45
|
+
this.kv = await js.views.kv(KV_BUCKET, { ttl: 120_000 });
|
|
40
46
|
return this;
|
|
41
47
|
}
|
|
42
48
|
|
|
@@ -111,7 +117,10 @@ class MeshRegistry {
|
|
|
111
117
|
for (const [toolName, manifest] of this.manifests) {
|
|
112
118
|
const kvKey = `${this.nodeId}.${toolName}`;
|
|
113
119
|
try {
|
|
114
|
-
await this.kv.put(kvKey, sc.encode(JSON.stringify(
|
|
120
|
+
await this.kv.put(kvKey, sc.encode(JSON.stringify({
|
|
121
|
+
...manifest,
|
|
122
|
+
last_heartbeat: new Date().toISOString(),
|
|
123
|
+
})));
|
|
115
124
|
} catch (err) {
|
|
116
125
|
console.error(`[mesh-registry] heartbeat failed for ${kvKey}: ${err.message}`);
|
|
117
126
|
}
|
package/lib/mesh-tasks.js
CHANGED
|
@@ -22,14 +22,28 @@ const KV_BUCKET = 'MESH_TASKS';
|
|
|
22
22
|
* released — automation exhausted all retries, needs human triage
|
|
23
23
|
* cancelled — manually cancelled
|
|
24
24
|
*/
|
|
25
|
+
/**
|
|
26
|
+
* Task statuses:
|
|
27
|
+
* queued — available for claiming
|
|
28
|
+
* claimed — agent has claimed, not yet started work
|
|
29
|
+
* running — agent is actively working
|
|
30
|
+
* pending_review — work done, awaiting human approval (requires_review gate)
|
|
31
|
+
* completed — agent reports success (or human approved)
|
|
32
|
+
* failed — agent reports failure or budget exceeded
|
|
33
|
+
* released — automation exhausted all retries, needs human triage
|
|
34
|
+
* cancelled — manually cancelled
|
|
35
|
+
*/
|
|
25
36
|
const TASK_STATUS = {
|
|
26
37
|
QUEUED: 'queued',
|
|
27
38
|
CLAIMED: 'claimed',
|
|
28
39
|
RUNNING: 'running',
|
|
40
|
+
PENDING_REVIEW: 'pending_review',
|
|
29
41
|
COMPLETED: 'completed',
|
|
30
42
|
FAILED: 'failed',
|
|
31
43
|
RELEASED: 'released',
|
|
32
44
|
CANCELLED: 'cancelled',
|
|
45
|
+
PROPOSED: 'proposed',
|
|
46
|
+
REJECTED: 'rejected',
|
|
33
47
|
};
|
|
34
48
|
|
|
35
49
|
/**
|
|
@@ -53,6 +67,10 @@ function createTask({
|
|
|
53
67
|
exclude_nodes = [],
|
|
54
68
|
llm_provider = null,
|
|
55
69
|
llm_model = null,
|
|
70
|
+
plan_id = null,
|
|
71
|
+
subtask_id = null,
|
|
72
|
+
role = null,
|
|
73
|
+
requires_review = null, // null = auto-compute from mode + metric
|
|
56
74
|
}) {
|
|
57
75
|
return {
|
|
58
76
|
task_id,
|
|
@@ -64,6 +82,8 @@ function createTask({
|
|
|
64
82
|
metric, // mechanical success check (e.g. "tests pass", "val_bpb < 0.99")
|
|
65
83
|
on_fail, // what to do on failure
|
|
66
84
|
scope, // which files/paths the agent can touch
|
|
85
|
+
role, // role profile ID (e.g. "solidity-dev") for prompt injection + output validation
|
|
86
|
+
requires_review, // null = auto-computed by daemon; true/false = explicit override
|
|
67
87
|
|
|
68
88
|
// Standard fields
|
|
69
89
|
success_criteria,
|
|
@@ -94,6 +114,10 @@ function createTask({
|
|
|
94
114
|
budget_deadline: null, // set when claimed: claimed_at + budget_minutes
|
|
95
115
|
last_activity: null, // updated by agent heartbeats — stall detection key
|
|
96
116
|
|
|
117
|
+
// Plan back-reference (O(1) lookup in checkPlanProgress)
|
|
118
|
+
plan_id, // parent plan ID (null if standalone task)
|
|
119
|
+
subtask_id, // subtask ID within the plan (null if standalone task)
|
|
120
|
+
|
|
97
121
|
// Result (filled by agent)
|
|
98
122
|
result: null, // { success, summary, artifacts, attempts }
|
|
99
123
|
attempts: [], // log of approaches tried
|
|
@@ -224,6 +248,49 @@ class TaskStore {
|
|
|
224
248
|
return task;
|
|
225
249
|
}
|
|
226
250
|
|
|
251
|
+
/**
|
|
252
|
+
* Mark a task as pending_review (work done, needs human approval).
|
|
253
|
+
* Stores the result but doesn't transition to completed.
|
|
254
|
+
*/
|
|
255
|
+
async markPendingReview(taskId, result) {
|
|
256
|
+
const task = await this.get(taskId);
|
|
257
|
+
if (!task) return null;
|
|
258
|
+
task.status = TASK_STATUS.PENDING_REVIEW;
|
|
259
|
+
task.result = result;
|
|
260
|
+
task.review_requested_at = new Date().toISOString();
|
|
261
|
+
await this.put(task);
|
|
262
|
+
return task;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Approve a pending_review task → completed.
|
|
267
|
+
*/
|
|
268
|
+
async markApproved(taskId) {
|
|
269
|
+
const task = await this.get(taskId);
|
|
270
|
+
if (!task) return null;
|
|
271
|
+
if (task.status !== TASK_STATUS.PENDING_REVIEW) return null;
|
|
272
|
+
task.status = TASK_STATUS.COMPLETED;
|
|
273
|
+
task.completed_at = new Date().toISOString();
|
|
274
|
+
task.reviewed_by = 'human';
|
|
275
|
+
await this.put(task);
|
|
276
|
+
return task;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Reject a pending_review task → re-queue with reason.
|
|
281
|
+
*/
|
|
282
|
+
async markRejected(taskId, reason) {
|
|
283
|
+
const task = await this.get(taskId);
|
|
284
|
+
if (!task) return null;
|
|
285
|
+
if (task.status !== TASK_STATUS.PENDING_REVIEW) return null;
|
|
286
|
+
task.status = TASK_STATUS.QUEUED;
|
|
287
|
+
task.rejection_reason = reason;
|
|
288
|
+
task.result = null; // clear previous result
|
|
289
|
+
task.review_requested_at = null;
|
|
290
|
+
await this.put(task);
|
|
291
|
+
return task;
|
|
292
|
+
}
|
|
293
|
+
|
|
227
294
|
/**
|
|
228
295
|
* Mark a task as failed with reason.
|
|
229
296
|
*/
|