openclaw-node-harness 2.0.4 → 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 +401 -12
- package/bin/mesh-bridge.js +66 -1
- package/bin/mesh-task-daemon.js +816 -26
- package/bin/mesh.js +403 -1
- 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 +293 -8
- package/lib/circling-parser.js +119 -0
- package/lib/hyperagent-store.mjs +652 -0
- package/lib/kanban-io.js +9 -0
- 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 +301 -1
- package/lib/mesh-harness.js +427 -0
- package/lib/mesh-plans.js +13 -5
- 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
|
});
|
|
@@ -510,6 +601,11 @@ function buildCollabPrompt(task, roundNumber, sharedIntel, myScope, myRole) {
|
|
|
510
601
|
}
|
|
511
602
|
}
|
|
512
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
|
+
|
|
513
609
|
if (task.success_criteria && task.success_criteria.length > 0) {
|
|
514
610
|
parts.push('## Success Criteria');
|
|
515
611
|
for (const c of task.success_criteria) {
|
|
@@ -625,6 +721,207 @@ function parseReflection(output) {
|
|
|
625
721
|
};
|
|
626
722
|
}
|
|
627
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
|
+
|
|
628
925
|
// ── Collaborative Task Execution ──────────────────────
|
|
629
926
|
|
|
630
927
|
/**
|
|
@@ -736,32 +1033,60 @@ async function executeCollabTask(task) {
|
|
|
736
1033
|
if (roundsDone) break;
|
|
737
1034
|
|
|
738
1035
|
const roundData = JSON.parse(sc.decode(roundMsg.data));
|
|
739
|
-
const { round_number, shared_intel, my_scope, my_role, mode, current_turn
|
|
1036
|
+
const { round_number, shared_intel, directed_input, my_scope, my_role, mode, current_turn,
|
|
1037
|
+
circling_phase, circling_step, circling_subround } = roundData;
|
|
740
1038
|
|
|
741
1039
|
// 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
1040
|
if (mode === 'sequential' && current_turn && current_turn !== NODE_ID) {
|
|
745
1041
|
log(`COLLAB R${round_number}: Not our turn (current: ${current_turn}). Waiting.`);
|
|
746
1042
|
continue;
|
|
747
1043
|
}
|
|
748
1044
|
|
|
749
|
-
|
|
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)})`);
|
|
750
1050
|
|
|
751
|
-
// Build
|
|
752
|
-
const prompt =
|
|
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);
|
|
753
1055
|
|
|
754
1056
|
if (DRY_RUN) {
|
|
755
1057
|
log(`[DRY RUN] Collab prompt:\n${prompt}`);
|
|
756
1058
|
break;
|
|
757
1059
|
}
|
|
758
1060
|
|
|
759
|
-
// Execute
|
|
1061
|
+
// Execute provider (LLM or shell)
|
|
760
1062
|
const llmResult = await runLLM(prompt, task, worktreePath);
|
|
761
1063
|
const output = llmResult.stdout || '';
|
|
762
1064
|
|
|
763
|
-
// Parse reflection
|
|
764
|
-
|
|
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
|
+
}
|
|
765
1090
|
|
|
766
1091
|
// List modified files
|
|
767
1092
|
let artifacts = [];
|
|
@@ -781,16 +1106,20 @@ async function executeCollabTask(task) {
|
|
|
781
1106
|
node_id: NODE_ID,
|
|
782
1107
|
round: round_number,
|
|
783
1108
|
summary: reflection.summary,
|
|
784
|
-
learnings: reflection.learnings,
|
|
1109
|
+
learnings: reflection.learnings || '',
|
|
785
1110
|
artifacts,
|
|
786
1111
|
confidence: reflection.confidence,
|
|
787
1112
|
vote: reflection.vote,
|
|
788
1113
|
parse_failed: reflection.parse_failed,
|
|
1114
|
+
// Circling extensions
|
|
1115
|
+
circling_step: isCircling ? circling_step : null,
|
|
1116
|
+
circling_artifacts: circlingArtifacts,
|
|
789
1117
|
});
|
|
790
1118
|
const parseTag = reflection.parse_failed ? ' [PARSE FAILED]' : '';
|
|
791
|
-
|
|
1119
|
+
const artCount = circlingArtifacts.length > 0 ? `, ${circlingArtifacts.length} artifact(s)` : '';
|
|
1120
|
+
log(`COLLAB ${stepLabel}: Reflection submitted (vote: ${reflection.vote}, conf: ${reflection.confidence}${parseTag}${artCount})`);
|
|
792
1121
|
} catch (err) {
|
|
793
|
-
log(`COLLAB
|
|
1122
|
+
log(`COLLAB ${stepLabel}: Reflection submit failed: ${err.message}`);
|
|
794
1123
|
}
|
|
795
1124
|
|
|
796
1125
|
// Check if session is done (converged/completed/aborted)
|
|
@@ -897,6 +1226,48 @@ async function executeTask(task) {
|
|
|
897
1226
|
continue;
|
|
898
1227
|
}
|
|
899
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
|
+
|
|
900
1271
|
// If no metric, trust LLM output and complete
|
|
901
1272
|
if (!task.metric) {
|
|
902
1273
|
const attemptRecord = {
|
|
@@ -911,11 +1282,20 @@ async function executeTask(task) {
|
|
|
911
1282
|
const mergeResult = commitAndMergeWorktree(worktreePath, task.task_id, summary);
|
|
912
1283
|
const keepBranch = mergeResult && !mergeResult.merged; // keep on merge conflict
|
|
913
1284
|
|
|
1285
|
+
// Post-commit validation (conventional commits, etc.)
|
|
1286
|
+
if (mergeResult?.committed) {
|
|
1287
|
+
runPostCommitValidation(getHarnessRules(), worktreePath, log);
|
|
1288
|
+
}
|
|
1289
|
+
|
|
914
1290
|
await natsRequest('mesh.tasks.complete', {
|
|
915
1291
|
task_id: task.task_id,
|
|
916
1292
|
result: {
|
|
917
1293
|
success: true, summary, artifacts: [],
|
|
918
1294
|
cost: sessionInfo?.cost || null,
|
|
1295
|
+
harness: {
|
|
1296
|
+
violations: harnessResult.violations,
|
|
1297
|
+
warnings: harnessResult.warnings,
|
|
1298
|
+
},
|
|
919
1299
|
sha: mergeResult?.sha || null,
|
|
920
1300
|
merged: mergeResult?.merged ?? null,
|
|
921
1301
|
},
|
|
@@ -943,6 +1323,11 @@ async function executeTask(task) {
|
|
|
943
1323
|
const mergeResult = commitAndMergeWorktree(worktreePath, task.task_id, summary);
|
|
944
1324
|
const keepBranch = mergeResult && !mergeResult.merged;
|
|
945
1325
|
|
|
1326
|
+
// Post-commit validation (conventional commits, etc.)
|
|
1327
|
+
if (mergeResult?.committed) {
|
|
1328
|
+
runPostCommitValidation(getHarnessRules(), worktreePath, log);
|
|
1329
|
+
}
|
|
1330
|
+
|
|
946
1331
|
await natsRequest('mesh.tasks.complete', {
|
|
947
1332
|
task_id: task.task_id,
|
|
948
1333
|
result: {
|
|
@@ -952,6 +1337,10 @@ async function executeTask(task) {
|
|
|
952
1337
|
cost: sessionInfo?.cost || null,
|
|
953
1338
|
sha: mergeResult?.sha || null,
|
|
954
1339
|
merged: mergeResult?.merged ?? null,
|
|
1340
|
+
harness: {
|
|
1341
|
+
violations: harnessResult.violations,
|
|
1342
|
+
warnings: harnessResult.warnings,
|
|
1343
|
+
},
|
|
955
1344
|
},
|
|
956
1345
|
});
|
|
957
1346
|
cleanupWorktree(worktreePath, keepBranch);
|
package/bin/mesh-bridge.js
CHANGED
|
@@ -285,6 +285,55 @@ function handleCollabEvent(eventType, taskId, data) {
|
|
|
285
285
|
log(`COLLAB ABORTED: ${taskId} — ${session.result?.summary || 'unknown reason'}`);
|
|
286
286
|
break;
|
|
287
287
|
|
|
288
|
+
// Circling Strategy events
|
|
289
|
+
case 'collab.circling_step_started': {
|
|
290
|
+
if (!dispatched.has(taskId)) {
|
|
291
|
+
// Auto-track CLI-submitted circling tasks
|
|
292
|
+
try {
|
|
293
|
+
const tasks = readTasks(ACTIVE_TASKS_PATH);
|
|
294
|
+
if (tasks.find(t => t.task_id === taskId && t.execution === 'mesh')) {
|
|
295
|
+
dispatched.add(taskId);
|
|
296
|
+
lastHeartbeat.set(taskId, Date.now());
|
|
297
|
+
log(`CIRCLING AUTO-TRACK: ${taskId} (CLI-submitted)`);
|
|
298
|
+
} else { break; }
|
|
299
|
+
} catch { break; }
|
|
300
|
+
}
|
|
301
|
+
const c = session.circling || {};
|
|
302
|
+
const stepName = c.phase === 'init' ? 'Init'
|
|
303
|
+
: c.phase === 'finalization' ? 'Finalization'
|
|
304
|
+
: `SR${c.current_subround}/${c.max_subrounds} Step${c.current_step}`;
|
|
305
|
+
log(`CIRCLING ${stepName}: started for ${taskId}`);
|
|
306
|
+
updateTaskInPlace(ACTIVE_TASKS_PATH, taskId, {
|
|
307
|
+
circling_phase: c.phase || null,
|
|
308
|
+
circling_subround: c.current_subround || 0,
|
|
309
|
+
circling_step: c.current_step || 0,
|
|
310
|
+
next_action: `${stepName} in progress (${session.nodes?.length || '?'} nodes)`,
|
|
311
|
+
updated_at: isoTimestamp(),
|
|
312
|
+
});
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
case 'collab.circling_gate': {
|
|
317
|
+
const cg = session.circling || {};
|
|
318
|
+
// Extract blocked reviewer summaries from the last round (if any)
|
|
319
|
+
const lastRound = session.rounds?.[session.rounds.length - 1];
|
|
320
|
+
const blockedVotes = lastRound?.reflections?.filter(r => r.vote === 'blocked') || [];
|
|
321
|
+
let gateMsg;
|
|
322
|
+
if (blockedVotes.length > 0) {
|
|
323
|
+
const reason = blockedVotes.map(r => r.summary).filter(Boolean).join('; ').slice(0, 150);
|
|
324
|
+
gateMsg = `[GATE] SR${cg.current_subround} blocked — ${reason || 'reviewer flagged concern'}`;
|
|
325
|
+
} else {
|
|
326
|
+
gateMsg = `[GATE] SR${cg.current_subround} complete — review reconciliationDoc and approve/reject`;
|
|
327
|
+
}
|
|
328
|
+
log(`CIRCLING GATE: ${taskId} — SR${cg.current_subround} waiting for approval`);
|
|
329
|
+
updateTaskInPlace(ACTIVE_TASKS_PATH, taskId, {
|
|
330
|
+
status: 'waiting-user',
|
|
331
|
+
next_action: gateMsg,
|
|
332
|
+
updated_at: isoTimestamp(),
|
|
333
|
+
});
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
|
|
288
337
|
default:
|
|
289
338
|
log(`COLLAB EVENT: ${eventType} for ${taskId}`);
|
|
290
339
|
}
|
|
@@ -730,6 +779,16 @@ async function main() {
|
|
|
730
779
|
const stalenessTimer = setInterval(checkStaleness, HEARTBEAT_CHECK_INTERVAL);
|
|
731
780
|
log(`Heartbeat staleness check: every ${HEARTBEAT_CHECK_INTERVAL / 1000}s (warn at ${STALE_WARNING_MS / 60000}m)`);
|
|
732
781
|
|
|
782
|
+
// Wake signal — MC publishes mesh.bridge.wake after creating a mesh task
|
|
783
|
+
// so the bridge picks it up in ~1s instead of waiting for the next poll cycle
|
|
784
|
+
let wakeResolve = null;
|
|
785
|
+
const wakeSub = nc.subscribe('mesh.bridge.wake', {
|
|
786
|
+
callback: () => {
|
|
787
|
+
log('WAKE: received wake signal, triggering immediate poll');
|
|
788
|
+
if (wakeResolve) { wakeResolve(); wakeResolve = null; }
|
|
789
|
+
},
|
|
790
|
+
});
|
|
791
|
+
|
|
733
792
|
// Dispatch loop (polls active-tasks.md)
|
|
734
793
|
while (running) {
|
|
735
794
|
try {
|
|
@@ -779,10 +838,16 @@ async function main() {
|
|
|
779
838
|
}
|
|
780
839
|
}
|
|
781
840
|
|
|
782
|
-
|
|
841
|
+
// Sleep until next poll OR wake signal, whichever comes first
|
|
842
|
+
await Promise.race([
|
|
843
|
+
new Promise(r => setTimeout(r, DISPATCH_INTERVAL)),
|
|
844
|
+
new Promise(r => { wakeResolve = r; }),
|
|
845
|
+
]);
|
|
846
|
+
wakeResolve = null;
|
|
783
847
|
}
|
|
784
848
|
|
|
785
849
|
clearInterval(stalenessTimer);
|
|
850
|
+
wakeSub.unsubscribe();
|
|
786
851
|
sub.unsubscribe();
|
|
787
852
|
await nc.drain();
|
|
788
853
|
log('Bridge stopped.');
|