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
@@ -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
- export function getProvider(name: "google" | "edge"): TtsProvider {
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: "google" | "edge" = "google"
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
- const fallback = preferred === "google" ? "edge" : "google";
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
- console.warn(
34
- `TTS provider "${preferred}" failed, falling back to "${fallback}": ${reason}`
35
- );
36
- const result = await getProvider(fallback).synthesize(req);
37
- return { ...result, actualProvider: fallback, fallbackReason: reason };
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
  }
@@ -34,7 +34,8 @@
34
34
  "**/*.tsx",
35
35
  ".next/types/**/*.ts",
36
36
  ".next/dev/types/**/*.ts",
37
- "**/*.mts"
37
+ "**/*.mts",
38
+ ".next/dev/dev/types/**/*.ts"
38
39
  ],
39
40
  "exclude": [
40
41
  "node_modules"
@@ -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",
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
- "nats": "^2.28.2"
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": "lead", "autostart": true },
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:** haiku (fast & cost-effective)
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:** opus (complex reasoning, high-impact decisions)
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:** sonnet (balanced analysis + documentation)
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:** sonnet (optimal for implementation)
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:** sonnet (balanced verification)
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:** sonnet (MCP coordination + analysis)
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:** sonnet (reading + writing capability)
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:** haiku (simple operations, cost-optimized)
431
+ **Model:** fast tier (e.g. haiku, gpt-4o-mini, gemini-flash)
432
432
 
433
433
  </details>