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,431 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
import useSWR from "swr";
|
|
5
|
+
import { useClusters } from "@/lib/hooks";
|
|
6
|
+
|
|
7
|
+
const fetcher = (url: string) => fetch(url).then((r) => r.json());
|
|
8
|
+
|
|
9
|
+
type ExecutionMode = "local" | "mesh" | "collab";
|
|
10
|
+
|
|
11
|
+
export interface ExecutionFields {
|
|
12
|
+
execution: string | null;
|
|
13
|
+
collaboration: Record<string, unknown> | null;
|
|
14
|
+
preferred_nodes: string[];
|
|
15
|
+
exclude_nodes: string[];
|
|
16
|
+
cluster_id: string | null;
|
|
17
|
+
metric: string | null;
|
|
18
|
+
budget_minutes: number;
|
|
19
|
+
scope: string[];
|
|
20
|
+
needs_approval: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface ExecutionConfigProps {
|
|
24
|
+
value: ExecutionFields;
|
|
25
|
+
onChange: (fields: ExecutionFields) => void;
|
|
26
|
+
disabled?: boolean; // true when task has meshTaskId (already submitted)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const MODES: { value: ExecutionMode; label: string }[] = [
|
|
30
|
+
{ value: "local", label: "Local" },
|
|
31
|
+
{ value: "mesh", label: "Mesh" },
|
|
32
|
+
{ value: "collab", label: "Collab" },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const COLLAB_MODES = ["parallel", "sequential", "review"] as const;
|
|
36
|
+
const CONVERGENCE_TYPES = ["unanimous", "majority", "coordinator"] as const;
|
|
37
|
+
const SCOPE_STRATEGIES = ["shared", "leader_only", "partitioned"] as const;
|
|
38
|
+
|
|
39
|
+
export function ExecutionConfig({ value, onChange, disabled }: ExecutionConfigProps) {
|
|
40
|
+
const mode: ExecutionMode =
|
|
41
|
+
value.execution === "mesh"
|
|
42
|
+
? value.collaboration
|
|
43
|
+
? "collab"
|
|
44
|
+
: "mesh"
|
|
45
|
+
: "local";
|
|
46
|
+
|
|
47
|
+
// Collab settings (parsed from collaboration JSON)
|
|
48
|
+
const collab = (value.collaboration ?? {}) as Record<string, unknown>;
|
|
49
|
+
const conv = (collab.convergence ?? {}) as Record<string, unknown>;
|
|
50
|
+
|
|
51
|
+
const [collabMode, setCollabMode] = useState<string>((collab.mode as string) || "parallel");
|
|
52
|
+
const [convergenceType, setConvergenceType] = useState<string>((conv.type as string) || "unanimous");
|
|
53
|
+
const [convergenceThreshold, setConvergenceThreshold] = useState<number>(
|
|
54
|
+
typeof conv.threshold === "number" ? conv.threshold * 100 : 66
|
|
55
|
+
);
|
|
56
|
+
const [maxRounds, setMaxRounds] = useState<number>((collab.max_rounds as number) || 5);
|
|
57
|
+
const [scopeStrategy, setScopeStrategy] = useState<string>((collab.scope_strategy as string) || "shared");
|
|
58
|
+
|
|
59
|
+
// Mesh settings
|
|
60
|
+
const [metric, setMetric] = useState(value.metric || "");
|
|
61
|
+
const [budgetMinutes, setBudgetMinutes] = useState(value.budget_minutes || 30);
|
|
62
|
+
const [scopeText, setScopeText] = useState((value.scope || []).join("\n"));
|
|
63
|
+
const [selectedNodes, setSelectedNodes] = useState<Set<string>>(
|
|
64
|
+
new Set(value.preferred_nodes || [])
|
|
65
|
+
);
|
|
66
|
+
const [clusterId, setClusterId] = useState(value.cluster_id || "");
|
|
67
|
+
|
|
68
|
+
// Available nodes
|
|
69
|
+
const { data: nodesData } = useSWR<{ nodes: Array<{ nodeId: string; status: string }> }>(
|
|
70
|
+
mode !== "local" ? "/api/mesh/nodes" : null,
|
|
71
|
+
fetcher,
|
|
72
|
+
{ refreshInterval: 10000 }
|
|
73
|
+
);
|
|
74
|
+
const nodes = nodesData?.nodes ?? [];
|
|
75
|
+
|
|
76
|
+
// Clusters (for collab mode)
|
|
77
|
+
const { clusters } = useClusters();
|
|
78
|
+
|
|
79
|
+
// Sync internal state back to parent
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
// Don't fire onChange during disabled state
|
|
82
|
+
if (disabled) return;
|
|
83
|
+
|
|
84
|
+
const scopeArr = scopeText
|
|
85
|
+
.split("\n")
|
|
86
|
+
.map((s) => s.trim())
|
|
87
|
+
.filter(Boolean);
|
|
88
|
+
|
|
89
|
+
if (mode === "local") {
|
|
90
|
+
onChange({
|
|
91
|
+
execution: "local",
|
|
92
|
+
collaboration: null,
|
|
93
|
+
preferred_nodes: [],
|
|
94
|
+
exclude_nodes: [],
|
|
95
|
+
cluster_id: null,
|
|
96
|
+
metric: null,
|
|
97
|
+
budget_minutes: 30,
|
|
98
|
+
scope: [],
|
|
99
|
+
needs_approval: true,
|
|
100
|
+
});
|
|
101
|
+
} else if (mode === "mesh") {
|
|
102
|
+
onChange({
|
|
103
|
+
execution: "mesh",
|
|
104
|
+
collaboration: null,
|
|
105
|
+
preferred_nodes: Array.from(selectedNodes),
|
|
106
|
+
exclude_nodes: [],
|
|
107
|
+
cluster_id: null,
|
|
108
|
+
metric: metric || null,
|
|
109
|
+
budget_minutes: budgetMinutes,
|
|
110
|
+
scope: scopeArr,
|
|
111
|
+
needs_approval: false,
|
|
112
|
+
});
|
|
113
|
+
} else {
|
|
114
|
+
// collab
|
|
115
|
+
const nodeIds = Array.from(selectedNodes);
|
|
116
|
+
onChange({
|
|
117
|
+
execution: "mesh",
|
|
118
|
+
collaboration: {
|
|
119
|
+
mode: collabMode,
|
|
120
|
+
min_nodes: Math.min(nodeIds.length || 2, 2),
|
|
121
|
+
max_nodes: nodeIds.length || 4,
|
|
122
|
+
join_window_s: 30,
|
|
123
|
+
max_rounds: maxRounds,
|
|
124
|
+
convergence: {
|
|
125
|
+
type: convergenceType,
|
|
126
|
+
threshold: convergenceThreshold / 100,
|
|
127
|
+
metric: null,
|
|
128
|
+
min_quorum: Math.min(nodeIds.length || 2, 2),
|
|
129
|
+
},
|
|
130
|
+
scope_strategy: scopeStrategy,
|
|
131
|
+
},
|
|
132
|
+
preferred_nodes: nodeIds,
|
|
133
|
+
exclude_nodes: [],
|
|
134
|
+
cluster_id: clusterId || null,
|
|
135
|
+
metric: metric || null,
|
|
136
|
+
budget_minutes: budgetMinutes,
|
|
137
|
+
scope: scopeArr,
|
|
138
|
+
needs_approval: false,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
142
|
+
}, [
|
|
143
|
+
mode,
|
|
144
|
+
collabMode,
|
|
145
|
+
convergenceType,
|
|
146
|
+
convergenceThreshold,
|
|
147
|
+
maxRounds,
|
|
148
|
+
scopeStrategy,
|
|
149
|
+
metric,
|
|
150
|
+
budgetMinutes,
|
|
151
|
+
scopeText,
|
|
152
|
+
selectedNodes,
|
|
153
|
+
clusterId,
|
|
154
|
+
disabled,
|
|
155
|
+
]);
|
|
156
|
+
|
|
157
|
+
const setMode = (m: ExecutionMode) => {
|
|
158
|
+
if (disabled) return;
|
|
159
|
+
if (m === "local") {
|
|
160
|
+
onChange({
|
|
161
|
+
execution: "local",
|
|
162
|
+
collaboration: null,
|
|
163
|
+
preferred_nodes: [],
|
|
164
|
+
exclude_nodes: [],
|
|
165
|
+
cluster_id: null,
|
|
166
|
+
metric: null,
|
|
167
|
+
budget_minutes: 30,
|
|
168
|
+
scope: [],
|
|
169
|
+
needs_approval: true,
|
|
170
|
+
});
|
|
171
|
+
} else if (m === "mesh") {
|
|
172
|
+
onChange({
|
|
173
|
+
execution: "mesh",
|
|
174
|
+
collaboration: null,
|
|
175
|
+
preferred_nodes: Array.from(selectedNodes),
|
|
176
|
+
exclude_nodes: [],
|
|
177
|
+
cluster_id: null,
|
|
178
|
+
metric: metric || null,
|
|
179
|
+
budget_minutes: budgetMinutes,
|
|
180
|
+
scope: scopeText.split("\n").map((s) => s.trim()).filter(Boolean),
|
|
181
|
+
needs_approval: false,
|
|
182
|
+
});
|
|
183
|
+
} else {
|
|
184
|
+
// Set collab — trigger the useEffect to build collaboration spec
|
|
185
|
+
onChange({
|
|
186
|
+
execution: "mesh",
|
|
187
|
+
collaboration: { mode: collabMode }, // placeholder, useEffect rebuilds
|
|
188
|
+
preferred_nodes: Array.from(selectedNodes),
|
|
189
|
+
exclude_nodes: [],
|
|
190
|
+
cluster_id: clusterId || null,
|
|
191
|
+
metric: metric || null,
|
|
192
|
+
budget_minutes: budgetMinutes,
|
|
193
|
+
scope: scopeText.split("\n").map((s) => s.trim()).filter(Boolean),
|
|
194
|
+
needs_approval: false,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const toggleNode = (nodeId: string) => {
|
|
200
|
+
setSelectedNodes((prev) => {
|
|
201
|
+
const next = new Set(prev);
|
|
202
|
+
if (next.has(nodeId)) next.delete(nodeId);
|
|
203
|
+
else next.add(nodeId);
|
|
204
|
+
return next;
|
|
205
|
+
});
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const inputCls =
|
|
209
|
+
"w-full rounded-md border border-border bg-background px-2 py-1.5 text-xs text-foreground focus:outline-none focus:ring-1 focus:ring-primary";
|
|
210
|
+
const btnCls = (active: boolean) =>
|
|
211
|
+
`px-2.5 py-1 text-[10px] rounded-md border transition-colors ${
|
|
212
|
+
active
|
|
213
|
+
? "border-primary bg-primary/10 text-primary"
|
|
214
|
+
: "border-border text-muted-foreground hover:text-foreground"
|
|
215
|
+
}`;
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
<div className="space-y-3">
|
|
219
|
+
{/* Mode selector */}
|
|
220
|
+
<div className="flex gap-2">
|
|
221
|
+
{MODES.map((m) => (
|
|
222
|
+
<button
|
|
223
|
+
key={m.value}
|
|
224
|
+
type="button"
|
|
225
|
+
onClick={() => setMode(m.value)}
|
|
226
|
+
disabled={disabled}
|
|
227
|
+
className={btnCls(mode === m.value)}
|
|
228
|
+
>
|
|
229
|
+
{m.label}
|
|
230
|
+
</button>
|
|
231
|
+
))}
|
|
232
|
+
{disabled && (
|
|
233
|
+
<span className="text-[10px] text-amber-400 self-center ml-auto">
|
|
234
|
+
Locked (submitted)
|
|
235
|
+
</span>
|
|
236
|
+
)}
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
{mode !== "local" && (
|
|
240
|
+
<>
|
|
241
|
+
{/* Collab-specific settings */}
|
|
242
|
+
{mode === "collab" && (
|
|
243
|
+
<div className="space-y-2">
|
|
244
|
+
<div>
|
|
245
|
+
<label className="block text-[10px] font-medium text-muted-foreground mb-1">
|
|
246
|
+
Collab Mode
|
|
247
|
+
</label>
|
|
248
|
+
<div className="flex gap-1.5">
|
|
249
|
+
{COLLAB_MODES.map((cm) => (
|
|
250
|
+
<button
|
|
251
|
+
key={cm}
|
|
252
|
+
type="button"
|
|
253
|
+
onClick={() => setCollabMode(cm)}
|
|
254
|
+
className={btnCls(collabMode === cm)}
|
|
255
|
+
>
|
|
256
|
+
{cm}
|
|
257
|
+
</button>
|
|
258
|
+
))}
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
<div>
|
|
263
|
+
<label className="block text-[10px] font-medium text-muted-foreground mb-1">
|
|
264
|
+
Convergence
|
|
265
|
+
</label>
|
|
266
|
+
<div className="flex gap-1.5">
|
|
267
|
+
{CONVERGENCE_TYPES.map((ct) => (
|
|
268
|
+
<button
|
|
269
|
+
key={ct}
|
|
270
|
+
type="button"
|
|
271
|
+
onClick={() => setConvergenceType(ct)}
|
|
272
|
+
className={btnCls(convergenceType === ct)}
|
|
273
|
+
>
|
|
274
|
+
{ct}
|
|
275
|
+
</button>
|
|
276
|
+
))}
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
|
|
280
|
+
{convergenceType === "majority" && (
|
|
281
|
+
<div>
|
|
282
|
+
<label className="block text-[10px] font-medium text-muted-foreground mb-1">
|
|
283
|
+
Threshold: {convergenceThreshold}%
|
|
284
|
+
</label>
|
|
285
|
+
<input
|
|
286
|
+
type="range"
|
|
287
|
+
min={51}
|
|
288
|
+
max={100}
|
|
289
|
+
value={convergenceThreshold}
|
|
290
|
+
onChange={(e) => setConvergenceThreshold(parseInt(e.target.value, 10))}
|
|
291
|
+
className="w-full"
|
|
292
|
+
/>
|
|
293
|
+
</div>
|
|
294
|
+
)}
|
|
295
|
+
|
|
296
|
+
<div className="grid grid-cols-2 gap-2">
|
|
297
|
+
<div>
|
|
298
|
+
<label className="block text-[10px] font-medium text-muted-foreground mb-1">
|
|
299
|
+
Max Rounds
|
|
300
|
+
</label>
|
|
301
|
+
<input
|
|
302
|
+
type="number"
|
|
303
|
+
min={1}
|
|
304
|
+
max={20}
|
|
305
|
+
value={maxRounds}
|
|
306
|
+
onChange={(e) => setMaxRounds(parseInt(e.target.value, 10) || 5)}
|
|
307
|
+
className={inputCls}
|
|
308
|
+
/>
|
|
309
|
+
</div>
|
|
310
|
+
<div>
|
|
311
|
+
<label className="block text-[10px] font-medium text-muted-foreground mb-1">
|
|
312
|
+
Scope Strategy
|
|
313
|
+
</label>
|
|
314
|
+
<select
|
|
315
|
+
value={scopeStrategy}
|
|
316
|
+
onChange={(e) => setScopeStrategy(e.target.value)}
|
|
317
|
+
className={inputCls}
|
|
318
|
+
>
|
|
319
|
+
{SCOPE_STRATEGIES.map((ss) => (
|
|
320
|
+
<option key={ss} value={ss}>
|
|
321
|
+
{ss}
|
|
322
|
+
</option>
|
|
323
|
+
))}
|
|
324
|
+
</select>
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
|
|
328
|
+
{/* Cluster selector */}
|
|
329
|
+
{clusters.length > 0 && (
|
|
330
|
+
<div>
|
|
331
|
+
<label className="block text-[10px] font-medium text-muted-foreground mb-1">
|
|
332
|
+
Cluster
|
|
333
|
+
</label>
|
|
334
|
+
<select
|
|
335
|
+
value={clusterId}
|
|
336
|
+
onChange={(e) => {
|
|
337
|
+
setClusterId(e.target.value);
|
|
338
|
+
if (e.target.value) {
|
|
339
|
+
const cluster = clusters.find((c) => c.id === e.target.value);
|
|
340
|
+
if (cluster) {
|
|
341
|
+
const memberIds = cluster.members.map((m) => m.nodeId);
|
|
342
|
+
setSelectedNodes(new Set(memberIds));
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}}
|
|
346
|
+
className={inputCls}
|
|
347
|
+
>
|
|
348
|
+
<option value="">No cluster (manual nodes)</option>
|
|
349
|
+
{clusters.map((c) => (
|
|
350
|
+
<option key={c.id} value={c.id}>
|
|
351
|
+
{c.name} ({c.members.length} nodes)
|
|
352
|
+
</option>
|
|
353
|
+
))}
|
|
354
|
+
</select>
|
|
355
|
+
</div>
|
|
356
|
+
)}
|
|
357
|
+
</div>
|
|
358
|
+
)}
|
|
359
|
+
|
|
360
|
+
{/* Shared mesh settings */}
|
|
361
|
+
<div className="grid grid-cols-2 gap-2">
|
|
362
|
+
<div>
|
|
363
|
+
<label className="block text-[10px] font-medium text-muted-foreground mb-1">
|
|
364
|
+
Metric
|
|
365
|
+
</label>
|
|
366
|
+
<input
|
|
367
|
+
type="text"
|
|
368
|
+
value={metric}
|
|
369
|
+
onChange={(e) => setMetric(e.target.value)}
|
|
370
|
+
placeholder="e.g., test pass rate"
|
|
371
|
+
className={`${inputCls} font-mono`}
|
|
372
|
+
/>
|
|
373
|
+
</div>
|
|
374
|
+
<div>
|
|
375
|
+
<label className="block text-[10px] font-medium text-muted-foreground mb-1">
|
|
376
|
+
Budget (min)
|
|
377
|
+
</label>
|
|
378
|
+
<input
|
|
379
|
+
type="number"
|
|
380
|
+
min={5}
|
|
381
|
+
max={480}
|
|
382
|
+
value={budgetMinutes}
|
|
383
|
+
onChange={(e) => setBudgetMinutes(parseInt(e.target.value, 10) || 30)}
|
|
384
|
+
className={inputCls}
|
|
385
|
+
/>
|
|
386
|
+
</div>
|
|
387
|
+
</div>
|
|
388
|
+
|
|
389
|
+
<div>
|
|
390
|
+
<label className="block text-[10px] font-medium text-muted-foreground mb-1">
|
|
391
|
+
Scope (file paths, one per line)
|
|
392
|
+
</label>
|
|
393
|
+
<textarea
|
|
394
|
+
value={scopeText}
|
|
395
|
+
onChange={(e) => setScopeText(e.target.value)}
|
|
396
|
+
rows={2}
|
|
397
|
+
className={`${inputCls} font-mono resize-none`}
|
|
398
|
+
placeholder="src/lib/ tests/"
|
|
399
|
+
/>
|
|
400
|
+
</div>
|
|
401
|
+
|
|
402
|
+
{/* Node selection */}
|
|
403
|
+
{nodes.length > 0 && (
|
|
404
|
+
<div>
|
|
405
|
+
<label className="block text-[10px] font-medium text-muted-foreground mb-1">
|
|
406
|
+
Nodes ({selectedNodes.size} selected)
|
|
407
|
+
</label>
|
|
408
|
+
<div className="flex flex-wrap gap-1.5">
|
|
409
|
+
{nodes.map((n) => (
|
|
410
|
+
<button
|
|
411
|
+
key={n.nodeId}
|
|
412
|
+
type="button"
|
|
413
|
+
onClick={() => toggleNode(n.nodeId)}
|
|
414
|
+
className={`px-2 py-0.5 text-[10px] rounded-full border transition-colors ${
|
|
415
|
+
selectedNodes.has(n.nodeId)
|
|
416
|
+
? "border-cyan-500 bg-cyan-500/15 text-cyan-400"
|
|
417
|
+
: "border-border text-muted-foreground hover:text-foreground"
|
|
418
|
+
} ${n.status !== "online" ? "opacity-50" : ""}`}
|
|
419
|
+
>
|
|
420
|
+
{n.nodeId.slice(0, 12)}
|
|
421
|
+
{n.status !== "online" && ` (${n.status})`}
|
|
422
|
+
</button>
|
|
423
|
+
))}
|
|
424
|
+
</div>
|
|
425
|
+
</div>
|
|
426
|
+
)}
|
|
427
|
+
</>
|
|
428
|
+
)}
|
|
429
|
+
</div>
|
|
430
|
+
);
|
|
431
|
+
}
|
|
@@ -67,17 +67,26 @@ export function KanbanBoard() {
|
|
|
67
67
|
);
|
|
68
68
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
69
69
|
|
|
70
|
-
// Filter tasks
|
|
70
|
+
// Filter tasks: exclude archived + roadmap hierarchy types, then apply search
|
|
71
71
|
const filteredTasks = useMemo(() => {
|
|
72
|
-
|
|
73
|
-
const q = searchQuery.toLowerCase();
|
|
74
|
-
return tasks.filter(
|
|
72
|
+
let filtered = tasks.filter(
|
|
75
73
|
(t) =>
|
|
76
|
-
t.
|
|
77
|
-
t.
|
|
78
|
-
|
|
79
|
-
|
|
74
|
+
t.status !== "archived" &&
|
|
75
|
+
t.type !== "project" &&
|
|
76
|
+
t.type !== "phase" &&
|
|
77
|
+
t.type !== "pipeline"
|
|
80
78
|
);
|
|
79
|
+
if (searchQuery.trim()) {
|
|
80
|
+
const q = searchQuery.toLowerCase();
|
|
81
|
+
filtered = filtered.filter(
|
|
82
|
+
(t) =>
|
|
83
|
+
t.id.toLowerCase().includes(q) ||
|
|
84
|
+
t.title.toLowerCase().includes(q) ||
|
|
85
|
+
(t.description || "").toLowerCase().includes(q) ||
|
|
86
|
+
(t.owner || "").toLowerCase().includes(q)
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
return filtered;
|
|
81
90
|
}, [tasks, searchQuery]);
|
|
82
91
|
|
|
83
92
|
// Build parent→children map and separate top-level vs child tasks
|
|
@@ -200,10 +209,29 @@ export function KanbanBoard() {
|
|
|
200
209
|
[]
|
|
201
210
|
);
|
|
202
211
|
|
|
212
|
+
// Done-gate state
|
|
213
|
+
const [doneConfirmTaskId, setDoneConfirmTaskId] = useState<string | null>(null);
|
|
214
|
+
|
|
203
215
|
const handleMoveTask = (taskId: string, targetColumn: string) => {
|
|
216
|
+
if (targetColumn === "done") {
|
|
217
|
+
// Prompt Gui to confirm — only they can mark tasks done
|
|
218
|
+
setDoneConfirmTaskId(taskId);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
204
221
|
updateTask(taskId, { kanbanColumn: targetColumn });
|
|
205
222
|
};
|
|
206
223
|
|
|
224
|
+
const confirmDone = () => {
|
|
225
|
+
if (doneConfirmTaskId) {
|
|
226
|
+
updateTask(doneConfirmTaskId, { kanbanColumn: "done", force_done: true } as Record<string, unknown>);
|
|
227
|
+
setDoneConfirmTaskId(null);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const cancelDone = () => {
|
|
232
|
+
setDoneConfirmTaskId(null);
|
|
233
|
+
};
|
|
234
|
+
|
|
207
235
|
const handleTaskClick = (task: Task) => {
|
|
208
236
|
if (bulkMode) {
|
|
209
237
|
toggleBulkSelect(task.id);
|
|
@@ -303,6 +331,12 @@ export function KanbanBoard() {
|
|
|
303
331
|
highlight={bulkMode}
|
|
304
332
|
bulkMode={bulkMode}
|
|
305
333
|
bulkSelection={bulkSelection}
|
|
334
|
+
onClearDone={col === "done" ? async () => {
|
|
335
|
+
// Archive all visible done tasks
|
|
336
|
+
for (const t of columns.done) {
|
|
337
|
+
await updateTask(t.id, { status: "archived" } as Record<string, unknown>);
|
|
338
|
+
}
|
|
339
|
+
} : undefined}
|
|
306
340
|
/>
|
|
307
341
|
</div>
|
|
308
342
|
{i < NUM_COLS - 1 && (
|
|
@@ -331,7 +365,13 @@ export function KanbanBoard() {
|
|
|
331
365
|
))}
|
|
332
366
|
<div className="w-px h-6 bg-border" />
|
|
333
367
|
<button
|
|
334
|
-
onClick={() =>
|
|
368
|
+
onClick={() => {
|
|
369
|
+
for (const id of bulkSelection) {
|
|
370
|
+
updateTask(id, { status: "done", force_done: true } as Record<string, unknown>);
|
|
371
|
+
}
|
|
372
|
+
setBulkSelection(new Set());
|
|
373
|
+
setBulkAction(null);
|
|
374
|
+
}}
|
|
335
375
|
className="px-2.5 py-1 text-xs rounded-md bg-green-500/15 text-green-400 border border-green-500/20 hover:bg-green-500/25 transition-colors"
|
|
336
376
|
>
|
|
337
377
|
Mark Done
|
|
@@ -359,6 +399,32 @@ export function KanbanBoard() {
|
|
|
359
399
|
>
|
|
360
400
|
<Plus className="h-5 w-5" />
|
|
361
401
|
</button>
|
|
402
|
+
|
|
403
|
+
{/* Done-gate confirmation dialog */}
|
|
404
|
+
{doneConfirmTaskId && (
|
|
405
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60">
|
|
406
|
+
<div className="w-full max-w-sm rounded-lg border border-border bg-card shadow-xl p-5">
|
|
407
|
+
<h3 className="text-sm font-semibold text-foreground mb-2">Mark as Done?</h3>
|
|
408
|
+
<p className="text-xs text-muted-foreground mb-4">
|
|
409
|
+
Only you can mark tasks as done. Agents and nodes land in Review for your approval.
|
|
410
|
+
</p>
|
|
411
|
+
<div className="flex items-center gap-2 justify-end">
|
|
412
|
+
<button
|
|
413
|
+
onClick={cancelDone}
|
|
414
|
+
className="px-3 py-1.5 text-xs rounded-md border border-border text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
|
|
415
|
+
>
|
|
416
|
+
Cancel
|
|
417
|
+
</button>
|
|
418
|
+
<button
|
|
419
|
+
onClick={confirmDone}
|
|
420
|
+
className="px-3 py-1.5 text-xs rounded-md bg-green-500/15 text-green-400 border border-green-500/20 hover:bg-green-500/25 transition-colors"
|
|
421
|
+
>
|
|
422
|
+
Confirm Done
|
|
423
|
+
</button>
|
|
424
|
+
</div>
|
|
425
|
+
</div>
|
|
426
|
+
</div>
|
|
427
|
+
)}
|
|
362
428
|
</>
|
|
363
429
|
);
|
|
364
430
|
}
|