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
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mesh-kv.ts — Sync engine for distributed Mission Control (Phase 2).
|
|
3
|
+
*
|
|
4
|
+
* Bridges NATS KV (mesh truth) ↔ local SQLite (MC database).
|
|
5
|
+
*
|
|
6
|
+
* Responsibilities:
|
|
7
|
+
* - Watch MESH_TASKS KV bucket for real-time updates
|
|
8
|
+
* - Mirror KV entries into local SQLite (upsert with revision tracking)
|
|
9
|
+
* - Push local task changes to KV via CAS writes
|
|
10
|
+
* - Handle conflict resolution (KV wins on workers, SQLite wins on lead)
|
|
11
|
+
*
|
|
12
|
+
* This module is imported by the sync/tasks.ts module and the hooks layer.
|
|
13
|
+
* It does NOT run as a standalone daemon — it's part of the MC process.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { getTasksKv, sc } from "@/lib/nats";
|
|
17
|
+
import { NODE_ID, NODE_ROLE } from "@/lib/config";
|
|
18
|
+
|
|
19
|
+
// ── Types ──
|
|
20
|
+
|
|
21
|
+
export interface MeshTaskEntry {
|
|
22
|
+
task_id: string;
|
|
23
|
+
title: string;
|
|
24
|
+
description: string;
|
|
25
|
+
status: string;
|
|
26
|
+
origin: string;
|
|
27
|
+
owner: string | null;
|
|
28
|
+
priority: number;
|
|
29
|
+
budget_minutes: number;
|
|
30
|
+
metric: string | null;
|
|
31
|
+
success_criteria: string[];
|
|
32
|
+
scope: string[];
|
|
33
|
+
tags: string[];
|
|
34
|
+
preferred_nodes: string[];
|
|
35
|
+
exclude_nodes: string[];
|
|
36
|
+
created_at: string;
|
|
37
|
+
claimed_at: string | null;
|
|
38
|
+
started_at: string | null;
|
|
39
|
+
completed_at: string | null;
|
|
40
|
+
last_activity: string | null;
|
|
41
|
+
result: Record<string, unknown> | null;
|
|
42
|
+
attempts: Array<Record<string, unknown>>;
|
|
43
|
+
[key: string]: unknown;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── KV Read Operations ──
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* List all tasks from MESH_TASKS KV bucket.
|
|
50
|
+
* Returns empty array if NATS is unavailable.
|
|
51
|
+
*/
|
|
52
|
+
export async function listMeshTasks(): Promise<MeshTaskEntry[]> {
|
|
53
|
+
const kv = await getTasksKv();
|
|
54
|
+
if (!kv) return [];
|
|
55
|
+
|
|
56
|
+
const tasks: MeshTaskEntry[] = [];
|
|
57
|
+
const keys = await kv.keys();
|
|
58
|
+
|
|
59
|
+
for await (const key of keys) {
|
|
60
|
+
const entry = await kv.get(key);
|
|
61
|
+
if (!entry?.value) continue;
|
|
62
|
+
try {
|
|
63
|
+
tasks.push(JSON.parse(sc.decode(entry.value)));
|
|
64
|
+
} catch {
|
|
65
|
+
// skip malformed
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return tasks;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get a single task from KV with its revision.
|
|
74
|
+
*/
|
|
75
|
+
export async function getMeshTask(
|
|
76
|
+
taskId: string
|
|
77
|
+
): Promise<{ task: MeshTaskEntry; revision: number } | null> {
|
|
78
|
+
const kv = await getTasksKv();
|
|
79
|
+
if (!kv) return null;
|
|
80
|
+
|
|
81
|
+
const entry = await kv.get(taskId);
|
|
82
|
+
if (!entry?.value) return null;
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
task: JSON.parse(sc.decode(entry.value)),
|
|
86
|
+
revision: entry.revision,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── KV Write Operations (CAS) ──
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Write a task to KV. Used by the lead to publish task state.
|
|
94
|
+
* No CAS — overwrites unconditionally. Use updateMeshTaskCAS for safe updates.
|
|
95
|
+
*/
|
|
96
|
+
export async function putMeshTask(task: MeshTaskEntry): Promise<number> {
|
|
97
|
+
const kv = await getTasksKv();
|
|
98
|
+
if (!kv) throw new Error("NATS KV unavailable");
|
|
99
|
+
|
|
100
|
+
const rev = await kv.put(task.task_id, sc.encode(JSON.stringify(task)));
|
|
101
|
+
return rev;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Update a task with CAS (Compare-And-Swap).
|
|
106
|
+
* Fails if the revision doesn't match (another node wrote since our read).
|
|
107
|
+
*
|
|
108
|
+
* @returns New revision number on success
|
|
109
|
+
* @throws On revision mismatch (caller should re-read and retry)
|
|
110
|
+
*/
|
|
111
|
+
export async function updateMeshTaskCAS(
|
|
112
|
+
taskId: string,
|
|
113
|
+
updates: Partial<MeshTaskEntry>,
|
|
114
|
+
expectedRevision: number
|
|
115
|
+
): Promise<number> {
|
|
116
|
+
const kv = await getTasksKv();
|
|
117
|
+
if (!kv) throw new Error("NATS KV unavailable");
|
|
118
|
+
|
|
119
|
+
// Read current
|
|
120
|
+
const entry = await kv.get(taskId);
|
|
121
|
+
if (!entry?.value) throw new Error(`Task ${taskId} not found in KV`);
|
|
122
|
+
|
|
123
|
+
const current = JSON.parse(sc.decode(entry.value));
|
|
124
|
+
|
|
125
|
+
// Authority check
|
|
126
|
+
if (NODE_ROLE !== "lead" && current.origin !== NODE_ID) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`Authority denied: worker ${NODE_ID} cannot update task from ${current.origin}`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Merge and write
|
|
133
|
+
const updated = { ...current, ...updates };
|
|
134
|
+
const rev = await kv.update(
|
|
135
|
+
taskId,
|
|
136
|
+
sc.encode(JSON.stringify(updated)),
|
|
137
|
+
expectedRevision
|
|
138
|
+
);
|
|
139
|
+
return rev;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Propose a new task from a worker node.
|
|
144
|
+
* Task is created with status "proposed" — the lead daemon validates.
|
|
145
|
+
*/
|
|
146
|
+
export async function proposeMeshTask(
|
|
147
|
+
task: Omit<MeshTaskEntry, "status" | "origin">
|
|
148
|
+
): Promise<MeshTaskEntry> {
|
|
149
|
+
const kv = await getTasksKv();
|
|
150
|
+
if (!kv) throw new Error("NATS KV unavailable");
|
|
151
|
+
|
|
152
|
+
const proposed: MeshTaskEntry = {
|
|
153
|
+
...task,
|
|
154
|
+
status: NODE_ROLE === "lead" ? "queued" : "proposed",
|
|
155
|
+
origin: NODE_ID,
|
|
156
|
+
} as MeshTaskEntry;
|
|
157
|
+
|
|
158
|
+
await kv.put(proposed.task_id, sc.encode(JSON.stringify(proposed)));
|
|
159
|
+
return proposed;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── KV Watcher ──
|
|
163
|
+
|
|
164
|
+
export interface KvWatchEvent {
|
|
165
|
+
key: string;
|
|
166
|
+
operation: "PUT" | "DEL";
|
|
167
|
+
task: MeshTaskEntry | null;
|
|
168
|
+
revision: number;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Start watching the MESH_TASKS KV bucket.
|
|
173
|
+
* Returns an async iterator of KvWatchEvent and a stop() function.
|
|
174
|
+
*
|
|
175
|
+
* Call stop() on cleanup to prevent zombie watchers leaking NATS connections.
|
|
176
|
+
*/
|
|
177
|
+
export async function watchMeshTasks(): Promise<{
|
|
178
|
+
events: AsyncIterable<KvWatchEvent>;
|
|
179
|
+
stop: () => void;
|
|
180
|
+
} | null> {
|
|
181
|
+
const kv = await getTasksKv();
|
|
182
|
+
if (!kv) return null;
|
|
183
|
+
|
|
184
|
+
const watcher = await kv.watch();
|
|
185
|
+
|
|
186
|
+
const events: AsyncIterable<KvWatchEvent> = {
|
|
187
|
+
[Symbol.asyncIterator]() {
|
|
188
|
+
return {
|
|
189
|
+
async next() {
|
|
190
|
+
const result = await (
|
|
191
|
+
watcher as AsyncIterable<any>
|
|
192
|
+
)[Symbol.asyncIterator]().next();
|
|
193
|
+
if (result.done) return { value: undefined, done: true };
|
|
194
|
+
|
|
195
|
+
const entry = result.value;
|
|
196
|
+
let task: MeshTaskEntry | null = null;
|
|
197
|
+
if (entry.value) {
|
|
198
|
+
try {
|
|
199
|
+
task = JSON.parse(sc.decode(entry.value));
|
|
200
|
+
} catch {
|
|
201
|
+
// malformed
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
value: {
|
|
207
|
+
key: entry.key,
|
|
208
|
+
operation: entry.value ? "PUT" : ("DEL" as const),
|
|
209
|
+
task,
|
|
210
|
+
revision: entry.revision,
|
|
211
|
+
},
|
|
212
|
+
done: false,
|
|
213
|
+
};
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
events,
|
|
221
|
+
stop: () => {
|
|
222
|
+
if (typeof (watcher as any).stop === "function") {
|
|
223
|
+
(watcher as any).stop();
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ── Merge Logic (for UI layer) ──
|
|
230
|
+
|
|
231
|
+
export interface MergedTask {
|
|
232
|
+
id: string;
|
|
233
|
+
title: string;
|
|
234
|
+
source: "sqlite" | "kv" | "merged";
|
|
235
|
+
[key: string]: unknown;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Merge local SQLite tasks with KV mesh tasks.
|
|
240
|
+
* Deduplicates by task ID.
|
|
241
|
+
*
|
|
242
|
+
* On lead: SQLite wins (has richer fields like kanbanColumn, sortOrder)
|
|
243
|
+
* On worker: KV wins (more up-to-date for mesh-coordinated tasks)
|
|
244
|
+
*/
|
|
245
|
+
export function mergeTasks(
|
|
246
|
+
sqliteTasks: Array<{ id: string; [key: string]: unknown }>,
|
|
247
|
+
kvTasks: Array<{ task_id: string; [key: string]: unknown }>,
|
|
248
|
+
nodeRole: "lead" | "worker" = NODE_ROLE
|
|
249
|
+
): MergedTask[] {
|
|
250
|
+
const merged = new Map<string, MergedTask>();
|
|
251
|
+
|
|
252
|
+
// SQLite tasks first
|
|
253
|
+
for (const t of sqliteTasks) {
|
|
254
|
+
merged.set(t.id, { ...t, id: t.id, title: String(t.title || ""), source: "sqlite" });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// KV tasks: on worker, KV wins for mesh tasks; on lead, SQLite wins
|
|
258
|
+
for (const t of kvTasks) {
|
|
259
|
+
const existing = merged.get(t.task_id);
|
|
260
|
+
if (!existing) {
|
|
261
|
+
merged.set(t.task_id, {
|
|
262
|
+
...t,
|
|
263
|
+
id: t.task_id,
|
|
264
|
+
title: String(t.title || ""),
|
|
265
|
+
source: "kv",
|
|
266
|
+
});
|
|
267
|
+
} else if (nodeRole === "worker") {
|
|
268
|
+
merged.set(t.task_id, {
|
|
269
|
+
...t,
|
|
270
|
+
id: t.task_id,
|
|
271
|
+
title: String(t.title || ""),
|
|
272
|
+
source: "kv",
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
// Lead: SQLite wins (don't overwrite)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return Array.from(merged.values());
|
|
279
|
+
}
|
|
@@ -3,6 +3,7 @@ import path from "path";
|
|
|
3
3
|
import { eq } from "drizzle-orm";
|
|
4
4
|
import { tasks } from "@/lib/db/schema";
|
|
5
5
|
import { ACTIVE_TASKS_MD } from "@/lib/config";
|
|
6
|
+
import { NODE_ROLE } from "@/lib/config";
|
|
6
7
|
import {
|
|
7
8
|
parseTasksMarkdown,
|
|
8
9
|
serializeTasksMarkdown,
|
|
@@ -118,6 +119,11 @@ export function syncTasksFromMarkdown(db: DrizzleDb): void {
|
|
|
118
119
|
metric: task.metric || null,
|
|
119
120
|
budgetMinutes: task.budgetMinutes || 30,
|
|
120
121
|
scope: task.scope?.length ? JSON.stringify(task.scope) : null,
|
|
122
|
+
// Collab routing fields
|
|
123
|
+
collaboration: task.collaboration ? JSON.stringify(task.collaboration) : null,
|
|
124
|
+
preferredNodes: task.preferredNodes?.length ? JSON.stringify(task.preferredNodes) : null,
|
|
125
|
+
excludeNodes: task.excludeNodes?.length ? JSON.stringify(task.excludeNodes) : null,
|
|
126
|
+
clusterId: task.clusterId || null,
|
|
121
127
|
updatedAt: task.updatedAt || now,
|
|
122
128
|
};
|
|
123
129
|
|
|
@@ -167,6 +173,11 @@ export function syncTasksFromMarkdown(db: DrizzleDb): void {
|
|
|
167
173
|
metric: task.metric || null,
|
|
168
174
|
budgetMinutes: task.budgetMinutes || 30,
|
|
169
175
|
scope: task.scope?.length ? JSON.stringify(task.scope) : null,
|
|
176
|
+
// Collab routing fields
|
|
177
|
+
collaboration: task.collaboration ? JSON.stringify(task.collaboration) : null,
|
|
178
|
+
preferredNodes: task.preferredNodes?.length ? JSON.stringify(task.preferredNodes) : null,
|
|
179
|
+
excludeNodes: task.excludeNodes?.length ? JSON.stringify(task.excludeNodes) : null,
|
|
180
|
+
clusterId: task.clusterId || null,
|
|
170
181
|
updatedAt: task.updatedAt || now,
|
|
171
182
|
createdAt: now,
|
|
172
183
|
})
|
|
@@ -178,7 +189,7 @@ export function syncTasksFromMarkdown(db: DrizzleDb): void {
|
|
|
178
189
|
// but PRESERVE roadmap/pipeline tasks (project, phase, pipeline types)
|
|
179
190
|
// and any tasks tagged with a project (they live in the DB, not markdown).
|
|
180
191
|
const allDbTasks = db
|
|
181
|
-
.select({ id: tasks.id, type: tasks.type, project: tasks.project })
|
|
192
|
+
.select({ id: tasks.id, type: tasks.type, project: tasks.project, execution: tasks.execution, status: tasks.status })
|
|
182
193
|
.from(tasks)
|
|
183
194
|
.all();
|
|
184
195
|
for (const row of allDbTasks) {
|
|
@@ -187,6 +198,8 @@ export function syncTasksFromMarkdown(db: DrizzleDb): void {
|
|
|
187
198
|
if (row.type === "project" || row.type === "phase" || row.type === "pipeline") continue;
|
|
188
199
|
// Preserve tasks owned by a project (imported via pipeline, not markdown-managed)
|
|
189
200
|
if (row.project) continue;
|
|
201
|
+
// Preserve in-flight mesh tasks (only clean up terminal ones)
|
|
202
|
+
if (row.execution === "mesh" && row.status !== "done" && row.status !== "cancelled") continue;
|
|
190
203
|
if (!incomingIds.has(row.id)) {
|
|
191
204
|
db.delete(tasks).where(eq(tasks.id, row.id)).run();
|
|
192
205
|
}
|
|
@@ -210,6 +223,10 @@ export function syncTasksFromMarkdownIfChanged(db: DrizzleDb): void {
|
|
|
210
223
|
* Tracks the mtime after write to avoid re-importing our own changes.
|
|
211
224
|
*/
|
|
212
225
|
export function syncTasksToMarkdown(db: DrizzleDb): void {
|
|
226
|
+
// Workers must NOT write to active-tasks.md — the lead owns this file.
|
|
227
|
+
// Writing from workers would cause conflict loops in the mesh sync.
|
|
228
|
+
if (NODE_ROLE === "worker") return;
|
|
229
|
+
|
|
213
230
|
const allTasks = db
|
|
214
231
|
.select()
|
|
215
232
|
.from(tasks)
|
|
@@ -250,6 +267,11 @@ export function syncTasksToMarkdown(db: DrizzleDb): void {
|
|
|
250
267
|
metric: t.metric || null,
|
|
251
268
|
budgetMinutes: t.budgetMinutes || 30,
|
|
252
269
|
scope: t.scope ? JSON.parse(t.scope) : [],
|
|
270
|
+
// Collab routing fields
|
|
271
|
+
collaboration: t.collaboration ? JSON.parse(t.collaboration) : null,
|
|
272
|
+
preferredNodes: t.preferredNodes ? JSON.parse(t.preferredNodes) : [],
|
|
273
|
+
excludeNodes: t.excludeNodes ? JSON.parse(t.excludeNodes) : [],
|
|
274
|
+
clusterId: t.clusterId || null,
|
|
253
275
|
updatedAt: t.updatedAt,
|
|
254
276
|
}));
|
|
255
277
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { getDb } from "@/lib/db";
|
|
2
|
+
import { tasks } from "@/lib/db/schema";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generate a task ID in the format T-YYYYMMDD-NNN.
|
|
6
|
+
* NNN is a zero-padded sequence number based on existing tasks for that date.
|
|
7
|
+
*/
|
|
8
|
+
export function generateTaskId(
|
|
9
|
+
db: ReturnType<typeof getDb>,
|
|
10
|
+
date: Date
|
|
11
|
+
): string {
|
|
12
|
+
const dateStr =
|
|
13
|
+
date.getFullYear().toString() +
|
|
14
|
+
(date.getMonth() + 1).toString().padStart(2, "0") +
|
|
15
|
+
date.getDate().toString().padStart(2, "0");
|
|
16
|
+
|
|
17
|
+
const prefix = `T-${dateStr}-`;
|
|
18
|
+
|
|
19
|
+
// Find highest existing sequence number for this date prefix
|
|
20
|
+
const existing = db
|
|
21
|
+
.select({ id: tasks.id })
|
|
22
|
+
.from(tasks)
|
|
23
|
+
.all()
|
|
24
|
+
.filter((t) => t.id.startsWith(prefix))
|
|
25
|
+
.map((t) => {
|
|
26
|
+
const seq = parseInt(t.id.slice(prefix.length), 10);
|
|
27
|
+
return isNaN(seq) ? 0 : seq;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const nextSeq = existing.length > 0 ? Math.max(...existing) + 1 : 1;
|
|
31
|
+
return `${prefix}${nextSeq.toString().padStart(3, "0")}`;
|
|
32
|
+
}
|
|
@@ -4,14 +4,28 @@ import { createEdgeTtsProvider } from "./edge";
|
|
|
4
4
|
|
|
5
5
|
export type { TtsRequest, TtsResponse, TtsProvider } from "./types";
|
|
6
6
|
|
|
7
|
+
// ── Pluggable TTS Provider Registry ──
|
|
8
|
+
// Built-in providers are registered by default. External code can register
|
|
9
|
+
// additional providers via registerTtsProvider() without modifying this file.
|
|
10
|
+
|
|
7
11
|
const providers: Record<string, () => TtsProvider> = {
|
|
8
12
|
google: createGoogleTtsProvider,
|
|
9
13
|
edge: createEdgeTtsProvider,
|
|
10
14
|
};
|
|
11
15
|
|
|
12
|
-
|
|
16
|
+
/** Register a custom TTS provider factory (e.g. "elevenlabs", "openai-tts"). */
|
|
17
|
+
export function registerTtsProvider(name: string, factory: () => TtsProvider): void {
|
|
18
|
+
providers[name] = factory;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** List all registered TTS provider names. */
|
|
22
|
+
export function listTtsProviders(): string[] {
|
|
23
|
+
return Object.keys(providers);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getProvider(name: string): TtsProvider {
|
|
13
27
|
const factory = providers[name];
|
|
14
|
-
if (!factory) throw new Error(`Unknown TTS provider: ${name}`);
|
|
28
|
+
if (!factory) throw new Error(`Unknown TTS provider: ${name}. Available: ${Object.keys(providers).join(", ")}`);
|
|
15
29
|
return factory();
|
|
16
30
|
}
|
|
17
31
|
|
|
@@ -22,18 +36,28 @@ export interface SynthesisResult extends TtsResponse {
|
|
|
22
36
|
|
|
23
37
|
export async function synthesizeWithFallback(
|
|
24
38
|
req: TtsRequest,
|
|
25
|
-
preferred:
|
|
39
|
+
preferred: string = "google"
|
|
26
40
|
): Promise<SynthesisResult> {
|
|
27
41
|
try {
|
|
28
42
|
const result = await getProvider(preferred).synthesize(req);
|
|
29
43
|
return { ...result, actualProvider: preferred };
|
|
30
44
|
} catch (err) {
|
|
31
|
-
|
|
45
|
+
// Find first available fallback that isn't the preferred provider
|
|
46
|
+
const fallbackNames = Object.keys(providers).filter((n) => n !== preferred);
|
|
32
47
|
const reason = err instanceof Error ? err.message : String(err);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
48
|
+
|
|
49
|
+
for (const fallback of fallbackNames) {
|
|
50
|
+
try {
|
|
51
|
+
console.warn(
|
|
52
|
+
`TTS provider "${preferred}" failed, trying "${fallback}": ${reason}`
|
|
53
|
+
);
|
|
54
|
+
const result = await getProvider(fallback).synthesize(req);
|
|
55
|
+
return { ...result, actualProvider: fallback, fallbackReason: reason };
|
|
56
|
+
} catch {
|
|
57
|
+
// try next fallback
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
throw new Error(`All TTS providers failed. Last error: ${reason}`);
|
|
38
62
|
}
|
|
39
63
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { defineConfig } from "vitest/config";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
resolve: {
|
|
6
|
+
alias: {
|
|
7
|
+
"@": path.resolve(__dirname, "src"),
|
|
8
|
+
},
|
|
9
|
+
},
|
|
10
|
+
test: {
|
|
11
|
+
include: ["src/**/*.test.ts"],
|
|
12
|
+
environment: "node",
|
|
13
|
+
},
|
|
14
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-node-harness",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "One-command installer for the OpenClaw node layer — identity, skills, souls, daemon, and Mission Control.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"openclaw-node": "./cli.js"
|
|
@@ -39,10 +39,23 @@
|
|
|
39
39
|
"type": "git",
|
|
40
40
|
"url": "https://github.com/moltyguibros-design/openclaw-node.git"
|
|
41
41
|
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"test": "node --test test/*.test.js test/*.test.mjs",
|
|
44
|
+
"test:unit": "node --test test/*.test.js test/*.test.mjs",
|
|
45
|
+
"test:mc": "cd mission-control && npx vitest run src/lib/__tests__/",
|
|
46
|
+
"test:integration": "node test/distributed-mc.test.js",
|
|
47
|
+
"test:all": "npm run test:unit && npm run test:mc && npm run test:integration"
|
|
48
|
+
},
|
|
42
49
|
"engines": {
|
|
43
50
|
"node": ">=18"
|
|
44
51
|
},
|
|
45
52
|
"dependencies": {
|
|
46
|
-
"
|
|
53
|
+
"@huggingface/transformers": "^3.0.0",
|
|
54
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
55
|
+
"better-sqlite3": "^11.7.0",
|
|
56
|
+
"js-yaml": "^4.1.1",
|
|
57
|
+
"nats": "^2.28.2",
|
|
58
|
+
"playwright": "^1.51.0",
|
|
59
|
+
"sqlite-vec": "^0.1.0"
|
|
47
60
|
}
|
|
48
61
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
{ "name": "openclaw-mesh-health-publisher", "role": "both", "autostart": true },
|
|
6
6
|
{ "name": "openclaw-mesh-deploy-listener", "role": "both", "autostart": true },
|
|
7
7
|
{ "name": "openclaw-mesh-tool-discord", "role": "lead", "autostart": true },
|
|
8
|
-
{ "name": "openclaw-mission-control", "role": "
|
|
8
|
+
{ "name": "openclaw-mission-control", "role": "both", "autostart": true },
|
|
9
9
|
{ "name": "openclaw-gateway", "role": "both", "autostart": true },
|
|
10
10
|
{ "name": "openclaw-lane-watchdog", "role": "both", "autostart": true },
|
|
11
11
|
{ "name": "openclaw-memory-daemon", "role": "both", "autostart": true },
|
|
@@ -50,7 +50,7 @@ Knowledge Discovery Specialist - expert in web research, documentation lookup, a
|
|
|
50
50
|
- If timeout reached: STOP → Report partial results → Indicate what's incomplete
|
|
51
51
|
- Uses graceful degradation: Full → Partial → Search Results Only → Failure Report
|
|
52
52
|
|
|
53
|
-
**Model:**
|
|
53
|
+
**Model:** fast tier (e.g. haiku, gpt-4o-mini, gemini-flash)
|
|
54
54
|
|
|
55
55
|
</details>
|
|
56
56
|
|
|
@@ -101,7 +101,7 @@ System Architect - strategic planner for React/Node.js/TypeScript enterprise app
|
|
|
101
101
|
- Props Drilling Max 2 Levels (then Context)
|
|
102
102
|
- Server State Separation (React Query/SWR)
|
|
103
103
|
|
|
104
|
-
**Model:**
|
|
104
|
+
**Model:** reasoning tier (e.g. opus, o1, deepseek-r1)
|
|
105
105
|
|
|
106
106
|
</details>
|
|
107
107
|
|
|
@@ -148,7 +148,7 @@ API Lifecycle Expert - specialist for REST/GraphQL APIs, TypeScript type systems
|
|
|
148
148
|
- [ ] Run `npm run typecheck`
|
|
149
149
|
```
|
|
150
150
|
|
|
151
|
-
**Model:**
|
|
151
|
+
**Model:** standard tier (e.g. sonnet, gpt-4o, gemini-pro)
|
|
152
152
|
|
|
153
153
|
</details>
|
|
154
154
|
|
|
@@ -207,7 +207,7 @@ Senior Full-Stack Developer - specialist for React/Node.js/TypeScript implementa
|
|
|
207
207
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
208
208
|
```
|
|
209
209
|
|
|
210
|
-
**Model:**
|
|
210
|
+
**Model:** standard tier (e.g. sonnet, gpt-4o, gemini-pro)
|
|
211
211
|
|
|
212
212
|
</details>
|
|
213
213
|
|
|
@@ -260,7 +260,7 @@ Code Quality Engineer - specialist for verification and quality assurance.
|
|
|
260
260
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
261
261
|
```
|
|
262
262
|
|
|
263
|
-
**Model:**
|
|
263
|
+
**Model:** standard tier (e.g. sonnet, gpt-4o, gemini-pro)
|
|
264
264
|
|
|
265
265
|
</details>
|
|
266
266
|
|
|
@@ -320,7 +320,7 @@ UX Quality Engineer - specialist for E2E testing, visual regression, accessibili
|
|
|
320
320
|
**BLOCKING:** Console errors, E2E failures, LCP > 4s, CLS > 0.25
|
|
321
321
|
**NON-BLOCKING:** Minor A11y issues, "needs improvement" performance
|
|
322
322
|
|
|
323
|
-
**Model:**
|
|
323
|
+
**Model:** standard tier (e.g. sonnet, gpt-4o, gemini-pro)
|
|
324
324
|
|
|
325
325
|
</details>
|
|
326
326
|
|
|
@@ -380,7 +380,7 @@ Technical Writer - specialist for developer documentation.
|
|
|
380
380
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
381
381
|
```
|
|
382
382
|
|
|
383
|
-
**Model:**
|
|
383
|
+
**Model:** standard tier (e.g. sonnet, gpt-4o, gemini-pro)
|
|
384
384
|
|
|
385
385
|
</details>
|
|
386
386
|
|
|
@@ -428,6 +428,6 @@ gh run view [run-id] --log-failed
|
|
|
428
428
|
Types: feat, fix, docs, style, refactor, test, chore
|
|
429
429
|
```
|
|
430
430
|
|
|
431
|
-
**Model:**
|
|
431
|
+
**Model:** fast tier (e.g. haiku, gpt-4o-mini, gemini-flash)
|
|
432
432
|
|
|
433
433
|
</details>
|