openclaw-node-harness 2.0.4 → 2.1.1

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 (134) hide show
  1. package/README.md +646 -3
  2. package/bin/hyperagent.mjs +419 -0
  3. package/bin/lane-watchdog.js +23 -2
  4. package/bin/mesh-agent.js +439 -28
  5. package/bin/mesh-bridge.js +69 -3
  6. package/bin/mesh-health-publisher.js +41 -1
  7. package/bin/mesh-task-daemon.js +821 -26
  8. package/bin/mesh.js +411 -20
  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 +296 -10
  29. package/lib/agent-activity.js +2 -2
  30. package/lib/circling-parser.js +119 -0
  31. package/lib/exec-safety.js +105 -0
  32. package/lib/hyperagent-store.mjs +652 -0
  33. package/lib/kanban-io.js +24 -31
  34. package/lib/llm-providers.js +16 -0
  35. package/lib/mcp-knowledge/bench.mjs +118 -0
  36. package/lib/mcp-knowledge/core.mjs +530 -0
  37. package/lib/mcp-knowledge/package.json +25 -0
  38. package/lib/mcp-knowledge/server.mjs +252 -0
  39. package/lib/mcp-knowledge/test.mjs +802 -0
  40. package/lib/memory-budget.mjs +261 -0
  41. package/lib/mesh-collab.js +483 -165
  42. package/lib/mesh-harness.js +427 -0
  43. package/lib/mesh-plans.js +79 -50
  44. package/lib/mesh-tasks.js +132 -49
  45. package/lib/nats-resolve.js +4 -4
  46. package/lib/plan-templates.js +226 -0
  47. package/lib/pre-compression-flush.mjs +322 -0
  48. package/lib/role-loader.js +292 -0
  49. package/lib/rule-loader.js +358 -0
  50. package/lib/session-store.mjs +461 -0
  51. package/lib/transcript-parser.mjs +292 -0
  52. package/mission-control/drizzle/soul_schema_update.sql +29 -0
  53. package/mission-control/drizzle.config.ts +1 -4
  54. package/mission-control/package-lock.json +1571 -83
  55. package/mission-control/package.json +6 -2
  56. package/mission-control/scripts/gen-chronology.js +3 -3
  57. package/mission-control/scripts/import-pipeline-v2.js +0 -16
  58. package/mission-control/scripts/import-pipeline.js +0 -15
  59. package/mission-control/src/app/api/cowork/clusters/[id]/members/route.ts +117 -0
  60. package/mission-control/src/app/api/cowork/clusters/[id]/route.ts +84 -0
  61. package/mission-control/src/app/api/cowork/clusters/route.ts +141 -0
  62. package/mission-control/src/app/api/cowork/dispatch/route.ts +128 -0
  63. package/mission-control/src/app/api/cowork/events/route.ts +65 -0
  64. package/mission-control/src/app/api/cowork/intervene/route.ts +259 -0
  65. package/mission-control/src/app/api/cowork/sessions/[id]/route.ts +37 -0
  66. package/mission-control/src/app/api/cowork/sessions/route.ts +64 -0
  67. package/mission-control/src/app/api/diagnostics/route.ts +97 -0
  68. package/mission-control/src/app/api/diagnostics/test-runner/route.ts +990 -0
  69. package/mission-control/src/app/api/memory/search/route.ts +6 -3
  70. package/mission-control/src/app/api/mesh/events/route.ts +95 -19
  71. package/mission-control/src/app/api/mesh/identity/route.ts +11 -0
  72. package/mission-control/src/app/api/mesh/tasks/[id]/route.ts +92 -0
  73. package/mission-control/src/app/api/mesh/tasks/route.ts +91 -0
  74. package/mission-control/src/app/api/souls/[id]/evolution/route.ts +21 -5
  75. package/mission-control/src/app/api/souls/[id]/prompt/route.ts +7 -1
  76. package/mission-control/src/app/api/souls/[id]/propagate/route.ts +14 -2
  77. package/mission-control/src/app/api/tasks/[id]/handoff/route.ts +8 -2
  78. package/mission-control/src/app/api/tasks/[id]/route.ts +90 -4
  79. package/mission-control/src/app/api/tasks/route.ts +21 -30
  80. package/mission-control/src/app/api/workspace/read/route.ts +11 -0
  81. package/mission-control/src/app/cowork/page.tsx +261 -0
  82. package/mission-control/src/app/diagnostics/page.tsx +385 -0
  83. package/mission-control/src/app/graph/page.tsx +26 -0
  84. package/mission-control/src/app/memory/page.tsx +1 -1
  85. package/mission-control/src/app/obsidian/page.tsx +36 -6
  86. package/mission-control/src/app/roadmap/page.tsx +24 -0
  87. package/mission-control/src/app/souls/page.tsx +2 -2
  88. package/mission-control/src/components/board/execution-config.tsx +431 -0
  89. package/mission-control/src/components/board/kanban-board.tsx +75 -9
  90. package/mission-control/src/components/board/kanban-column.tsx +135 -19
  91. package/mission-control/src/components/board/task-card.tsx +55 -2
  92. package/mission-control/src/components/board/unified-task-dialog.tsx +82 -4
  93. package/mission-control/src/components/cowork/cluster-card.tsx +176 -0
  94. package/mission-control/src/components/cowork/create-cluster-dialog.tsx +251 -0
  95. package/mission-control/src/components/cowork/dispatch-form.tsx +423 -0
  96. package/mission-control/src/components/cowork/role-picker.tsx +102 -0
  97. package/mission-control/src/components/cowork/session-card.tsx +284 -0
  98. package/mission-control/src/components/layout/sidebar.tsx +39 -2
  99. package/mission-control/src/lib/__tests__/daily-log.test.ts +82 -0
  100. package/mission-control/src/lib/__tests__/memory-md.test.ts +87 -0
  101. package/mission-control/src/lib/__tests__/mesh-kv-sync.test.ts +465 -0
  102. package/mission-control/src/lib/__tests__/mocks/mock-kv.ts +131 -0
  103. package/mission-control/src/lib/__tests__/status-kanban.test.ts +46 -0
  104. package/mission-control/src/lib/__tests__/task-markdown.test.ts +188 -0
  105. package/mission-control/src/lib/__tests__/wikilinks.test.ts +175 -0
  106. package/mission-control/src/lib/config.ts +67 -0
  107. package/mission-control/src/lib/db/index.ts +85 -1
  108. package/mission-control/src/lib/db/schema.ts +61 -3
  109. package/mission-control/src/lib/hooks.ts +309 -0
  110. package/mission-control/src/lib/memory/entities.ts +3 -2
  111. package/mission-control/src/lib/memory/extract.ts +2 -1
  112. package/mission-control/src/lib/memory/retrieval.ts +3 -2
  113. package/mission-control/src/lib/nats.ts +66 -1
  114. package/mission-control/src/lib/parsers/task-markdown.ts +52 -2
  115. package/mission-control/src/lib/parsers/transcript.ts +4 -4
  116. package/mission-control/src/lib/scheduler.ts +12 -11
  117. package/mission-control/src/lib/sync/mesh-kv.ts +279 -0
  118. package/mission-control/src/lib/sync/tasks.ts +23 -1
  119. package/mission-control/src/lib/task-id.ts +32 -0
  120. package/mission-control/src/lib/tts/index.ts +33 -9
  121. package/mission-control/src/middleware.ts +82 -0
  122. package/mission-control/tsconfig.json +2 -1
  123. package/mission-control/vitest.config.ts +14 -0
  124. package/package.json +15 -2
  125. package/services/launchd/ai.openclaw.log-rotate.plist +11 -0
  126. package/services/launchd/ai.openclaw.mesh-deploy-listener.plist +4 -0
  127. package/services/launchd/ai.openclaw.mesh-health-publisher.plist +4 -0
  128. package/services/launchd/ai.openclaw.mission-control.plist +1 -1
  129. package/services/service-manifest.json +1 -1
  130. package/skills/cc-godmode/references/agents.md +8 -8
  131. package/uninstall.sh +37 -9
  132. package/workspace-bin/memory-daemon.mjs +199 -5
  133. package/workspace-bin/session-search.mjs +204 -0
  134. package/workspace-bin/web-fetch.mjs +65 -0
