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.
- package/README.md +646 -3
- package/bin/hyperagent.mjs +419 -0
- package/bin/lane-watchdog.js +23 -2
- package/bin/mesh-agent.js +439 -28
- package/bin/mesh-bridge.js +69 -3
- package/bin/mesh-health-publisher.js +41 -1
- package/bin/mesh-task-daemon.js +821 -26
- package/bin/mesh.js +411 -20
- package/config/claude-settings.json +95 -0
- package/config/daemon.json.template +2 -1
- package/config/git-hooks/pre-commit +13 -0
- package/config/git-hooks/pre-push +12 -0
- package/config/harness-rules.json +174 -0
- package/config/plan-templates/team-bugfix.yaml +52 -0
- package/config/plan-templates/team-deploy.yaml +50 -0
- package/config/plan-templates/team-feature.yaml +71 -0
- package/config/roles/qa-engineer.yaml +36 -0
- package/config/roles/solidity-dev.yaml +51 -0
- package/config/roles/tech-architect.yaml +36 -0
- package/config/rules/framework/solidity.md +22 -0
- package/config/rules/framework/typescript.md +21 -0
- package/config/rules/framework/unity.md +21 -0
- package/config/rules/universal/design-docs.md +18 -0
- package/config/rules/universal/git-hygiene.md +18 -0
- package/config/rules/universal/security.md +19 -0
- package/config/rules/universal/test-standards.md +19 -0
- package/identity/DELEGATION.md +6 -6
- package/install.sh +296 -10
- package/lib/agent-activity.js +2 -2
- package/lib/circling-parser.js +119 -0
- package/lib/exec-safety.js +105 -0
- package/lib/hyperagent-store.mjs +652 -0
- package/lib/kanban-io.js +24 -31
- package/lib/llm-providers.js +16 -0
- package/lib/mcp-knowledge/bench.mjs +118 -0
- package/lib/mcp-knowledge/core.mjs +530 -0
- package/lib/mcp-knowledge/package.json +25 -0
- package/lib/mcp-knowledge/server.mjs +252 -0
- package/lib/mcp-knowledge/test.mjs +802 -0
- package/lib/memory-budget.mjs +261 -0
- package/lib/mesh-collab.js +483 -165
- package/lib/mesh-harness.js +427 -0
- package/lib/mesh-plans.js +79 -50
- package/lib/mesh-tasks.js +132 -49
- package/lib/nats-resolve.js +4 -4
- package/lib/plan-templates.js +226 -0
- package/lib/pre-compression-flush.mjs +322 -0
- package/lib/role-loader.js +292 -0
- package/lib/rule-loader.js +358 -0
- package/lib/session-store.mjs +461 -0
- package/lib/transcript-parser.mjs +292 -0
- package/mission-control/drizzle/soul_schema_update.sql +29 -0
- package/mission-control/drizzle.config.ts +1 -4
- package/mission-control/package-lock.json +1571 -83
- package/mission-control/package.json +6 -2
- package/mission-control/scripts/gen-chronology.js +3 -3
- package/mission-control/scripts/import-pipeline-v2.js +0 -16
- package/mission-control/scripts/import-pipeline.js +0 -15
- package/mission-control/src/app/api/cowork/clusters/[id]/members/route.ts +117 -0
- package/mission-control/src/app/api/cowork/clusters/[id]/route.ts +84 -0
- package/mission-control/src/app/api/cowork/clusters/route.ts +141 -0
- package/mission-control/src/app/api/cowork/dispatch/route.ts +128 -0
- package/mission-control/src/app/api/cowork/events/route.ts +65 -0
- package/mission-control/src/app/api/cowork/intervene/route.ts +259 -0
- package/mission-control/src/app/api/cowork/sessions/[id]/route.ts +37 -0
- package/mission-control/src/app/api/cowork/sessions/route.ts +64 -0
- package/mission-control/src/app/api/diagnostics/route.ts +97 -0
- package/mission-control/src/app/api/diagnostics/test-runner/route.ts +990 -0
- package/mission-control/src/app/api/memory/search/route.ts +6 -3
- package/mission-control/src/app/api/mesh/events/route.ts +95 -19
- package/mission-control/src/app/api/mesh/identity/route.ts +11 -0
- package/mission-control/src/app/api/mesh/tasks/[id]/route.ts +92 -0
- package/mission-control/src/app/api/mesh/tasks/route.ts +91 -0
- package/mission-control/src/app/api/souls/[id]/evolution/route.ts +21 -5
- package/mission-control/src/app/api/souls/[id]/prompt/route.ts +7 -1
- package/mission-control/src/app/api/souls/[id]/propagate/route.ts +14 -2
- package/mission-control/src/app/api/tasks/[id]/handoff/route.ts +8 -2
- package/mission-control/src/app/api/tasks/[id]/route.ts +90 -4
- package/mission-control/src/app/api/tasks/route.ts +21 -30
- package/mission-control/src/app/api/workspace/read/route.ts +11 -0
- package/mission-control/src/app/cowork/page.tsx +261 -0
- package/mission-control/src/app/diagnostics/page.tsx +385 -0
- package/mission-control/src/app/graph/page.tsx +26 -0
- package/mission-control/src/app/memory/page.tsx +1 -1
- package/mission-control/src/app/obsidian/page.tsx +36 -6
- package/mission-control/src/app/roadmap/page.tsx +24 -0
- package/mission-control/src/app/souls/page.tsx +2 -2
- package/mission-control/src/components/board/execution-config.tsx +431 -0
- package/mission-control/src/components/board/kanban-board.tsx +75 -9
- package/mission-control/src/components/board/kanban-column.tsx +135 -19
- package/mission-control/src/components/board/task-card.tsx +55 -2
- package/mission-control/src/components/board/unified-task-dialog.tsx +82 -4
- package/mission-control/src/components/cowork/cluster-card.tsx +176 -0
- package/mission-control/src/components/cowork/create-cluster-dialog.tsx +251 -0
- package/mission-control/src/components/cowork/dispatch-form.tsx +423 -0
- package/mission-control/src/components/cowork/role-picker.tsx +102 -0
- package/mission-control/src/components/cowork/session-card.tsx +284 -0
- package/mission-control/src/components/layout/sidebar.tsx +39 -2
- package/mission-control/src/lib/__tests__/daily-log.test.ts +82 -0
- package/mission-control/src/lib/__tests__/memory-md.test.ts +87 -0
- package/mission-control/src/lib/__tests__/mesh-kv-sync.test.ts +465 -0
- package/mission-control/src/lib/__tests__/mocks/mock-kv.ts +131 -0
- package/mission-control/src/lib/__tests__/status-kanban.test.ts +46 -0
- package/mission-control/src/lib/__tests__/task-markdown.test.ts +188 -0
- package/mission-control/src/lib/__tests__/wikilinks.test.ts +175 -0
- package/mission-control/src/lib/config.ts +67 -0
- package/mission-control/src/lib/db/index.ts +85 -1
- package/mission-control/src/lib/db/schema.ts +61 -3
- package/mission-control/src/lib/hooks.ts +309 -0
- package/mission-control/src/lib/memory/entities.ts +3 -2
- package/mission-control/src/lib/memory/extract.ts +2 -1
- package/mission-control/src/lib/memory/retrieval.ts +3 -2
- package/mission-control/src/lib/nats.ts +66 -1
- package/mission-control/src/lib/parsers/task-markdown.ts +52 -2
- package/mission-control/src/lib/parsers/transcript.ts +4 -4
- package/mission-control/src/lib/scheduler.ts +12 -11
- package/mission-control/src/lib/sync/mesh-kv.ts +279 -0
- package/mission-control/src/lib/sync/tasks.ts +23 -1
- package/mission-control/src/lib/task-id.ts +32 -0
- package/mission-control/src/lib/tts/index.ts +33 -9
- package/mission-control/src/middleware.ts +82 -0
- package/mission-control/tsconfig.json +2 -1
- package/mission-control/vitest.config.ts +14 -0
- package/package.json +15 -2
- package/services/launchd/ai.openclaw.log-rotate.plist +11 -0
- package/services/launchd/ai.openclaw.mesh-deploy-listener.plist +4 -0
- package/services/launchd/ai.openclaw.mesh-health-publisher.plist +4 -0
- package/services/launchd/ai.openclaw.mission-control.plist +1 -1
- package/services/service-manifest.json +1 -1
- package/skills/cc-godmode/references/agents.md +8 -8
- package/uninstall.sh +37 -9
- package/workspace-bin/memory-daemon.mjs +199 -5
- package/workspace-bin/session-search.mjs +204 -0
- package/workspace-bin/web-fetch.mjs +65 -0
|
@@ -0,0 +1,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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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": "
|
|
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: "
|
|
322
|
+
body: JSON.stringify({ action, reviewedBy: "main-agent" }),
|
|
323
323
|
}
|
|
324
324
|
);
|
|
325
325
|
|