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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
135
|
+
// Build deploy command — filter 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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
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
|
-
|
|
166
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
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 ────────────────────────────────────────────────────────
|
package/bin/mesh-deploy.js
CHANGED
|
@@ -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'),
|