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