openclaw-node-harness 2.0.4 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +646 -3
- package/bin/hyperagent.mjs +419 -0
- package/bin/lane-watchdog.js +23 -2
- package/bin/mesh-agent.js +439 -28
- package/bin/mesh-bridge.js +69 -3
- package/bin/mesh-health-publisher.js +41 -1
- package/bin/mesh-task-daemon.js +821 -26
- package/bin/mesh.js +411 -20
- 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 +296 -10
- package/lib/agent-activity.js +2 -2
- package/lib/circling-parser.js +119 -0
- package/lib/exec-safety.js +105 -0
- package/lib/hyperagent-store.mjs +652 -0
- package/lib/kanban-io.js +24 -31
- package/lib/llm-providers.js +16 -0
- package/lib/mcp-knowledge/bench.mjs +118 -0
- package/lib/mcp-knowledge/core.mjs +530 -0
- package/lib/mcp-knowledge/package.json +25 -0
- package/lib/mcp-knowledge/server.mjs +252 -0
- package/lib/mcp-knowledge/test.mjs +802 -0
- package/lib/memory-budget.mjs +261 -0
- package/lib/mesh-collab.js +483 -165
- package/lib/mesh-harness.js +427 -0
- package/lib/mesh-plans.js +79 -50
- package/lib/mesh-tasks.js +132 -49
- package/lib/nats-resolve.js +4 -4
- package/lib/plan-templates.js +226 -0
- package/lib/pre-compression-flush.mjs +322 -0
- package/lib/role-loader.js +292 -0
- package/lib/rule-loader.js +358 -0
- package/lib/session-store.mjs +461 -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/memory/search/route.ts +6 -3
- 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/souls/[id]/evolution/route.ts +21 -5
- package/mission-control/src/app/api/souls/[id]/prompt/route.ts +7 -1
- package/mission-control/src/app/api/souls/[id]/propagate/route.ts +14 -2
- package/mission-control/src/app/api/tasks/[id]/handoff/route.ts +8 -2
- 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/api/workspace/read/route.ts +11 -0
- 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 +67 -0
- package/mission-control/src/lib/db/index.ts +85 -1
- 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/memory/extract.ts +2 -1
- package/mission-control/src/lib/memory/retrieval.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/src/middleware.ts +82 -0
- package/mission-control/tsconfig.json +2 -1
- package/mission-control/vitest.config.ts +14 -0
- package/package.json +15 -2
- package/services/launchd/ai.openclaw.log-rotate.plist +11 -0
- package/services/launchd/ai.openclaw.mesh-deploy-listener.plist +4 -0
- package/services/launchd/ai.openclaw.mesh-health-publisher.plist +4 -0
- package/services/launchd/ai.openclaw.mission-control.plist +1 -1
- package/services/service-manifest.json +1 -1
- package/skills/cc-godmode/references/agents.md +8 -8
- package/uninstall.sh +37 -9
- package/workspace-bin/memory-daemon.mjs +199 -5
- package/workspace-bin/session-search.mjs +204 -0
- package/workspace-bin/web-fetch.mjs +65 -0
package/bin/mesh-agent.js
CHANGED
|
@@ -36,14 +36,17 @@
|
|
|
36
36
|
*/
|
|
37
37
|
|
|
38
38
|
const { connect, StringCodec } = require('nats');
|
|
39
|
-
const { spawn, execSync } = require('child_process');
|
|
39
|
+
const { spawn, execSync, execFileSync } = require('child_process');
|
|
40
40
|
const os = require('os');
|
|
41
41
|
const path = require('path');
|
|
42
42
|
const fs = require('fs');
|
|
43
43
|
const { getActivityState, getSessionInfo } = require('../lib/agent-activity');
|
|
44
|
+
const { loadAllRules, matchRules, formatRulesForPrompt, detectFrameworks, activateFrameworkRules } = require('../lib/rule-loader');
|
|
45
|
+
const { loadHarnessRules, runMeshHarness, runPostCommitValidation, formatHarnessForPrompt } = require('../lib/mesh-harness');
|
|
46
|
+
const { findRole, formatRoleForPrompt } = require('../lib/role-loader');
|
|
44
47
|
|
|
45
48
|
const sc = StringCodec();
|
|
46
|
-
const { NATS_URL } = require('../lib/nats-resolve');
|
|
49
|
+
const { NATS_URL, natsConnectOpts } = require('../lib/nats-resolve');
|
|
47
50
|
const { resolveProvider, resolveModel } = require('../lib/llm-providers');
|
|
48
51
|
const NODE_ID = process.env.MESH_NODE_ID || os.hostname().toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
49
52
|
const POLL_INTERVAL = parseInt(process.env.MESH_POLL_INTERVAL || '15000'); // 15s between polls
|
|
@@ -51,6 +54,75 @@ const MAX_ATTEMPTS = parseInt(process.env.MESH_MAX_ATTEMPTS || '3');
|
|
|
51
54
|
const HEARTBEAT_INTERVAL = parseInt(process.env.MESH_HEARTBEAT_INTERVAL || '60000'); // 60s heartbeat
|
|
52
55
|
const WORKSPACE = process.env.MESH_WORKSPACE || path.join(process.env.HOME, '.openclaw', 'workspace');
|
|
53
56
|
|
|
57
|
+
// ── Rule loading (cached at startup, framework-filtered) ─────────────────
|
|
58
|
+
const RULES_DIR = process.env.OPENCLAW_RULES_DIR || path.join(process.env.HOME, '.openclaw', 'rules');
|
|
59
|
+
const HARNESS_PATH = process.env.OPENCLAW_HARNESS_RULES || path.join(process.env.HOME, '.openclaw', 'harness-rules.json');
|
|
60
|
+
let cachedRules = null;
|
|
61
|
+
let cachedHarnessRules = null;
|
|
62
|
+
|
|
63
|
+
function getRules() {
|
|
64
|
+
if (!cachedRules) {
|
|
65
|
+
const allRules = loadAllRules(RULES_DIR);
|
|
66
|
+
const detected = detectFrameworks(WORKSPACE);
|
|
67
|
+
cachedRules = activateFrameworkRules(allRules, detected);
|
|
68
|
+
}
|
|
69
|
+
return cachedRules;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function getHarnessRules() {
|
|
73
|
+
if (!cachedHarnessRules) {
|
|
74
|
+
cachedHarnessRules = loadHarnessRules(HARNESS_PATH, 'mesh');
|
|
75
|
+
}
|
|
76
|
+
return cachedHarnessRules;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Inject matching coding rules into a prompt parts array based on task scope.
|
|
81
|
+
*/
|
|
82
|
+
function injectRules(parts, scope) {
|
|
83
|
+
// Path-scoped coding standards
|
|
84
|
+
const matched = matchRules(getRules(), scope || []);
|
|
85
|
+
if (matched.length > 0) {
|
|
86
|
+
parts.push(formatRulesForPrompt(matched));
|
|
87
|
+
parts.push('');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Harness behavioral rules (soft enforcement — LLM-agnostic prompt injection)
|
|
91
|
+
const harnessText = formatHarnessForPrompt(getHarnessRules());
|
|
92
|
+
if (harnessText) {
|
|
93
|
+
parts.push(harnessText);
|
|
94
|
+
parts.push('');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── Role loading (cached per role ID) ─────────────────
|
|
99
|
+
const ROLE_DIRS = [
|
|
100
|
+
path.join(process.env.HOME, '.openclaw', 'roles'),
|
|
101
|
+
path.join(__dirname, '..', 'config', 'roles'),
|
|
102
|
+
];
|
|
103
|
+
const roleCache = new Map();
|
|
104
|
+
|
|
105
|
+
function getRole(roleId) {
|
|
106
|
+
if (!roleId) return null;
|
|
107
|
+
if (roleCache.has(roleId)) return roleCache.get(roleId);
|
|
108
|
+
const role = findRole(roleId, ROLE_DIRS);
|
|
109
|
+
if (role) roleCache.set(roleId, role);
|
|
110
|
+
return role;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Inject role profile into prompt parts array.
|
|
115
|
+
*/
|
|
116
|
+
function injectRole(parts, roleId) {
|
|
117
|
+
const role = getRole(roleId);
|
|
118
|
+
if (!role) return;
|
|
119
|
+
const roleText = formatRoleForPrompt(role);
|
|
120
|
+
if (roleText) {
|
|
121
|
+
parts.push(roleText);
|
|
122
|
+
parts.push('');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
54
126
|
// ── CLI args ──────────────────────────────────────────
|
|
55
127
|
|
|
56
128
|
const args = process.argv.slice(2);
|
|
@@ -126,8 +198,9 @@ function buildInitialPrompt(task) {
|
|
|
126
198
|
}
|
|
127
199
|
|
|
128
200
|
if (task.metric) {
|
|
201
|
+
const safeMetric = isAllowedMetric(task.metric) ? task.metric : '[metric command filtered for security]';
|
|
129
202
|
parts.push(`## Verification`);
|
|
130
|
-
parts.push(`Run this command to check your work: \`${
|
|
203
|
+
parts.push(`Run this command to check your work: \`${safeMetric}\``);
|
|
131
204
|
parts.push(`Your changes are only accepted if this command exits with code 0.`);
|
|
132
205
|
parts.push('');
|
|
133
206
|
}
|
|
@@ -141,12 +214,19 @@ function buildInitialPrompt(task) {
|
|
|
141
214
|
parts.push('');
|
|
142
215
|
}
|
|
143
216
|
|
|
217
|
+
// Inject path-scoped coding rules matching task scope
|
|
218
|
+
injectRules(parts, task.scope);
|
|
219
|
+
|
|
220
|
+
// Inject role profile (responsibilities, boundaries, framework)
|
|
221
|
+
injectRole(parts, task.role);
|
|
222
|
+
|
|
144
223
|
parts.push('## Instructions');
|
|
145
224
|
parts.push('- Read the relevant files before making changes.');
|
|
146
225
|
parts.push('- Make minimal, focused changes. Do not add scope beyond what is asked.');
|
|
147
226
|
parts.push('- If you hit a blocker you cannot resolve, explain what is blocking you clearly.');
|
|
148
227
|
if (task.metric) {
|
|
149
|
-
|
|
228
|
+
const safeMetric = isAllowedMetric(task.metric) ? task.metric : '[metric command filtered for security]';
|
|
229
|
+
parts.push(`- After making changes, run \`${safeMetric}\` to verify.`);
|
|
150
230
|
parts.push('- If verification fails, analyze the failure and iterate on your approach.');
|
|
151
231
|
}
|
|
152
232
|
|
|
@@ -186,8 +266,9 @@ function buildRetryPrompt(task, previousAttempts, attemptNumber) {
|
|
|
186
266
|
}
|
|
187
267
|
|
|
188
268
|
if (task.metric) {
|
|
269
|
+
const safeMetric = isAllowedMetric(task.metric) ? task.metric : '[metric command filtered for security]';
|
|
189
270
|
parts.push(`## Verification`);
|
|
190
|
-
parts.push(`Run: \`${
|
|
271
|
+
parts.push(`Run: \`${safeMetric}\``);
|
|
191
272
|
parts.push(`Must exit code 0.`);
|
|
192
273
|
parts.push('');
|
|
193
274
|
}
|
|
@@ -200,12 +281,19 @@ function buildRetryPrompt(task, previousAttempts, attemptNumber) {
|
|
|
200
281
|
parts.push('');
|
|
201
282
|
}
|
|
202
283
|
|
|
284
|
+
// Inject path-scoped coding rules matching task scope
|
|
285
|
+
injectRules(parts, task.scope);
|
|
286
|
+
|
|
287
|
+
// Inject role profile (responsibilities, boundaries, framework)
|
|
288
|
+
injectRole(parts, task.role);
|
|
289
|
+
|
|
203
290
|
parts.push('## Instructions');
|
|
204
291
|
parts.push('- Do NOT repeat a failed approach. Try something different.');
|
|
205
292
|
parts.push('- Read the relevant files before making changes.');
|
|
206
293
|
parts.push('- Make minimal, focused changes.');
|
|
207
294
|
if (task.metric) {
|
|
208
|
-
|
|
295
|
+
const safeMetric = isAllowedMetric(task.metric) ? task.metric : '[metric command filtered for security]';
|
|
296
|
+
parts.push(`- Run \`${safeMetric}\` to verify before finishing.`);
|
|
209
297
|
}
|
|
210
298
|
|
|
211
299
|
return parts.join('\n');
|
|
@@ -221,6 +309,9 @@ const WORKTREE_BASE = process.env.MESH_WORKTREE_BASE || path.join(process.env.HO
|
|
|
221
309
|
* On failure, returns null (falls back to shared workspace).
|
|
222
310
|
*/
|
|
223
311
|
function createWorktree(taskId) {
|
|
312
|
+
if (!/^[\w][\w.-]{0,127}$/.test(taskId)) {
|
|
313
|
+
throw new Error(`Invalid taskId: contains unsafe characters`);
|
|
314
|
+
}
|
|
224
315
|
const worktreePath = path.join(WORKTREE_BASE, taskId);
|
|
225
316
|
const branch = `mesh/${taskId}`;
|
|
226
317
|
|
|
@@ -231,19 +322,19 @@ function createWorktree(taskId) {
|
|
|
231
322
|
if (fs.existsSync(worktreePath)) {
|
|
232
323
|
log(`Cleaning stale worktree: ${worktreePath}`);
|
|
233
324
|
try {
|
|
234
|
-
|
|
325
|
+
execFileSync('git', ['worktree', 'remove', '--force', worktreePath], { cwd: WORKSPACE, timeout: 10000 });
|
|
235
326
|
} catch {
|
|
236
327
|
// If git worktree remove fails, manually clean up
|
|
237
328
|
fs.rmSync(worktreePath, { recursive: true, force: true });
|
|
238
329
|
}
|
|
239
330
|
// Also clean up the branch if it exists
|
|
240
331
|
try {
|
|
241
|
-
|
|
332
|
+
execFileSync('git', ['branch', '-D', branch], { cwd: WORKSPACE, timeout: 5000, stdio: 'ignore' });
|
|
242
333
|
} catch { /* branch may not exist */ }
|
|
243
334
|
}
|
|
244
335
|
|
|
245
336
|
// Create new worktree branched off HEAD
|
|
246
|
-
|
|
337
|
+
execFileSync('git', ['worktree', 'add', '-b', branch, worktreePath, 'HEAD'], {
|
|
247
338
|
cwd: WORKSPACE,
|
|
248
339
|
timeout: 30000,
|
|
249
340
|
stdio: 'pipe',
|
|
@@ -284,7 +375,14 @@ function commitAndMergeWorktree(worktreePath, taskId, summary) {
|
|
|
284
375
|
// Stage and commit all changes
|
|
285
376
|
execSync('git add -A', { cwd: worktreePath, timeout: 10000, stdio: 'pipe' });
|
|
286
377
|
const commitMsg = `mesh(${taskId}): ${(summary || 'task completed').slice(0, 72)}`;
|
|
287
|
-
|
|
378
|
+
|
|
379
|
+
// Pre-commit validation: check conventional commit format before committing
|
|
380
|
+
const conventionalPattern = /^(feat|fix|docs|style|refactor|test|chore|build|ci|perf|revert|mesh)(\(.+\))?: .+/;
|
|
381
|
+
if (!conventionalPattern.test(commitMsg)) {
|
|
382
|
+
log(`WARNING: commit message doesn't follow conventional format: "${commitMsg}"`);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
execFileSync('git', ['commit', '-m', commitMsg], {
|
|
288
386
|
cwd: worktreePath, timeout: 10000, stdio: 'pipe',
|
|
289
387
|
});
|
|
290
388
|
|
|
@@ -300,7 +398,7 @@ function commitAndMergeWorktree(worktreePath, taskId, summary) {
|
|
|
300
398
|
const mergeMsg = `Merge ${branch}: ${taskId}`;
|
|
301
399
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
302
400
|
try {
|
|
303
|
-
|
|
401
|
+
execFileSync('git', ['merge', '--no-ff', branch, '-m', mergeMsg], {
|
|
304
402
|
cwd: WORKSPACE, timeout: 30000, stdio: 'pipe',
|
|
305
403
|
});
|
|
306
404
|
log(`Merged ${branch} into main${attempt > 0 ? ' (retry succeeded)' : ''}`);
|
|
@@ -338,13 +436,13 @@ function cleanupWorktree(worktreePath, keep = false) {
|
|
|
338
436
|
const branch = `mesh/${taskId}`;
|
|
339
437
|
|
|
340
438
|
try {
|
|
341
|
-
|
|
439
|
+
execFileSync('git', ['worktree', 'remove', '--force', worktreePath], {
|
|
342
440
|
cwd: WORKSPACE,
|
|
343
441
|
timeout: 10000,
|
|
344
442
|
stdio: 'pipe',
|
|
345
443
|
});
|
|
346
444
|
if (!keep) {
|
|
347
|
-
|
|
445
|
+
execFileSync('git', ['branch', '-D', branch], {
|
|
348
446
|
cwd: WORKSPACE,
|
|
349
447
|
timeout: 5000,
|
|
350
448
|
stdio: 'ignore',
|
|
@@ -416,9 +514,10 @@ function runLLM(prompt, task, worktreePath) {
|
|
|
416
514
|
|
|
417
515
|
let stdout = '';
|
|
418
516
|
let stderr = '';
|
|
517
|
+
const MAX_OUTPUT = 1024 * 1024; // 1MB cap
|
|
419
518
|
|
|
420
|
-
child.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
421
|
-
child.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
519
|
+
child.stdout.on('data', (d) => { if (stdout.length < MAX_OUTPUT) stdout += d.toString().slice(0, MAX_OUTPUT - stdout.length); });
|
|
520
|
+
child.stderr.on('data', (d) => { if (stderr.length < MAX_OUTPUT) stderr += d.toString().slice(0, MAX_OUTPUT - stderr.length); });
|
|
422
521
|
|
|
423
522
|
child.on('close', (code) => {
|
|
424
523
|
clearInterval(heartbeatTimer);
|
|
@@ -434,10 +533,23 @@ function runLLM(prompt, task, worktreePath) {
|
|
|
434
533
|
|
|
435
534
|
// ── Metric Evaluation ─────────────────────────────────
|
|
436
535
|
|
|
536
|
+
const ALLOWED_METRIC_PREFIXES = [
|
|
537
|
+
'npm test', 'npm run', 'node ', 'pytest', 'cargo test',
|
|
538
|
+
'go test', 'make test', 'jest', 'vitest', 'mocha',
|
|
539
|
+
];
|
|
540
|
+
|
|
541
|
+
function isAllowedMetric(cmd) {
|
|
542
|
+
return ALLOWED_METRIC_PREFIXES.some(prefix => cmd.startsWith(prefix));
|
|
543
|
+
}
|
|
544
|
+
|
|
437
545
|
/**
|
|
438
546
|
* Run the task's metric command. Returns { passed, output }.
|
|
439
547
|
*/
|
|
440
548
|
function evaluateMetric(metric, cwd) {
|
|
549
|
+
if (!isAllowedMetric(metric)) {
|
|
550
|
+
log(`WARNING: Metric command blocked by security filter: ${metric}`);
|
|
551
|
+
return Promise.resolve({ passed: false, output: 'Metric command blocked by security filter' });
|
|
552
|
+
}
|
|
441
553
|
return new Promise((resolve) => {
|
|
442
554
|
const child = spawn('bash', ['-c', metric], {
|
|
443
555
|
cwd: cwd || WORKSPACE,
|
|
@@ -510,6 +622,11 @@ function buildCollabPrompt(task, roundNumber, sharedIntel, myScope, myRole) {
|
|
|
510
622
|
}
|
|
511
623
|
}
|
|
512
624
|
|
|
625
|
+
// Inject path-scoped coding rules matching task scope
|
|
626
|
+
const collabScope = Array.isArray(myScope) ? myScope.map(s => s.replace('[REVIEW-ONLY] ', '')) : task.scope;
|
|
627
|
+
injectRules(parts, collabScope);
|
|
628
|
+
injectRole(parts, task.role);
|
|
629
|
+
|
|
513
630
|
if (task.success_criteria && task.success_criteria.length > 0) {
|
|
514
631
|
parts.push('## Success Criteria');
|
|
515
632
|
for (const c of task.success_criteria) {
|
|
@@ -625,6 +742,207 @@ function parseReflection(output) {
|
|
|
625
742
|
};
|
|
626
743
|
}
|
|
627
744
|
|
|
745
|
+
// ── Circling Strategy: Prompt Builder + Parser ───────
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Build a prompt for a circling strategy step.
|
|
749
|
+
* Role-specific: Worker gets different instructions than Reviewers at each step.
|
|
750
|
+
*/
|
|
751
|
+
function buildCirclingPrompt(task, circlingData) {
|
|
752
|
+
const { circling_phase, circling_step, circling_subround, directed_input, my_role } = circlingData;
|
|
753
|
+
const isWorker = my_role === 'worker';
|
|
754
|
+
const parts = [];
|
|
755
|
+
|
|
756
|
+
parts.push(`# Task: ${task.title}`);
|
|
757
|
+
parts.push('');
|
|
758
|
+
|
|
759
|
+
switch (circling_phase) {
|
|
760
|
+
case 'init':
|
|
761
|
+
if (isWorker) {
|
|
762
|
+
parts.push('## Your Role: WORKER (Central Authority)');
|
|
763
|
+
parts.push('');
|
|
764
|
+
parts.push('You are the Worker in a Circling Strategy collaboration. You OWN the deliverable.');
|
|
765
|
+
parts.push('Produce your initial work artifact (v0) — your first draft/implementation based on the task plan below.');
|
|
766
|
+
parts.push('');
|
|
767
|
+
} else {
|
|
768
|
+
parts.push('## Your Role: REVIEWER');
|
|
769
|
+
parts.push('');
|
|
770
|
+
parts.push('You are a Reviewer in a Circling Strategy collaboration.');
|
|
771
|
+
parts.push('Produce your **reviewStrategy** — your methodology and focus areas for reviewing this type of work.');
|
|
772
|
+
parts.push('Define: what you will look for, what frameworks you will apply, what your evaluation criteria are.');
|
|
773
|
+
parts.push('');
|
|
774
|
+
}
|
|
775
|
+
break;
|
|
776
|
+
|
|
777
|
+
case 'circling':
|
|
778
|
+
if (circling_step === 1) {
|
|
779
|
+
// Step 1 — Review Pass
|
|
780
|
+
parts.push(`## Sub-Round ${circling_subround}, Step 1: Review Pass`);
|
|
781
|
+
parts.push('');
|
|
782
|
+
if (isWorker) {
|
|
783
|
+
parts.push('## Your Role: WORKER — Analyze Review Strategies');
|
|
784
|
+
parts.push('');
|
|
785
|
+
parts.push('Below are the review STRATEGIES from both reviewers (NOT their review findings).');
|
|
786
|
+
parts.push('Analyze each strategy: what they focus on well, what they miss, how they could improve.');
|
|
787
|
+
parts.push('Your feedback will help reviewers sharpen their approaches.');
|
|
788
|
+
parts.push('Do NOT touch the work artifact in this step.');
|
|
789
|
+
parts.push('');
|
|
790
|
+
} else {
|
|
791
|
+
parts.push('## Your Role: REVIEWER — Review the Work Artifact');
|
|
792
|
+
parts.push('');
|
|
793
|
+
parts.push('Below is the work artifact. Review it using your review strategy.');
|
|
794
|
+
parts.push('Produce concrete, actionable findings. For each finding, specify:');
|
|
795
|
+
parts.push('- What you found (the issue or observation)');
|
|
796
|
+
parts.push('- Where in the artifact (location/line/section)');
|
|
797
|
+
parts.push('- Why it matters (impact)');
|
|
798
|
+
parts.push('- What you recommend (suggested fix or improvement)');
|
|
799
|
+
parts.push('');
|
|
800
|
+
}
|
|
801
|
+
} else if (circling_step === 2) {
|
|
802
|
+
// Step 2 — Integration + Refinement
|
|
803
|
+
parts.push(`## Sub-Round ${circling_subround}, Step 2: Integration & Refinement`);
|
|
804
|
+
parts.push('');
|
|
805
|
+
if (isWorker) {
|
|
806
|
+
parts.push('## Your Role: WORKER — Judge Reviews & Update Artifact');
|
|
807
|
+
parts.push('');
|
|
808
|
+
parts.push('Below are the review artifacts from both reviewers.');
|
|
809
|
+
parts.push('For EACH review finding, judge it:');
|
|
810
|
+
parts.push('- **ACCEPTED**: implement the suggestion and note where');
|
|
811
|
+
parts.push('- **REJECTED**: explain why with reasoning');
|
|
812
|
+
parts.push('- **MODIFIED**: implement differently and explain why');
|
|
813
|
+
parts.push('');
|
|
814
|
+
parts.push('Then produce your UPDATED work artifact incorporating accepted changes.');
|
|
815
|
+
parts.push('Also produce a reconciliationDoc summarizing all judgments.');
|
|
816
|
+
parts.push('');
|
|
817
|
+
parts.push('You must output TWO artifacts using the multi-artifact format below.');
|
|
818
|
+
parts.push('');
|
|
819
|
+
} else {
|
|
820
|
+
parts.push('## Your Role: REVIEWER — Refine Your Strategy');
|
|
821
|
+
parts.push('');
|
|
822
|
+
parts.push("Below is the Worker's analysis of your review strategy (and the other reviewer's).");
|
|
823
|
+
parts.push("Evaluate the Worker's feedback honestly:");
|
|
824
|
+
parts.push("- If the feedback is valid, adjust your strategy accordingly");
|
|
825
|
+
parts.push("- If you disagree, explain why and keep your approach");
|
|
826
|
+
parts.push('Produce your REFINED reviewStrategy.');
|
|
827
|
+
parts.push('');
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
break;
|
|
831
|
+
|
|
832
|
+
case 'finalization':
|
|
833
|
+
parts.push('## FINALIZATION');
|
|
834
|
+
parts.push('');
|
|
835
|
+
if (isWorker) {
|
|
836
|
+
parts.push('## Your Role: WORKER — Final Delivery');
|
|
837
|
+
parts.push('');
|
|
838
|
+
parts.push('Produce your FINAL formatted work artifact.');
|
|
839
|
+
parts.push('Also produce a completionDiff — a checklist comparing original plan items vs what was delivered:');
|
|
840
|
+
parts.push(' [x] Item implemented');
|
|
841
|
+
parts.push(' [ ] Item NOT implemented — reason / deferred to follow-up');
|
|
842
|
+
parts.push('');
|
|
843
|
+
parts.push('You must output TWO artifacts using the multi-artifact format below.');
|
|
844
|
+
parts.push('');
|
|
845
|
+
} else {
|
|
846
|
+
parts.push('## Your Role: REVIEWER — Final Sign-Off');
|
|
847
|
+
parts.push('');
|
|
848
|
+
parts.push('Review the final work artifact against the original task plan.');
|
|
849
|
+
parts.push('Either:');
|
|
850
|
+
parts.push('- **APPROVE**: vote "converged" — the work meets quality standards');
|
|
851
|
+
parts.push('- **FLAG CONCERN**: vote "blocked" — describe remaining critical concerns');
|
|
852
|
+
parts.push('');
|
|
853
|
+
}
|
|
854
|
+
break;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// Add directed input
|
|
858
|
+
if (directed_input) {
|
|
859
|
+
parts.push('---');
|
|
860
|
+
parts.push('');
|
|
861
|
+
parts.push(directed_input);
|
|
862
|
+
parts.push('');
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// Add artifact output instructions
|
|
866
|
+
const isMultiArtifact = isWorker && (circling_step === 2 || circling_phase === 'finalization');
|
|
867
|
+
parts.push('---');
|
|
868
|
+
parts.push('');
|
|
869
|
+
|
|
870
|
+
if (isMultiArtifact) {
|
|
871
|
+
// Multi-artifact output format (Worker Step 2 and Finalization)
|
|
872
|
+
const art1Type = circling_phase === 'finalization' ? 'workArtifact' : 'workArtifact';
|
|
873
|
+
const art2Type = circling_phase === 'finalization' ? 'completionDiff' : 'reconciliationDoc';
|
|
874
|
+
|
|
875
|
+
parts.push('## Output Format (TWO artifacts required)');
|
|
876
|
+
parts.push('');
|
|
877
|
+
parts.push('Produce your first artifact, then wrap it:');
|
|
878
|
+
parts.push('');
|
|
879
|
+
parts.push('===CIRCLING_ARTIFACT===');
|
|
880
|
+
parts.push(`type: ${art1Type}`);
|
|
881
|
+
parts.push('===END_ARTIFACT===');
|
|
882
|
+
parts.push('');
|
|
883
|
+
parts.push('Then produce your second artifact:');
|
|
884
|
+
parts.push('');
|
|
885
|
+
parts.push('===CIRCLING_ARTIFACT===');
|
|
886
|
+
parts.push(`type: ${art2Type}`);
|
|
887
|
+
parts.push('===END_ARTIFACT===');
|
|
888
|
+
parts.push('');
|
|
889
|
+
parts.push('Then end with:');
|
|
890
|
+
} else {
|
|
891
|
+
parts.push('## Output Format');
|
|
892
|
+
parts.push('');
|
|
893
|
+
parts.push('Produce your work above, then end with:');
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// Determine the correct type for single-artifact output
|
|
897
|
+
let artifactType = 'workArtifact'; // default
|
|
898
|
+
if (!isWorker) {
|
|
899
|
+
if (circling_phase === 'init' || circling_step === 2) artifactType = 'reviewStrategy';
|
|
900
|
+
else if (circling_step === 1) artifactType = 'reviewArtifact';
|
|
901
|
+
else if (circling_phase === 'finalization') artifactType = 'reviewSignOff';
|
|
902
|
+
} else {
|
|
903
|
+
if (circling_step === 1) artifactType = 'workerReviewsAnalysis';
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
parts.push('');
|
|
907
|
+
parts.push('===CIRCLING_REFLECTION===');
|
|
908
|
+
parts.push(`type: ${artifactType}`);
|
|
909
|
+
parts.push('summary: [1-2 sentences about what you did]');
|
|
910
|
+
parts.push('confidence: [0.0 to 1.0]');
|
|
911
|
+
if (circling_phase === 'finalization') {
|
|
912
|
+
parts.push('vote: [converged|blocked]');
|
|
913
|
+
} else {
|
|
914
|
+
parts.push('vote: [continue|converged|blocked]');
|
|
915
|
+
}
|
|
916
|
+
parts.push('===END_REFLECTION===');
|
|
917
|
+
parts.push('');
|
|
918
|
+
parts.push('Rules:');
|
|
919
|
+
parts.push('- Begin your output with the artifact content DIRECTLY. Do NOT include any preamble, explanation, or commentary before the artifact. Any text before the artifact delimiters (or before the reflection block for single-artifact output) is treated as part of the artifact.');
|
|
920
|
+
parts.push('- confidence: a number between 0.0 and 1.0');
|
|
921
|
+
if (circling_phase === 'finalization') {
|
|
922
|
+
parts.push('- vote: "converged" (work meets quality standards) or "blocked" (critical issue remains)');
|
|
923
|
+
} else {
|
|
924
|
+
parts.push('- vote: "continue" (more work needed), "converged" (satisfied), or "blocked" (critical issue)');
|
|
925
|
+
}
|
|
926
|
+
parts.push('- The reflection block MUST be the last thing in your response');
|
|
927
|
+
|
|
928
|
+
return parts.join('\n');
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// Parser extracted to lib/circling-parser.js — single source of truth for both
|
|
932
|
+
// production code and tests. Zero external dependencies.
|
|
933
|
+
const { parseCirclingReflection: _parseCircling } = require('../lib/circling-parser');
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* Parse a circling strategy output. Delegates to lib/circling-parser.js
|
|
937
|
+
* with agent-specific options (logger, legacy fallback).
|
|
938
|
+
*/
|
|
939
|
+
function parseCirclingReflection(output) {
|
|
940
|
+
return _parseCircling(output, {
|
|
941
|
+
log: (msg) => log(msg),
|
|
942
|
+
legacyParser: parseReflection,
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
|
|
628
946
|
// ── Collaborative Task Execution ──────────────────────
|
|
629
947
|
|
|
630
948
|
/**
|
|
@@ -736,32 +1054,60 @@ async function executeCollabTask(task) {
|
|
|
736
1054
|
if (roundsDone) break;
|
|
737
1055
|
|
|
738
1056
|
const roundData = JSON.parse(sc.decode(roundMsg.data));
|
|
739
|
-
const { round_number, shared_intel, my_scope, my_role, mode, current_turn
|
|
1057
|
+
const { round_number, shared_intel, directed_input, my_scope, my_role, mode, current_turn,
|
|
1058
|
+
circling_phase, circling_step, circling_subround } = roundData;
|
|
740
1059
|
|
|
741
1060
|
// Sequential mode safety guard: skip if it's not our turn.
|
|
742
|
-
// The daemon (notifySequentialTurn) only sends to the current-turn node,
|
|
743
|
-
// so this should not normally trigger. Kept as a defensive check.
|
|
744
1061
|
if (mode === 'sequential' && current_turn && current_turn !== NODE_ID) {
|
|
745
1062
|
log(`COLLAB R${round_number}: Not our turn (current: ${current_turn}). Waiting.`);
|
|
746
1063
|
continue;
|
|
747
1064
|
}
|
|
748
1065
|
|
|
749
|
-
|
|
1066
|
+
const isCircling = mode === 'circling_strategy';
|
|
1067
|
+
const stepLabel = isCircling
|
|
1068
|
+
? `${circling_phase === 'init' ? 'Init' : circling_phase === 'finalization' ? 'Final' : `SR${circling_subround}/S${circling_step}`}`
|
|
1069
|
+
: `R${round_number}`;
|
|
1070
|
+
log(`COLLAB ${stepLabel}: Starting work (role: ${my_role}, scope: ${JSON.stringify(my_scope)})`);
|
|
750
1071
|
|
|
751
|
-
// Build
|
|
752
|
-
const prompt =
|
|
1072
|
+
// Build prompt — circling uses directed inputs, other modes use shared intel
|
|
1073
|
+
const prompt = isCircling
|
|
1074
|
+
? buildCirclingPrompt(task, roundData)
|
|
1075
|
+
: buildCollabPrompt(task, round_number, shared_intel, my_scope, my_role);
|
|
753
1076
|
|
|
754
1077
|
if (DRY_RUN) {
|
|
755
1078
|
log(`[DRY RUN] Collab prompt:\n${prompt}`);
|
|
756
1079
|
break;
|
|
757
1080
|
}
|
|
758
1081
|
|
|
759
|
-
// Execute
|
|
1082
|
+
// Execute provider (LLM or shell)
|
|
760
1083
|
const llmResult = await runLLM(prompt, task, worktreePath);
|
|
761
1084
|
const output = llmResult.stdout || '';
|
|
762
1085
|
|
|
763
|
-
// Parse reflection
|
|
764
|
-
|
|
1086
|
+
// Parse reflection — circling uses delimiter-based parser, others use JSON-block parser
|
|
1087
|
+
let reflection;
|
|
1088
|
+
let circlingArtifacts = [];
|
|
1089
|
+
|
|
1090
|
+
if (llmResult.provider === 'shell') {
|
|
1091
|
+
reflection = {
|
|
1092
|
+
summary: output.trim().slice(-500) || '(no output)',
|
|
1093
|
+
learnings: '',
|
|
1094
|
+
confidence: llmResult.exitCode === 0 ? 1.0 : 0.0,
|
|
1095
|
+
vote: llmResult.exitCode === 0 ? 'converged' : 'continue',
|
|
1096
|
+
parse_failed: false,
|
|
1097
|
+
};
|
|
1098
|
+
} else if (isCircling) {
|
|
1099
|
+
const circResult = parseCirclingReflection(output);
|
|
1100
|
+
reflection = {
|
|
1101
|
+
summary: circResult.summary,
|
|
1102
|
+
learnings: '',
|
|
1103
|
+
confidence: circResult.confidence,
|
|
1104
|
+
vote: circResult.vote,
|
|
1105
|
+
parse_failed: circResult.parse_failed,
|
|
1106
|
+
};
|
|
1107
|
+
circlingArtifacts = circResult.circling_artifacts;
|
|
1108
|
+
} else {
|
|
1109
|
+
reflection = parseReflection(output);
|
|
1110
|
+
}
|
|
765
1111
|
|
|
766
1112
|
// List modified files
|
|
767
1113
|
let artifacts = [];
|
|
@@ -781,16 +1127,20 @@ async function executeCollabTask(task) {
|
|
|
781
1127
|
node_id: NODE_ID,
|
|
782
1128
|
round: round_number,
|
|
783
1129
|
summary: reflection.summary,
|
|
784
|
-
learnings: reflection.learnings,
|
|
1130
|
+
learnings: reflection.learnings || '',
|
|
785
1131
|
artifacts,
|
|
786
1132
|
confidence: reflection.confidence,
|
|
787
1133
|
vote: reflection.vote,
|
|
788
1134
|
parse_failed: reflection.parse_failed,
|
|
1135
|
+
// Circling extensions
|
|
1136
|
+
circling_step: isCircling ? circling_step : null,
|
|
1137
|
+
circling_artifacts: circlingArtifacts,
|
|
789
1138
|
});
|
|
790
1139
|
const parseTag = reflection.parse_failed ? ' [PARSE FAILED]' : '';
|
|
791
|
-
|
|
1140
|
+
const artCount = circlingArtifacts.length > 0 ? `, ${circlingArtifacts.length} artifact(s)` : '';
|
|
1141
|
+
log(`COLLAB ${stepLabel}: Reflection submitted (vote: ${reflection.vote}, conf: ${reflection.confidence}${parseTag}${artCount})`);
|
|
792
1142
|
} catch (err) {
|
|
793
|
-
log(`COLLAB
|
|
1143
|
+
log(`COLLAB ${stepLabel}: Reflection submit failed: ${err.message}`);
|
|
794
1144
|
}
|
|
795
1145
|
|
|
796
1146
|
// Check if session is done (converged/completed/aborted)
|
|
@@ -897,6 +1247,48 @@ async function executeTask(task) {
|
|
|
897
1247
|
continue;
|
|
898
1248
|
}
|
|
899
1249
|
|
|
1250
|
+
// ── Mesh Harness: Mechanical Enforcement (LLM-agnostic) ──
|
|
1251
|
+
// Runs AFTER LLM exits successfully, BEFORE commit. This is the hard
|
|
1252
|
+
// enforcement layer — it doesn't depend on the LLM obeying prompt rules.
|
|
1253
|
+
const harnessResult = runMeshHarness({
|
|
1254
|
+
rules: getHarnessRules(),
|
|
1255
|
+
worktreePath,
|
|
1256
|
+
taskScope: task.scope,
|
|
1257
|
+
llmOutput: llmResult.stdout,
|
|
1258
|
+
hasMetric: !!task.metric,
|
|
1259
|
+
log,
|
|
1260
|
+
role: getRole(task.role),
|
|
1261
|
+
});
|
|
1262
|
+
|
|
1263
|
+
if (!harnessResult.pass) {
|
|
1264
|
+
log(`HARNESS BLOCKED: ${harnessResult.violations.length} violation(s)`);
|
|
1265
|
+
for (const v of harnessResult.violations) {
|
|
1266
|
+
log(` - [${v.rule}] ${v.message}`);
|
|
1267
|
+
}
|
|
1268
|
+
// Scope violations were already reverted by enforceScopeCheck.
|
|
1269
|
+
// Secret violations block the commit entirely.
|
|
1270
|
+
const hasSecrets = harnessResult.violations.some(v => v.rule === 'no-hardcoded-secrets');
|
|
1271
|
+
if (hasSecrets) {
|
|
1272
|
+
const attemptRecord = {
|
|
1273
|
+
approach: `Attempt ${attempt}: harness blocked — secrets detected`,
|
|
1274
|
+
result: harnessResult.violations.map(v => v.message).join('; '),
|
|
1275
|
+
keep: false,
|
|
1276
|
+
};
|
|
1277
|
+
attempts.push(attemptRecord);
|
|
1278
|
+
await natsRequest('mesh.tasks.attempt', { task_id: task.task_id, ...attemptRecord });
|
|
1279
|
+
log(`Attempt ${attempt}: harness blocked commit (secrets). Retrying.`);
|
|
1280
|
+
continue;
|
|
1281
|
+
}
|
|
1282
|
+
// Non-secret violations (scope reverts, output blocks): proceed with warnings
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
if (harnessResult.warnings.length > 0) {
|
|
1286
|
+
log(`HARNESS WARNINGS: ${harnessResult.warnings.length}`);
|
|
1287
|
+
for (const w of harnessResult.warnings) {
|
|
1288
|
+
log(` - [${w.rule}] ${w.message}`);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
|
|
900
1292
|
// If no metric, trust LLM output and complete
|
|
901
1293
|
if (!task.metric) {
|
|
902
1294
|
const attemptRecord = {
|
|
@@ -911,11 +1303,20 @@ async function executeTask(task) {
|
|
|
911
1303
|
const mergeResult = commitAndMergeWorktree(worktreePath, task.task_id, summary);
|
|
912
1304
|
const keepBranch = mergeResult && !mergeResult.merged; // keep on merge conflict
|
|
913
1305
|
|
|
1306
|
+
// Post-commit validation (conventional commits, etc.)
|
|
1307
|
+
if (mergeResult?.committed) {
|
|
1308
|
+
runPostCommitValidation(getHarnessRules(), worktreePath, log);
|
|
1309
|
+
}
|
|
1310
|
+
|
|
914
1311
|
await natsRequest('mesh.tasks.complete', {
|
|
915
1312
|
task_id: task.task_id,
|
|
916
1313
|
result: {
|
|
917
1314
|
success: true, summary, artifacts: [],
|
|
918
1315
|
cost: sessionInfo?.cost || null,
|
|
1316
|
+
harness: {
|
|
1317
|
+
violations: harnessResult.violations,
|
|
1318
|
+
warnings: harnessResult.warnings,
|
|
1319
|
+
},
|
|
919
1320
|
sha: mergeResult?.sha || null,
|
|
920
1321
|
merged: mergeResult?.merged ?? null,
|
|
921
1322
|
},
|
|
@@ -943,6 +1344,11 @@ async function executeTask(task) {
|
|
|
943
1344
|
const mergeResult = commitAndMergeWorktree(worktreePath, task.task_id, summary);
|
|
944
1345
|
const keepBranch = mergeResult && !mergeResult.merged;
|
|
945
1346
|
|
|
1347
|
+
// Post-commit validation (conventional commits, etc.)
|
|
1348
|
+
if (mergeResult?.committed) {
|
|
1349
|
+
runPostCommitValidation(getHarnessRules(), worktreePath, log);
|
|
1350
|
+
}
|
|
1351
|
+
|
|
946
1352
|
await natsRequest('mesh.tasks.complete', {
|
|
947
1353
|
task_id: task.task_id,
|
|
948
1354
|
result: {
|
|
@@ -952,6 +1358,10 @@ async function executeTask(task) {
|
|
|
952
1358
|
cost: sessionInfo?.cost || null,
|
|
953
1359
|
sha: mergeResult?.sha || null,
|
|
954
1360
|
merged: mergeResult?.merged ?? null,
|
|
1361
|
+
harness: {
|
|
1362
|
+
violations: harnessResult.violations,
|
|
1363
|
+
warnings: harnessResult.warnings,
|
|
1364
|
+
},
|
|
955
1365
|
},
|
|
956
1366
|
});
|
|
957
1367
|
cleanupWorktree(worktreePath, keepBranch);
|
|
@@ -1005,8 +1415,9 @@ async function main() {
|
|
|
1005
1415
|
log(` Poll interval: ${POLL_INTERVAL / 1000}s`);
|
|
1006
1416
|
log(` Mode: ${ONCE ? 'single task' : 'continuous'} ${DRY_RUN ? '(dry run)' : ''}`);
|
|
1007
1417
|
|
|
1418
|
+
const natsOpts = natsConnectOpts();
|
|
1008
1419
|
nc = await connect({
|
|
1009
|
-
|
|
1420
|
+
...natsOpts,
|
|
1010
1421
|
timeout: 5000,
|
|
1011
1422
|
reconnect: true,
|
|
1012
1423
|
maxReconnectAttempts: 10,
|