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
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useState } from "react";
|
|
4
|
-
import { TaskCard } from "./task-card";
|
|
3
|
+
import { useState, useMemo } from "react";
|
|
4
|
+
import { TaskCard, SignetCard } from "./task-card";
|
|
5
5
|
import type { Task } from "@/lib/hooks";
|
|
6
|
+
import { X, ChevronDown } from "lucide-react";
|
|
7
|
+
|
|
8
|
+
export type CardSize = "full" | "compact" | "signet";
|
|
6
9
|
|
|
7
10
|
const COLUMN_LABELS: Record<string, string> = {
|
|
8
11
|
backlog: "Backlog",
|
|
@@ -18,17 +21,26 @@ const COLUMN_DOT_COLORS: Record<string, string> = {
|
|
|
18
21
|
done: "bg-zinc-400",
|
|
19
22
|
};
|
|
20
23
|
|
|
24
|
+
const DONE_WINDOWS = [
|
|
25
|
+
{ key: "1d", label: "Today", ms: 24 * 60 * 60 * 1000 },
|
|
26
|
+
{ key: "3d", label: "3 days", ms: 3 * 24 * 60 * 60 * 1000 },
|
|
27
|
+
{ key: "7d", label: "Week", ms: 7 * 24 * 60 * 60 * 1000 },
|
|
28
|
+
{ key: "all", label: "All", ms: 0 },
|
|
29
|
+
] as const;
|
|
30
|
+
|
|
21
31
|
interface KanbanColumnProps {
|
|
22
32
|
column: string;
|
|
23
33
|
tasks: Task[];
|
|
24
34
|
childrenMap: Record<string, Task[]>;
|
|
25
35
|
onTaskClick: (task: Task) => void;
|
|
26
36
|
onMoveTask: (taskId: string, targetColumn: string) => void;
|
|
37
|
+
onClearDone?: () => void;
|
|
27
38
|
label?: string;
|
|
28
39
|
dotColor?: string;
|
|
29
40
|
highlight?: boolean;
|
|
30
41
|
bulkMode?: boolean;
|
|
31
42
|
bulkSelection?: Set<string>;
|
|
43
|
+
cardSize?: CardSize;
|
|
32
44
|
}
|
|
33
45
|
|
|
34
46
|
export function KanbanColumn({
|
|
@@ -37,13 +49,33 @@ export function KanbanColumn({
|
|
|
37
49
|
childrenMap,
|
|
38
50
|
onTaskClick,
|
|
39
51
|
onMoveTask,
|
|
52
|
+
onClearDone,
|
|
40
53
|
label,
|
|
41
54
|
dotColor,
|
|
42
55
|
highlight,
|
|
43
56
|
bulkMode,
|
|
44
57
|
bulkSelection,
|
|
58
|
+
cardSize = column === "done" ? "signet" : "full",
|
|
45
59
|
}: KanbanColumnProps) {
|
|
46
60
|
const [dragOver, setDragOver] = useState(false);
|
|
61
|
+
const [doneWindow, setDoneWindow] = useState<string>("3d");
|
|
62
|
+
const [doneWindowOpen, setDoneWindowOpen] = useState(false);
|
|
63
|
+
// Track expanded signets (signet → full card inline)
|
|
64
|
+
const [expandedSignets, setExpandedSignets] = useState<Set<string>>(new Set());
|
|
65
|
+
|
|
66
|
+
// Filter done column by time window
|
|
67
|
+
const filteredTasks = useMemo(() => {
|
|
68
|
+
if (column !== "done" || doneWindow === "all") return tasks;
|
|
69
|
+
const windowDef = DONE_WINDOWS.find((w) => w.key === doneWindow);
|
|
70
|
+
if (!windowDef || windowDef.ms === 0) return tasks;
|
|
71
|
+
const cutoff = Date.now() - windowDef.ms;
|
|
72
|
+
return tasks.filter((t) => {
|
|
73
|
+
const ts = new Date(t.updatedAt).getTime();
|
|
74
|
+
return ts >= cutoff;
|
|
75
|
+
});
|
|
76
|
+
}, [tasks, column, doneWindow]);
|
|
77
|
+
|
|
78
|
+
const hiddenCount = tasks.length - filteredTasks.length;
|
|
47
79
|
|
|
48
80
|
return (
|
|
49
81
|
<div
|
|
@@ -75,26 +107,110 @@ export function KanbanColumn({
|
|
|
75
107
|
{label ?? COLUMN_LABELS[column] ?? column}
|
|
76
108
|
</span>
|
|
77
109
|
<span className="ml-auto text-[10px] text-muted-foreground">
|
|
78
|
-
{
|
|
110
|
+
{filteredTasks.length}
|
|
111
|
+
{hiddenCount > 0 && (
|
|
112
|
+
<span className="text-muted-foreground/40"> ({hiddenCount} older)</span>
|
|
113
|
+
)}
|
|
79
114
|
</span>
|
|
80
115
|
</div>
|
|
81
116
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
117
|
+
{/* Done column controls */}
|
|
118
|
+
{column === "done" && (
|
|
119
|
+
<div className="flex items-center gap-1 px-2 py-1.5 border-b border-border/50">
|
|
120
|
+
{/* Time window selector */}
|
|
121
|
+
<div className="relative">
|
|
122
|
+
<button
|
|
123
|
+
onClick={() => setDoneWindowOpen(!doneWindowOpen)}
|
|
124
|
+
className="flex items-center gap-1 px-2 py-0.5 text-[10px] text-muted-foreground rounded hover:bg-accent transition-colors"
|
|
125
|
+
>
|
|
126
|
+
{DONE_WINDOWS.find((w) => w.key === doneWindow)?.label ?? "3 days"}
|
|
127
|
+
<ChevronDown className="h-2.5 w-2.5" />
|
|
128
|
+
</button>
|
|
129
|
+
{doneWindowOpen && (
|
|
130
|
+
<div className="absolute top-full left-0 mt-0.5 z-20 bg-card border border-border rounded-md shadow-lg py-0.5">
|
|
131
|
+
{DONE_WINDOWS.map((w) => (
|
|
132
|
+
<button
|
|
133
|
+
key={w.key}
|
|
134
|
+
onClick={() => {
|
|
135
|
+
setDoneWindow(w.key);
|
|
136
|
+
setDoneWindowOpen(false);
|
|
137
|
+
}}
|
|
138
|
+
className={`block w-full text-left px-3 py-1 text-[10px] hover:bg-accent transition-colors ${
|
|
139
|
+
doneWindow === w.key ? "text-foreground font-medium" : "text-muted-foreground"
|
|
140
|
+
}`}
|
|
141
|
+
>
|
|
142
|
+
{w.label}
|
|
143
|
+
</button>
|
|
144
|
+
))}
|
|
145
|
+
</div>
|
|
146
|
+
)}
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
{/* Clear done button */}
|
|
150
|
+
{onClearDone && filteredTasks.length > 0 && (
|
|
151
|
+
<button
|
|
152
|
+
onClick={onClearDone}
|
|
153
|
+
className="ml-auto flex items-center gap-1 px-2 py-0.5 text-[10px] text-muted-foreground hover:text-red-400 rounded hover:bg-red-500/10 transition-colors"
|
|
154
|
+
title="Archive visible done tasks"
|
|
155
|
+
>
|
|
156
|
+
<X className="h-2.5 w-2.5" />
|
|
157
|
+
Clear
|
|
158
|
+
</button>
|
|
159
|
+
)}
|
|
160
|
+
</div>
|
|
161
|
+
)}
|
|
162
|
+
|
|
163
|
+
<div className={`flex-1 overflow-y-auto p-2 ${cardSize === "signet" ? "space-y-1" : "space-y-2"} min-h-[120px]`}>
|
|
164
|
+
{filteredTasks.map((task) => {
|
|
165
|
+
const isExpandedSignet = expandedSignets.has(task.id);
|
|
166
|
+
const effectiveSize = isExpandedSignet ? "full" : cardSize;
|
|
167
|
+
|
|
168
|
+
if (effectiveSize === "signet") {
|
|
169
|
+
return (
|
|
170
|
+
<SignetCard
|
|
171
|
+
key={task.id}
|
|
172
|
+
task={task}
|
|
173
|
+
onClick={() => onTaskClick(task)}
|
|
174
|
+
onExpand={() => {
|
|
175
|
+
setExpandedSignets((prev) => {
|
|
176
|
+
const next = new Set(prev);
|
|
177
|
+
next.add(task.id);
|
|
178
|
+
return next;
|
|
179
|
+
});
|
|
180
|
+
}}
|
|
181
|
+
/>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<TaskCard
|
|
187
|
+
key={task.id}
|
|
188
|
+
task={task}
|
|
189
|
+
children={childrenMap[task.id] ?? []}
|
|
190
|
+
currentColumn={column}
|
|
191
|
+
onClick={() => {
|
|
192
|
+
if (isExpandedSignet) {
|
|
193
|
+
// Expanded signet → click opens edit dialog
|
|
194
|
+
onTaskClick(task);
|
|
195
|
+
// Collapse back to signet
|
|
196
|
+
setExpandedSignets((prev) => {
|
|
197
|
+
const next = new Set(prev);
|
|
198
|
+
next.delete(task.id);
|
|
199
|
+
return next;
|
|
200
|
+
});
|
|
201
|
+
} else {
|
|
202
|
+
onTaskClick(task);
|
|
203
|
+
}
|
|
204
|
+
}}
|
|
205
|
+
onTaskClick={onTaskClick}
|
|
206
|
+
onMove={(targetCol) => onMoveTask(task.id, targetCol)}
|
|
207
|
+
selected={bulkMode && bulkSelection?.has(task.id)}
|
|
208
|
+
bulkMode={bulkMode}
|
|
209
|
+
/>
|
|
210
|
+
);
|
|
211
|
+
})}
|
|
212
|
+
|
|
213
|
+
{filteredTasks.length === 0 && (
|
|
98
214
|
<div className="flex items-center justify-center h-20 text-xs text-muted-foreground/50">
|
|
99
215
|
{dragOver ? "Drop here" : "No tasks"}
|
|
100
216
|
</div>
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
const AGENT_NAME = process.env.NEXT_PUBLIC_AGENT_NAME || "Daedalus";
|
|
4
|
+
|
|
5
|
+
import { useState, useMemo } from "react";
|
|
4
6
|
import { formatDistanceToNow } from "date-fns";
|
|
5
7
|
import {
|
|
6
8
|
User,
|
|
@@ -84,11 +86,22 @@ export function TaskCard({
|
|
|
84
86
|
const [addingItem, setAddingItem] = useState(false);
|
|
85
87
|
const [newItemTitle, setNewItemTitle] = useState("");
|
|
86
88
|
const [saving, setSaving] = useState(false);
|
|
89
|
+
const collabData = useMemo(() => {
|
|
90
|
+
try {
|
|
91
|
+
return task.collaboration
|
|
92
|
+
? typeof task.collaboration === "string"
|
|
93
|
+
? JSON.parse(task.collaboration)
|
|
94
|
+
: task.collaboration
|
|
95
|
+
: null;
|
|
96
|
+
} catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}, [task.collaboration]);
|
|
87
100
|
const statusColor = STATUS_COLORS[task.status] ?? STATUS_COLORS.queued;
|
|
88
101
|
const colIdx = COLUMNS_ORDER.indexOf(currentColumn);
|
|
89
102
|
const isLive = task.id === "__LIVE_SESSION__";
|
|
90
103
|
const isActive = !isLive && task.status === "running";
|
|
91
|
-
const isDaedalusWorking = isActive && task.owner?.toLowerCase() ===
|
|
104
|
+
const isDaedalusWorking = isActive && task.owner?.toLowerCase() === AGENT_NAME.toLowerCase() && !!task.acknowledgedAt;
|
|
92
105
|
const isMeta = isMetaTask(task);
|
|
93
106
|
const metaColor = task.color || '#7c3aed';
|
|
94
107
|
const MetaIcon = isMeta ? TYPE_ICONS[task.type || ''] || null : null;
|
|
@@ -235,6 +248,11 @@ export function TaskCard({
|
|
|
235
248
|
MESH{task.meshNode ? ` · ${task.meshNode}` : ""}
|
|
236
249
|
</span>
|
|
237
250
|
)}
|
|
251
|
+
{collabData && (
|
|
252
|
+
<span className="inline-flex items-center gap-0.5 text-[9px] font-mono px-1.5 py-0.5 rounded bg-purple-500/20 text-purple-400">
|
|
253
|
+
{collabData.mode}{collabData.max_rounds ? ` · ${collabData.max_rounds}R` : ""}
|
|
254
|
+
</span>
|
|
255
|
+
)}
|
|
238
256
|
{task.owner && (
|
|
239
257
|
task.status === "running" ? (
|
|
240
258
|
<span className="inline-flex items-center gap-1 text-[10px] font-semibold rounded-full px-2 py-0.5 bg-purple-500/20 text-purple-400">
|
|
@@ -452,3 +470,38 @@ export function TaskCard({
|
|
|
452
470
|
</div>
|
|
453
471
|
);
|
|
454
472
|
}
|
|
473
|
+
|
|
474
|
+
/* ─── Signet Card ─── compact pill for done column */
|
|
475
|
+
|
|
476
|
+
interface SignetCardProps {
|
|
477
|
+
task: Task;
|
|
478
|
+
onClick: () => void;
|
|
479
|
+
onExpand: () => void;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export function SignetCard({ task, onClick, onExpand }: SignetCardProps) {
|
|
483
|
+
const statusColor = STATUS_COLORS[task.status] ?? STATUS_COLORS.queued;
|
|
484
|
+
const ownerInitial = task.owner ? task.owner.charAt(0).toUpperCase() : "";
|
|
485
|
+
|
|
486
|
+
return (
|
|
487
|
+
<div
|
|
488
|
+
className="group flex items-center gap-1.5 rounded-md border border-border/50 bg-card px-2 py-1 cursor-pointer hover:bg-accent/30 hover:border-border transition-colors"
|
|
489
|
+
onClick={onExpand}
|
|
490
|
+
onDoubleClick={(e) => {
|
|
491
|
+
e.stopPropagation();
|
|
492
|
+
onClick();
|
|
493
|
+
}}
|
|
494
|
+
title={`${task.title} — ${task.status}${task.owner ? ` (${task.owner})` : ""}\nClick to expand, double-click to edit`}
|
|
495
|
+
>
|
|
496
|
+
<span className={`inline-block h-1.5 w-1.5 rounded-full shrink-0 ${statusColor.split(" ")[0].replace("/20", "/60").replace("/10", "/50")}`} />
|
|
497
|
+
<span className="text-[11px] text-foreground truncate flex-1 min-w-0">
|
|
498
|
+
{task.title}
|
|
499
|
+
</span>
|
|
500
|
+
{ownerInitial && (
|
|
501
|
+
<span className="text-[9px] text-muted-foreground shrink-0 font-medium">
|
|
502
|
+
{ownerInitial}
|
|
503
|
+
</span>
|
|
504
|
+
)}
|
|
505
|
+
</div>
|
|
506
|
+
);
|
|
507
|
+
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
const AGENT_NAME = process.env.NEXT_PUBLIC_AGENT_NAME || "Daedalus";
|
|
4
|
+
|
|
3
5
|
import { useState, useEffect, useCallback, useRef } from "react";
|
|
4
|
-
import { X, Zap, Paperclip, Trash2, ArrowLeftFromLine, ArrowRightFromLine, Clock, User } from "lucide-react";
|
|
6
|
+
import { X, Zap, Paperclip, Trash2, ArrowLeftFromLine, ArrowRightFromLine, Clock, User, Cpu } from "lucide-react";
|
|
5
7
|
import { createTask, updateTask, createProject, type Task } from "@/lib/hooks";
|
|
8
|
+
import { ExecutionConfig, type ExecutionFields } from "./execution-config";
|
|
6
9
|
|
|
7
10
|
const TYPES = ["project", "pipeline", "phase"] as const;
|
|
8
11
|
type HierarchyType = (typeof TYPES)[number];
|
|
@@ -90,6 +93,17 @@ export function UnifiedTaskDialog({
|
|
|
90
93
|
const [showInCalendar, setShowInCalendar] = useState(false);
|
|
91
94
|
const [acknowledgedAt, setAcknowledgedAt] = useState<string | null>(null);
|
|
92
95
|
const [artifacts, setArtifacts] = useState<string[]>([]);
|
|
96
|
+
const [execFields, setExecFields] = useState<ExecutionFields>({
|
|
97
|
+
execution: "local",
|
|
98
|
+
collaboration: null,
|
|
99
|
+
preferred_nodes: [],
|
|
100
|
+
exclude_nodes: [],
|
|
101
|
+
cluster_id: null,
|
|
102
|
+
metric: null,
|
|
103
|
+
budget_minutes: 30,
|
|
104
|
+
scope: [],
|
|
105
|
+
needs_approval: true,
|
|
106
|
+
});
|
|
93
107
|
const [dragOver, setDragOver] = useState(false);
|
|
94
108
|
const [saving, setSaving] = useState(false);
|
|
95
109
|
const initializedForOpen = useRef(false);
|
|
@@ -147,6 +161,24 @@ export function UnifiedTaskDialog({
|
|
|
147
161
|
setShowInCalendar(!!item.showInCalendar);
|
|
148
162
|
setAcknowledgedAt(item.acknowledgedAt ?? null);
|
|
149
163
|
setArtifacts(item.artifacts ?? []);
|
|
164
|
+
// Execution config
|
|
165
|
+
const collabParsed = item.collaboration
|
|
166
|
+
? (typeof item.collaboration === "string" ? (() => { try { return JSON.parse(item.collaboration as string); } catch { return null; } })() : item.collaboration)
|
|
167
|
+
: null;
|
|
168
|
+
const prefNodes = item.preferredNodes
|
|
169
|
+
? (typeof item.preferredNodes === "string" ? (() => { try { return JSON.parse(item.preferredNodes as string); } catch { return []; } })() : item.preferredNodes)
|
|
170
|
+
: [];
|
|
171
|
+
setExecFields({
|
|
172
|
+
execution: item.execution || "local",
|
|
173
|
+
collaboration: collabParsed,
|
|
174
|
+
preferred_nodes: prefNodes,
|
|
175
|
+
exclude_nodes: [],
|
|
176
|
+
cluster_id: item.clusterId || null,
|
|
177
|
+
metric: item.metric || null,
|
|
178
|
+
budget_minutes: item.budgetMinutes || 30,
|
|
179
|
+
scope: item.scope ? (typeof item.scope === "string" ? (() => { try { return JSON.parse(item.scope as string); } catch { return []; } })() : item.scope) : [],
|
|
180
|
+
needs_approval: item.needsApproval !== 0,
|
|
181
|
+
});
|
|
150
182
|
} else {
|
|
151
183
|
setId("");
|
|
152
184
|
setTitle("");
|
|
@@ -172,6 +204,17 @@ export function UnifiedTaskDialog({
|
|
|
172
204
|
setCapacityClass("normal");
|
|
173
205
|
setAutoPriority(0);
|
|
174
206
|
setArtifacts([]);
|
|
207
|
+
setExecFields({
|
|
208
|
+
execution: "local",
|
|
209
|
+
collaboration: null,
|
|
210
|
+
preferred_nodes: [],
|
|
211
|
+
exclude_nodes: [],
|
|
212
|
+
cluster_id: null,
|
|
213
|
+
metric: null,
|
|
214
|
+
budget_minutes: 30,
|
|
215
|
+
scope: [],
|
|
216
|
+
needs_approval: true,
|
|
217
|
+
});
|
|
175
218
|
}
|
|
176
219
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
177
220
|
}, [open, item, defaultType, defaultParentId, defaultScheduledDate]);
|
|
@@ -306,7 +349,7 @@ export function UnifiedTaskDialog({
|
|
|
306
349
|
start_date: isHierarchyMode ? (startDate || null) : undefined,
|
|
307
350
|
end_date: isHierarchyMode ? (endDate || null) : undefined,
|
|
308
351
|
color: isHierarchyMode ? (color || null) : undefined,
|
|
309
|
-
needs_approval: needsApproval,
|
|
352
|
+
needs_approval: !isHierarchyMode && execFields.execution === "mesh" ? false : needsApproval,
|
|
310
353
|
trigger_kind: triggerKind,
|
|
311
354
|
trigger_at: triggerAt || null,
|
|
312
355
|
trigger_cron: triggerCron || null,
|
|
@@ -316,6 +359,17 @@ export function UnifiedTaskDialog({
|
|
|
316
359
|
auto_priority: autoPriority,
|
|
317
360
|
show_in_calendar: showInCalendar,
|
|
318
361
|
artifacts: artifacts.length > 0 ? artifacts : null,
|
|
362
|
+
// Execution fields (non-hierarchy only)
|
|
363
|
+
...(!isHierarchyMode ? {
|
|
364
|
+
execution: execFields.execution,
|
|
365
|
+
collaboration: execFields.collaboration,
|
|
366
|
+
preferred_nodes: execFields.preferred_nodes,
|
|
367
|
+
exclude_nodes: execFields.exclude_nodes,
|
|
368
|
+
cluster_id: execFields.cluster_id,
|
|
369
|
+
metric: execFields.metric,
|
|
370
|
+
budget_minutes: execFields.budget_minutes,
|
|
371
|
+
scope: execFields.scope,
|
|
372
|
+
} : {}),
|
|
319
373
|
} as Record<string, unknown>);
|
|
320
374
|
if (result?.error) {
|
|
321
375
|
alert(`Update failed: ${result.error}`);
|
|
@@ -377,7 +431,7 @@ export function UnifiedTaskDialog({
|
|
|
377
431
|
scheduled_date: scheduledDate || undefined,
|
|
378
432
|
project: project || undefined,
|
|
379
433
|
parent_id: parentId || undefined,
|
|
380
|
-
needs_approval: needsApproval,
|
|
434
|
+
needs_approval: execFields.execution === "mesh" ? false : needsApproval,
|
|
381
435
|
trigger_kind: triggerKind,
|
|
382
436
|
trigger_at: triggerAt || undefined,
|
|
383
437
|
trigger_cron: triggerCron || undefined,
|
|
@@ -386,6 +440,15 @@ export function UnifiedTaskDialog({
|
|
|
386
440
|
capacity_class: capacityClass,
|
|
387
441
|
auto_priority: autoPriority || undefined,
|
|
388
442
|
artifacts: artifacts.length > 0 ? artifacts : undefined,
|
|
443
|
+
// Execution fields
|
|
444
|
+
execution: execFields.execution || undefined,
|
|
445
|
+
collaboration: execFields.collaboration || undefined,
|
|
446
|
+
preferred_nodes: execFields.preferred_nodes.length ? execFields.preferred_nodes : undefined,
|
|
447
|
+
exclude_nodes: execFields.exclude_nodes.length ? execFields.exclude_nodes : undefined,
|
|
448
|
+
cluster_id: execFields.cluster_id || undefined,
|
|
449
|
+
metric: execFields.metric || undefined,
|
|
450
|
+
budget_minutes: execFields.budget_minutes || undefined,
|
|
451
|
+
scope: execFields.scope.length ? execFields.scope : undefined,
|
|
389
452
|
});
|
|
390
453
|
}
|
|
391
454
|
onClose();
|
|
@@ -430,7 +493,7 @@ export function UnifiedTaskDialog({
|
|
|
430
493
|
</span>
|
|
431
494
|
</label>
|
|
432
495
|
{/* Daedalus acknowledgment checkbox — shows when auto-dispatched */}
|
|
433
|
-
{item && status === "running" && owner?.toLowerCase() ===
|
|
496
|
+
{item && status === "running" && owner?.toLowerCase() === AGENT_NAME.toLowerCase() && (
|
|
434
497
|
<label className="flex items-center gap-2 cursor-pointer group ml-2">
|
|
435
498
|
<input
|
|
436
499
|
type="checkbox"
|
|
@@ -884,6 +947,21 @@ export function UnifiedTaskDialog({
|
|
|
884
947
|
</div>
|
|
885
948
|
</div>
|
|
886
949
|
|
|
950
|
+
{/* Execution section */}
|
|
951
|
+
{!isHierarchyMode && (
|
|
952
|
+
<div className="border border-border/50 rounded-lg p-3 space-y-3">
|
|
953
|
+
<p className="text-xs font-semibold text-foreground flex items-center gap-1.5">
|
|
954
|
+
<Cpu className="h-3.5 w-3.5 text-cyan-400" />
|
|
955
|
+
Execution
|
|
956
|
+
</p>
|
|
957
|
+
<ExecutionConfig
|
|
958
|
+
value={execFields}
|
|
959
|
+
onChange={setExecFields}
|
|
960
|
+
disabled={!!item?.meshTaskId}
|
|
961
|
+
/>
|
|
962
|
+
</div>
|
|
963
|
+
)}
|
|
964
|
+
|
|
887
965
|
{/* Next Action */}
|
|
888
966
|
<div>
|
|
889
967
|
<label className="block text-xs font-medium text-muted-foreground mb-1">
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import {
|
|
5
|
+
ChevronDown,
|
|
6
|
+
ChevronRight,
|
|
7
|
+
Plus,
|
|
8
|
+
Trash2,
|
|
9
|
+
Send,
|
|
10
|
+
} from "lucide-react";
|
|
11
|
+
import type { ClusterView } from "@/lib/hooks";
|
|
12
|
+
import { deleteCluster, removeClusterMember, updateClusterMember } from "@/lib/hooks";
|
|
13
|
+
import { RoleBadge, RolePicker } from "./role-picker";
|
|
14
|
+
|
|
15
|
+
interface ClusterCardProps {
|
|
16
|
+
cluster: ClusterView;
|
|
17
|
+
onDispatch: (clusterId: string) => void;
|
|
18
|
+
onAddNode: (clusterId: string) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function ClusterCard({
|
|
22
|
+
cluster,
|
|
23
|
+
onDispatch,
|
|
24
|
+
onAddNode,
|
|
25
|
+
}: ClusterCardProps) {
|
|
26
|
+
const [expanded, setExpanded] = useState(false);
|
|
27
|
+
|
|
28
|
+
const onlineCount = cluster.members.filter(
|
|
29
|
+
(m) => m.nodeStatus === "online"
|
|
30
|
+
).length;
|
|
31
|
+
|
|
32
|
+
const handleDelete = async () => {
|
|
33
|
+
if (!confirm(`Archive cluster "${cluster.name}"?`)) return;
|
|
34
|
+
await deleteCluster(cluster.id);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const handleRemoveMember = async (nodeId: string) => {
|
|
38
|
+
if (!confirm(`Remove ${nodeId} from ${cluster.name}?`)) return;
|
|
39
|
+
await removeClusterMember(cluster.id, nodeId);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const handleRoleChange = async (nodeId: string, role: string) => {
|
|
43
|
+
await updateClusterMember(cluster.id, nodeId, role);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const statusDot: Record<string, string> = {
|
|
47
|
+
online: "bg-green-400",
|
|
48
|
+
degraded: "bg-yellow-400",
|
|
49
|
+
offline: "bg-zinc-600",
|
|
50
|
+
unknown: "bg-zinc-600",
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className="rounded-lg border border-border bg-card">
|
|
55
|
+
{/* Header */}
|
|
56
|
+
<div
|
|
57
|
+
className="flex items-center justify-between px-4 py-3 cursor-pointer"
|
|
58
|
+
onClick={() => setExpanded(!expanded)}
|
|
59
|
+
>
|
|
60
|
+
<div className="flex items-center gap-3">
|
|
61
|
+
{expanded ? (
|
|
62
|
+
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
|
63
|
+
) : (
|
|
64
|
+
<ChevronRight className="h-4 w-4 text-muted-foreground" />
|
|
65
|
+
)}
|
|
66
|
+
{cluster.color && (
|
|
67
|
+
<span
|
|
68
|
+
className="h-3 w-3 rounded-full"
|
|
69
|
+
style={{ backgroundColor: cluster.color }}
|
|
70
|
+
/>
|
|
71
|
+
)}
|
|
72
|
+
<span className="font-medium text-sm">{cluster.name}</span>
|
|
73
|
+
<span className="rounded-full bg-accent px-2 py-0.5 text-[10px] text-muted-foreground">
|
|
74
|
+
{cluster.members.length} nodes
|
|
75
|
+
</span>
|
|
76
|
+
<span className="text-[10px] text-green-400">
|
|
77
|
+
{onlineCount} online
|
|
78
|
+
</span>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
{/* Members */}
|
|
83
|
+
<div className="flex flex-wrap gap-1.5 px-4 pb-2">
|
|
84
|
+
{cluster.members.map((m) => (
|
|
85
|
+
<span
|
|
86
|
+
key={m.nodeId}
|
|
87
|
+
className="inline-flex items-center gap-1.5 rounded-md bg-accent/30 px-2 py-1 text-xs"
|
|
88
|
+
>
|
|
89
|
+
<span
|
|
90
|
+
className={`h-1.5 w-1.5 rounded-full ${statusDot[m.nodeStatus] ?? "bg-zinc-600"}`}
|
|
91
|
+
/>
|
|
92
|
+
<span className="font-mono text-[11px]">
|
|
93
|
+
{m.nodeId.split("-")[0]}
|
|
94
|
+
</span>
|
|
95
|
+
<RoleBadge role={m.role} />
|
|
96
|
+
</span>
|
|
97
|
+
))}
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
{/* Defaults */}
|
|
101
|
+
<div className="flex gap-3 px-4 pb-3 text-[10px] text-muted-foreground">
|
|
102
|
+
<span>{cluster.defaultMode}</span>
|
|
103
|
+
<span>{cluster.defaultConvergence}</span>
|
|
104
|
+
<span>max {cluster.maxRounds} rounds</span>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
{/* Actions */}
|
|
108
|
+
<div className="flex gap-2 px-4 pb-3">
|
|
109
|
+
<button
|
|
110
|
+
onClick={(e) => {
|
|
111
|
+
e.stopPropagation();
|
|
112
|
+
onDispatch(cluster.id);
|
|
113
|
+
}}
|
|
114
|
+
className="flex items-center gap-1 rounded px-2 py-1 text-xs bg-cyan-500/10 text-cyan-400 hover:bg-cyan-500/20 transition-colors"
|
|
115
|
+
>
|
|
116
|
+
<Send className="h-3 w-3" />
|
|
117
|
+
Assign Task
|
|
118
|
+
</button>
|
|
119
|
+
<button
|
|
120
|
+
onClick={(e) => {
|
|
121
|
+
e.stopPropagation();
|
|
122
|
+
onAddNode(cluster.id);
|
|
123
|
+
}}
|
|
124
|
+
className="flex items-center gap-1 rounded px-2 py-1 text-xs bg-accent text-muted-foreground hover:text-foreground transition-colors"
|
|
125
|
+
>
|
|
126
|
+
<Plus className="h-3 w-3" />
|
|
127
|
+
Add Node
|
|
128
|
+
</button>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
{/* Expanded: member management */}
|
|
132
|
+
{expanded && (
|
|
133
|
+
<div className="border-t border-border px-4 py-3 space-y-2">
|
|
134
|
+
{cluster.description && (
|
|
135
|
+
<p className="text-xs text-muted-foreground mb-3">
|
|
136
|
+
{cluster.description}
|
|
137
|
+
</p>
|
|
138
|
+
)}
|
|
139
|
+
{cluster.members.map((m) => (
|
|
140
|
+
<div
|
|
141
|
+
key={m.nodeId}
|
|
142
|
+
className="flex items-center justify-between rounded bg-accent/20 px-3 py-2"
|
|
143
|
+
>
|
|
144
|
+
<div className="flex items-center gap-2">
|
|
145
|
+
<span
|
|
146
|
+
className={`h-2 w-2 rounded-full ${statusDot[m.nodeStatus] ?? "bg-zinc-600"}`}
|
|
147
|
+
/>
|
|
148
|
+
<span className="font-mono text-xs">{m.nodeId}</span>
|
|
149
|
+
</div>
|
|
150
|
+
<div className="flex items-center gap-2">
|
|
151
|
+
<RolePicker
|
|
152
|
+
value={m.role}
|
|
153
|
+
onChange={(role) => handleRoleChange(m.nodeId, role)}
|
|
154
|
+
/>
|
|
155
|
+
<button
|
|
156
|
+
onClick={() => handleRemoveMember(m.nodeId)}
|
|
157
|
+
className="text-muted-foreground hover:text-red-400 transition-colors"
|
|
158
|
+
>
|
|
159
|
+
<Trash2 className="h-3.5 w-3.5" />
|
|
160
|
+
</button>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
))}
|
|
164
|
+
<div className="flex justify-end pt-2">
|
|
165
|
+
<button
|
|
166
|
+
onClick={handleDelete}
|
|
167
|
+
className="text-[10px] text-red-400/60 hover:text-red-400 transition-colors"
|
|
168
|
+
>
|
|
169
|
+
Archive cluster
|
|
170
|
+
</button>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
);
|
|
176
|
+
}
|