openclaw-node-harness 2.0.3 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/README.md +646 -3
  2. package/bin/hyperagent.mjs +419 -0
  3. package/bin/mesh-agent.js +603 -81
  4. package/bin/mesh-bridge.js +340 -11
  5. package/bin/mesh-deploy-listener.js +119 -97
  6. package/bin/mesh-deploy.js +8 -0
  7. package/bin/mesh-task-daemon.js +1005 -40
  8. package/bin/mesh.js +423 -6
  9. package/config/claude-settings.json +95 -0
  10. package/config/daemon.json.template +2 -1
  11. package/config/git-hooks/pre-commit +13 -0
  12. package/config/git-hooks/pre-push +12 -0
  13. package/config/harness-rules.json +174 -0
  14. package/config/plan-templates/team-bugfix.yaml +52 -0
  15. package/config/plan-templates/team-deploy.yaml +50 -0
  16. package/config/plan-templates/team-feature.yaml +71 -0
  17. package/config/roles/qa-engineer.yaml +36 -0
  18. package/config/roles/solidity-dev.yaml +51 -0
  19. package/config/roles/tech-architect.yaml +36 -0
  20. package/config/rules/framework/solidity.md +22 -0
  21. package/config/rules/framework/typescript.md +21 -0
  22. package/config/rules/framework/unity.md +21 -0
  23. package/config/rules/universal/design-docs.md +18 -0
  24. package/config/rules/universal/git-hygiene.md +18 -0
  25. package/config/rules/universal/security.md +19 -0
  26. package/config/rules/universal/test-standards.md +19 -0
  27. package/identity/DELEGATION.md +6 -6
  28. package/install.sh +300 -8
  29. package/lib/circling-parser.js +119 -0
  30. package/lib/hyperagent-store.mjs +652 -0
  31. package/lib/kanban-io.js +59 -10
  32. package/lib/mcp-knowledge/bench.mjs +118 -0
  33. package/lib/mcp-knowledge/core.mjs +528 -0
  34. package/lib/mcp-knowledge/package.json +25 -0
  35. package/lib/mcp-knowledge/server.mjs +245 -0
  36. package/lib/mcp-knowledge/test.mjs +802 -0
  37. package/lib/memory-budget.mjs +261 -0
  38. package/lib/mesh-collab.js +354 -4
  39. package/lib/mesh-harness.js +427 -0
  40. package/lib/mesh-plans.js +13 -5
  41. package/lib/mesh-registry.js +11 -2
  42. package/lib/mesh-tasks.js +67 -0
  43. package/lib/plan-templates.js +226 -0
  44. package/lib/pre-compression-flush.mjs +320 -0
  45. package/lib/role-loader.js +292 -0
  46. package/lib/rule-loader.js +358 -0
  47. package/lib/session-store.mjs +458 -0
  48. package/lib/transcript-parser.mjs +292 -0
  49. package/mission-control/drizzle/soul_schema_update.sql +29 -0
  50. package/mission-control/drizzle.config.ts +1 -4
  51. package/mission-control/package-lock.json +1571 -83
  52. package/mission-control/package.json +6 -2
  53. package/mission-control/scripts/gen-chronology.js +3 -3
  54. package/mission-control/scripts/import-pipeline-v2.js +0 -16
  55. package/mission-control/scripts/import-pipeline.js +0 -15
  56. package/mission-control/src/app/api/cowork/clusters/[id]/members/route.ts +117 -0
  57. package/mission-control/src/app/api/cowork/clusters/[id]/route.ts +84 -0
  58. package/mission-control/src/app/api/cowork/clusters/route.ts +141 -0
  59. package/mission-control/src/app/api/cowork/dispatch/route.ts +128 -0
  60. package/mission-control/src/app/api/cowork/events/route.ts +65 -0
  61. package/mission-control/src/app/api/cowork/intervene/route.ts +259 -0
  62. package/mission-control/src/app/api/cowork/sessions/[id]/route.ts +37 -0
  63. package/mission-control/src/app/api/cowork/sessions/route.ts +64 -0
  64. package/mission-control/src/app/api/diagnostics/route.ts +97 -0
  65. package/mission-control/src/app/api/diagnostics/test-runner/route.ts +990 -0
  66. package/mission-control/src/app/api/mesh/events/route.ts +95 -19
  67. package/mission-control/src/app/api/mesh/identity/route.ts +11 -0
  68. package/mission-control/src/app/api/mesh/tasks/[id]/route.ts +92 -0
  69. package/mission-control/src/app/api/mesh/tasks/route.ts +91 -0
  70. package/mission-control/src/app/api/tasks/[id]/handoff/route.ts +1 -1
  71. package/mission-control/src/app/api/tasks/[id]/route.ts +90 -4
  72. package/mission-control/src/app/api/tasks/route.ts +21 -30
  73. package/mission-control/src/app/cowork/page.tsx +261 -0
  74. package/mission-control/src/app/diagnostics/page.tsx +385 -0
  75. package/mission-control/src/app/graph/page.tsx +26 -0
  76. package/mission-control/src/app/memory/page.tsx +1 -1
  77. package/mission-control/src/app/obsidian/page.tsx +36 -6
  78. package/mission-control/src/app/roadmap/page.tsx +24 -0
  79. package/mission-control/src/app/souls/page.tsx +2 -2
  80. package/mission-control/src/components/board/execution-config.tsx +431 -0
  81. package/mission-control/src/components/board/kanban-board.tsx +75 -9
  82. package/mission-control/src/components/board/kanban-column.tsx +135 -19
  83. package/mission-control/src/components/board/task-card.tsx +55 -2
  84. package/mission-control/src/components/board/unified-task-dialog.tsx +82 -4
  85. package/mission-control/src/components/cowork/cluster-card.tsx +176 -0
  86. package/mission-control/src/components/cowork/create-cluster-dialog.tsx +251 -0
  87. package/mission-control/src/components/cowork/dispatch-form.tsx +423 -0
  88. package/mission-control/src/components/cowork/role-picker.tsx +102 -0
  89. package/mission-control/src/components/cowork/session-card.tsx +284 -0
  90. package/mission-control/src/components/layout/sidebar.tsx +39 -2
  91. package/mission-control/src/lib/__tests__/daily-log.test.ts +82 -0
  92. package/mission-control/src/lib/__tests__/memory-md.test.ts +87 -0
  93. package/mission-control/src/lib/__tests__/mesh-kv-sync.test.ts +465 -0
  94. package/mission-control/src/lib/__tests__/mocks/mock-kv.ts +131 -0
  95. package/mission-control/src/lib/__tests__/status-kanban.test.ts +46 -0
  96. package/mission-control/src/lib/__tests__/task-markdown.test.ts +188 -0
  97. package/mission-control/src/lib/__tests__/wikilinks.test.ts +175 -0
  98. package/mission-control/src/lib/config.ts +58 -0
  99. package/mission-control/src/lib/db/index.ts +69 -0
  100. package/mission-control/src/lib/db/schema.ts +61 -3
  101. package/mission-control/src/lib/hooks.ts +309 -0
  102. package/mission-control/src/lib/memory/entities.ts +3 -2
  103. package/mission-control/src/lib/nats.ts +66 -1
  104. package/mission-control/src/lib/parsers/task-markdown.ts +52 -2
  105. package/mission-control/src/lib/parsers/transcript.ts +4 -4
  106. package/mission-control/src/lib/scheduler.ts +12 -11
  107. package/mission-control/src/lib/sync/mesh-kv.ts +279 -0
  108. package/mission-control/src/lib/sync/tasks.ts +23 -1
  109. package/mission-control/src/lib/task-id.ts +32 -0
  110. package/mission-control/src/lib/tts/index.ts +33 -9
  111. package/mission-control/tsconfig.json +2 -1
  112. package/mission-control/vitest.config.ts +14 -0
  113. package/package.json +15 -2
  114. package/services/service-manifest.json +1 -1
  115. package/skills/cc-godmode/references/agents.md +8 -8
  116. package/workspace-bin/memory-daemon.mjs +199 -5
  117. package/workspace-bin/session-search.mjs +204 -0
  118. package/workspace-bin/web-fetch.mjs +65 -0
