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
package/bin/mesh-agent.js
CHANGED
|
@@ -41,6 +41,9 @@ 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
49
|
const { NATS_URL } = require('../lib/nats-resolve');
|
|
@@ -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);
|
|
@@ -141,6 +213,12 @@ function buildInitialPrompt(task) {
|
|
|
141
213
|
parts.push('');
|
|
142
214
|
}
|
|
143
215
|
|
|
216
|
+
// Inject path-scoped coding rules matching task scope
|
|
217
|
+
injectRules(parts, task.scope);
|
|
218
|
+
|
|
219
|
+
// Inject role profile (responsibilities, boundaries, framework)
|
|
220
|
+
injectRole(parts, task.role);
|
|
221
|
+
|
|
144
222
|
parts.push('## Instructions');
|
|
145
223
|
parts.push('- Read the relevant files before making changes.');
|
|
146
224
|
parts.push('- Make minimal, focused changes. Do not add scope beyond what is asked.');
|
|
@@ -200,6 +278,12 @@ function buildRetryPrompt(task, previousAttempts, attemptNumber) {
|
|
|
200
278
|
parts.push('');
|
|
201
279
|
}
|
|
202
280
|
|
|
281
|
+
// Inject path-scoped coding rules matching task scope
|
|
282
|
+
injectRules(parts, task.scope);
|
|
283
|
+
|
|
284
|
+
// Inject role profile (responsibilities, boundaries, framework)
|
|
285
|
+
injectRole(parts, task.role);
|
|
286
|
+
|
|
203
287
|
parts.push('## Instructions');
|
|
204
288
|
parts.push('- Do NOT repeat a failed approach. Try something different.');
|
|
205
289
|
parts.push('- Read the relevant files before making changes.');
|
|
@@ -284,6 +368,13 @@ function commitAndMergeWorktree(worktreePath, taskId, summary) {
|
|
|
284
368
|
// Stage and commit all changes
|
|
285
369
|
execSync('git add -A', { cwd: worktreePath, timeout: 10000, stdio: 'pipe' });
|
|
286
370
|
const commitMsg = `mesh(${taskId}): ${(summary || 'task completed').slice(0, 72)}`;
|
|
371
|
+
|
|
372
|
+
// Pre-commit validation: check conventional commit format before committing
|
|
373
|
+
const conventionalPattern = /^(feat|fix|docs|style|refactor|test|chore|build|ci|perf|revert|mesh)(\(.+\))?: .+/;
|
|
374
|
+
if (!conventionalPattern.test(commitMsg)) {
|
|
375
|
+
log(`WARNING: commit message doesn't follow conventional format: "${commitMsg}"`);
|
|
376
|
+
}
|
|
377
|
+
|
|
287
378
|
execSync(`git commit -m "${commitMsg.replace(/"/g, '\\"')}"`, {
|
|
288
379
|
cwd: worktreePath, timeout: 10000, stdio: 'pipe',
|
|
289
380
|
});
|
|
@@ -294,18 +385,31 @@ function commitAndMergeWorktree(worktreePath, taskId, summary) {
|
|
|
294
385
|
|
|
295
386
|
log(`Committed ${sha} on ${branch}: ${commitMsg}`);
|
|
296
387
|
|
|
297
|
-
// Merge into main (from workspace)
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
388
|
+
// Merge into main (from workspace).
|
|
389
|
+
// Parallel collab: multiple nodes may merge concurrently. If the first attempt
|
|
390
|
+
// fails (e.g., another node merged first), retry once after pulling.
|
|
391
|
+
const mergeMsg = `Merge ${branch}: ${taskId}`;
|
|
392
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
393
|
+
try {
|
|
394
|
+
execSync(`git merge --no-ff "${branch}" -m "${mergeMsg.replace(/"/g, '\\"')}"`, {
|
|
395
|
+
cwd: WORKSPACE, timeout: 30000, stdio: 'pipe',
|
|
396
|
+
});
|
|
397
|
+
log(`Merged ${branch} into main${attempt > 0 ? ' (retry succeeded)' : ''}`);
|
|
398
|
+
return { committed: true, merged: true, sha };
|
|
399
|
+
} catch (mergeErr) {
|
|
400
|
+
execSync('git merge --abort', { cwd: WORKSPACE, timeout: 5000, stdio: 'ignore' });
|
|
401
|
+
if (attempt === 0) {
|
|
402
|
+
// First failure: pull and retry (handles race with parallel merge)
|
|
403
|
+
try {
|
|
404
|
+
log(`Merge attempt 1 failed for ${branch} — fast-forward pulling and retrying`);
|
|
405
|
+
execSync('git pull --ff-only', { cwd: WORKSPACE, timeout: 15000, stdio: 'pipe' });
|
|
406
|
+
} catch { /* best effort pull */ }
|
|
407
|
+
} else {
|
|
408
|
+
// Second failure: real conflict — keep branch for human resolution
|
|
409
|
+
log(`MERGE CONFLICT on ${branch} — branch kept for manual resolution`);
|
|
410
|
+
return { committed: true, merged: false, sha, conflict: true };
|
|
411
|
+
}
|
|
412
|
+
}
|
|
309
413
|
}
|
|
310
414
|
} catch (err) {
|
|
311
415
|
log(`Commit/merge warning: ${err.message}`);
|
|
@@ -377,14 +481,23 @@ function runLLM(prompt, task, worktreePath) {
|
|
|
377
481
|
timeout: (task.budget_minutes || 30) * 60 * 1000, // kill if exceeds budget
|
|
378
482
|
});
|
|
379
483
|
|
|
380
|
-
// Heartbeat: signal daemon with activity state
|
|
484
|
+
// Heartbeat: signal daemon with activity state.
|
|
485
|
+
// getActivityState reads Claude JSONL files — only useful for Claude provider.
|
|
486
|
+
// For other providers, send a basic heartbeat (process alive = active).
|
|
487
|
+
const isClaude = provider.name === 'claude';
|
|
381
488
|
const heartbeatTimer = setInterval(async () => {
|
|
382
489
|
try {
|
|
383
|
-
const activity = await getActivityState(cleanCwd);
|
|
384
490
|
const payload = { task_id: task.task_id };
|
|
385
|
-
if (
|
|
386
|
-
|
|
387
|
-
|
|
491
|
+
if (isClaude) {
|
|
492
|
+
const activity = await getActivityState(cleanCwd);
|
|
493
|
+
if (activity) {
|
|
494
|
+
payload.activity_state = activity.state;
|
|
495
|
+
payload.activity_timestamp = activity.timestamp?.toISOString();
|
|
496
|
+
}
|
|
497
|
+
} else {
|
|
498
|
+
// Non-Claude: process is running → active
|
|
499
|
+
payload.activity_state = 'active';
|
|
500
|
+
payload.activity_timestamp = new Date().toISOString();
|
|
388
501
|
}
|
|
389
502
|
await natsRequest('mesh.tasks.heartbeat', payload);
|
|
390
503
|
} catch {
|
|
@@ -488,6 +601,11 @@ function buildCollabPrompt(task, roundNumber, sharedIntel, myScope, myRole) {
|
|
|
488
601
|
}
|
|
489
602
|
}
|
|
490
603
|
|
|
604
|
+
// Inject path-scoped coding rules matching task scope
|
|
605
|
+
const collabScope = Array.isArray(myScope) ? myScope.map(s => s.replace('[REVIEW-ONLY] ', '')) : task.scope;
|
|
606
|
+
injectRules(parts, collabScope);
|
|
607
|
+
injectRole(parts, task.role);
|
|
608
|
+
|
|
491
609
|
if (task.success_criteria && task.success_criteria.length > 0) {
|
|
492
610
|
parts.push('## Success Criteria');
|
|
493
611
|
for (const c of task.success_criteria) {
|
|
@@ -603,6 +721,207 @@ function parseReflection(output) {
|
|
|
603
721
|
};
|
|
604
722
|
}
|
|
605
723
|
|
|
724
|
+
// ── Circling Strategy: Prompt Builder + Parser ───────
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Build a prompt for a circling strategy step.
|
|
728
|
+
* Role-specific: Worker gets different instructions than Reviewers at each step.
|
|
729
|
+
*/
|
|
730
|
+
function buildCirclingPrompt(task, circlingData) {
|
|
731
|
+
const { circling_phase, circling_step, circling_subround, directed_input, my_role } = circlingData;
|
|
732
|
+
const isWorker = my_role === 'worker';
|
|
733
|
+
const parts = [];
|
|
734
|
+
|
|
735
|
+
parts.push(`# Task: ${task.title}`);
|
|
736
|
+
parts.push('');
|
|
737
|
+
|
|
738
|
+
switch (circling_phase) {
|
|
739
|
+
case 'init':
|
|
740
|
+
if (isWorker) {
|
|
741
|
+
parts.push('## Your Role: WORKER (Central Authority)');
|
|
742
|
+
parts.push('');
|
|
743
|
+
parts.push('You are the Worker in a Circling Strategy collaboration. You OWN the deliverable.');
|
|
744
|
+
parts.push('Produce your initial work artifact (v0) — your first draft/implementation based on the task plan below.');
|
|
745
|
+
parts.push('');
|
|
746
|
+
} else {
|
|
747
|
+
parts.push('## Your Role: REVIEWER');
|
|
748
|
+
parts.push('');
|
|
749
|
+
parts.push('You are a Reviewer in a Circling Strategy collaboration.');
|
|
750
|
+
parts.push('Produce your **reviewStrategy** — your methodology and focus areas for reviewing this type of work.');
|
|
751
|
+
parts.push('Define: what you will look for, what frameworks you will apply, what your evaluation criteria are.');
|
|
752
|
+
parts.push('');
|
|
753
|
+
}
|
|
754
|
+
break;
|
|
755
|
+
|
|
756
|
+
case 'circling':
|
|
757
|
+
if (circling_step === 1) {
|
|
758
|
+
// Step 1 — Review Pass
|
|
759
|
+
parts.push(`## Sub-Round ${circling_subround}, Step 1: Review Pass`);
|
|
760
|
+
parts.push('');
|
|
761
|
+
if (isWorker) {
|
|
762
|
+
parts.push('## Your Role: WORKER — Analyze Review Strategies');
|
|
763
|
+
parts.push('');
|
|
764
|
+
parts.push('Below are the review STRATEGIES from both reviewers (NOT their review findings).');
|
|
765
|
+
parts.push('Analyze each strategy: what they focus on well, what they miss, how they could improve.');
|
|
766
|
+
parts.push('Your feedback will help reviewers sharpen their approaches.');
|
|
767
|
+
parts.push('Do NOT touch the work artifact in this step.');
|
|
768
|
+
parts.push('');
|
|
769
|
+
} else {
|
|
770
|
+
parts.push('## Your Role: REVIEWER — Review the Work Artifact');
|
|
771
|
+
parts.push('');
|
|
772
|
+
parts.push('Below is the work artifact. Review it using your review strategy.');
|
|
773
|
+
parts.push('Produce concrete, actionable findings. For each finding, specify:');
|
|
774
|
+
parts.push('- What you found (the issue or observation)');
|
|
775
|
+
parts.push('- Where in the artifact (location/line/section)');
|
|
776
|
+
parts.push('- Why it matters (impact)');
|
|
777
|
+
parts.push('- What you recommend (suggested fix or improvement)');
|
|
778
|
+
parts.push('');
|
|
779
|
+
}
|
|
780
|
+
} else if (circling_step === 2) {
|
|
781
|
+
// Step 2 — Integration + Refinement
|
|
782
|
+
parts.push(`## Sub-Round ${circling_subround}, Step 2: Integration & Refinement`);
|
|
783
|
+
parts.push('');
|
|
784
|
+
if (isWorker) {
|
|
785
|
+
parts.push('## Your Role: WORKER — Judge Reviews & Update Artifact');
|
|
786
|
+
parts.push('');
|
|
787
|
+
parts.push('Below are the review artifacts from both reviewers.');
|
|
788
|
+
parts.push('For EACH review finding, judge it:');
|
|
789
|
+
parts.push('- **ACCEPTED**: implement the suggestion and note where');
|
|
790
|
+
parts.push('- **REJECTED**: explain why with reasoning');
|
|
791
|
+
parts.push('- **MODIFIED**: implement differently and explain why');
|
|
792
|
+
parts.push('');
|
|
793
|
+
parts.push('Then produce your UPDATED work artifact incorporating accepted changes.');
|
|
794
|
+
parts.push('Also produce a reconciliationDoc summarizing all judgments.');
|
|
795
|
+
parts.push('');
|
|
796
|
+
parts.push('You must output TWO artifacts using the multi-artifact format below.');
|
|
797
|
+
parts.push('');
|
|
798
|
+
} else {
|
|
799
|
+
parts.push('## Your Role: REVIEWER — Refine Your Strategy');
|
|
800
|
+
parts.push('');
|
|
801
|
+
parts.push("Below is the Worker's analysis of your review strategy (and the other reviewer's).");
|
|
802
|
+
parts.push("Evaluate the Worker's feedback honestly:");
|
|
803
|
+
parts.push("- If the feedback is valid, adjust your strategy accordingly");
|
|
804
|
+
parts.push("- If you disagree, explain why and keep your approach");
|
|
805
|
+
parts.push('Produce your REFINED reviewStrategy.');
|
|
806
|
+
parts.push('');
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
break;
|
|
810
|
+
|
|
811
|
+
case 'finalization':
|
|
812
|
+
parts.push('## FINALIZATION');
|
|
813
|
+
parts.push('');
|
|
814
|
+
if (isWorker) {
|
|
815
|
+
parts.push('## Your Role: WORKER — Final Delivery');
|
|
816
|
+
parts.push('');
|
|
817
|
+
parts.push('Produce your FINAL formatted work artifact.');
|
|
818
|
+
parts.push('Also produce a completionDiff — a checklist comparing original plan items vs what was delivered:');
|
|
819
|
+
parts.push(' [x] Item implemented');
|
|
820
|
+
parts.push(' [ ] Item NOT implemented — reason / deferred to follow-up');
|
|
821
|
+
parts.push('');
|
|
822
|
+
parts.push('You must output TWO artifacts using the multi-artifact format below.');
|
|
823
|
+
parts.push('');
|
|
824
|
+
} else {
|
|
825
|
+
parts.push('## Your Role: REVIEWER — Final Sign-Off');
|
|
826
|
+
parts.push('');
|
|
827
|
+
parts.push('Review the final work artifact against the original task plan.');
|
|
828
|
+
parts.push('Either:');
|
|
829
|
+
parts.push('- **APPROVE**: vote "converged" — the work meets quality standards');
|
|
830
|
+
parts.push('- **FLAG CONCERN**: vote "blocked" — describe remaining critical concerns');
|
|
831
|
+
parts.push('');
|
|
832
|
+
}
|
|
833
|
+
break;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// Add directed input
|
|
837
|
+
if (directed_input) {
|
|
838
|
+
parts.push('---');
|
|
839
|
+
parts.push('');
|
|
840
|
+
parts.push(directed_input);
|
|
841
|
+
parts.push('');
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// Add artifact output instructions
|
|
845
|
+
const isMultiArtifact = isWorker && (circling_step === 2 || circling_phase === 'finalization');
|
|
846
|
+
parts.push('---');
|
|
847
|
+
parts.push('');
|
|
848
|
+
|
|
849
|
+
if (isMultiArtifact) {
|
|
850
|
+
// Multi-artifact output format (Worker Step 2 and Finalization)
|
|
851
|
+
const art1Type = circling_phase === 'finalization' ? 'workArtifact' : 'workArtifact';
|
|
852
|
+
const art2Type = circling_phase === 'finalization' ? 'completionDiff' : 'reconciliationDoc';
|
|
853
|
+
|
|
854
|
+
parts.push('## Output Format (TWO artifacts required)');
|
|
855
|
+
parts.push('');
|
|
856
|
+
parts.push('Produce your first artifact, then wrap it:');
|
|
857
|
+
parts.push('');
|
|
858
|
+
parts.push('===CIRCLING_ARTIFACT===');
|
|
859
|
+
parts.push(`type: ${art1Type}`);
|
|
860
|
+
parts.push('===END_ARTIFACT===');
|
|
861
|
+
parts.push('');
|
|
862
|
+
parts.push('Then produce your second artifact:');
|
|
863
|
+
parts.push('');
|
|
864
|
+
parts.push('===CIRCLING_ARTIFACT===');
|
|
865
|
+
parts.push(`type: ${art2Type}`);
|
|
866
|
+
parts.push('===END_ARTIFACT===');
|
|
867
|
+
parts.push('');
|
|
868
|
+
parts.push('Then end with:');
|
|
869
|
+
} else {
|
|
870
|
+
parts.push('## Output Format');
|
|
871
|
+
parts.push('');
|
|
872
|
+
parts.push('Produce your work above, then end with:');
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// Determine the correct type for single-artifact output
|
|
876
|
+
let artifactType = 'workArtifact'; // default
|
|
877
|
+
if (!isWorker) {
|
|
878
|
+
if (circling_phase === 'init' || circling_step === 2) artifactType = 'reviewStrategy';
|
|
879
|
+
else if (circling_step === 1) artifactType = 'reviewArtifact';
|
|
880
|
+
else if (circling_phase === 'finalization') artifactType = 'reviewSignOff';
|
|
881
|
+
} else {
|
|
882
|
+
if (circling_step === 1) artifactType = 'workerReviewsAnalysis';
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
parts.push('');
|
|
886
|
+
parts.push('===CIRCLING_REFLECTION===');
|
|
887
|
+
parts.push(`type: ${artifactType}`);
|
|
888
|
+
parts.push('summary: [1-2 sentences about what you did]');
|
|
889
|
+
parts.push('confidence: [0.0 to 1.0]');
|
|
890
|
+
if (circling_phase === 'finalization') {
|
|
891
|
+
parts.push('vote: [converged|blocked]');
|
|
892
|
+
} else {
|
|
893
|
+
parts.push('vote: [continue|converged|blocked]');
|
|
894
|
+
}
|
|
895
|
+
parts.push('===END_REFLECTION===');
|
|
896
|
+
parts.push('');
|
|
897
|
+
parts.push('Rules:');
|
|
898
|
+
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.');
|
|
899
|
+
parts.push('- confidence: a number between 0.0 and 1.0');
|
|
900
|
+
if (circling_phase === 'finalization') {
|
|
901
|
+
parts.push('- vote: "converged" (work meets quality standards) or "blocked" (critical issue remains)');
|
|
902
|
+
} else {
|
|
903
|
+
parts.push('- vote: "continue" (more work needed), "converged" (satisfied), or "blocked" (critical issue)');
|
|
904
|
+
}
|
|
905
|
+
parts.push('- The reflection block MUST be the last thing in your response');
|
|
906
|
+
|
|
907
|
+
return parts.join('\n');
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// Parser extracted to lib/circling-parser.js — single source of truth for both
|
|
911
|
+
// production code and tests. Zero external dependencies.
|
|
912
|
+
const { parseCirclingReflection: _parseCircling } = require('../lib/circling-parser');
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* Parse a circling strategy output. Delegates to lib/circling-parser.js
|
|
916
|
+
* with agent-specific options (logger, legacy fallback).
|
|
917
|
+
*/
|
|
918
|
+
function parseCirclingReflection(output) {
|
|
919
|
+
return _parseCircling(output, {
|
|
920
|
+
log: (msg) => log(msg),
|
|
921
|
+
legacyParser: parseReflection,
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
|
|
606
925
|
// ── Collaborative Task Execution ──────────────────────
|
|
607
926
|
|
|
608
927
|
/**
|
|
@@ -649,6 +968,13 @@ async function executeCollabTask(task) {
|
|
|
649
968
|
return;
|
|
650
969
|
}
|
|
651
970
|
|
|
971
|
+
// Subscribe to round notifications BEFORE joining — prevents race condition
|
|
972
|
+
// where the daemon starts round 1 immediately upon last node joining,
|
|
973
|
+
// but the joining node hasn't subscribed yet and misses the notification.
|
|
974
|
+
const roundSub = nc.subscribe(`mesh.collab.${sessionId}.node.${NODE_ID}.round`);
|
|
975
|
+
let roundsDone = false;
|
|
976
|
+
let lastKnownSessionStatus = null; // tracks why rounds ended (completed/aborted/converged)
|
|
977
|
+
|
|
652
978
|
// Join the session using the discovered session_id
|
|
653
979
|
let session;
|
|
654
980
|
try {
|
|
@@ -659,6 +985,7 @@ async function executeCollabTask(task) {
|
|
|
659
985
|
session = joinResult;
|
|
660
986
|
} catch (err) {
|
|
661
987
|
log(`COLLAB JOIN FAILED: ${err.message} (session: ${sessionId})`);
|
|
988
|
+
roundSub.unsubscribe();
|
|
662
989
|
await natsRequest('mesh.tasks.fail', {
|
|
663
990
|
task_id: task.task_id,
|
|
664
991
|
reason: `Failed to join collab session ${sessionId}: ${err.message}`,
|
|
@@ -669,6 +996,7 @@ async function executeCollabTask(task) {
|
|
|
669
996
|
|
|
670
997
|
if (!session) {
|
|
671
998
|
log(`COLLAB JOIN RETURNED NULL for session ${sessionId}`);
|
|
999
|
+
roundSub.unsubscribe();
|
|
672
1000
|
await natsRequest('mesh.tasks.fail', {
|
|
673
1001
|
task_id: task.task_id,
|
|
674
1002
|
reason: `Collab session ${sessionId} rejected join (full, closed, or duplicate node).`,
|
|
@@ -684,87 +1012,146 @@ async function executeCollabTask(task) {
|
|
|
684
1012
|
const worktreePath = createWorktree(`${task.task_id}-${NODE_ID}`);
|
|
685
1013
|
const taskDir = worktreePath || WORKSPACE;
|
|
686
1014
|
|
|
687
|
-
//
|
|
688
|
-
const
|
|
689
|
-
|
|
1015
|
+
// Periodic session heartbeat — detects abort/completion while waiting for rounds
|
|
1016
|
+
const sessionHeartbeat = setInterval(async () => {
|
|
1017
|
+
try {
|
|
1018
|
+
const status = await natsRequest('mesh.collab.status', { session_id: sessionId }, 5000);
|
|
1019
|
+
if (['aborted', 'completed'].includes(status.status)) {
|
|
1020
|
+
log(`COLLAB HEARTBEAT: Session ${sessionId} is ${status.status}. Unsubscribing.`);
|
|
1021
|
+
lastKnownSessionStatus = status.status;
|
|
1022
|
+
roundsDone = true;
|
|
1023
|
+
roundSub.unsubscribe();
|
|
1024
|
+
}
|
|
1025
|
+
} catch { /* best effort */ }
|
|
1026
|
+
}, 10000);
|
|
690
1027
|
|
|
691
1028
|
// Signal start
|
|
692
1029
|
await natsRequest('mesh.tasks.start', { task_id: task.task_id }).catch(() => {});
|
|
693
1030
|
|
|
694
|
-
|
|
695
|
-
|
|
1031
|
+
try {
|
|
1032
|
+
for await (const roundMsg of roundSub) {
|
|
1033
|
+
if (roundsDone) break;
|
|
696
1034
|
|
|
697
|
-
|
|
698
|
-
|
|
1035
|
+
const roundData = JSON.parse(sc.decode(roundMsg.data));
|
|
1036
|
+
const { round_number, shared_intel, directed_input, my_scope, my_role, mode, current_turn,
|
|
1037
|
+
circling_phase, circling_step, circling_subround } = roundData;
|
|
699
1038
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
1039
|
+
// Sequential mode safety guard: skip if it's not our turn.
|
|
1040
|
+
if (mode === 'sequential' && current_turn && current_turn !== NODE_ID) {
|
|
1041
|
+
log(`COLLAB R${round_number}: Not our turn (current: ${current_turn}). Waiting.`);
|
|
1042
|
+
continue;
|
|
1043
|
+
}
|
|
705
1044
|
|
|
706
|
-
|
|
1045
|
+
const isCircling = mode === 'circling_strategy';
|
|
1046
|
+
const stepLabel = isCircling
|
|
1047
|
+
? `${circling_phase === 'init' ? 'Init' : circling_phase === 'finalization' ? 'Final' : `SR${circling_subround}/S${circling_step}`}`
|
|
1048
|
+
: `R${round_number}`;
|
|
1049
|
+
log(`COLLAB ${stepLabel}: Starting work (role: ${my_role}, scope: ${JSON.stringify(my_scope)})`);
|
|
707
1050
|
|
|
708
|
-
|
|
709
|
-
|
|
1051
|
+
// Build prompt — circling uses directed inputs, other modes use shared intel
|
|
1052
|
+
const prompt = isCircling
|
|
1053
|
+
? buildCirclingPrompt(task, roundData)
|
|
1054
|
+
: buildCollabPrompt(task, round_number, shared_intel, my_scope, my_role);
|
|
710
1055
|
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
1056
|
+
if (DRY_RUN) {
|
|
1057
|
+
log(`[DRY RUN] Collab prompt:\n${prompt}`);
|
|
1058
|
+
break;
|
|
1059
|
+
}
|
|
715
1060
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
1061
|
+
// Execute provider (LLM or shell)
|
|
1062
|
+
const llmResult = await runLLM(prompt, task, worktreePath);
|
|
1063
|
+
const output = llmResult.stdout || '';
|
|
1064
|
+
|
|
1065
|
+
// Parse reflection — circling uses delimiter-based parser, others use JSON-block parser
|
|
1066
|
+
let reflection;
|
|
1067
|
+
let circlingArtifacts = [];
|
|
1068
|
+
|
|
1069
|
+
if (llmResult.provider === 'shell') {
|
|
1070
|
+
reflection = {
|
|
1071
|
+
summary: output.trim().slice(-500) || '(no output)',
|
|
1072
|
+
learnings: '',
|
|
1073
|
+
confidence: llmResult.exitCode === 0 ? 1.0 : 0.0,
|
|
1074
|
+
vote: llmResult.exitCode === 0 ? 'converged' : 'continue',
|
|
1075
|
+
parse_failed: false,
|
|
1076
|
+
};
|
|
1077
|
+
} else if (isCircling) {
|
|
1078
|
+
const circResult = parseCirclingReflection(output);
|
|
1079
|
+
reflection = {
|
|
1080
|
+
summary: circResult.summary,
|
|
1081
|
+
learnings: '',
|
|
1082
|
+
confidence: circResult.confidence,
|
|
1083
|
+
vote: circResult.vote,
|
|
1084
|
+
parse_failed: circResult.parse_failed,
|
|
1085
|
+
};
|
|
1086
|
+
circlingArtifacts = circResult.circling_artifacts;
|
|
1087
|
+
} else {
|
|
1088
|
+
reflection = parseReflection(output);
|
|
1089
|
+
}
|
|
719
1090
|
|
|
720
|
-
|
|
721
|
-
|
|
1091
|
+
// List modified files
|
|
1092
|
+
let artifacts = [];
|
|
1093
|
+
try {
|
|
1094
|
+
if (worktreePath) {
|
|
1095
|
+
const status = require('child_process').execSync('git status --porcelain', {
|
|
1096
|
+
cwd: worktreePath, timeout: 5000, encoding: 'utf-8',
|
|
1097
|
+
}).trim();
|
|
1098
|
+
artifacts = status.split('\n').filter(Boolean).map(line => line.slice(3));
|
|
1099
|
+
}
|
|
1100
|
+
} catch { /* best effort */ }
|
|
722
1101
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
1102
|
+
// Submit reflection
|
|
1103
|
+
try {
|
|
1104
|
+
await natsRequest('mesh.collab.reflect', {
|
|
1105
|
+
session_id: sessionId,
|
|
1106
|
+
node_id: NODE_ID,
|
|
1107
|
+
round: round_number,
|
|
1108
|
+
summary: reflection.summary,
|
|
1109
|
+
learnings: reflection.learnings || '',
|
|
1110
|
+
artifacts,
|
|
1111
|
+
confidence: reflection.confidence,
|
|
1112
|
+
vote: reflection.vote,
|
|
1113
|
+
parse_failed: reflection.parse_failed,
|
|
1114
|
+
// Circling extensions
|
|
1115
|
+
circling_step: isCircling ? circling_step : null,
|
|
1116
|
+
circling_artifacts: circlingArtifacts,
|
|
1117
|
+
});
|
|
1118
|
+
const parseTag = reflection.parse_failed ? ' [PARSE FAILED]' : '';
|
|
1119
|
+
const artCount = circlingArtifacts.length > 0 ? `, ${circlingArtifacts.length} artifact(s)` : '';
|
|
1120
|
+
log(`COLLAB ${stepLabel}: Reflection submitted (vote: ${reflection.vote}, conf: ${reflection.confidence}${parseTag}${artCount})`);
|
|
1121
|
+
} catch (err) {
|
|
1122
|
+
log(`COLLAB ${stepLabel}: Reflection submit failed: ${err.message}`);
|
|
731
1123
|
}
|
|
732
|
-
} catch { /* best effort */ }
|
|
733
1124
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
confidence: reflection.confidence,
|
|
744
|
-
vote: reflection.vote,
|
|
745
|
-
parse_failed: reflection.parse_failed,
|
|
746
|
-
});
|
|
747
|
-
const parseTag = reflection.parse_failed ? ' [PARSE FAILED]' : '';
|
|
748
|
-
log(`COLLAB R${round_number}: Reflection submitted (vote: ${reflection.vote}, conf: ${reflection.confidence}${parseTag})`);
|
|
749
|
-
} catch (err) {
|
|
750
|
-
log(`COLLAB R${round_number}: Reflection submit failed: ${err.message}`);
|
|
1125
|
+
// Check if session is done (converged/completed/aborted)
|
|
1126
|
+
try {
|
|
1127
|
+
const status = await natsRequest('mesh.collab.status', { session_id: sessionId });
|
|
1128
|
+
if (['converged', 'completed', 'aborted'].includes(status.status)) {
|
|
1129
|
+
log(`COLLAB: Session ${sessionId} is ${status.status}. Done.`);
|
|
1130
|
+
lastKnownSessionStatus = status.status;
|
|
1131
|
+
roundsDone = true;
|
|
1132
|
+
}
|
|
1133
|
+
} catch { /* continue listening */ }
|
|
751
1134
|
}
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
const status = await natsRequest('mesh.collab.status', { session_id: sessionId });
|
|
756
|
-
if (['converged', 'completed', 'aborted'].includes(status.status)) {
|
|
757
|
-
log(`COLLAB: Session ${sessionId} is ${status.status}. Done.`);
|
|
758
|
-
roundsDone = true;
|
|
759
|
-
}
|
|
760
|
-
} catch { /* continue listening */ }
|
|
1135
|
+
} finally {
|
|
1136
|
+
clearInterval(sessionHeartbeat);
|
|
1137
|
+
roundSub.unsubscribe();
|
|
761
1138
|
}
|
|
762
1139
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
//
|
|
766
|
-
|
|
767
|
-
|
|
1140
|
+
// Commit and merge only on successful convergence — don't merge partial
|
|
1141
|
+
// work from aborted/failed sessions into main.
|
|
1142
|
+
// Uses lastKnownSessionStatus (set during round loop or heartbeat) instead of
|
|
1143
|
+
// a fresh network read, which could see stale state due to NATS latency.
|
|
1144
|
+
try {
|
|
1145
|
+
if (['completed', 'converged'].includes(lastKnownSessionStatus)) {
|
|
1146
|
+
const mergeResult = commitAndMergeWorktree(worktreePath, `${task.task_id}-${NODE_ID}`, `collab contribution from ${NODE_ID}`);
|
|
1147
|
+
cleanupWorktree(worktreePath, mergeResult && !mergeResult?.merged);
|
|
1148
|
+
} else {
|
|
1149
|
+
log(`COLLAB: Session ${sessionId} ended as ${lastKnownSessionStatus || 'unknown'} — discarding worktree`);
|
|
1150
|
+
cleanupWorktree(worktreePath, false);
|
|
1151
|
+
}
|
|
1152
|
+
} catch (err) {
|
|
1153
|
+
log(`COLLAB WORKTREE CLEANUP FAILED: ${err.message}`);
|
|
1154
|
+
}
|
|
768
1155
|
|
|
769
1156
|
writeAgentState('idle', null);
|
|
770
1157
|
log(`COLLAB DONE: ${task.task_id} (node: ${NODE_ID})`);
|
|
@@ -839,6 +1226,48 @@ async function executeTask(task) {
|
|
|
839
1226
|
continue;
|
|
840
1227
|
}
|
|
841
1228
|
|
|
1229
|
+
// ── Mesh Harness: Mechanical Enforcement (LLM-agnostic) ──
|
|
1230
|
+
// Runs AFTER LLM exits successfully, BEFORE commit. This is the hard
|
|
1231
|
+
// enforcement layer — it doesn't depend on the LLM obeying prompt rules.
|
|
1232
|
+
const harnessResult = runMeshHarness({
|
|
1233
|
+
rules: getHarnessRules(),
|
|
1234
|
+
worktreePath,
|
|
1235
|
+
taskScope: task.scope,
|
|
1236
|
+
llmOutput: llmResult.stdout,
|
|
1237
|
+
hasMetric: !!task.metric,
|
|
1238
|
+
log,
|
|
1239
|
+
role: getRole(task.role),
|
|
1240
|
+
});
|
|
1241
|
+
|
|
1242
|
+
if (!harnessResult.pass) {
|
|
1243
|
+
log(`HARNESS BLOCKED: ${harnessResult.violations.length} violation(s)`);
|
|
1244
|
+
for (const v of harnessResult.violations) {
|
|
1245
|
+
log(` - [${v.rule}] ${v.message}`);
|
|
1246
|
+
}
|
|
1247
|
+
// Scope violations were already reverted by enforceScopeCheck.
|
|
1248
|
+
// Secret violations block the commit entirely.
|
|
1249
|
+
const hasSecrets = harnessResult.violations.some(v => v.rule === 'no-hardcoded-secrets');
|
|
1250
|
+
if (hasSecrets) {
|
|
1251
|
+
const attemptRecord = {
|
|
1252
|
+
approach: `Attempt ${attempt}: harness blocked — secrets detected`,
|
|
1253
|
+
result: harnessResult.violations.map(v => v.message).join('; '),
|
|
1254
|
+
keep: false,
|
|
1255
|
+
};
|
|
1256
|
+
attempts.push(attemptRecord);
|
|
1257
|
+
await natsRequest('mesh.tasks.attempt', { task_id: task.task_id, ...attemptRecord });
|
|
1258
|
+
log(`Attempt ${attempt}: harness blocked commit (secrets). Retrying.`);
|
|
1259
|
+
continue;
|
|
1260
|
+
}
|
|
1261
|
+
// Non-secret violations (scope reverts, output blocks): proceed with warnings
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
if (harnessResult.warnings.length > 0) {
|
|
1265
|
+
log(`HARNESS WARNINGS: ${harnessResult.warnings.length}`);
|
|
1266
|
+
for (const w of harnessResult.warnings) {
|
|
1267
|
+
log(` - [${w.rule}] ${w.message}`);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
|
|
842
1271
|
// If no metric, trust LLM output and complete
|
|
843
1272
|
if (!task.metric) {
|
|
844
1273
|
const attemptRecord = {
|
|
@@ -853,11 +1282,20 @@ async function executeTask(task) {
|
|
|
853
1282
|
const mergeResult = commitAndMergeWorktree(worktreePath, task.task_id, summary);
|
|
854
1283
|
const keepBranch = mergeResult && !mergeResult.merged; // keep on merge conflict
|
|
855
1284
|
|
|
1285
|
+
// Post-commit validation (conventional commits, etc.)
|
|
1286
|
+
if (mergeResult?.committed) {
|
|
1287
|
+
runPostCommitValidation(getHarnessRules(), worktreePath, log);
|
|
1288
|
+
}
|
|
1289
|
+
|
|
856
1290
|
await natsRequest('mesh.tasks.complete', {
|
|
857
1291
|
task_id: task.task_id,
|
|
858
1292
|
result: {
|
|
859
1293
|
success: true, summary, artifacts: [],
|
|
860
1294
|
cost: sessionInfo?.cost || null,
|
|
1295
|
+
harness: {
|
|
1296
|
+
violations: harnessResult.violations,
|
|
1297
|
+
warnings: harnessResult.warnings,
|
|
1298
|
+
},
|
|
861
1299
|
sha: mergeResult?.sha || null,
|
|
862
1300
|
merged: mergeResult?.merged ?? null,
|
|
863
1301
|
},
|
|
@@ -885,6 +1323,11 @@ async function executeTask(task) {
|
|
|
885
1323
|
const mergeResult = commitAndMergeWorktree(worktreePath, task.task_id, summary);
|
|
886
1324
|
const keepBranch = mergeResult && !mergeResult.merged;
|
|
887
1325
|
|
|
1326
|
+
// Post-commit validation (conventional commits, etc.)
|
|
1327
|
+
if (mergeResult?.committed) {
|
|
1328
|
+
runPostCommitValidation(getHarnessRules(), worktreePath, log);
|
|
1329
|
+
}
|
|
1330
|
+
|
|
888
1331
|
await natsRequest('mesh.tasks.complete', {
|
|
889
1332
|
task_id: task.task_id,
|
|
890
1333
|
result: {
|
|
@@ -894,6 +1337,10 @@ async function executeTask(task) {
|
|
|
894
1337
|
cost: sessionInfo?.cost || null,
|
|
895
1338
|
sha: mergeResult?.sha || null,
|
|
896
1339
|
merged: mergeResult?.merged ?? null,
|
|
1340
|
+
harness: {
|
|
1341
|
+
violations: harnessResult.violations,
|
|
1342
|
+
warnings: harnessResult.warnings,
|
|
1343
|
+
},
|
|
897
1344
|
},
|
|
898
1345
|
});
|
|
899
1346
|
cleanupWorktree(worktreePath, keepBranch);
|
|
@@ -986,8 +1433,82 @@ async function main() {
|
|
|
986
1433
|
})();
|
|
987
1434
|
log(` Listening: mesh.agent.${NODE_ID}.alive`);
|
|
988
1435
|
|
|
1436
|
+
// Subscribe to collab recruit broadcasts — allows this node to join
|
|
1437
|
+
// collab sessions without being the claiming node
|
|
1438
|
+
const recruitSub = nc.subscribe('mesh.collab.*.recruit');
|
|
1439
|
+
(async () => {
|
|
1440
|
+
for await (const msg of recruitSub) {
|
|
1441
|
+
try {
|
|
1442
|
+
const recruit = JSON.parse(sc.decode(msg.data));
|
|
1443
|
+
if (currentTaskId) continue; // busy
|
|
1444
|
+
|
|
1445
|
+
// Fetch task to check preferences and get collab spec
|
|
1446
|
+
const task = await natsRequest('mesh.tasks.get', { task_id: recruit.task_id }, 5000);
|
|
1447
|
+
if (!task || !task.collaboration) continue;
|
|
1448
|
+
if (task.owner === NODE_ID) continue; // we claimed it, already handling
|
|
1449
|
+
|
|
1450
|
+
// Check preferred_nodes
|
|
1451
|
+
if (task.preferred_nodes && task.preferred_nodes.length > 0) {
|
|
1452
|
+
if (!task.preferred_nodes.includes(NODE_ID)) continue;
|
|
1453
|
+
}
|
|
1454
|
+
// Check exclude_nodes
|
|
1455
|
+
if (task.exclude_nodes && task.exclude_nodes.length > 0) {
|
|
1456
|
+
if (task.exclude_nodes.includes(NODE_ID)) continue;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
log(`RECRUIT: Joining collab session ${recruit.session_id} for task ${recruit.task_id}`);
|
|
1460
|
+
currentTaskId = task.task_id;
|
|
1461
|
+
await executeCollabTask(task);
|
|
1462
|
+
currentTaskId = null;
|
|
1463
|
+
} catch (err) {
|
|
1464
|
+
log(`RECRUIT ERROR: ${err.message}`);
|
|
1465
|
+
currentTaskId = null;
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
})();
|
|
1469
|
+
log(` Listening: mesh.collab.*.recruit (collab recruiting)`);
|
|
1470
|
+
|
|
1471
|
+
// Also poll for recruiting sessions on idle — catches recruits we missed
|
|
1472
|
+
async function checkRecruitingSessions() {
|
|
1473
|
+
if (currentTaskId) return; // busy
|
|
1474
|
+
try {
|
|
1475
|
+
const sessions = await natsRequest('mesh.collab.recruiting', {}, 5000);
|
|
1476
|
+
if (!sessions || !Array.isArray(sessions) || sessions.length === 0) return;
|
|
1477
|
+
|
|
1478
|
+
for (const s of sessions) {
|
|
1479
|
+
if (currentTaskId) break; // became busy
|
|
1480
|
+
// Skip if we already joined this session
|
|
1481
|
+
if (s.node_ids && s.node_ids.includes(NODE_ID)) continue;
|
|
1482
|
+
// Skip if session is full
|
|
1483
|
+
if (s.max_nodes && s.current_nodes >= s.max_nodes) continue;
|
|
1484
|
+
|
|
1485
|
+
// Fetch task to check preferences
|
|
1486
|
+
const task = await natsRequest('mesh.tasks.get', { task_id: s.task_id }, 5000);
|
|
1487
|
+
if (!task || !task.collaboration) continue;
|
|
1488
|
+
if (task.preferred_nodes && task.preferred_nodes.length > 0) {
|
|
1489
|
+
if (!task.preferred_nodes.includes(NODE_ID)) continue;
|
|
1490
|
+
}
|
|
1491
|
+
if (task.exclude_nodes && task.exclude_nodes.length > 0) {
|
|
1492
|
+
if (task.exclude_nodes.includes(NODE_ID)) continue;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
log(`RECRUIT POLL: Joining collab session ${s.session_id} for task ${s.task_id}`);
|
|
1496
|
+
currentTaskId = task.task_id;
|
|
1497
|
+
await executeCollabTask(task);
|
|
1498
|
+
currentTaskId = null;
|
|
1499
|
+
}
|
|
1500
|
+
} catch { /* silent — recruiting poll is best-effort */ }
|
|
1501
|
+
}
|
|
1502
|
+
|
|
989
1503
|
while (running) {
|
|
990
1504
|
try {
|
|
1505
|
+
// Check for recruiting collab sessions before trying to claim
|
|
1506
|
+
await checkRecruitingSessions();
|
|
1507
|
+
if (currentTaskId) {
|
|
1508
|
+
await new Promise(r => setTimeout(r, POLL_INTERVAL));
|
|
1509
|
+
continue;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
991
1512
|
// Claim next available task (longer timeout — KV operations on remote NATS can be slow)
|
|
992
1513
|
const task = await natsRequest('mesh.tasks.claim', { node_id: NODE_ID }, 60000);
|
|
993
1514
|
|
|
@@ -1035,3 +1556,4 @@ main().catch(err => {
|
|
|
1035
1556
|
console.error(`[mesh-agent] Fatal: ${err.message}`);
|
|
1036
1557
|
process.exit(1);
|
|
1037
1558
|
});
|
|
1559
|
+
// deploy-v7f0130b
|