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,10 +1,34 @@
1
- import { getNats, sc } from "@/lib/nats";
1
+ import { getNats, getTasksKv, sc } from "@/lib/nats";
2
2
 
3
3
  export const dynamic = "force-dynamic";
4
4
 
5
5
  /**
6
- * SSE endpoint that subscribes to mesh.events.> and pushes to connected browsers.
7
- * Falls back gracefully if NATS is unavailable.
6
+ * MANUAL VERIFICATION PROTOCOL (SSE dual-iterator cleanup):
7
+ *
8
+ * The zombie watcher leak is the highest-risk bug in this route.
9
+ * It cannot be unit tested (no real HTTP server in Vitest).
10
+ *
11
+ * To verify after deployment:
12
+ * 1. Open MC on worker node in browser
13
+ * 2. Open DevTools > Network tab > filter "events" (SSE stream)
14
+ * 3. Trigger a task state change on the lead (create/complete a task)
15
+ * 4. Verify the SSE event arrives in the stream ("kv.task.updated" event type)
16
+ * 5. Close the browser tab
17
+ * 6. Check NATS connections: curl http://localhost:8222/connz | jq '.connections | length'
18
+ * -> Connection count should drop by 1 (the SSE watcher + sub are cleaned up)
19
+ * 7. Repeat with 3 tabs open -> close all -> verify count drops by 3
20
+ *
21
+ * If connection count doesn't drop: the cancel() handler isn't calling watcher.stop()
22
+ * or sub.unsubscribe(). Check the dual-iterator lifecycle in this file.
23
+ */
24
+
25
+ /**
26
+ * SSE endpoint: dual-iterator (NATS pub/sub + KV watcher).
27
+ *
28
+ * Iterator 1: NATS subscription on mesh.events.> (task lifecycle events)
29
+ * Iterator 2: KV watcher on MESH_TASKS (real-time task state changes)
30
+ *
31
+ * Both feed into a single SSE stream. On client disconnect, both are cleaned up.
8
32
  */
