openclaw-node-harness 2.0.4 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/README.md +646 -3
  2. package/bin/hyperagent.mjs +419 -0
  3. package/bin/mesh-agent.js +401 -12
  4. package/bin/mesh-bridge.js +66 -1
  5. package/bin/mesh-task-daemon.js +816 -26
  6. package/bin/mesh.js +403 -1
  7. package/config/claude-settings.json +95 -0
  8. package/config/daemon.json.template +2 -1
  9. package/config/git-hooks/pre-commit +13 -0
  10. package/config/git-hooks/pre-push +12 -0
  11. package/config/harness-rules.json +174 -0
  12. package/config/plan-templates/team-bugfix.yaml +52 -0
  13. package/config/plan-templates/team-deploy.yaml +50 -0
  14. package/config/plan-templates/team-feature.yaml +71 -0
  15. package/config/roles/qa-engineer.yaml +36 -0
  16. package/config/roles/solidity-dev.yaml +51 -0
  17. package/config/roles/tech-architect.yaml +36 -0
  18. package/config/rules/framework/solidity.md +22 -0
  19. package/config/rules/framework/typescript.md +21 -0
  20. package/config/rules/framework/unity.md +21 -0
  21. package/config/rules/universal/design-docs.md +18 -0
  22. package/config/rules/universal/git-hygiene.md +18 -0
  23. package/config/rules/universal/security.md +19 -0
  24. package/config/rules/universal/test-standards.md +19 -0
  25. package/identity/DELEGATION.md +6 -6
  26. package/install.sh +293 -8
  27. package/lib/circling-parser.js +119 -0
  28. package/lib/hyperagent-store.mjs +652 -0
  29. package/lib/kanban-io.js +9 -0
  30. package/lib/mcp-knowledge/bench.mjs +118 -0
  31. package/lib/mcp-knowledge/core.mjs +528 -0
  32. package/lib/mcp-knowledge/package.json +25 -0
  33. package/lib/mcp-knowledge/server.mjs +245 -0
  34. package/lib/mcp-knowledge/test.mjs +802 -0
  35. package/lib/memory-budget.mjs +261 -0
  36. package/lib/mesh-collab.js +301 -1
  37. package/lib/mesh-harness.js +427 -0
  38. package/lib/mesh-plans.js +13 -5
  39. package/lib/mesh-tasks.js +67 -0
  40. package/lib/plan-templates.js +226 -0
  41. package/lib/pre-compression-flush.mjs +320 -0
  42. package/lib/role-loader.js +292 -0
  43. package/lib/rule-loader.js +358 -0
  44. package/lib/session-store.mjs +458 -0
  45. package/lib/transcript-parser.mjs +292 -0
  46. package/mission-control/drizzle/soul_schema_update.sql +29 -0
  47. package/mission-control/drizzle.config.ts +1 -4
  48. package/mission-control/package-lock.json +1571 -83
  49. package/mission-control/package.json +6 -2
  50. package/mission-control/scripts/gen-chronology.js +3 -3
  51. package/mission-control/scripts/import-pipeline-v2.js +0 -16
  52. package/mission-control/scripts/import-pipeline.js +0 -15
  53. package/mission-control/src/app/api/cowork/clusters/[id]/members/route.ts +117 -0
  54. package/mission-control/src/app/api/cowork/clusters/[id]/route.ts +84 -0
  55. package/mission-control/src/app/api/cowork/clusters/route.ts +141 -0
  56. package/mission-control/src/app/api/cowork/dispatch/route.ts +128 -0
  57. package/mission-control/src/app/api/cowork/events/route.ts +65 -0
  58. package/mission-control/src/app/api/cowork/intervene/route.ts +259 -0
  59. package/mission-control/src/app/api/cowork/sessions/[id]/route.ts +37 -0
  60. package/mission-control/src/app/api/cowork/sessions/route.ts +64 -0
  61. package/mission-control/src/app/api/diagnostics/route.ts +97 -0
  62. package/mission-control/src/app/api/diagnostics/test-runner/route.ts +990 -0
  63. package/mission-control/src/app/api/mesh/events/route.ts +95 -19
  64. package/mission-control/src/app/api/mesh/identity/route.ts +11 -0
  65. package/mission-control/src/app/api/mesh/tasks/[id]/route.ts +92 -0
  66. package/mission-control/src/app/api/mesh/tasks/route.ts +91 -0
  67. package/mission-control/src/app/api/tasks/[id]/handoff/route.ts +1 -1
  68. package/mission-control/src/app/api/tasks/[id]/route.ts +90 -4
  69. package/mission-control/src/app/api/tasks/route.ts +21 -30
  70. package/mission-control/src/app/cowork/page.tsx +261 -0
  71. package/mission-control/src/app/diagnostics/page.tsx +385 -0
  72. package/mission-control/src/app/graph/page.tsx +26 -0
  73. package/mission-control/src/app/memory/page.tsx +1 -1
  74. package/mission-control/src/app/obsidian/page.tsx +36 -6
  75. package/mission-control/src/app/roadmap/page.tsx +24 -0
  76. package/mission-control/src/app/souls/page.tsx +2 -2
  77. package/mission-control/src/components/board/execution-config.tsx +431 -0
  78. package/mission-control/src/components/board/kanban-board.tsx +75 -9
  79. package/mission-control/src/components/board/kanban-column.tsx +135 -19
  80. package/mission-control/src/components/board/task-card.tsx +55 -2
  81. package/mission-control/src/components/board/unified-task-dialog.tsx +82 -4
  82. package/mission-control/src/components/cowork/cluster-card.tsx +176 -0
  83. package/mission-control/src/components/cowork/create-cluster-dialog.tsx +251 -0
  84. package/mission-control/src/components/cowork/dispatch-form.tsx +423 -0
  85. package/mission-control/src/components/cowork/role-picker.tsx +102 -0
  86. package/mission-control/src/components/cowork/session-card.tsx +284 -0
  87. package/mission-control/src/components/layout/sidebar.tsx +39 -2
  88. package/mission-control/src/lib/__tests__/daily-log.test.ts +82 -0
  89. package/mission-control/src/lib/__tests__/memory-md.test.ts +87 -0
  90. package/mission-control/src/lib/__tests__/mesh-kv-sync.test.ts +465 -0
  91. package/mission-control/src/lib/__tests__/mocks/mock-kv.ts +131 -0
  92. package/mission-control/src/lib/__tests__/status-kanban.test.ts +46 -0
  93. package/mission-control/src/lib/__tests__/task-markdown.test.ts +188 -0
  94. package/mission-control/src/lib/__tests__/wikilinks.test.ts +175 -0
  95. package/mission-control/src/lib/config.ts +58 -0
  96. package/mission-control/src/lib/db/index.ts +69 -0
  97. package/mission-control/src/lib/db/schema.ts +61 -3
  98. package/mission-control/src/lib/hooks.ts +309 -0
  99. package/mission-control/src/lib/memory/entities.ts +3 -2
  100. package/mission-control/src/lib/nats.ts +66 -1
  101. package/mission-control/src/lib/parsers/task-markdown.ts +52 -2
  102. package/mission-control/src/lib/parsers/transcript.ts +4 -4
  103. package/mission-control/src/lib/scheduler.ts +12 -11
  104. package/mission-control/src/lib/sync/mesh-kv.ts +279 -0
  105. package/mission-control/src/lib/sync/tasks.ts +23 -1
  106. package/mission-control/src/lib/task-id.ts +32 -0
  107. package/mission-control/src/lib/tts/index.ts +33 -9
  108. package/mission-control/tsconfig.json +2 -1
  109. package/mission-control/vitest.config.ts +14 -0
  110. package/package.json +15 -2
  111. package/services/service-manifest.json +1 -1
  112. package/skills/cc-godmode/references/agents.md +8 -8
  113. package/workspace-bin/memory-daemon.mjs +199 -5
  114. package/workspace-bin/session-search.mjs +204 -0
  115. package/workspace-bin/web-fetch.mjs +65 -0
