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
@@ -0,0 +1,385 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState, useCallback } from "react";
4
+ import { RefreshCw, CheckCircle, XCircle, Play, AlertTriangle, Clock } from "lucide-react";
5
+
6
+ // ── Types ──
7
+
8
+ interface DiagnosticData {
9
+ tasks: {
10
+ total: number;
11
+ byStatus: Array<{ status: string; count: number }>;
12
+ byType: Array<{ type: string; count: number }>;
13
+ byKanban: Array<{ kanban_column: string; count: number }>;
14
+ };
15
+ memory: { docs: number; items: number; entities: number; relations: number };
16
+ cowork: { clusters: number; members: number };
17
+ sync: { exists: boolean; taskCount: number; roundTripOk: boolean; diffLines: number };
18
+ nats: string;
19
+ workspace: boolean;
20
+ }
21
+
22
+ interface TestResult {
23
+ suite: string;
24
+ name: string;
25
+ status: "pass" | "fail" | "skip";
26
+ detail?: string;
27
+ durationMs: number;
28
+ }
29
+
30
+ interface TestReport {
31
+ summary: { total: number; passed: number; failed: number; skipped: number; durationMs: number };
32
+ results: TestResult[];
33
+ timestamp: string;
34
+ }
35
+
36
+ // ── Helpers ──
37
+
38
+ function StatusIcon({ ok }: { ok: boolean }) {
39
+ return ok ? <CheckCircle className="h-4 w-4 text-green-400" /> : <XCircle className="h-4 w-4 text-red-400" />;
40
+ }
41
+
42
+ function StatRow({ label, value, warn }: { label: string; value: string | number; warn?: boolean }) {
43
+ return (
44
+ <div className="flex items-center justify-between py-1.5 border-b border-border/30">
45
+ <span className="text-xs text-muted-foreground">{label}</span>
46
+ <span className={`text-xs font-mono ${warn ? "text-yellow-400" : "text-foreground"}`}>{value}</span>
47
+ </div>
48
+ );
49
+ }
50
+
51
+ // ── Tabs ──
52
+
53
+ type Tab = "health" | "tests" | "logs";
54
+
55
+ // ── Page ──
56
+
57
+ export default function DiagnosticsPage() {
58
+ const [tab, setTab] = useState<Tab>("tests");
59
+ const [data, setData] = useState<DiagnosticData | null>(null);
60
+ const [healthLoading, setHealthLoading] = useState(false);
61
+ const [healthError, setHealthError] = useState<string | null>(null);
62
+
63
+ const [testReport, setTestReport] = useState<TestReport | null>(null);
64
+ const [testRunning, setTestRunning] = useState(false);
65
+ const [testError, setTestError] = useState<string | null>(null);
66
+
67
+ const [logs, setLogs] = useState<string[]>([]);
68
+
69
+ const log = useCallback((msg: string) => {
70
+ const ts = new Date().toLocaleTimeString("en-CA", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" });
71
+ setLogs((prev) => [`[${ts}] ${msg}`, ...prev].slice(0, 500));
72
+ }, []);
73
+
74
+ // ── Health fetch ──
75
+
76
+ const fetchHealth = async () => {
77
+ setHealthLoading(true);
78
+ setHealthError(null);
79
+ log("Fetching system health...");
80
+ try {
81
+ const res = await fetch("/api/diagnostics");
82
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
83
+ const d = await res.json();
84
+ setData(d);
85
+ log(`Health OK — ${d.tasks.total} tasks, ${d.memory.docs} docs, NATS: ${d.nats}`);
86
+ } catch (e) {
87
+ const msg = (e as Error).message;
88
+ setHealthError(msg);
89
+ log(`Health FAIL: ${msg}`);
90
+ } finally {
91
+ setHealthLoading(false);
92
+ }
93
+ };
94
+
95
+ // ── Test runner ──
96
+
97
+ const runTests = async () => {
98
+ setTestRunning(true);
99
+ setTestError(null);
100
+ setTestReport(null);
101
+ log("Starting test suite...");
102
+ try {
103
+ const res = await fetch("/api/diagnostics/test-runner", { method: "POST" });
104
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
105
+ const report: TestReport = await res.json();
106
+ setTestReport(report);
107
+ const { passed, failed, total, durationMs } = report.summary;
108
+ log(`Tests complete: ${passed}/${total} passed, ${failed} failed (${durationMs}ms)`);
109
+ if (failed > 0) {
110
+ for (const r of report.results.filter((r) => r.status === "fail")) {
111
+ log(` FAIL: [${r.suite}] ${r.name} — ${r.detail || "no detail"}`);
112
+ }
113
+ }
114
+ } catch (e) {
115
+ const msg = (e as Error).message;
116
+ setTestError(msg);
117
+ log(`Test runner error: ${msg}`);
118
+ } finally {
119
+ setTestRunning(false);
120
+ }
121
+ };
122
+
123
+ useEffect(() => {
124
+ fetchHealth();
125
+ }, []);
126
+
127
+ // ── Group test results by suite ──
128
+ const suites = testReport
129
+ ? Array.from(
130
+ testReport.results.reduce((map, r) => {
131
+ if (!map.has(r.suite)) map.set(r.suite, []);
132
+ map.get(r.suite)!.push(r);
133
+ return map;
134
+ }, new Map<string, TestResult[]>())
135
+ )
136
+ : [];
137
+
138
+ return (
139
+ <div className="h-full flex flex-col">
140
+ {/* Header */}
141
+ <header className="border-b border-border px-6 py-4 flex items-center justify-between shrink-0">
142
+ <div>
143
+ <h1 className="text-xl font-bold text-foreground">Diagnostics</h1>
144
+ <p className="text-xs text-muted-foreground mt-0.5">System health, integration tests, and logs</p>
145
+ </div>
146
+ <div className="flex items-center gap-2">
147
+ {/* Tab switcher */}
148
+ <div className="flex rounded-lg bg-accent/50 p-0.5 mr-3">
149
+ {(["tests", "health", "logs"] as Tab[]).map((t) => (
150
+ <button
151
+ key={t}
152
+ onClick={() => setTab(t)}
153
+ className={`rounded-md px-3 py-1.5 text-xs font-medium transition-colors ${
154
+ tab === t ? "bg-card text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"
155
+ }`}
156
+ >
157
+ {t === "tests" ? "Tests" : t === "health" ? "Health" : "Logs"}
158
+ </button>
159
+ ))}
160
+ </div>
161
+ <button
162
+ onClick={runTests}
163
+ disabled={testRunning}
164
+ className="flex items-center gap-2 px-4 py-2 rounded-lg bg-primary text-primary-foreground text-xs font-medium hover:bg-primary/90 transition-colors disabled:opacity-50"
165
+ >
166
+ {testRunning ? (
167
+ <RefreshCw className="h-3.5 w-3.5 animate-spin" />
168
+ ) : (
169
+ <Play className="h-3.5 w-3.5" />
170
+ )}
171
+ {testRunning ? "Running..." : "Run All Tests"}
172
+ </button>
173
+ </div>
174
+ </header>
175
+
176
+ <div className="flex-1 overflow-y-auto p-6">
177
+ {/* ═══ TESTS TAB ═══ */}
178
+ {tab === "tests" && (
179
+ <div className="space-y-4">
180
+ {/* Summary bar */}
181
+ {testReport && (
182
+ <div className={`flex items-center gap-4 rounded-lg border px-4 py-3 ${
183
+ testReport.summary.failed === 0 ? "border-green-500/30 bg-green-500/5" : "border-red-500/30 bg-red-500/5"
184
+ }`}>
185
+ {testReport.summary.failed === 0 ? (
186
+ <CheckCircle className="h-5 w-5 text-green-400" />
187
+ ) : (
188
+ <XCircle className="h-5 w-5 text-red-400" />
189
+ )}
190
+ <div className="flex-1">
191
+ <span className="text-sm font-semibold text-foreground">
192
+ {testReport.summary.failed === 0 ? "All Tests Passed" : `${testReport.summary.failed} Test${testReport.summary.failed > 1 ? "s" : ""} Failed`}
193
+ </span>
194
+ <span className="text-xs text-muted-foreground ml-3">
195
+ {testReport.summary.passed} passed, {testReport.summary.failed} failed, {testReport.summary.total} total
196
+ </span>
197
+ </div>
198
+ <div className="flex items-center gap-1 text-xs text-muted-foreground">
199
+ <Clock className="h-3 w-3" />
200
+ {testReport.summary.durationMs}ms
201
+ </div>
202
+ </div>
203
+ )}
204
+
205
+ {testError && (
206
+ <div className="rounded-lg border border-red-500/30 bg-red-500/5 px-4 py-3 text-xs text-red-400">
207
+ Test runner error: {testError}
208
+ </div>
209
+ )}
210
+
211
+ {!testReport && !testRunning && !testError && (
212
+ <div className="flex flex-col items-center justify-center py-16 text-muted-foreground gap-3">
213
+ <Play className="h-8 w-8 opacity-30" />
214
+ <p className="text-sm">Click "Run All Tests" to start the integration test suite</p>
215
+ <p className="text-xs text-muted-foreground/60 max-w-md text-center">
216
+ Tests exercise: status mapping, task CRUD, done-gate enforcement, markdown parser round-trip,
217
+ DB sync, cowork clusters, memory/graph tables, schema integrity, NATS connectivity, and workspace health.
218
+ </p>
219
+ </div>
220
+ )}
221
+
222
+ {/* Results by suite */}
223
+ {suites.map(([suite, tests]) => {
224
+ const allPass = tests.every((t) => t.status === "pass");
225
+ const failCount = tests.filter((t) => t.status === "fail").length;
226
+ return (
227
+ <div key={suite} className="rounded-lg border border-border bg-card">
228
+ <div className="flex items-center gap-2 px-4 py-2.5 border-b border-border/50">
229
+ {allPass ? (
230
+ <CheckCircle className="h-3.5 w-3.5 text-green-400" />
231
+ ) : (
232
+ <XCircle className="h-3.5 w-3.5 text-red-400" />
233
+ )}
234
+ <span className="text-xs font-semibold text-foreground">{suite}</span>
235
+ <span className="text-[10px] text-muted-foreground ml-auto">
236
+ {tests.length - failCount}/{tests.length}
237
+ </span>
238
+ </div>
239
+ <div className="divide-y divide-border/30">
240
+ {tests.map((t, i) => (
241
+ <div key={i} className="flex items-center gap-2 px-4 py-1.5">
242
+ {t.status === "pass" ? (
243
+ <span className="h-1.5 w-1.5 rounded-full bg-green-400 shrink-0" />
244
+ ) : t.status === "fail" ? (
245
+ <span className="h-1.5 w-1.5 rounded-full bg-red-400 shrink-0" />
246
+ ) : (
247
+ <span className="h-1.5 w-1.5 rounded-full bg-zinc-500 shrink-0" />
248
+ )}
249
+ <span className={`text-xs flex-1 ${t.status === "fail" ? "text-red-400" : "text-foreground"}`}>
250
+ {t.name}
251
+ </span>
252
+ {t.detail && (
253
+ <span className="text-[10px] text-muted-foreground font-mono truncate max-w-[200px]">
254
+ {t.detail}
255
+ </span>
256
+ )}
257
+ <span className="text-[10px] text-muted-foreground/50 tabular-nums w-10 text-right shrink-0">
258
+ {t.durationMs}ms
259
+ </span>
260
+ </div>
261
+ ))}
262
+ </div>
263
+ </div>
264
+ );
265
+ })}
266
+ </div>
267
+ )}
268
+
269
+ {/* ═══ HEALTH TAB ═══ */}
270
+ {tab === "health" && (
271
+ <div className="space-y-4">
272
+ <div className="flex justify-end">
273
+ <button
274
+ onClick={fetchHealth}
275
+ disabled={healthLoading}
276
+ className="flex items-center gap-2 px-3 py-1.5 rounded-md border border-border text-xs text-muted-foreground hover:text-foreground hover:bg-accent transition-colors disabled:opacity-50"
277
+ >
278
+ <RefreshCw className={`h-3.5 w-3.5 ${healthLoading ? "animate-spin" : ""}`} />
279
+ Refresh
280
+ </button>
281
+ </div>
282
+
283
+ {healthError && (
284
+ <div className="rounded-lg border border-red-500/30 bg-red-500/5 px-4 py-3 text-xs text-red-400">
285
+ {healthError}
286
+ </div>
287
+ )}
288
+
289
+ {data && (
290
+ <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
291
+ {/* System Status */}
292
+ <div className="rounded-lg border border-border bg-card p-4">
293
+ <h2 className="text-sm font-semibold text-foreground mb-3">System Status</h2>
294
+ <div className="space-y-2">
295
+ <div className="flex items-center gap-2"><StatusIcon ok={data.workspace} /><span className="text-xs">Workspace</span></div>
296
+ <div className="flex items-center gap-2"><StatusIcon ok={data.nats === "connected"} /><span className="text-xs">NATS: {data.nats}</span></div>
297
+ <div className="flex items-center gap-2"><StatusIcon ok={data.sync.exists} /><span className="text-xs">active-tasks.md: {data.sync.exists ? "found" : "missing"}</span></div>
298
+ <div className="flex items-center gap-2">
299
+ <StatusIcon ok={data.sync.roundTripOk} />
300
+ <span className="text-xs">
301
+ Parser round-trip: {data.sync.roundTripOk ? "OK" : "DRIFT"}
302
+ {data.sync.diffLines > 0 && ` (${data.sync.diffLines} line diff)`}
303
+ </span>
304
+ </div>
305
+ </div>
306
+ </div>
307
+
308
+ {/* Tasks */}
309
+ <div className="rounded-lg border border-border bg-card p-4">
310
+ <h2 className="text-sm font-semibold text-foreground mb-3">Tasks ({data.tasks.total})</h2>
311
+ <div className="mb-3">
312
+ <p className="text-[10px] text-muted-foreground uppercase tracking-wider mb-1">By Status</p>
313
+ {data.tasks.byStatus.map((s) => <StatRow key={s.status} label={s.status || "(empty)"} value={s.count} />)}
314
+ </div>
315
+ <div className="mb-3">
316
+ <p className="text-[10px] text-muted-foreground uppercase tracking-wider mb-1">By Kanban</p>
317
+ {data.tasks.byKanban.map((k) => <StatRow key={k.kanban_column} label={k.kanban_column || "(empty)"} value={k.count} />)}
318
+ </div>
319
+ <div>
320
+ <p className="text-[10px] text-muted-foreground uppercase tracking-wider mb-1">By Type</p>
321
+ {data.tasks.byType.map((t) => <StatRow key={t.type} label={t.type || "task"} value={t.count} />)}
322
+ </div>
323
+ </div>
324
+
325
+ {/* Memory */}
326
+ <div className="rounded-lg border border-border bg-card p-4">
327
+ <h2 className="text-sm font-semibold text-foreground mb-3">Memory</h2>
328
+ <StatRow label="Indexed docs" value={data.memory.docs} warn={data.memory.docs === 0} />
329
+ <StatRow label="Active items" value={data.memory.items} />
330
+ <StatRow label="Entities (graph)" value={data.memory.entities} warn={data.memory.entities === 0} />
331
+ <StatRow label="Active relations" value={data.memory.relations} />
332
+ </div>
333
+
334
+ {/* Cowork */}
335
+ <div className="rounded-lg border border-border bg-card p-4">
336
+ <h2 className="text-sm font-semibold text-foreground mb-3">Cowork</h2>
337
+ <StatRow label="Active clusters" value={data.cowork.clusters} />
338
+ <StatRow label="Cluster members" value={data.cowork.members} />
339
+ </div>
340
+
341
+ {/* Sync */}
342
+ <div className="rounded-lg border border-border bg-card p-4">
343
+ <h2 className="text-sm font-semibold text-foreground mb-3">Markdown Sync</h2>
344
+ <StatRow label="Tasks in markdown" value={data.sync.taskCount} />
345
+ <StatRow label="Round-trip" value={data.sync.roundTripOk ? "PASS" : "FAIL"} warn={!data.sync.roundTripOk} />
346
+ <StatRow label="Line diff" value={`${data.sync.diffLines}`} warn={data.sync.diffLines > 5} />
347
+ </div>
348
+ </div>
349
+ )}
350
+ </div>
351
+ )}
352
+
353
+ {/* ═══ LOGS TAB ═══ */}
354
+ {tab === "logs" && (
355
+ <div className="space-y-3">
356
+ <div className="flex items-center justify-between">
357
+ <span className="text-xs text-muted-foreground">{logs.length} entries</span>
358
+ <button
359
+ onClick={() => setLogs([])}
360
+ className="text-xs text-muted-foreground hover:text-foreground transition-colors"
361
+ >
362
+ Clear
363
+ </button>
364
+ </div>
365
+ <div className="rounded-lg border border-border bg-[#0a0a0f] p-4 font-mono text-[11px] leading-5 max-h-[600px] overflow-y-auto">
366
+ {logs.length === 0 ? (
367
+ <span className="text-muted-foreground/50">No logs yet. Run tests or refresh health to see output.</span>
368
+ ) : (
369
+ logs.map((line, i) => (
370
+ <div key={i} className={`${
371
+ line.includes("FAIL") ? "text-red-400" :
372
+ line.includes("OK") || line.includes("passed") ? "text-green-400" :
373
+ "text-muted-foreground"
374
+ }`}>
375
+ {line}
376
+ </div>
377
+ ))
378
+ )}
379
+ </div>
380
+ </div>
381
+ )}
382
+ </div>
383
+ </div>
384
+ );
385
+ }
@@ -211,6 +211,32 @@ export default function GraphPage() {
211
211
  );
212
212
  }