@@ -7,7 +7,9 @@ import { statusToKanban } from "@/lib/parsers/task-markdown";
7
7
  import { syncTasksFromMarkdownIfChanged, syncTasksToMarkdown } from "@/lib/sync/tasks";
8
8
  import { logActivity } from "@/lib/activity";
9
9
  import { schedulerTick } from "@/lib/scheduler";
10
- import { WORKSPACE_ROOT } from "@/lib/config";
10
+ import { generateTaskId } from "@/lib/task-id";
11
+ import { getNats, sc } from "@/lib/nats";
12
+ import { WORKSPACE_ROOT, AGENT_NAME } from "@/lib/config";
11
13
  import path from "path";
12
14
 
13
15
  /**
@@ -67,7 +69,7 @@ export async function GET(request: NextRequest) {
67
69
  title: liveWork.title,
68
70
  status: "running",
69
71
  kanbanColumn: "in_progress",
70
- owner: "daedalus",
72
+ owner: AGENT_NAME.toLowerCase(),
71
73
  nextAction: liveWork.nextAction || null,
72
74
  updatedAt: new Date().toISOString(),
73
75
  createdAt: new Date().toISOString(),
@@ -186,6 +188,15 @@ export async function POST(request: NextRequest) {
186
188
  isRecurring: body.is_recurring ? 1 : 0,
187
189
  capacityClass: body.capacity_class || "normal",
188
190
  autoPriority: body.auto_priority || 0,
191
+ // Execution fields
192
+ execution: body.execution || null,
193
+ collaboration: body.collaboration ? JSON.stringify(body.collaboration) : null,
194
+ preferredNodes: body.preferred_nodes?.length ? JSON.stringify(body.preferred_nodes) : null,
195
+ excludeNodes: body.exclude_nodes?.length ? JSON.stringify(body.exclude_nodes) : null,
196
+ clusterId: body.cluster_id || null,
197
+ metric: body.metric || null,
198
+ budgetMinutes: body.budget_minutes || 30,
199
+ scope: body.scope?.length ? JSON.stringify(body.scope) : null,
189
200
  updatedAt: now.toISOString(),
190
201
  createdAt: now.toISOString(),
191
202
  })
@@ -200,6 +211,13 @@ export async function POST(request: NextRequest) {
200
211
  // Immediately run scheduler tick so auto-start tasks dispatch without waiting for poll
201
212
  schedulerTick();
202
213
 
214
+ // Wake bridge for mesh tasks
215
+ if (body.execution === "mesh") {
216
+ getNats().then((nc) => {
217
+ if (nc) nc.publish("mesh.bridge.wake", sc.encode(""));
218
+ }).catch(() => {});
219
+ }
220
+
203
221
  const created = db
204
222
  .select()
205
223
  .from(tasks)
@@ -223,31 +241,4 @@ export async function POST(request: NextRequest) {
223
241
  { status: 500 }
224
242
  );
225
243
  }
226
- }
227
-
228
- /**
229
- * Generate a task ID in the format T-YYYYMMDD-NNN.
230
- * NNN is a zero-padded sequence number based on existing tasks for that date.
231
- */
232
- function generateTaskId(db: ReturnType<typeof getDb>, date: Date): string {
233
- const dateStr =
234
- date.getFullYear().toString() +
235
- (date.getMonth() + 1).toString().padStart(2, "0") +
236
- date.getDate().toString().padStart(2, "0");
237
-
238
- const prefix = `T-${dateStr}-`;
239
-
240
- // Find highest existing sequence number for this date prefix
241
- const existing = db
242
- .select({ id: tasks.id })
243
- .from(tasks)
244
- .all()
245
- .filter((t) => t.id.startsWith(prefix))
246
- .map((t) => {
247
- const seq = parseInt(t.id.slice(prefix.length), 10);
248
- return isNaN(seq) ? 0 : seq;
249
- });
250
-
251
- const nextSeq = existing.length > 0 ? Math.max(...existing) + 1 : 1;
252
- return `${prefix}${nextSeq.toString().padStart(3, "0")}`;
253
- }
244
+ }
@@ -25,6 +25,17 @@ export async function GET(request: NextRequest) {
25
25
  return NextResponse.json({ error: "File not found" }, { status: 404 });
26
26
  }