9
33
  export async function GET() {
10
34
  const nc = await getNats();
@@ -13,16 +37,17 @@ export async function GET() {
13
37
  }
14
38
 
15
39
  const sub = nc.subscribe("mesh.events.>");
40
+ const kv = await getTasksKv();
41
+ let watcher: any = null;
16
42
  let closed = false;
17
43
 
18
44
  const stream = new ReadableStream({
19
45
  async start(controller) {
20
46
  const encoder = new TextEncoder();
21
47
 
22
- // Send initial keepalive
23
48
  controller.enqueue(encoder.encode(": connected\n\n"));
24
49
 
25
- // Keepalive every 30s to prevent proxy/browser timeout
50
+ // Keepalive every 30s
26
51
  const keepalive = setInterval(() => {
27
52
  if (!closed) {
28
53
  try {
@@ -33,26 +58,77 @@ export async function GET() {
33
58
  }
34
59
  }, 30000);
35
60
 
36
- try {
37
- for await (const msg of sub) {
38
- if (closed) break;
39
- try {
40
- const data = sc.decode(msg.data);
41
- const eventType = msg.subject.replace("mesh.events.", "");
42
- controller.enqueue(
43
- encoder.encode(`event: ${eventType}\ndata: ${data}\n\n`)
44
- );
45
- } catch {
46
- // skip malformed messages
61
+ // Iterator 1: NATS pub/sub events
62
+ (async () => {
63
+ try {
64
+ for await (const msg of sub) {
65
+ if (closed) break;
66
+ try {
67
+ const data = sc.decode(msg.data);
68
+ const eventType = msg.subject.replace("mesh.events.", "");
69
+ controller.enqueue(
70
+ encoder.encode(`event: ${eventType}\ndata: ${data}\n\n`)
71
+ );
72
+ } catch {
73
+ // skip malformed
74
+ }
47
75
  }
76
+ } catch {
77
+ // subscription ended
78
+ }
79
+ })();
80
+
81
+ // Iterator 2: KV watcher (task state changes)
82
+ if (kv) {
83
+ try {
84
+ watcher = await kv.watch();
85
+ (async () => {
86
+ try {
87
+ for await (const entry of watcher) {
88
+ if (closed) break;
89
+ try {
90
+ const task = entry.value
91
+ ? JSON.parse(sc.decode(entry.value))
92
+ : null;
93
+ const payload = JSON.stringify({
94
+ key: entry.key,
95
+ operation: entry.value ? "PUT" : "DEL",
96
+ task,
97
+ revision: entry.revision,
98
+ });
99
+ controller.enqueue(
100
+ encoder.encode(`event: kv.task.updated\ndata: ${payload}\n\n`)
101
+ );
102
+ } catch {
103
+ // skip malformed
104
+ }
105
+ }
106
+ } catch {
107
+ // watcher ended
108
+ }
109
+ })();
110
+ } catch {
111
+ // KV watch failed — continue with pub/sub only
48
112
  }
49
- } finally {
50
- clearInterval(keepalive);
51
113
  }
114
+
115
+ // Wait for cancel (client disconnect)
116
+ await new Promise<void>((resolve) => {
117
+ const check = setInterval(() => {
118
+ if (closed) {
119
+ clearInterval(check);
120
+ clearInterval(keepalive);
121
+ resolve();
122
+ }
123
+ }, 1000);
124
+ });
52
125
  },
53
126
  cancel() {
54
127
  closed = true;
55
128
  sub.unsubscribe();
129
+ if (watcher && typeof watcher.stop === "function") {
130
+ watcher.stop();
131
+ }
56
132
  },
57
133
  });
58
134
 
@@ -61,7 +137,7 @@ export async function GET() {
61
137
  "Content-Type": "text/event-stream",
62
138
  "Cache-Control": "no-cache, no-transform",
63
139
  Connection: "keep-alive",
64
- "X-Accel-Buffering": "no", // disable nginx buffering
140
+ "X-Accel-Buffering": "no",
65
141
  },
66
142
  });
67
143
  }
@@ -0,0 +1,11 @@
1
+ import { NODE_ID, NODE_ROLE, NODE_PLATFORM } from "@/lib/config";
2
+
3
+ export const dynamic = "force-dynamic";
4
+
5
+ export async function GET() {
6
+ return Response.json({
7
+ nodeId: NODE_ID,
8
+ role: NODE_ROLE,
9
+ platform: NODE_PLATFORM,
10
+ });
11
+ }
@@ -0,0 +1,92 @@
1
+ import { getTasksKv, sc } from "@/lib/nats";
2
+ import { NODE_ID, NODE_ROLE } from "@/lib/config";
3
+
4
+ export const dynamic = "force-dynamic";
5
+
6
+ /**
7
+ * GET /api/mesh/tasks/:id — Get a single task from KV.
8
+ */
9
+ export async function GET(
10
+ _req: Request,
11
+ { params }: { params: Promise<{ id: string }> }
12
+ ) {
13
+ const { id } = await params;
14
+ const kv = await getTasksKv();
15
+ if (!kv) {
16
+ return Response.json({ error: "NATS unavailable" }, { status: 503 });
17
+ }
18
+
19
+ const entry = await kv.get(id);
20
+ if (!entry?.value) {
21
+ return Response.json({ error: "not found" }, { status: 404 });
22
+ }
23
+
24
+ const task = JSON.parse(sc.decode(entry.value));
25
+ return Response.json({ task, revision: entry.revision });
26
+ }
27
+
28
+ /**
29
+ * PATCH /api/mesh/tasks/:id — Update a task with CAS.
30
+ *
31
+ * Authority rules:
32
+ * - Lead can update any task
33
+ * - Workers can only update tasks they originated
34
+ * - Revision must match (CAS) to prevent stale writes
35
+ */
36
+ export async function PATCH(
37
+ req: Request,
38
+ { params }: { params: Promise<{ id: string }> }
39
+ ) {
40
+ const { id } = await params;
41
+ const kv = await getTasksKv();
42
+ if (!kv) {
43
+ return Response.json({ error: "NATS unavailable" }, { status: 503 });
44
+ }
45
+
46
+ const body = await req.json();
47
+ const { revision, ...updates } = body;
48
+
49
+ if (!revision) {
50
+ return Response.json({ error: "revision is required for CAS update" }, { status: 400 });
51
+ }
52
+
53
+ // Read current state
54
+ const entry = await kv.get(id);
55
+ if (!entry?.value) {
56
+ return Response.json({ error: "not found" }, { status: 404 });
57
+ }
58
+
59
+ const current = JSON.parse(sc.decode(entry.value));
60
+
61
+ // Authority check
62
+ if (NODE_ROLE !== "lead" && current.origin !== NODE_ID) {
63
+ return Response.json(
64
+ { error: "workers can only update tasks they originated" },
65
+ { status: 403 }
66
+ );
67
+ }
68
+
69
+ // Merge updates
70
+ const updated = { ...current, ...updates };
71
+
72
+ // CAS write
73
+ try {
74
+ await kv.update(id, sc.encode(JSON.stringify(updated)), revision);
75
+ } catch (err: any) {
76
+ // Revision mismatch — return current state so client can retry
77
+ const freshEntry = await kv.get(id);
78
+ const freshTask = freshEntry?.value
79
+ ? JSON.parse(sc.decode(freshEntry.value))
80
+ : null;
81
+ return Response.json(
82
+ {
83
+ error: `revision mismatch: expected ${revision}, got ${entry.revision}`,
84
+ currentTask: freshTask,
85
+ currentRevision: freshEntry?.revision,
86
+ },
87
+ { status: 409 }
88
+ );
89
+ }
90
+
91
+ return Response.json({ ok: true, task: updated });
92
+ }
@@ -0,0 +1,91 @@
1
+ import { getTasksKv, sc } from "@/lib/nats";
2
+ import { NODE_ID, NODE_ROLE } from "@/lib/config";
3
+ import crypto from "crypto";
4
+
5
+ export const dynamic = "force-dynamic";
6
+
7
+ /**
8
+ * GET /api/mesh/tasks — List all tasks from NATS KV.
9
+ * Available on all nodes (read from shared KV).
10
+ */
11
+ export async function GET() {
12
+ const kv = await getTasksKv();
13
+ if (!kv) {
14
+ return Response.json({ tasks: [], natsAvailable: false });
15
+ }
16
+
17
+ const tasks: any[] = [];
18
+ const keys = await kv.keys();
19
+ for await (const key of keys) {
20
+ const entry = await kv.get(key);
21
+ if (!entry?.value) continue;
22
+ try {
23
+ tasks.push(JSON.parse(sc.decode(entry.value)));
24
+ } catch {
25
+ // skip malformed
26
+ }
27
+ }
28
+
29
+ tasks.sort((a, b) => {
30
+ if ((b.priority || 0) !== (a.priority || 0)) return (b.priority || 0) - (a.priority || 0);
31
+ return new Date(a.created_at || 0).getTime() - new Date(b.created_at || 0).getTime();
32
+ });
33
+
34
+ return Response.json({ tasks, natsAvailable: true });
35
+ }
36
+
37
+ /**
38
+ * POST /api/mesh/tasks — Propose a new task.
39
+ *
40
+ * On lead: creates directly with status "queued".
41
+ * On worker: creates with status "proposed" — daemon validates within 30s.
42
+ */
43
+ export async function POST(req: Request) {
44
+ const kv = await getTasksKv();
45
+ if (!kv) {
46
+ return Response.json({ error: "NATS unavailable" }, { status: 503 });
47
+ }
48
+
49
+ const body = await req.json();
50
+ if (!body.title) {
51
+ return Response.json({ error: "title is required" }, { status: 400 });
52
+ }
53
+
54
+ const suffix = crypto.randomBytes(3).toString("hex");
55
+ const now = new Date();
56
+ const dateStr =
57
+ now.getFullYear().toString() +
58
+ (now.getMonth() + 1).toString().padStart(2, "0") +
59
+ now.getDate().toString().padStart(2, "0");
60
+ const taskId = body.task_id || `T-${dateStr}-${suffix}`;
61
+
62
+ const status = NODE_ROLE === "lead" ? "queued" : "proposed";
63
+
64
+ const task = {
65
+ task_id: taskId,
66
+ title: body.title,
67
+ description: body.description || "",
68
+ status,
69
+ origin: NODE_ID,
70
+ owner: null,
71
+ priority: body.priority || 0,
72
+ budget_minutes: body.budget_minutes || 30,
73
+ metric: body.metric || null,
74
+ success_criteria: body.success_criteria || [],
75
+ scope: body.scope || [],
76
+ tags: body.tags || [],
77
+ preferred_nodes: body.preferred_nodes || [],
78
+ exclude_nodes: body.exclude_nodes || [],
79
+ created_at: now.toISOString(),
80
+ claimed_at: null,
81
+ started_at: null,
82
+ completed_at: null,
83
+ last_activity: null,
84
+ result: null,
85
+ attempts: [],
86
+ };
87
+
88
+ await kv.put(taskId, sc.encode(JSON.stringify(task)));
89
+
90
+ return Response.json({ ok: true, task }, { status: 201 });
91
+ }
@@ -39,7 +39,7 @@ export async function POST(
39
39
  return NextResponse.json({ error: "Task not found" }, { status: 404 });
40
40
  }
41
41
 
42
- const fromSoul = task.soulId || "daedalus";
42
+ const fromSoul = task.soulId || "main-agent";
43
43
 
44
44
  // Create handoff document in ClawVault
45
45
  await fs.mkdir(HANDOFFS_DIR, { recursive: true });
@@ -6,6 +6,8 @@ import { statusToKanban, kanbanToStatus } from "@/lib/parsers/task-markdown";
6
6
  import { syncTasksToMarkdown } from "@/lib/sync/tasks";
7
7
  import { logActivity } from "@/lib/activity";
8
8
  import { gatewayNotify } from "@/lib/gateway-notify";
9
+ import { AGENT_NAME, HUMAN_NAME } from "@/lib/config";
10
+ import { getNats, sc } from "@/lib/nats";
9
11
 
10
12
  /**
11
13
  * Push a notification message to the OpenClaw TUI via gateway chat.send + abort.
@@ -17,6 +19,22 @@ function notifyAgent(text: string) {
17
19
  });
18
20
  }
19
21
 
22
+ /**
23
+ * Cancel a running mesh task via NATS. Best-effort — ignores failures.
24
+ */
25
+ async function cancelMeshTask(taskId: string, execution: string | null, status: string) {
26
+ if (execution === "mesh" && status !== "done" && status !== "cancelled") {
27
+ const nc = await getNats();
28
+ if (nc) {
29
+ nc.request(
30
+ "mesh.tasks.cancel",
31
+ sc.encode(JSON.stringify({ task_id: taskId })),
32
+ { timeout: 5000 }
33
+ ).catch(() => {});
34
+ }
35
+ }
36
+ }
37
+
20
38
  /**
21
39
  * DELETE /api/tasks/[id]
22
40
  * Delete a task (and its children) by ID.
@@ -58,6 +76,9 @@ export async function DELETE(
58
76
  collectDescendants(id);
59
77
  idsToDelete.push(id); // add the root task last
60
78
 
79
+ // Cancel mesh task if running
80
+ await cancelMeshTask(id, existing.execution, existing.status);
81
+
61
82
  // Delete all collected tasks + their dependency edges
62
83
  for (const delId of idsToDelete) {
63
84
  db.delete(dependencies)
@@ -109,6 +130,22 @@ export async function PATCH(
109
130
  const body = await request.json();
110
131
  const now = new Date().toISOString();
111
132
 
133
+ // Transition guard: block execution mode changes on in-flight mesh tasks
134
+ if (body.execution !== undefined && body.execution !== existing.execution) {
135
+ if (existing.meshTaskId) {
136
+ return NextResponse.json(
137
+ { error: "Cannot change execution mode after task has been submitted to mesh" },
138
+ { status: 400 }
139
+ );
140
+ }
141
+ if (existing.status !== "queued" && existing.status !== "ready") {
142
+ return NextResponse.json(
143
+ { error: `Cannot change execution mode while task is ${existing.status}` },
144
+ { status: 400 }
145
+ );
146
+ }
147
+ }
148
+
112
149
  // Build the update set from provided fields
113
150
  const update: Record<string, unknown> = { updatedAt: now };
114
151
 
@@ -130,6 +167,17 @@ export async function PATCH(
130
167
  update.status = kanbanToStatus(body.kanbanColumn);
131
168
  }
132
169
  }
170
+
171
+ // Done-gate: only the human operator can mark tasks as done (via force_done flag).
172
+ // Without force_done, redirect done→review so tasks land in waiting-user.
173
+ const targetStatus = update.status as string | undefined;
174
+ const targetColumn = update.kanbanColumn as string | undefined;
175
+ if (!body.force_done) {
176
+ if (targetStatus === "done" || targetColumn === "done") {
177
+ update.status = "waiting-user";
178
+ update.kanbanColumn = "review";
179
+ }
180
+ }
133
181
  if (body.owner !== undefined) {
134
182
  update.owner = body.owner || null;
135
183
  }
@@ -203,18 +251,56 @@ export async function PATCH(
203
251
  if (body.acknowledged_at !== undefined) {
204
252
  update.acknowledgedAt = body.acknowledged_at || null;
205
253
  }
254
+ // Mesh execution fields
255
+ if (body.execution !== undefined) {
256
+ update.execution = body.execution || null;
257
+ }
258
+ if (body.collaboration !== undefined) {
259
+ update.collaboration = body.collaboration ? JSON.stringify(body.collaboration) : null;
260
+ }
261
+ if (body.preferred_nodes !== undefined) {
262
+ update.preferredNodes = body.preferred_nodes?.length ? JSON.stringify(body.preferred_nodes) : null;
263
+ }
264
+ if (body.exclude_nodes !== undefined) {
265
+ update.excludeNodes = body.exclude_nodes?.length ? JSON.stringify(body.exclude_nodes) : null;
266
+ }
267
+ if (body.cluster_id !== undefined) {
268
+ update.clusterId = body.cluster_id || null;
269
+ }
270
+ if (body.metric !== undefined) {
271
+ update.metric = body.metric || null;
272
+ }
273
+ if (body.budget_minutes !== undefined) {
274
+ update.budgetMinutes = body.budget_minutes;
275
+ }
276
+ if (body.scope !== undefined) {
277
+ update.scope = body.scope?.length ? JSON.stringify(body.scope) : null;
278
+ }
206
279
 
207
280
  // Auto-set owner when task moves to running manually (no explicit owner change)
208
281
  const effectiveStatus = (update.status as string) ?? existing.status;
209
282
  if (effectiveStatus === "running" && body.owner === undefined && !existing.owner) {
210
- update.owner = "Gui";
283
+ update.owner = HUMAN_NAME;
211
284
  }
212
285
 
213
286
  db.update(tasks).set(update).where(eq(tasks.id, id)).run();
214
287
 
288
+ // Cancel mesh task if status changed to cancelled
289
+ if (update.status === "cancelled") {
290
+ await cancelMeshTask(id, existing.execution, existing.status);
291
+ }
292
+
215
293
  // Sync back to markdown
216
294
  syncTasksToMarkdown(db);
217
295
 
296
+ // Wake bridge when execution is set to mesh (for immediate pickup)
297
+ const effectiveExecution = (update.execution as string) ?? existing.execution;
298
+ const effectiveApprovalNum = (update.needsApproval as number) ?? existing.needsApproval;
299
+ if (effectiveExecution === "mesh" && effectiveApprovalNum === 0) {
300
+ const nc = await getNats();
301
+ if (nc) nc.publish("mesh.bridge.wake", sc.encode(""));
302
+ }
303
+
218
304
  // Log activity
219
305
  const movedTo = body.kanban_column || body.kanbanColumn;
220
306
  const parts: string[] = [];
@@ -229,14 +315,14 @@ export async function PATCH(
229
315
  id
230
316
  );
231
317
 
232
- // Push notification to TUI only for Daedalus autostart tasks
318
+ // Push notification to TUI only for agent autostart tasks
233
319
  if (movedTo || body.status) {
234
320
  const effectiveOwner = (update.owner as string) ?? existing.owner;
235
321
  const effectiveApproval = (update.needsApproval as number) ?? existing.needsApproval;
236
- const isDaedalus = effectiveOwner === "Daedalus";
322
+ const isAgent = effectiveOwner === AGENT_NAME;
237
323
  const isAutostart = effectiveApproval === 0;
238
324
 
239
- if (isDaedalus && isAutostart) {
325
+ if (isAgent && isAutostart) {
240
326
  const fromCol = existing.kanbanColumn || existing.status;
241
327
  const toCol = movedTo || body.status;
242
328
  notifyAgent(`MC-Kanban: "${existing.title}" has been moved from ${fromCol} to ${toCol}`);
@@ -7,7 +7,9 @@ import { statusToKanban } from "@/lib/parsers/task-markdown";
7
7
  import { syncTasksFromMarkdownIfChanged, syncTasksToMarkdown } from "@/lib/sync/tasks";
8
8
  import { logActivity } from "@/lib/activity";
9
9
  import { schedulerTick } from "@/lib/scheduler";
10
- import { WORKSPACE_ROOT } from "@/lib/config";
10
+ import { generateTaskId } from "@/lib/task-id";
11
+ import { getNats, sc } from "@/lib/nats";
12
+ import { WORKSPACE_ROOT, AGENT_NAME } from "@/lib/config";
11
13
  import path from "path";
12
14
 
13
15
  /**
@@ -67,7 +69,7 @@ export async function GET(request: NextRequest) {
67
69
  title: liveWork.title,
68
70
  status: "running",
69
71
  kanbanColumn: "in_progress",
70
- owner: "daedalus",
72
+ owner: AGENT_NAME.toLowerCase(),
71
73
  nextAction: liveWork.nextAction || null,
72
74
  updatedAt: new Date().toISOString(),
73
75
  createdAt: new Date().toISOString(),
@@ -186,6 +188,15 @@ export async function POST(request: NextRequest) {
186
188
  isRecurring: body.is_recurring ? 1 : 0,
187
189
  capacityClass: body.capacity_class || "normal",
188
190
  autoPriority: body.auto_priority || 0,
191
+ // Execution fields
192
+ execution: body.execution || null,
193
+ collaboration: body.collaboration ? JSON.stringify(body.collaboration) : null,
194
+ preferredNodes: body.preferred_nodes?.length ? JSON.stringify(body.preferred_nodes) : null,
195
+ excludeNodes: body.exclude_nodes?.length ? JSON.stringify(body.exclude_nodes) : null,
196
+ clusterId: body.cluster_id || null,
197
+ metric: body.metric || null,
198
+ budgetMinutes: body.budget_minutes || 30,
199
+ scope: body.scope?.length ? JSON.stringify(body.scope) : null,
189
200
  updatedAt: now.toISOString(),
190
201
  createdAt: now.toISOString(),
191
202
  })
@@ -200,6 +211,13 @@ export async function POST(request: NextRequest) {
200
211
  // Immediately run scheduler tick so auto-start tasks dispatch without waiting for poll
201
212
  schedulerTick();
202
213
 
214
+ // Wake bridge for mesh tasks
215
+ if (body.execution === "mesh") {
216
+ getNats().then((nc) => {
217
+ if (nc) nc.publish("mesh.bridge.wake", sc.encode(""));
218
+ }).catch(() => {});
219
+ }
220
+
203
221
  const created = db
204
222
  .select()
205
223
  .from(tasks)
@@ -223,31 +241,4 @@ export async function POST(request: NextRequest) {
223
241
  { status: 500 }
224
242
  );
225
243
  }
226
- }
227
-
228
- /**
229
- * Generate a task ID in the format T-YYYYMMDD-NNN.
230
- * NNN is a zero-padded sequence number based on existing tasks for that date.
231
- */
232
- function generateTaskId(db: ReturnType<typeof getDb>, date: Date): string {
233
- const dateStr =
234
- date.getFullYear().toString() +
235
- (date.getMonth() + 1).toString().padStart(2, "0") +
236
- date.getDate().toString().padStart(2, "0");
237
-
238
- const prefix = `T-${dateStr}-`;
239
-
240
- // Find highest existing sequence number for this date prefix
241
- const existing = db
242
- .select({ id: tasks.id })
243
- .from(tasks)
244
- .all()
245
- .filter((t) => t.id.startsWith(prefix))
246
- .map((t) => {
247
- const seq = parseInt(t.id.slice(prefix.length), 10);
248
- return isNaN(seq) ? 0 : seq;
249
- });
250
-
251
- const nextSeq = existing.length > 0 ? Math.max(...existing) + 1 : 1;
252
- return `${prefix}${nextSeq.toString().padStart(3, "0")}`;
253
- }
244
+ }