openclaw-node-harness 2.0.4 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/README.md +646 -3
  2. package/bin/hyperagent.mjs +419 -0
  3. package/bin/mesh-agent.js +401 -12
  4. package/bin/mesh-bridge.js +66 -1
  5. package/bin/mesh-task-daemon.js +816 -26
  6. package/bin/mesh.js +403 -1
  7. package/config/claude-settings.json +95 -0
  8. package/config/daemon.json.template +2 -1
  9. package/config/git-hooks/pre-commit +13 -0
  10. package/config/git-hooks/pre-push +12 -0
  11. package/config/harness-rules.json +174 -0
  12. package/config/plan-templates/team-bugfix.yaml +52 -0
  13. package/config/plan-templates/team-deploy.yaml +50 -0
  14. package/config/plan-templates/team-feature.yaml +71 -0
  15. package/config/roles/qa-engineer.yaml +36 -0
  16. package/config/roles/solidity-dev.yaml +51 -0
  17. package/config/roles/tech-architect.yaml +36 -0
  18. package/config/rules/framework/solidity.md +22 -0
  19. package/config/rules/framework/typescript.md +21 -0
  20. package/config/rules/framework/unity.md +21 -0
  21. package/config/rules/universal/design-docs.md +18 -0
  22. package/config/rules/universal/git-hygiene.md +18 -0
  23. package/config/rules/universal/security.md +19 -0
  24. package/config/rules/universal/test-standards.md +19 -0
  25. package/identity/DELEGATION.md +6 -6
  26. package/install.sh +293 -8
  27. package/lib/circling-parser.js +119 -0
  28. package/lib/hyperagent-store.mjs +652 -0
  29. package/lib/kanban-io.js +9 -0
  30. package/lib/mcp-knowledge/bench.mjs +118 -0
  31. package/lib/mcp-knowledge/core.mjs +528 -0
  32. package/lib/mcp-knowledge/package.json +25 -0
  33. package/lib/mcp-knowledge/server.mjs +245 -0
  34. package/lib/mcp-knowledge/test.mjs +802 -0
  35. package/lib/memory-budget.mjs +261 -0
  36. package/lib/mesh-collab.js +301 -1
  37. package/lib/mesh-harness.js +427 -0
  38. package/lib/mesh-plans.js +13 -5
  39. package/lib/mesh-tasks.js +67 -0
  40. package/lib/plan-templates.js +226 -0
  41. package/lib/pre-compression-flush.mjs +320 -0
  42. package/lib/role-loader.js +292 -0
  43. package/lib/rule-loader.js +358 -0
  44. package/lib/session-store.mjs +458 -0
  45. package/lib/transcript-parser.mjs +292 -0
  46. package/mission-control/drizzle/soul_schema_update.sql +29 -0
  47. package/mission-control/drizzle.config.ts +1 -4
  48. package/mission-control/package-lock.json +1571 -83
  49. package/mission-control/package.json +6 -2
  50. package/mission-control/scripts/gen-chronology.js +3 -3
  51. package/mission-control/scripts/import-pipeline-v2.js +0 -16
  52. package/mission-control/scripts/import-pipeline.js +0 -15
  53. package/mission-control/src/app/api/cowork/clusters/[id]/members/route.ts +117 -0
  54. package/mission-control/src/app/api/cowork/clusters/[id]/route.ts +84 -0
  55. package/mission-control/src/app/api/cowork/clusters/route.ts +141 -0
  56. package/mission-control/src/app/api/cowork/dispatch/route.ts +128 -0
  57. package/mission-control/src/app/api/cowork/events/route.ts +65 -0
  58. package/mission-control/src/app/api/cowork/intervene/route.ts +259 -0
  59. package/mission-control/src/app/api/cowork/sessions/[id]/route.ts +37 -0
  60. package/mission-control/src/app/api/cowork/sessions/route.ts +64 -0
  61. package/mission-control/src/app/api/diagnostics/route.ts +97 -0
  62. package/mission-control/src/app/api/diagnostics/test-runner/route.ts +990 -0
  63. package/mission-control/src/app/api/mesh/events/route.ts +95 -19
  64. package/mission-control/src/app/api/mesh/identity/route.ts +11 -0
  65. package/mission-control/src/app/api/mesh/tasks/[id]/route.ts +92 -0
  66. package/mission-control/src/app/api/mesh/tasks/route.ts +91 -0
  67. package/mission-control/src/app/api/tasks/[id]/handoff/route.ts +1 -1
  68. package/mission-control/src/app/api/tasks/[id]/route.ts +90 -4
  69. package/mission-control/src/app/api/tasks/route.ts +21 -30
  70. package/mission-control/src/app/cowork/page.tsx +261 -0
  71. package/mission-control/src/app/diagnostics/page.tsx +385 -0
  72. package/mission-control/src/app/graph/page.tsx +26 -0
  73. package/mission-control/src/app/memory/page.tsx +1 -1
  74. package/mission-control/src/app/obsidian/page.tsx +36 -6
  75. package/mission-control/src/app/roadmap/page.tsx +24 -0
  76. package/mission-control/src/app/souls/page.tsx +2 -2
  77. package/mission-control/src/components/board/execution-config.tsx +431 -0
  78. package/mission-control/src/components/board/kanban-board.tsx +75 -9
  79. package/mission-control/src/components/board/kanban-column.tsx +135 -19
  80. package/mission-control/src/components/board/task-card.tsx +55 -2
  81. package/mission-control/src/components/board/unified-task-dialog.tsx +82 -4
  82. package/mission-control/src/components/cowork/cluster-card.tsx +176 -0
  83. package/mission-control/src/components/cowork/create-cluster-dialog.tsx +251 -0
  84. package/mission-control/src/components/cowork/dispatch-form.tsx +423 -0
  85. package/mission-control/src/components/cowork/role-picker.tsx +102 -0
  86. package/mission-control/src/components/cowork/session-card.tsx +284 -0
  87. package/mission-control/src/components/layout/sidebar.tsx +39 -2
  88. package/mission-control/src/lib/__tests__/daily-log.test.ts +82 -0
  89. package/mission-control/src/lib/__tests__/memory-md.test.ts +87 -0
  90. package/mission-control/src/lib/__tests__/mesh-kv-sync.test.ts +465 -0
  91. package/mission-control/src/lib/__tests__/mocks/mock-kv.ts +131 -0
  92. package/mission-control/src/lib/__tests__/status-kanban.test.ts +46 -0
  93. package/mission-control/src/lib/__tests__/task-markdown.test.ts +188 -0
  94. package/mission-control/src/lib/__tests__/wikilinks.test.ts +175 -0
  95. package/mission-control/src/lib/config.ts +58 -0
  96. package/mission-control/src/lib/db/index.ts +69 -0
  97. package/mission-control/src/lib/db/schema.ts +61 -3
  98. package/mission-control/src/lib/hooks.ts +309 -0
  99. package/mission-control/src/lib/memory/entities.ts +3 -2
  100. package/mission-control/src/lib/nats.ts +66 -1
  101. package/mission-control/src/lib/parsers/task-markdown.ts +52 -2
  102. package/mission-control/src/lib/parsers/transcript.ts +4 -4
  103. package/mission-control/src/lib/scheduler.ts +12 -11
  104. package/mission-control/src/lib/sync/mesh-kv.ts +279 -0
  105. package/mission-control/src/lib/sync/tasks.ts +23 -1
  106. package/mission-control/src/lib/task-id.ts +32 -0
  107. package/mission-control/src/lib/tts/index.ts +33 -9
  108. package/mission-control/tsconfig.json +2 -1
  109. package/mission-control/vitest.config.ts +14 -0
  110. package/package.json +15 -2
  111. package/services/service-manifest.json +1 -1
  112. package/skills/cc-godmode/references/agents.md +8 -8
  113. package/workspace-bin/memory-daemon.mjs +199 -5
  114. package/workspace-bin/session-search.mjs +204 -0
  115. package/workspace-bin/web-fetch.mjs +65 -0
@@ -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
- {tasks.length}
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
- <div className="flex-1 overflow-y-auto p-2 space-y-2 min-h-[120px]">
83
- {tasks.map((task) => (
84
- <TaskCard
85
- key={task.id}
86
- task={task}
87
- children={childrenMap[task.id] ?? []}
88
- currentColumn={column}
89
- onClick={() => onTaskClick(task)}
90
- onTaskClick={onTaskClick}
91
- onMove={(targetCol) => onMoveTask(task.id, targetCol)}
92
- selected={bulkMode && bulkSelection?.has(task.id)}
93
- bulkMode={bulkMode}
94
- />
95
- ))}
96
-
97
- {tasks.length === 0 && (
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
- import { useState } from "react";
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() === "daedalus" && !!task.acknowledgedAt;
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() === "daedalus" && (
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
+ }