@@ -25,8 +25,12 @@ const os = require('os');
25
25
 
26
26
  const NODE_ID = process.env.OPENCLAW_NODE_ID ||
27
27
  os.hostname().toLowerCase().replace(/[^a-z0-9-]/g, '-');
28
+ // NOTE: REPO_DIR defaults to ~/openclaw (runtime). The git repo lives at
29
+ // ~/openclaw-node. See mesh-deploy.js "Two-directory problem" comment.
28
30
  const REPO_DIR = process.env.OPENCLAW_REPO_DIR ||
29
31
  path.join(os.homedir(), 'openclaw');
32
+ const REPO_REMOTE_URL = process.env.OPENCLAW_REPO_URL ||
33
+ 'https://github.com/moltyguibros-design/openclaw-node.git';
30
34
  const DEPLOY_SCRIPT = path.join(REPO_DIR, 'bin', 'mesh-deploy.js');
31
35
 
32
36
  const { NATS_URL, natsConnectOpts } = require('../lib/nats-resolve');
@@ -60,122 +64,140 @@ let deploying = false; // prevent concurrent deploys
60
64
 
61
65
  async function executeDeploy(trigger, resultsKv, nodesKv) {
62
66
  if (deploying) {
63
- console.log(`[deploy-listener] Already deploying — ignoring trigger for ${trigger.sha}`);
67
+ console.log(`[deploy-listener] Already deploying — ignoring trigger for ${trigger.sha} (from ${trigger.initiator || 'unknown'})`);
64
68
  return;
65
69
  }
66
70
 
67
71
  deploying = true;
68
72
  const startedAt = new Date().toISOString();
69
- const resultKey = `${trigger.sha}-${NODE_ID}`;
73
+ // Sanitize sha for NATS KV key safety (KV rejects whitespace, path seps, etc.)
74
+ const safeSha = (trigger.sha || 'unknown').replace(/[^a-fA-F0-9.-]/g, '');
75
+ const resultKey = `${safeSha}-${NODE_ID}`;
70
76
 
71
- console.log(`[deploy-listener] ═══ Deploy triggered: ${trigger.sha} by ${trigger.initiator} ═══`);
72
-
73
- // Write "deploying" status so lead sees we're working
74
77
  try {
75
- await resultsKv.put(resultKey, sc.encode(JSON.stringify({
76
- nodeId: NODE_ID, sha: trigger.sha, status: 'deploying', startedAt,
77
- })));
78
- } catch {}
78
+ console.log(`[deploy-listener] ═══ Deploy triggered: ${trigger.sha} by ${trigger.initiator} ═══`);
79
79
 
80
- const result = {
81
- nodeId: NODE_ID,
82
- sha: trigger.sha,
83
- status: 'success',
84
- startedAt,
85
- completedAt: null,
86
- durationSeconds: 0,
87
- componentsDeployed: [],
88
- warnings: [],
89
- errors: [],
90
- log: '',
91
- };
80
+ // Write "deploying" status so lead sees we're working
81
+ try {
82
+ await resultsKv.put(resultKey, sc.encode(JSON.stringify({
83
+ nodeId: NODE_ID, sha: trigger.sha, status: 'deploying', startedAt,
84
+ })));
85
+ } catch {}
92
86
 
93
- try {
94
- // Verify repo exists
95
- if (!fs.existsSync(path.join(REPO_DIR, '.git'))) {
96
- throw new Error(`Repo not found at ${REPO_DIR}`);
97
- }
87
+ const result = {
88
+ nodeId: NODE_ID,
89
+ sha: trigger.sha,
90
+ status: 'success',
91
+ startedAt,
92
+ completedAt: null,
93
+ durationSeconds: 0,
94
+ componentsDeployed: [],
95
+ warnings: [],
96
+ errors: [],
97
+ log: '',
98
+ };
98
99
 
99
- // Validate branch name to prevent command injection (trigger.branch comes from NATS)
100
- const branch = (trigger.branch || 'main').replace(/[^a-zA-Z0-9._/-]/g, '');
101
- if (!branch || branch !== (trigger.branch || 'main')) {
102
- throw new Error(`Invalid branch name: ${trigger.branch}`);
103
- }
100
+ try {
101
+ // Validate branch name to prevent command injection (trigger.branch comes from NATS)
102
+ const branch = (trigger.branch || 'main').replace(/[^a-zA-Z0-9._/-]/g, '');
103
+ if (!branch || branch !== (trigger.branch || 'main')) {
104
+ throw new Error(`Invalid branch name: ${trigger.branch}`);
105
+ }
104
106
 
105
- // Git fetch + ff merge
106
- execSync(`git fetch origin ${branch}`, {
107
- cwd: REPO_DIR, encoding: 'utf8', timeout: 60000,
108
- });
109
- execSync(`git merge origin/${branch} --ff-only`, {
110
- cwd: REPO_DIR, encoding: 'utf8', timeout: 30000,
111
- });
112
-
113
- // Build deploy command filter requested components against what this node runs
114
- let cmd = `node "${DEPLOY_SCRIPT}" --local`;
115
- if (trigger.components && !trigger.components.includes('all')) {
116
- const applicable = trigger.components.filter(c => NODE_COMPONENTS.has(c));
117
- if (applicable.length === 0) {
118
- console.log(`[deploy-listener] No applicable components for role=${NODE_ROLE} — skipping`);
119
- result.status = 'skipped';
120
- result.log = `No matching components for role ${NODE_ROLE}`;
121
- deploying = false;
122
- result.completedAt = new Date().toISOString();
123
- try { await resultsKv.put(resultKey, sc.encode(JSON.stringify(result))); } catch {}
124
- return;
107
+ // Bootstrap git repo if directory exists but .git doesn't
108
+ // (provisioner may have copied files without git clone)
109
+ if (!fs.existsSync(path.join(REPO_DIR, '.git'))) {
110
+ if (!fs.existsSync(REPO_DIR)) {
111
+ throw new Error(`Repo dir not found at ${REPO_DIR}`);
112
+ }
113
+ console.log(`[deploy-listener] No .git found — bootstrapping git repo`);
114
+ execSync('git init', { cwd: REPO_DIR, encoding: 'utf8', timeout: 10000 });
115
+ execSync(`git remote add origin ${REPO_REMOTE_URL}`, {
116
+ cwd: REPO_DIR, encoding: 'utf8', timeout: 10000,
117
+ });
118
+ execSync(`git fetch origin ${branch}`, {
119
+ cwd: REPO_DIR, encoding: 'utf8', timeout: 60000,
120
+ });
121
+ execSync(`git reset --hard origin/${branch}`, {
122
+ cwd: REPO_DIR, encoding: 'utf8', timeout: 30000,
123
+ });
124
+ console.log(`[deploy-listener] Git bootstrapped from origin/${branch}`);
125
+ } else {
126
+ // Normal path: fetch + ff merge
127
+ execSync(`git fetch origin ${branch}`, {
128
+ cwd: REPO_DIR, encoding: 'utf8', timeout: 60000,
129
+ });
130
+ execSync(`git merge origin/${branch} --ff-only`, {
131
+ cwd: REPO_DIR, encoding: 'utf8', timeout: 30000,
132
+ });
125
133
  }
126
- for (const c of applicable) cmd += ` --component ${c}`;
127
- }
128
- if (trigger.force) cmd += ' --force';
129
-
130
- console.log(`[deploy-listener] Running: ${cmd}`);
131
- const output = execSync(cmd, {
132
- cwd: REPO_DIR,
133
- encoding: 'utf8',
134
- timeout: 300000, // 5 min max (npm install can be slow)
135
- env: { ...process.env, OPENCLAW_REPO_DIR: REPO_DIR },
136
- });
137
-
138
- result.log = output.slice(-5000);
139
- result.status = 'success';
140
- result.sha = execSync('git rev-parse --short HEAD', {
141
- cwd: REPO_DIR, encoding: 'utf8',
142
- }).trim();
143
134
 
144
- console.log(`[deploy-listener] Successnow at ${result.sha}`);
135
+ // Build deploy commandfilter requested components against what this node runs
136
+ let cmd = `"${process.execPath}" "${DEPLOY_SCRIPT}" --local`;
137
+ if (trigger.components && !trigger.components.includes('all')) {
138
+ const applicable = trigger.components.filter(c => NODE_COMPONENTS.has(c));
139
+ if (applicable.length === 0) {
140
+ console.log(`[deploy-listener] No applicable components for role=${NODE_ROLE} — skipping`);
141
+ result.status = 'skipped';
142
+ result.log = `No matching components for role ${NODE_ROLE}`;
143
+ result.completedAt = new Date().toISOString();
144
+ try { await resultsKv.put(resultKey, sc.encode(JSON.stringify(result))); } catch {}
145
+ return;
146
+ }
147
+ for (const c of applicable) cmd += ` --component ${c}`;
148
+ }
149
+ if (trigger.force) cmd += ' --force';
145
150
 
146
- } catch (err) {
147
- result.status = 'failed';
148
- result.errors.push(err.message);
149
- result.log = (err.stdout || err.stderr || err.message).slice(-5000);
150
- console.error(`[deploy-listener] Deploy FAILED: ${err.message}`);
151
- }
151
+ console.log(`[deploy-listener] Running: ${cmd}`);
152
+ const output = execSync(cmd, {
153
+ cwd: REPO_DIR,
154
+ encoding: 'utf8',
155
+ timeout: 300000, // 5 min max (npm install can be slow)
156
+ env: { ...process.env, OPENCLAW_REPO_DIR: REPO_DIR },
157
+ });
152
158
 
153
- result.completedAt = new Date().toISOString();
154
- result.durationSeconds = Math.round(
155
- (new Date(result.completedAt) - new Date(result.startedAt)) / 1000
156
- );
159
+ result.log = output.slice(-5000);
160
+ result.status = 'success';
161
+ result.sha = execSync('git rev-parse --short HEAD', {
162
+ cwd: REPO_DIR, encoding: 'utf8',
163
+ }).trim();
157
164
 
158
- // Write final result to KV
159
- try {
160
- await resultsKv.put(resultKey, sc.encode(JSON.stringify(result)));
161
- } catch (err) {
162
- console.error(`[deploy-listener] Failed to write result: ${err.message}`);
163
- }
165
+ console.log(`[deploy-listener] Success now at ${result.sha}`);
164
166
 
165
- // Update our deployVersion in the nodes registry
166
- if (result.status === 'success' && nodesKv) {
167
+ } catch (err) {
168
+ result.status = 'failed';
169
+ result.errors.push(err.message);
170
+ result.log = (err.stdout || err.stderr || err.message).slice(-5000);
171
+ console.error(`[deploy-listener] Deploy FAILED: ${err.message}`);
172
+ }
173
+
174
+ result.completedAt = new Date().toISOString();
175
+ result.durationSeconds = Math.round(
176
+ (new Date(result.completedAt) - new Date(result.startedAt)) / 1000
177
+ );
178
+
179
+ // Write final result to KV
167
180
  try {
168
- const existing = await nodesKv.get(NODE_ID);
169
- if (existing && existing.value) {
170
- const node = JSON.parse(sc.decode(existing.value));
171
- node.deployVersion = result.sha;
172
- node.lastDeploy = result.completedAt;
173
- await nodesKv.put(NODE_ID, sc.encode(JSON.stringify(node)));
174
- }
175
- } catch {}
176
- }
181
+ await resultsKv.put(resultKey, sc.encode(JSON.stringify(result)));
182
+ } catch (err) {
183
+ console.error(`[deploy-listener] Failed to write result: ${err.message}`);
184
+ }
177
185
 
178
- deploying = false;
186
+ // Update our deployVersion in the nodes registry
187
+ if (result.status === 'success' && nodesKv) {
188
+ try {
189
+ const existing = await nodesKv.get(NODE_ID);
190
+ if (existing && existing.value) {
191
+ const node = JSON.parse(sc.decode(existing.value));
192
+ node.deployVersion = result.sha;
193
+ node.lastDeploy = result.completedAt;
194
+ await nodesKv.put(NODE_ID, sc.encode(JSON.stringify(node)));
195
+ }
196
+ } catch {}
197
+ }
198
+ } finally {
199
+ deploying = false;
200
+ }
179
201
  }
180
202
 
181
203
  // ── Auto-Catch-Up ────────────────────────────────────────────────────────
@@ -49,6 +49,14 @@ const HOME = os.homedir();
49
49
  const DEPLOY_BRANCH = process.env.OPENCLAW_DEPLOY_BRANCH || 'main';
50
50
  const REPO_DIR = process.env.OPENCLAW_REPO_DIR || path.join(HOME, 'openclaw');
51
51
 
52
+ // KNOWN ISSUE: Two-directory problem
53
+ // ~/openclaw-node is the git repo (source of truth). mesh-deploy pulls into it,
54
+ // then copies files to ~/openclaw (the runtime location). These can drift if
55
+ // files are edited directly in ~/openclaw without back-porting to the repo.
56
+ // Resolution path: unify to a single directory. Either symlink ~/openclaw →
57
+ // ~/openclaw-node, or change REPO_DIR default + DIRS to point at the same tree.
58
+ // Until then, mesh-deploy.js is the only sanctioned way to propagate changes.
59
+
52
60
  // Standard directory layout
53
61
  const DIRS = {
54
62
  OPENCLAW_HOME: path.join(HOME, '.openclaw'),