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.
Files changed (118) hide show
  1. package/README.md +646 -3
  2. package/bin/hyperagent.mjs +419 -0
  3. package/bin/mesh-agent.js +603 -81
  4. package/bin/mesh-bridge.js +340 -11
  5. package/bin/mesh-deploy-listener.js +119 -97
  6. package/bin/mesh-deploy.js +8 -0
  7. package/bin/mesh-task-daemon.js +1005 -40
  8. package/bin/mesh.js +423 -6
  9. package/config/claude-settings.json +95 -0
  10. package/config/daemon.json.template +2 -1
  11. package/config/git-hooks/pre-commit +13 -0
  12. package/config/git-hooks/pre-push +12 -0
  13. package/config/harness-rules.json +174 -0
  14. package/config/plan-templates/team-bugfix.yaml +52 -0
  15. package/config/plan-templates/team-deploy.yaml +50 -0
  16. package/config/plan-templates/team-feature.yaml +71 -0
  17. package/config/roles/qa-engineer.yaml +36 -0
  18. package/config/roles/solidity-dev.yaml +51 -0
  19. package/config/roles/tech-architect.yaml +36 -0
  20. package/config/rules/framework/solidity.md +22 -0
  21. package/config/rules/framework/typescript.md +21 -0
  22. package/config/rules/framework/unity.md +21 -0
  23. package/config/rules/universal/design-docs.md +18 -0
  24. package/config/rules/universal/git-hygiene.md +18 -0
  25. package/config/rules/universal/security.md +19 -0
  26. package/config/rules/universal/test-standards.md +19 -0
  27. package/identity/DELEGATION.md +6 -6
  28. package/install.sh +300 -8
  29. package/lib/circling-parser.js +119 -0
  30. package/lib/hyperagent-store.mjs +652 -0
  31. package/lib/kanban-io.js +59 -10
  32. package/lib/mcp-knowledge/bench.mjs +118 -0
  33. package/lib/mcp-knowledge/core.mjs +528 -0
  34. package/lib/mcp-knowledge/package.json +25 -0
  35. package/lib/mcp-knowledge/server.mjs +245 -0
  36. package/lib/mcp-knowledge/test.mjs +802 -0
  37. package/lib/memory-budget.mjs +261 -0
  38. package/lib/mesh-collab.js +354 -4
  39. package/lib/mesh-harness.js +427 -0
  40. package/lib/mesh-plans.js +13 -5
  41. package/lib/mesh-registry.js +11 -2
  42. package/lib/mesh-tasks.js +67 -0
  43. package/lib/plan-templates.js +226 -0
  44. package/lib/pre-compression-flush.mjs +320 -0
  45. package/lib/role-loader.js +292 -0
  46. package/lib/rule-loader.js +358 -0
  47. package/lib/session-store.mjs +458 -0
  48. package/lib/transcript-parser.mjs +292 -0
  49. package/mission-control/drizzle/soul_schema_update.sql +29 -0
  50. package/mission-control/drizzle.config.ts +1 -4
  51. package/mission-control/package-lock.json +1571 -83
  52. package/mission-control/package.json +6 -2
  53. package/mission-control/scripts/gen-chronology.js +3 -3
  54. package/mission-control/scripts/import-pipeline-v2.js +0 -16
  55. package/mission-control/scripts/import-pipeline.js +0 -15
  56. package/mission-control/src/app/api/cowork/clusters/[id]/members/route.ts +117 -0
  57. package/mission-control/src/app/api/cowork/clusters/[id]/route.ts +84 -0
  58. package/mission-control/src/app/api/cowork/clusters/route.ts +141 -0
  59. package/mission-control/src/app/api/cowork/dispatch/route.ts +128 -0
  60. package/mission-control/src/app/api/cowork/events/route.ts +65 -0
  61. package/mission-control/src/app/api/cowork/intervene/route.ts +259 -0
  62. package/mission-control/src/app/api/cowork/sessions/[id]/route.ts +37 -0
  63. package/mission-control/src/app/api/cowork/sessions/route.ts +64 -0
  64. package/mission-control/src/app/api/diagnostics/route.ts +97 -0
  65. package/mission-control/src/app/api/diagnostics/test-runner/route.ts +990 -0
  66. package/mission-control/src/app/api/mesh/events/route.ts +95 -19
  67. package/mission-control/src/app/api/mesh/identity/route.ts +11 -0
  68. package/mission-control/src/app/api/mesh/tasks/[id]/route.ts +92 -0
  69. package/mission-control/src/app/api/mesh/tasks/route.ts +91 -0
  70. package/mission-control/src/app/api/tasks/[id]/handoff/route.ts +1 -1
  71. package/mission-control/src/app/api/tasks/[id]/route.ts +90 -4
  72. package/mission-control/src/app/api/tasks/route.ts +21 -30
  73. package/mission-control/src/app/cowork/page.tsx +261 -0
  74. package/mission-control/src/app/diagnostics/page.tsx +385 -0
  75. package/mission-control/src/app/graph/page.tsx +26 -0
  76. package/mission-control/src/app/memory/page.tsx +1 -1
  77. package/mission-control/src/app/obsidian/page.tsx +36 -6
  78. package/mission-control/src/app/roadmap/page.tsx +24 -0
  79. package/mission-control/src/app/souls/page.tsx +2 -2
  80. package/mission-control/src/components/board/execution-config.tsx +431 -0
  81. package/mission-control/src/components/board/kanban-board.tsx +75 -9
  82. package/mission-control/src/components/board/kanban-column.tsx +135 -19
  83. package/mission-control/src/components/board/task-card.tsx +55 -2
  84. package/mission-control/src/components/board/unified-task-dialog.tsx +82 -4
  85. package/mission-control/src/components/cowork/cluster-card.tsx +176 -0
  86. package/mission-control/src/components/cowork/create-cluster-dialog.tsx +251 -0
  87. package/mission-control/src/components/cowork/dispatch-form.tsx +423 -0
  88. package/mission-control/src/components/cowork/role-picker.tsx +102 -0
  89. package/mission-control/src/components/cowork/session-card.tsx +284 -0
  90. package/mission-control/src/components/layout/sidebar.tsx +39 -2
  91. package/mission-control/src/lib/__tests__/daily-log.test.ts +82 -0
  92. package/mission-control/src/lib/__tests__/memory-md.test.ts +87 -0
  93. package/mission-control/src/lib/__tests__/mesh-kv-sync.test.ts +465 -0
  94. package/mission-control/src/lib/__tests__/mocks/mock-kv.ts +131 -0
  95. package/mission-control/src/lib/__tests__/status-kanban.test.ts +46 -0
  96. package/mission-control/src/lib/__tests__/task-markdown.test.ts +188 -0
  97. package/mission-control/src/lib/__tests__/wikilinks.test.ts +175 -0
  98. package/mission-control/src/lib/config.ts +58 -0
  99. package/mission-control/src/lib/db/index.ts +69 -0
  100. package/mission-control/src/lib/db/schema.ts +61 -3
  101. package/mission-control/src/lib/hooks.ts +309 -0
  102. package/mission-control/src/lib/memory/entities.ts +3 -2
  103. package/mission-control/src/lib/nats.ts +66 -1
  104. package/mission-control/src/lib/parsers/task-markdown.ts +52 -2
  105. package/mission-control/src/lib/parsers/transcript.ts +4 -4
  106. package/mission-control/src/lib/scheduler.ts +12 -11
  107. package/mission-control/src/lib/sync/mesh-kv.ts +279 -0
  108. package/mission-control/src/lib/sync/tasks.ts +23 -1
  109. package/mission-control/src/lib/task-id.ts +32 -0
  110. package/mission-control/src/lib/tts/index.ts +33 -9
  111. package/mission-control/tsconfig.json +2 -1
  112. package/mission-control/vitest.config.ts +14 -0
  113. package/package.json +15 -2
  114. package/services/service-manifest.json +1 -1
  115. package/skills/cc-godmode/references/agents.md +8 -8
  116. package/workspace-bin/memory-daemon.mjs +199 -5
  117. package/workspace-bin/session-search.mjs +204 -0
  118. 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', // not yet dispatched (waiting for wave)
31
- QUEUED: 'queued', // dispatched to queue
32
- RUNNING: 'running', // actively being worked
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;
@@ -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
- this.kv = await js.views.kv(KV_BUCKET);
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(manifest)));
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
  */