@@ -0,0 +1,259 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { getNats, getCollabKv, sc } from "@/lib/nats";
3
+ import { eq } from "drizzle-orm";
4
+ import { getDb } from "@/lib/db";
5
+ import { tasks } from "@/lib/db/schema";
6
+ import { syncTasksToMarkdown } from "@/lib/sync/tasks";
7
+
8
+ export const dynamic = "force-dynamic";
9
+
10
+ /**
11
+ * POST /api/cowork/intervene
12
+ *
13
+ * Operator interventions on collab sessions.
14
+ * Actions: abort, force_converge, remove_node
15
+ */
16
+ export async function POST(request: NextRequest) {
17
+ const nc = await getNats();
18
+ if (!nc) {
19
+ return NextResponse.json({ error: "NATS unavailable" }, { status: 503 });
20
+ }
21
+
22
+ const kv = await getCollabKv();
23
+ if (!kv) {
24
+ return NextResponse.json(
25
+ { error: "Collab KV unavailable" },
26
+ { status: 503 }
27
+ );
28
+ }
29
+
30
+ const { action, sessionId, nodeId } = await request.json();
31
+
32
+ if (!sessionId) {
33
+ return NextResponse.json(
34
+ { error: "sessionId required" },
35
+ { status: 400 }
36
+ );
37
+ }
38
+
39
+ // Read current session
40
+ let session: any;
41
+ try {
42
+ const entry = await kv.get(sessionId);
43
+ if (!entry || !entry.value) {
44
+ return NextResponse.json(
45
+ { error: "Session not found" },
46
+ { status: 404 }
47
+ );
48
+ }
49
+ session = JSON.parse(sc.decode(entry.value));
50
+ } catch (err) {
51
+ return NextResponse.json(
52
+ { error: (err as Error).message },
53
+ { status: 500 }
54
+ );
55
+ }
56
+
57
+ const now = new Date().toISOString();
58
+ const audit = (event: string, detail?: string) => {
59
+ if (!session.audit_log) session.audit_log = [];
60
+ session.audit_log.push({ ts: now, event, detail, source: "mission-control" });
61
+ };
62
+
63
+ try {
64
+ switch (action) {
65
+ case "abort": {
66
+ session.status = "aborted";
67
+ session.completed_at = now;
68
+ audit("manual_abort");
69
+
70
+ await kv.put(sessionId, sc.encode(JSON.stringify(session)));
71
+
72
+ // Cancel parent task via daemon RPC
73
+ try {
74
+ await nc.request(
75
+ "mesh.tasks.cancel",
76
+ sc.encode(JSON.stringify({ task_id: session.task_id })),
77
+ { timeout: 5000 }
78
+ );
79
+ } catch {
80
+ // Daemon may be down — KV is already updated
81
+ }
82
+
83
+ // Notify subscribers
84
+ nc.publish(
85
+ "mesh.events.collab.aborted",
86
+ sc.encode(
87
+ JSON.stringify({
88
+ session_id: sessionId,
89
+ task_id: session.task_id,
90
+ })
91
+ )
92
+ );
93
+
94
+ // Update Kanban for immediate feedback
95
+ try {
96
+ const db = getDb();
97
+ db.update(tasks)
98
+ .set({ status: "cancelled", kanbanColumn: "done", updatedAt: now })
99
+ .where(eq(tasks.id, session.task_id))
100
+ .run();
101
+ syncTasksToMarkdown(db);
102
+ } catch { /* best-effort */ }
103
+
104
+ return NextResponse.json({ ok: true, action: "aborted" });
105
+ }
106
+
107
+ case "force_converge": {
108
+ // Inject synthetic reflections for nodes that haven't submitted
109
+ const currentRound =
110
+ session.rounds?.[session.rounds.length - 1];
111
+ if (currentRound) {
112
+ const submittedNodes = new Set(
113
+ (currentRound.reflections || []).map((r: any) => r.node_id)
114
+ );
115
+ const activeNodes = (session.nodes || []).filter(
116
+ (n: any) => n.status !== "dead"
117
+ );
118
+
119
+ for (const node of activeNodes) {
120
+ if (!submittedNodes.has(node.node_id)) {
121
+ if (!currentRound.reflections) currentRound.reflections = [];
122
+ currentRound.reflections.push({
123
+ node_id: node.node_id,
124
+ summary: "Synthetic reflection (force-converged by operator)",
125
+ learnings: "",
126
+ artifacts: [],
127
+ confidence: 0.5,
128
+ vote: "converged",
129
+ synthetic: true,
130
+ submitted_at: now,
131
+ });
132
+ }
133
+ }
134
+ currentRound.completed_at = now;
135
+ }
136
+
137
+ session.status = "converged";
138
+ audit("force_converge");
139
+
140
+ // Order matters: KV first, then event, then complete parent task.
141
+ // If mesh.tasks.complete fires before KV is updated, the task closes
142
+ // but the session shows as still active in the UI until next SWR poll.
143
+ await kv.put(sessionId, sc.encode(JSON.stringify(session)));
144
+
145
+ nc.publish(
146
+ "mesh.events.collab.converged",
147
+ sc.encode(
148
+ JSON.stringify({
149
+ session_id: sessionId,
150
+ task_id: session.task_id,
151
+ forced: true,
152
+ })
153
+ )
154
+ );
155
+
156
+ // Complete parent task via daemon RPC — critical fix:
157
+ // without this, daemon's in-memory task stays running forever
158
+ try {
159
+ await nc.request(
160
+ "mesh.tasks.complete",
161
+ sc.encode(
162
+ JSON.stringify({
163
+ task_id: session.task_id,
164
+ result: {
165
+ success: true,
166
+ summary: "Force-converged by operator via Mission Control",
167
+ forced: true,
168
+ },
169
+ })
170
+ ),
171
+ { timeout: 5000 }
172
+ );
173
+ } catch {
174
+ // Daemon may be down — session is at least marked converged in KV
175
+ }
176
+
177
+ // Update Kanban for immediate feedback
178
+ try {
179
+ const db = getDb();
180
+ db.update(tasks)
181
+ .set({ status: "done", kanbanColumn: "done", updatedAt: now })
182
+ .where(eq(tasks.id, session.task_id))
183
+ .run();
184
+ syncTasksToMarkdown(db);
185
+ } catch { /* best-effort */ }
186
+
187
+ return NextResponse.json({ ok: true, action: "force_converged" });
188
+ }
189
+
190
+ case "remove_node": {
191
+ if (!nodeId) {
192
+ return NextResponse.json(
193
+ { error: "nodeId required for remove_node" },
194
+ { status: 400 }
195
+ );
196
+ }
197
+
198
+ // Use daemon RPC so it updates both KV and in-memory state atomically
199
+ // This avoids stale quorum in the daemon's evaluateRound()
200
+ try {
201
+ await nc.request(
202
+ "mesh.collab.leave",
203
+ sc.encode(
204
+ JSON.stringify({
205
+ session_id: sessionId,
206
+ node_id: nodeId,
207
+ })
208
+ ),
209
+ { timeout: 5000 }
210
+ );
211
+ } catch {
212
+ // Fallback: direct KV write if daemon doesn't handle leave RPC
213
+ const node = (session.nodes || []).find(
214
+ (n: any) => n.node_id === nodeId
215
+ );
216
+ if (node) {
217
+ node.status = "dead";
218
+ audit("node_removed", nodeId);
219
+ await kv.put(sessionId, sc.encode(JSON.stringify(session)));
220
+ }
221
+ }
222
+
223
+ // Notify the node to stop
224
+ nc.publish(
225
+ `mesh.agent.${nodeId}.stall`,
226
+ sc.encode(
227
+ JSON.stringify({
228
+ task_id: session.task_id,
229
+ reason: "Removed by operator",
230
+ })
231
+ )
232
+ );
233
+
234
+ nc.publish(
235
+ "mesh.events.collab.node_removed",
236
+ sc.encode(
237
+ JSON.stringify({
238
+ session_id: sessionId,
239
+ node_id: nodeId,
240
+ })
241
+ )
242
+ );
243
+
244
+ return NextResponse.json({ ok: true, action: "node_removed", nodeId });
245
+ }
246
+
247
+ default:
248
+ return NextResponse.json(
249
+ { error: `Unknown action: ${action}` },
250
+ { status: 400 }
251
+ );
252
+ }
253
+ } catch (err) {
254
+ return NextResponse.json(
255
+ { error: (err as Error).message },
256
+ { status: 500 }
257
+ );
258
+ }
259
+ }
@@ -0,0 +1,37 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { getCollabKv, sc } from "@/lib/nats";
3
+
4
+ export const dynamic = "force-dynamic";
5
+
6
+ /**
7
+ * GET /api/cowork/sessions/[id]
8
+ *
9
+ * Single session detail from NATS MESH_COLLAB KV.
10
+ */
11
+ export async function GET(
12
+ _request: NextRequest,
13
+ { params }: { params: Promise<{ id: string }> }
14
+ ) {
15
+ const { id } = await params;
16
+ const kv = await getCollabKv();
17
+ if (!kv) {
18
+ return NextResponse.json({ error: "NATS unavailable" }, { status: 503 });
19
+ }
20
+
21
+ try {
22
+ const entry = await kv.get(id);
23
+ if (!entry || !entry.value) {
24
+ return NextResponse.json(
25
+ { error: "Session not found" },
26
+ { status: 404 }
27
+ );
28
+ }
29
+ const session = JSON.parse(sc.decode(entry.value));
30
+ return NextResponse.json(session);
31
+ } catch (err) {
32
+ return NextResponse.json(
33
+ { error: (err as Error).message },
34
+ { status: 500 }
35
+ );
36
+ }
37
+ }
@@ -0,0 +1,64 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { getCollabKv, sc } from "@/lib/nats";
3
+
4
+ export const dynamic = "force-dynamic";
5
+
6
+ /**
7
+ * GET /api/cowork/sessions
8
+ *
9
+ * Read all collab sessions from NATS MESH_COLLAB KV.
10
+ * Optional ?status= filter (e.g., "active", "recruiting", "completed").
11
+ * Returns { sessions[], natsAvailable }.
12
+ */
13
+ export async function GET(request: NextRequest) {
14
+ const kv = await getCollabKv();
15
+ if (!kv) {
16
+ return NextResponse.json({ sessions: [], natsAvailable: false });
17
+ }
18
+
19
+ // Supports single status (?status=active) or comma-separated (?status=active,recruiting)
20
+ const statusParam = request.nextUrl.searchParams.get("status");
21
+ const statusFilter = statusParam ? new Set(statusParam.split(",")) : null;
22
+
23
+ const sessions: unknown[] = [];
24
+ try {
25
+ const keys: string[] = [];
26
+ const keyIter = await kv.keys();
27
+ for await (const key of keyIter) keys.push(key);
28
+
29
+ for (const key of keys) {
30
+ try {
31
+ const entry = await kv.get(key);
32
+ if (!entry || !entry.value) continue;
33
+ const session = JSON.parse(sc.decode(entry.value));
34
+ if (statusFilter && !statusFilter.has(session.status)) continue;
35
+ sessions.push(session);
36
+ } catch {
37
+ // Skip malformed entries
38
+ }
39
+ }
40
+ } catch (err) {
41
+ console.error("[cowork/sessions] KV scan error:", (err as Error).message);
42
+ }
43
+
44
+ // Sort: active/recruiting first, then by created_at desc
45
+ const statusOrder: Record<string, number> = {
46
+ active: 0,
47
+ recruiting: 1,
48
+ converged: 2,
49
+ completed: 3,
50
+ aborted: 4,
51
+ };
52
+
53
+ sessions.sort((a: any, b: any) => {
54
+ const aOrder = statusOrder[a.status] ?? 5;
55
+ const bOrder = statusOrder[b.status] ?? 5;
56
+ if (aOrder !== bOrder) return aOrder - bOrder;
57
+ return (
58
+ new Date(b.created_at || 0).getTime() -
59
+ new Date(a.created_at || 0).getTime()
60
+ );
61
+ });
62
+
63
+ return NextResponse.json({ sessions, natsAvailable: true });
64
+ }
@@ -0,0 +1,97 @@
1
+ import { NextResponse } from "next/server";
2
+ import { getRawDb } from "@/lib/db";
3
+ import { getNats } from "@/lib/nats";
4
+ import fs from "fs";
5
+ import { ACTIVE_TASKS_MD, WORKSPACE_ROOT } from "@/lib/config";
6
+ import { parseTasksMarkdown, serializeTasksMarkdown } from "@/lib/parsers/task-markdown";
7
+
8
+ export const dynamic = "force-dynamic";
9
+
10
+ export async function GET() {
11
+ const raw = getRawDb();
12
+
13
+ // Task stats
14
+ const tasksByStatus = raw
15
+ .prepare("SELECT status, COUNT(*) as count FROM tasks GROUP BY status ORDER BY count DESC")
16
+ .all() as Array<{ status: string; count: number }>;
17
+
18
+ const tasksByType = raw
19
+ .prepare("SELECT type, COUNT(*) as count FROM tasks GROUP BY type ORDER BY count DESC")
20
+ .all() as Array<{ type: string; count: number }>;
21
+
22
+ const tasksByKanban = raw
23
+ .prepare("SELECT kanban_column, COUNT(*) as count FROM tasks GROUP BY kanban_column ORDER BY count DESC")
24
+ .all() as Array<{ kanban_column: string; count: number }>;
25
+
26
+ const totalTasks = raw.prepare("SELECT COUNT(*) as count FROM tasks").get() as { count: number };
27
+
28
+ // Memory stats
29
+ const memoryDocCount = (raw.prepare("SELECT COUNT(*) as count FROM memory_docs").get() as { count: number }).count;
30
+ const memoryItemCount = (raw.prepare("SELECT COUNT(*) as count FROM memory_items WHERE status = 'active'").get() as { count: number }).count;
31
+ const entityCount = (raw.prepare("SELECT COUNT(*) as count FROM memory_entities").get() as { count: number }).count;
32
+ const relationCount = (raw.prepare("SELECT COUNT(*) as count FROM memory_relations WHERE valid_to IS NULL").get() as { count: number }).count;
33
+
34
+ // Cluster stats
35
+ const clusterCount = (raw.prepare("SELECT COUNT(*) as count FROM clusters WHERE status = 'active'").get() as { count: number }).count;
36
+ const clusterMemberCount = (raw.prepare("SELECT COUNT(*) as count FROM cluster_members").get() as { count: number }).count;
37
+
38
+ // Sync health
39
+ let syncHealth: { exists: boolean; taskCount: number; roundTripOk: boolean; diffLines: number } = {
40
+ exists: false,
41
+ taskCount: 0,
42
+ roundTripOk: false,
43
+ diffLines: 0,
44
+ };
45
+
46
+ if (fs.existsSync(ACTIVE_TASKS_MD)) {
47
+ const content = fs.readFileSync(ACTIVE_TASKS_MD, "utf-8");
48
+ const parsed = parseTasksMarkdown(content);
49
+ const reserialized = serializeTasksMarkdown(parsed);
50
+ const reparsed = parseTasksMarkdown(reserialized);
51
+
52
+ // Compare field-by-field (ignoring whitespace differences in serialization)
53
+ const roundTripOk = parsed.length === reparsed.length &&
54
+ parsed.every((t, i) => t.id === reparsed[i].id && t.title === reparsed[i].title && t.status === reparsed[i].status);
55
+
56
+ syncHealth = {
57
+ exists: true,
58
+ taskCount: parsed.length,
59
+ roundTripOk,
60
+ diffLines: Math.abs(content.split("\n").length - reserialized.split("\n").length),
61
+ };
62
+ }
63
+
64
+ // NATS status
65
+ let natsStatus = "unavailable";
66
+ try {
67
+ const nc = await getNats();
68
+ natsStatus = nc ? "connected" : "unavailable";
69
+ } catch {
70
+ natsStatus = "error";
71
+ }
72
+
73
+ // Workspace
74
+ const workspaceExists = fs.existsSync(WORKSPACE_ROOT);
75
+
76
+ return NextResponse.json({
77
+ tasks: {
78
+ total: totalTasks.count,
79
+ byStatus: tasksByStatus,
80
+ byType: tasksByType,
81
+ byKanban: tasksByKanban,
82
+ },
83
+ memory: {
84
+ docs: memoryDocCount,
85
+ items: memoryItemCount,
86
+ entities: entityCount,
87
+ relations: relationCount,
88
+ },
89
+ cowork: {
90
+ clusters: clusterCount,
91
+ members: clusterMemberCount,
92
+ },
93
+ sync: syncHealth,
94
+ nats: natsStatus,
95
+ workspace: workspaceExists,
96
+ });
97
+ }