27
27
 
28
+ // Defeat symlink traversal: resolve the real path and re-check prefix
29
+ const realPath = fs.realpathSync(absPath);
30
+ const realRoot = fs.realpathSync(WORKSPACE_ROOT);
31
+ if (!realPath.startsWith(realRoot + path.sep) && realPath !== realRoot) {
32
+ return NextResponse.json({ error: "Path traversal denied" }, { status: 403 });
33
+ }
34
+
35
+ if (!fs.existsSync(realPath)) {
36
+ return NextResponse.json({ error: "File not found" }, { status: 404 });
37
+ }
38
+
28
39
  const stat = fs.statSync(absPath);
29
40
  if (stat.isDirectory()) {
30
41
  return NextResponse.json({ error: "Path is a directory" }, { status: 400 });
@@ -0,0 +1,261 @@
1
+ "use client";
2
+
3
+ import { useState, useMemo } from "react";
4
+ import { AlertTriangle, Plus, Users2 } from "lucide-react";
5
+ import {
6
+ useCollabSessions,
7
+ useClusters,
8
+ useTasks,
9
+ addClusterMember,
10
+ } from "@/lib/hooks";
11
+ import { SessionCard } from "@/components/cowork/session-card";
12
+ import { ClusterCard } from "@/components/cowork/cluster-card";
13
+ import { CreateClusterDialog } from "@/components/cowork/create-cluster-dialog";
14
+ import { DispatchForm } from "@/components/cowork/dispatch-form";
15
+ import useSWR from "swr";
16
+ import { RolePicker } from "@/components/cowork/role-picker";
17
+
18
+ const fetcher = (url: string) => fetch(url).then((r) => r.json());
19
+
20
+ type Tab = "sessions" | "clusters" | "dispatch";
21
+
22
+ export default function CoworkPage() {
23
+ const [tab, setTab] = useState<Tab>("sessions");
24
+ const [createOpen, setCreateOpen] = useState(false);
25
+ const [addNodeTarget, setAddNodeTarget] = useState<string | null>(null);
26
+
27
+ const { sessions: activeSessions, natsAvailable } = useCollabSessions(
28
+ "recruiting,active",
29
+ 5000
30
+ );
31
+ const { sessions: recentSessions } = useCollabSessions(
32
+ "converged,completed,aborted",
33
+ 30000
34
+ );
35
+ const { clusters } = useClusters();
36
+ const { tasks } = useTasks();
37
+
38
+ // Build a map from task_id to Task for cross-referencing sessions
39
+ const taskMap = useMemo(() => {
40
+ const map = new Map<string, typeof tasks[0]>();
41
+ for (const t of tasks) map.set(t.id, t);
42
+ return map;
43
+ }, [tasks]);
44
+
45
+ const handleDispatchFromCluster = (clusterId: string) => {
46
+ setTab("dispatch");
47
+ };
48
+
49
+ return (
50
+ <div className="flex flex-col h-full">
51
+ {/* Header */}
52
+ <header className="flex items-center justify-between border-b border-border px-6 py-4">
53
+ <div className="flex items-center gap-4">
54
+ <h1 className="text-lg font-semibold">Cowork</h1>
55
+ {activeSessions.length > 0 && (
56
+ <span className="flex items-center gap-1.5 rounded-full bg-green-500/10 px-2.5 py-0.5 text-xs text-green-400">
57
+ <span className="h-1.5 w-1.5 rounded-full bg-green-400 animate-pulse" />
58
+ {activeSessions.length} active
59
+ </span>
60
+ )}
61
+ <span className="rounded-full bg-accent px-2.5 py-0.5 text-xs text-muted-foreground">
62
+ {clusters.length} clusters
63
+ </span>
64
+ </div>
65
+
66
+ {/* Tab bar */}
67
+ <div className="flex rounded-lg bg-accent/50 p-0.5">
68
+ {(["sessions", "clusters", "dispatch"] as Tab[]).map((t) => (
69
+ <button
70
+ key={t}
71
+ onClick={() => setTab(t)}
72
+ className={`rounded-md px-3 py-1.5 text-xs font-medium transition-colors ${
73
+ tab === t
74
+ ? "bg-card text-foreground shadow-sm"
75
+ : "text-muted-foreground hover:text-foreground"
76
+ }`}
77
+ >
78
+ {t.charAt(0).toUpperCase() + t.slice(1)}
79
+ </button>
80
+ ))}
81
+ </div>
82
+ </header>
83
+
84
+ {/* Content */}
85
+ <main className="flex-1 overflow-y-auto p-6 space-y-6">
86
+ {/* Sessions tab */}
87
+ {tab === "sessions" && (
88
+ <>
89
+ {!natsAvailable && (
90
+ <div className="flex items-center gap-2 rounded-lg border border-yellow-500/20 bg-yellow-500/5 px-4 py-3 text-xs text-yellow-400">
91
+ <AlertTriangle className="h-4 w-4" />
92
+ NATS unavailable — session data may be stale
93
+ </div>
94
+ )}
95
+
96
+ {activeSessions.length > 0 && (
97
+ <section>
98
+ <h2 className="text-sm font-medium text-muted-foreground mb-3">
99
+ Active Sessions
100
+ </h2>
101
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
102
+ {activeSessions.map((s) => (
103
+ <SessionCard key={s.session_id} session={s} linkedTask={taskMap.get(s.task_id)} />
104
+ ))}
105
+ </div>
106
+ </section>
107
+ )}
108
+
109
+ {recentSessions.length > 0 && (
110
+ <section>
111
+ <h2 className="text-sm font-medium text-muted-foreground mb-3">
112
+ Recent Sessions
113
+ </h2>
114
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
115
+ {recentSessions.slice(0, 8).map((s) => (
116
+ <SessionCard key={s.session_id} session={s} linkedTask={taskMap.get(s.task_id)} />
117
+ ))}
118
+ </div>
119
+ </section>
120
+ )}
121
+
122
+ {activeSessions.length === 0 && recentSessions.length === 0 && (
123
+ <div className="flex flex-col items-center justify-center py-16 text-muted-foreground">
124
+ <Users2 className="h-8 w-8 mb-3 opacity-30" />
125
+ <p className="text-sm">No collab sessions yet</p>
126
+ <p className="text-xs mt-1">
127
+ Dispatch a task from the Dispatch tab to start one
128
+ </p>
129
+ </div>
130
+ )}
131
+ </>
132
+ )}
133
+
134
+ {/* Clusters tab */}
135
+ {tab === "clusters" && (
136
+ <>
137
+ <div className="flex items-center justify-between">
138
+ <h2 className="text-sm font-medium text-muted-foreground">
139
+ Clusters
140
+ </h2>
141
+ <button
142
+ onClick={() => setCreateOpen(true)}
143
+ className="flex items-center gap-1 rounded px-3 py-1.5 text-xs bg-accent-foreground text-accent transition-colors hover:opacity-90"
144
+ >
145
+ <Plus className="h-3.5 w-3.5" />
146
+ New Cluster
147
+ </button>
148
+ </div>
149
+
150
+ {clusters.length > 0 ? (
151
+ <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3">
152
+ {clusters.map((c) => (
153
+ <ClusterCard
154
+ key={c.id}
155
+ cluster={c}
156
+ onDispatch={handleDispatchFromCluster}
157
+ onAddNode={(id) => setAddNodeTarget(id)}
158
+ />
159
+ ))}
160
+ </div>
161
+ ) : (
162
+ <div className="flex flex-col items-center justify-center py-16 text-muted-foreground">
163
+ <Users2 className="h-8 w-8 mb-3 opacity-30" />
164
+ <p className="text-sm">No clusters yet</p>
165
+ <p className="text-xs mt-1">
166
+ Create one to organize your mesh nodes into teams
167
+ </p>
168
+ </div>
169
+ )}
170
+
171
+ <CreateClusterDialog
172
+ open={createOpen}
173
+ onClose={() => setCreateOpen(false)}
174
+ />
175
+
176
+ {/* Add node mini-dialog */}
177
+ {addNodeTarget && (
178
+ <AddNodeDialog
179
+ clusterId={addNodeTarget}
180
+ existingNodeIds={
181
+ clusters
182
+ .find((c) => c.id === addNodeTarget)
183
+ ?.members.map((m) => m.nodeId) ?? []
184
+ }
185
+ onClose={() => setAddNodeTarget(null)}
186
+ />
187
+ )}
188
+ </>
189
+ )}
190
+
191
+ {/* Dispatch tab */}
192
+ {tab === "dispatch" && <DispatchForm />}
193
+ </main>
194
+ </div>
195
+ );
196
+ }
197
+
198
+ function AddNodeDialog({
199
+ clusterId,
200
+ existingNodeIds,
201
+ onClose,
202
+ }: {
203
+ clusterId: string;
204
+ existingNodeIds: string[];
205
+ onClose: () => void;
206
+ }) {
207
+ const { data } = useSWR<{
208
+ nodes: Array<{ nodeId: string; status: string }>;
209
+ }>("/api/mesh/nodes", fetcher);
210
+ const meshNodes = (data?.nodes ?? []).filter(
211
+ (n) => !existingNodeIds.includes(n.nodeId)
212
+ );
213
+
214
+ const [selectedRole, setSelectedRole] = useState("worker");
215
+
216
+ const handleAdd = async (nodeId: string) => {
217
+ await addClusterMember(clusterId, nodeId, selectedRole);
218
+ onClose();
219
+ };
220
+
221
+ return (
222
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60">
223
+ <div className="w-full max-w-sm rounded-lg border border-border bg-card shadow-xl">
224
+ <div className="flex items-center justify-between border-b border-border px-4 py-3">
225
+ <span className="text-sm font-semibold">Add Node</span>
226
+ <button
227
+ onClick={onClose}
228
+ className="text-muted-foreground hover:text-foreground text-xs"
229
+ >
230
+ close
231
+ </button>
232
+ </div>
233
+ <div className="px-4 py-3 space-y-2">
234
+ <div className="flex items-center gap-2 mb-3">
235
+ <span className="text-xs text-muted-foreground">Role:</span>
236
+ <RolePicker value={selectedRole} onChange={setSelectedRole} />
237
+ </div>
238
+ {meshNodes.map((node) => (
239
+ <button
240
+ key={node.nodeId}
241
+ onClick={() => handleAdd(node.nodeId)}
242
+ className="flex w-full items-center gap-2 rounded px-3 py-2 text-xs hover:bg-accent/50 transition-colors"
243
+ >
244
+ <span
245
+ className={`h-2 w-2 rounded-full ${
246
+ node.status === "online" ? "bg-green-400" : "bg-zinc-600"
247
+ }`}
248
+ />
249
+ <span className="font-mono">{node.nodeId}</span>
250
+ </button>
251
+ ))}
252
+ {meshNodes.length === 0 && (
253
+ <p className="text-xs text-muted-foreground py-2">
254
+ All nodes already in this cluster
255
+ </p>
256
+ )}
257
+ </div>
258
+ </div>
259
+ </div>
260
+ );
261
+ }