213
213
 
214
+ // Empty state: no entities seeded
215
+ if (!loading && graphData && graphData.nodes.length === 0) {
216
+ return (
217
+ <div className="h-full flex flex-col items-center justify-center text-muted-foreground gap-3">
218
+ <p className="text-sm">No entities in the knowledge graph</p>
219
+ <p className="text-xs text-muted-foreground/60 max-w-xs text-center">
220
+ Seed known entities to populate the graph with people, projects, tools, and their relationships.
221
+ </p>
222
+ <button
223
+ onClick={async () => {
224
+ setLoading(true);
225
+ try {
226
+ await fetch("/api/memory/graph", { method: "POST" });
227
+ window.location.reload();
228
+ } catch {
229
+ setLoading(false);
230
+ }
231
+ }}
232
+ className="flex items-center gap-2 px-4 py-2 rounded-lg bg-primary text-primary-foreground text-sm hover:bg-primary/90 transition-colors"
233
+ >
234
+ Seed Known Entities
235
+ </button>
236
+ </div>
237
+ );
238
+ }
239
+
214
240
  return (
215
241
  <div className="h-full flex flex-col">
216
242
  <header className="border-b border-border px-6 py-3 flex items-center justify-between shrink-0">
@@ -30,7 +30,7 @@ export default function MemoryPage() {
30
30
  const isSearching = debouncedQuery.length >= 2;
31
31
 
32
32
  // Debounce search input
33
- const debounceRef = useRef<NodeJS.Timeout>();
33
+ const debounceRef = useRef<NodeJS.Timeout>(undefined);
34
34
  const handleInputChange = useCallback(
35
35
  (value: string) => {
36
36
  setQuery(value);
@@ -17,6 +17,7 @@ import {
17
17
  PanelLeftClose,
18
18
  PanelLeftOpen,
19
19
  Filter,
20
+ RefreshCw,
20
21
  } from "lucide-react";
21
22
 
22
23
  const SOURCE_FILTERS = [
@@ -35,6 +36,18 @@ export default function ObsidianPage() {
35
36
  const [showFileTree, setShowFileTree] = useState(true);
36
37
  const [sourceFilter, setSourceFilter] = useState<string>("all");
37
38
  const [showOrphans, setShowOrphans] = useState(true);
39
+ const [indexing, setIndexing] = useState(false);
40
+
41
+ const handleIndexWorkspace = async () => {
42
+ setIndexing(true);
43
+ try {
44
+ await fetch("/api/memory/flush", { method: "POST" });
45
+ // Force SWR to revalidate the graph
46
+ window.location.reload();
47
+ } catch {
48
+ setIndexing(false);
49
+ }
50
+ };
38
51
 
39
52
  // Try indexed doc first (has metadata), fall back to raw workspace file
40
53
  const { doc: indexedDoc, isLoading: idxLoading } = useMemoryDoc(selectedPath);
@@ -213,12 +226,29 @@ export default function ObsidianPage() {
213
226
 
214
227
  {/* Center: Graph */}
215
228
  <div className="flex-1 overflow-hidden">
216
- <ObsidianGraph
217
- graph={filteredGraph}
218
- isLoading={graphLoading}
219
- selectedNode={selectedPath}
220
- onNodeClick={handleNodeClick}
221
- />
229
+ {!graphLoading && filteredGraph && filteredGraph.nodes.length === 0 ? (
230
+ <div className="h-full flex flex-col items-center justify-center text-muted-foreground gap-3">
231
+ <p className="text-sm">No documents indexed yet</p>
232
+ <p className="text-xs text-muted-foreground/60 max-w-xs text-center">
233
+ Index your workspace to build the wikilink graph from your markdown files.
234
+ </p>
235
+ <button
236
+ onClick={handleIndexWorkspace}
237
+ disabled={indexing}
238
+ className="flex items-center gap-2 px-4 py-2 rounded-lg bg-primary text-primary-foreground text-sm hover:bg-primary/90 transition-colors disabled:opacity-50"
239
+ >
240
+ <RefreshCw className={`h-4 w-4 ${indexing ? "animate-spin" : ""}`} />
241
+ {indexing ? "Indexing..." : "Index Workspace"}
242
+ </button>
243
+ </div>
244
+ ) : (
245
+ <ObsidianGraph
246
+ graph={filteredGraph}
247
+ isLoading={graphLoading}
248
+ selectedNode={selectedPath}
249
+ onNodeClick={handleNodeClick}
250
+ />
251
+ )}
222
252
  </div>
223
253
 
224
254
  {/* Right: Reader panel */}
@@ -38,6 +38,7 @@ import {
38
38
  ArrowLeftFromLine,
39
39
  ArrowRightFromLine,
40
40
  Clock,
41
+ Send,
41
42
  } from "lucide-react";
42
43
  import {
43
44
  useProjects,
@@ -1298,6 +1299,29 @@ export default function RoadmapPage() {
1298
1299
  >
1299
1300
  <GitBranch className="h-3 w-3" />
1300
1301
  </button>
1302
+
1303
+ {/* Send to Kanban — only for leaf task nodes */}
1304
+ {node.type === "task" && (
1305
+ node.status === "queued" || node.status === "not started" ? (
1306
+ <button
1307
+ onClick={async (e) => {
1308
+ e.stopPropagation();
1309
+ await updateTask(node.id, {
1310
+ kanbanColumn: "backlog",
1311
+ status: "queued",
1312
+ } as Record<string, unknown>);
1313
+ }}
1314
+ className="opacity-0 group-hover:opacity-100 text-muted-foreground hover:text-primary p-0.5 mr-1 transition-colors"
1315
+ title="Send to Kanban backlog"
1316
+ >
1317
+ <Send className="h-3 w-3" />
1318
+ </button>
1319
+ ) : (
1320
+ <span className="text-[9px] text-green-400 shrink-0 px-1">
1321
+ In Kanban
1322
+ </span>
1323
+ )
1324
+ )}
1301
1325
  </div>
1302
1326
  );
1303
1327
  })}
@@ -269,7 +269,7 @@ touch ~/.openclaw/souls/my-soul/evolution/events.jsonl`}
269
269
  },
270
270
  "specializations": ["domain1", "domain2"],
271
271
  "evolutionEnabled": true,
272
- "parentSoul": "daedalus"
272
+ "parentSoul": "main-agent"
273
273
  }'`}
274
274
  </pre>
275
275
  <p className="text-xs text-muted-foreground mt-2">
@@ -319,7 +319,7 @@ export default function SoulsPage() {
319
319
  {
320
320
  method: "PATCH",
321
321
  headers: { "Content-Type": "application/json" },
322
- body: JSON.stringify({ action, reviewedBy: "daedalus" }),
322
+ body: JSON.stringify({ action, reviewedBy: "main-agent" }),
323
323
  }
324
324
  );
325
325