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
@@ -35,6 +35,11 @@ export interface Task {
35
35
  metric: string | null;
36
36
  budgetMinutes: number | null;
37
37
  scope: string | null;
38
+ // Collab routing fields
39
+ collaboration: string | null;
40
+ preferredNodes: string | null;
41
+ excludeNodes: string | null;
42
+ clusterId: string | null;
38
43
  showInCalendar: number | null;
39
44
  acknowledgedAt: string | null;
40
45
  updatedAt: string;
@@ -451,6 +456,213 @@ export function useBurndown(projectId: string | null) {
451
456
  return { burndown: data ?? null, error, isLoading };
452
457
  }
453
458
 
459
+ // --- Cowork: Clusters & Collab Sessions ---
460
+
461
+ export interface ClusterMemberView {
462
+ id: number;
463
+ nodeId: string;
464
+ role: string;
465
+ nodeStatus: string;
466
+ createdAt: string;
467
+ }
468
+
469
+ export interface ClusterView {
470
+ id: string;
471
+ name: string;
472
+ description: string | null;
473
+ color: string | null;
474
+ defaultMode: string;
475
+ defaultConvergence: string;
476
+ convergenceThreshold: number;
477
+ maxRounds: number;
478
+ status: string;
479
+ members: ClusterMemberView[];
480
+ updatedAt: string;
481
+ createdAt: string;
482
+ }
483
+
484
+ export interface CollabNode {
485
+ node_id: string;
486
+ role: string;
487
+ scope: string[];
488
+ joined_at: string;
489
+ status: string;
490
+ }
491
+
492
+ export interface CollabReflection {
493
+ node_id: string;
494
+ summary: string;
495
+ learnings: string;
496
+ artifacts: string[];
497
+ confidence: number;
498
+ vote: string;
499
+ parse_failed?: boolean;
500
+ synthetic?: boolean;
501
+ submitted_at: string;
502
+ }
503
+
504
+ export interface CollabRound {
505
+ round_number: number;
506
+ started_at: string;
507
+ completed_at: string | null;
508
+ shared_intel: string;
509
+ reflections: CollabReflection[];
510
+ }
511
+
512
+ export interface CollabSession {
513
+ session_id: string;
514
+ task_id: string;
515
+ mode: string;
516
+ status: string;
517
+ min_nodes: number;
518
+ max_nodes: number | null;
519
+ nodes: CollabNode[];
520
+ current_round: number;
521
+ max_rounds: number;
522
+ rounds: CollabRound[];
523
+ convergence: {
524
+ type: string;
525
+ threshold: number;
526
+ metric: string | null;
527
+ min_quorum: number;
528
+ };
529
+ scope_strategy: string;
530
+ result: Record<string, unknown> | null;
531
+ audit_log: Array<{ ts: string; event: string; [k: string]: unknown }>;
532
+ created_at: string;
533
+ completed_at: string | null;
534
+ }
535
+
536
+ export function useClusters() {
537
+ const { data, error, isLoading } = useSWR<{ clusters: ClusterView[] }>(
538
+ "/api/cowork/clusters",
539
+ fetcher,
540
+ { refreshInterval: 10000 }
541
+ );
542
+ return { clusters: data?.clusters ?? [], error, isLoading };
543
+ }
544
+
545
+ export function useCollabSessions(status?: string, refreshInterval = 5000) {
546
+ const params = status ? `?status=${status}` : "";
547
+ const { data, error, isLoading } = useSWR<{
548
+ sessions: CollabSession[];
549
+ natsAvailable: boolean;
550
+ }>(`/api/cowork/sessions${params}`, fetcher, { refreshInterval });
551
+ return {
552
+ sessions: data?.sessions ?? [],
553
+ natsAvailable: data?.natsAvailable ?? true,
554
+ error,
555
+ isLoading,
556
+ };
557
+ }
558
+
559
+ export async function createCluster(params: {
560
+ name: string;
561
+ description?: string;
562
+ color?: string;
563
+ defaultMode?: string;
564
+ defaultConvergence?: string;
565
+ convergenceThreshold?: number;
566
+ maxRounds?: number;
567
+ members?: Array<{ nodeId: string; role: string }>;
568
+ }) {
569
+ const res = await fetch("/api/cowork/clusters", {
570
+ method: "POST",
571
+ headers: { "Content-Type": "application/json" },
572
+ body: JSON.stringify(params),
573
+ });
574
+ const data = await res.json();
575
+ await mutate("/api/cowork/clusters");
576
+ return data;
577
+ }
578
+
579
+ export async function updateCluster(id: string, updates: Record<string, unknown>) {
580
+ const res = await fetch(`/api/cowork/clusters/${id}`, {
581
+ method: "PATCH",
582
+ headers: { "Content-Type": "application/json" },
583
+ body: JSON.stringify(updates),
584
+ });
585
+ const data = await res.json();
586
+ await mutate("/api/cowork/clusters");
587
+ return data;
588
+ }
589
+
590
+ export async function deleteCluster(id: string) {
591
+ await fetch(`/api/cowork/clusters/${id}`, { method: "DELETE" });
592
+ await mutate("/api/cowork/clusters");
593
+ }
594
+
595
+ export async function addClusterMember(clusterId: string, nodeId: string, role: string) {
596
+ const res = await fetch(`/api/cowork/clusters/${clusterId}/members`, {
597
+ method: "POST",
598
+ headers: { "Content-Type": "application/json" },
599
+ body: JSON.stringify({ nodeId, role }),
600
+ });
601
+ const data = await res.json();
602
+ await mutate("/api/cowork/clusters");
603
+ return data;
604
+ }
605
+
606
+ export async function updateClusterMember(clusterId: string, nodeId: string, role: string) {
607
+ const res = await fetch(`/api/cowork/clusters/${clusterId}/members`, {
608
+ method: "PATCH",
609
+ headers: { "Content-Type": "application/json" },
610
+ body: JSON.stringify({ nodeId, role }),
611
+ });
612
+ const data = await res.json();
613
+ await mutate("/api/cowork/clusters");
614
+ return data;
615
+ }
616
+
617
+ export async function removeClusterMember(clusterId: string, nodeId: string) {
618
+ await fetch(
619
+ `/api/cowork/clusters/${clusterId}/members?nodeId=${encodeURIComponent(nodeId)}`,
620
+ { method: "DELETE" }
621
+ );
622
+ await mutate("/api/cowork/clusters");
623
+ }
624
+
625
+ export async function dispatchCollabTask(params: {
626
+ title: string;
627
+ description?: string;
628
+ clusterId?: string;
629
+ nodes?: Array<{ nodeId: string; role: string }>;
630
+ mode?: string;
631
+ convergence?: { type: string; threshold?: number; metric?: string };
632
+ scopeStrategy?: string;
633
+ budgetMinutes?: number;
634
+ maxRounds?: number;
635
+ metric?: string;
636
+ scope?: string[];
637
+ }) {
638
+ const res = await fetch("/api/cowork/dispatch", {
639
+ method: "POST",
640
+ headers: { "Content-Type": "application/json" },
641
+ body: JSON.stringify(params),
642
+ });
643
+ const data = await res.json();
644
+ await mutate("/api/cowork/sessions");
645
+ await mutate("/api/cowork/clusters");
646
+ await mutate("/api/tasks");
647
+ return data;
648
+ }
649
+
650
+ export async function interveneSession(params: {
651
+ action: "abort" | "force_converge" | "remove_node";
652
+ sessionId: string;
653
+ nodeId?: string;
654
+ }) {
655
+ const res = await fetch("/api/cowork/intervene", {
656
+ method: "POST",
657
+ headers: { "Content-Type": "application/json" },
658
+ body: JSON.stringify(params),
659
+ });
660
+ const data = await res.json();
661
+ await mutate("/api/cowork/sessions");
662
+ await mutate("/api/tasks");
663
+ return data;
664
+ }
665
+
454
666
  // --- Mesh SSE integration ---
455
667
 
456
668
  /**
@@ -470,6 +682,14 @@ export function useMeshSSE() {
470
682
  mutate("/api/activity");
471
683
  };
472
684
 
685
+ const invalidateCollab = () => {
686
+ mutate("/api/cowork/sessions");
687
+ mutate("/api/cowork/clusters");
688
+ mutate("/api/tasks");
689
+ mutate("/api/activity");
690
+ };
691
+
692
+ // Task events
473
693
  es.addEventListener("completed", invalidate);
474
694
  es.addEventListener("claimed", invalidate);
475
695
  es.addEventListener("started", invalidate);
@@ -478,6 +698,22 @@ export function useMeshSSE() {
478
698
  es.addEventListener("released", invalidate);
479
699
  es.addEventListener("cancelled", invalidate);
480
700
 
701
+ // Collab events (mesh.events.collab.* → event type is "collab.{action}")
702
+ es.addEventListener("collab.created", invalidateCollab);
703
+ es.addEventListener("collab.joined", invalidateCollab);
704
+ es.addEventListener("collab.round_started", invalidateCollab);
705
+ es.addEventListener("collab.reflection_received", invalidateCollab);
706
+ es.addEventListener("collab.converged", invalidateCollab);
707
+ es.addEventListener("collab.completed", invalidateCollab);
708
+ es.addEventListener("collab.aborted", invalidateCollab);
709
+ es.addEventListener("collab.node_removed", invalidateCollab);
710
+
711
+ // KV task state changes (from dual-iterator watcher)
712
+ es.addEventListener("kv.task.updated", () => {
713
+ mutate("/api/mesh/tasks");
714
+ mutate("/api/tasks");
715
+ });
716
+
481
717
  es.onerror = () => {
482
718
  // EventSource auto-reconnects
483
719
  };
@@ -521,6 +757,79 @@ export function useTokenUsage(period: "today" | "week" | "month" = "today") {
521
757
  return { tokenData: data ?? null, error, isLoading };
522
758
  }
523
759
 
760
+ // --- Mesh Tasks (Distributed MC) ---
761
+
762
+ export interface MeshTask {
763
+ task_id: string;
764
+ title: string;
765
+ description: string;
766
+ status: string;
767
+ origin: string;
768
+ owner: string | null;
769
+ priority: number;
770
+ budget_minutes: number;
771
+ metric: string | null;
772
+ created_at: string;
773
+ [key: string]: unknown;
774
+ }
775
+
776
+ export function useMeshTasks() {
777
+ const { data, error, isLoading } = useSWR<{
778
+ tasks: MeshTask[];
779
+ natsAvailable: boolean;
780
+ }>("/api/mesh/tasks", fetcher, {
781
+ refreshInterval: 5000,
782
+ });
783
+ return {
784
+ meshTasks: data?.tasks ?? [],
785
+ natsAvailable: data?.natsAvailable ?? false,
786
+ error,
787
+ isLoading,
788
+ };
789
+ }
790
+
791
+ export function useNodeIdentity() {
792
+ const { data, error, isLoading } = useSWR<{
793
+ nodeId: string;
794
+ role: "lead" | "worker";
795
+ platform: string;
796
+ }>("/api/mesh/identity", fetcher);
797
+ return { identity: data ?? null, error, isLoading };
798
+ }
799
+
800
+ export async function createMeshTask(task: {
801
+ title: string;
802
+ description?: string;
803
+ priority?: number;
804
+ budget_minutes?: number;
805
+ metric?: string;
806
+ [key: string]: unknown;
807
+ }) {
808
+ const res = await fetch("/api/mesh/tasks", {
809
+ method: "POST",
810
+ headers: { "Content-Type": "application/json" },
811
+ body: JSON.stringify(task),
812
+ });
813
+ const data = await res.json();
814
+ await mutate("/api/mesh/tasks");
815
+ return data;
816
+ }
817
+
818
+ export async function updateMeshTask(
819
+ id: string,
820
+ updates: Record<string, unknown>,
821
+ revision: number
822
+ ) {
823
+ const res = await fetch(`/api/mesh/tasks/${id}`, {
824
+ method: "PATCH",
825
+ headers: { "Content-Type": "application/json" },
826
+ body: JSON.stringify({ ...updates, revision }),
827
+ });
828
+ const data = await res.json();
829
+ await mutate("/api/mesh/tasks");
830
+ return data;
831
+ }
832
+
524
833
  // --- Scheduler ---
525
834
 
526
835
  export function useSchedulerTick(intervalMs = 30_000) {
@@ -12,6 +12,7 @@
12
12
  */
13
13
 
14
14
  import { getRawDb } from "../db";
15
+ import { AGENT_NAME, HUMAN_NAME } from "../config";
15
16
 
16
17
  // ── Types ──
17
18
 
@@ -32,8 +33,8 @@ export interface RelationMatch {
32
33
  // Bootstrap the graph with known entities. The extraction loop will discover new ones.
33
34
 
34
35
  const KNOWN_ENTITIES: EntityMatch[] = [
35
- { name: "Gui", type: "person" },
36
- { name: "Daedalus", type: "person" },
36
+ { name: HUMAN_NAME, type: "person" },
37
+ { name: AGENT_NAME, type: "person" },
37
38
  { name: "Arcane", type: "project", aliases: ["Arcane Rapture", "arcane-rapture"] },
38
39
  { name: "Mission Control", type: "project", aliases: ["MC", "mission-control"] },
39
40
  { name: "OpenClaw", type: "project", aliases: ["openclaw"] },
@@ -11,6 +11,8 @@ interface NatsGlobals {
11
11
  __nats_nc?: NatsConnection | null;
12
12
  __nats_connectingSince?: number | null;
13
13
  __nats_healthKv?: KV | null;
14
+ __nats_collabKv?: KV | null;
15
+ __nats_tasksKv?: KV | null;
14
16
  }
15
17
 
16
18
  const g = globalThis as unknown as NatsGlobals;
@@ -45,6 +47,7 @@ const NATS_URL = resolveNatsUrl();
45
47
 
46
48
  // KV bucket name — nodes write here, MC reads
47
49
  const HEALTH_BUCKET = "MESH_NODE_HEALTH";
50
+ const TASKS_BUCKET = "MESH_TASKS";
48
51
 
49
52
  /**
50
53
  * Get or create a singleton NATS connection.
@@ -73,16 +76,22 @@ export async function getNats(): Promise<NatsConnection | null> {
73
76
  });
74
77
  console.log("[nats] connected to", NATS_URL);
75
78
 
76
- // Reset KV handle on reconnect so it's re-fetched fresh
79
+ // Reset KV handles on reconnect so they're re-fetched fresh
77
80
  g.__nats_healthKv = null;
81
+ g.__nats_collabKv = null;
82
+ g.__nats_tasksKv = null;
78
83
 
79
84
  g.__nats_nc.closed().then(() => {
80
85
  console.log("[nats] connection closed — will reconnect on next request");
81
86
  g.__nats_nc = null;
82
87
  g.__nats_healthKv = null;
88
+ g.__nats_collabKv = null;
89
+ g.__nats_tasksKv = null;
83
90
  }).catch(() => {
84
91
  g.__nats_nc = null;
85
92
  g.__nats_healthKv = null;
93
+ g.__nats_collabKv = null;
94
+ g.__nats_tasksKv = null;
86
95
  });
87
96
 
88
97
  return g.__nats_nc;
@@ -123,4 +132,60 @@ export async function getHealthKv(): Promise<KV | null> {
123
132
  }
124
133
  }
125
134
 
135
+ // KV bucket for collab sessions — daemon writes, MC reads
136
+ const COLLAB_BUCKET = "MESH_COLLAB";
137
+
138
+ /**
139
+ * Get the MESH_COLLAB KV bucket (JetStream).
140
+ *
141
+ * Collab sessions are written by mesh-collab-daemon, MC reads only.
142
+ * No TTL — sessions are managed by daemon lifecycle, not expiry.
143
+ * Returns null if NATS is unavailable.
144
+ */
145
+ export async function getCollabKv(): Promise<KV | null> {
146
+ if (g.__nats_collabKv) return g.__nats_collabKv;
147
+
148
+ const conn = await getNats();
149
+ if (!conn) return null;
150
+
151
+ try {
152
+ const js = conn.jetstream();
153
+ g.__nats_collabKv = await js.views.kv(COLLAB_BUCKET, {
154
+ history: 1,
155
+ });
156
+ return g.__nats_collabKv;
157
+ } catch (err) {
158
+ console.error("[nats] Collab KV bucket error:", (err as Error).message);
159
+ return null;
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Get the MESH_TASKS KV bucket (JetStream).
165
+ *
166
+ * Tasks are coordinated through this bucket — the lead writes,
167
+ * workers read via watch. On worker nodes, this enables the
168
+ * Kanban to display mesh tasks in real-time.
169
+ *
170
+ * Creates the bucket on first call if it doesn't exist (idempotent).
171
+ * Returns null if NATS is unavailable.
172
+ */
173
+ export async function getTasksKv(): Promise<KV | null> {
174
+ if (g.__nats_tasksKv) return g.__nats_tasksKv;
175
+
176
+ const conn = await getNats();
177
+ if (!conn) return null;
178
+
179
+ try {
180
+ const js = conn.jetstream();
181
+ g.__nats_tasksKv = await js.views.kv(TASKS_BUCKET, {
182
+ history: 1,
183
+ });
184
+ return g.__nats_tasksKv;
185
+ } catch (err) {
186
+ console.error("[nats] Tasks KV bucket error:", (err as Error).message);
187
+ return null;
188
+ }
189
+ }
190
+
126
191
  export { sc, StringCodec };
@@ -36,6 +36,11 @@ export interface ParsedTask {
36
36
  metric: string | null;
37
37
  budgetMinutes: number;
38
38
  scope: string[];
39
+ // Collab routing fields
40
+ collaboration: Record<string, unknown> | null;
41
+ preferredNodes: string[];
42
+ excludeNodes: string[];
43
+ clusterId: string | null;
39
44
  }
40
45
 
41
46
  /* ------------------------------------------------------------------ */
@@ -51,6 +56,7 @@ const STATUS_TO_KANBAN: Record<string, string> = {
51
56
  "waiting-user": "review",
52
57
  done: "done",
53
58
  cancelled: "done",
59
+ archived: "done", // hidden from view but still in done column if shown
54
60
  };
55
61
 
56
62
  const KANBAN_TO_STATUS: Record<string, string> = {
@@ -90,7 +96,7 @@ export function parseTasksMarkdown(content: string): ParsedTask[] {
90
96
  const lines = liveSection.split("\n");
91
97
 
92
98
  let current: Partial<ParsedTask> | null = null;
93
- let currentArrayKey: "successCriteria" | "artifacts" | "scope" | null = null;
99
+ let currentArrayKey: "successCriteria" | "artifacts" | "scope" | "preferredNodes" | "excludeNodes" | null = null;
94
100
 
95
101
  function flush() {
96
102
  if (current && current.id) {
@@ -125,6 +131,10 @@ export function parseTasksMarkdown(content: string): ParsedTask[] {
125
131
  metric: current.metric ?? null,
126
132
  budgetMinutes: current.budgetMinutes ?? 30,
127
133
  scope: current.scope ?? [],
134
+ collaboration: current.collaboration ?? null,
135
+ preferredNodes: current.preferredNodes ?? [],
136
+ excludeNodes: current.excludeNodes ?? [],
137
+ clusterId: current.clusterId ?? null,
128
138
  });
129
139
  }
130
140
  }
@@ -134,7 +144,7 @@ export function parseTasksMarkdown(content: string): ParsedTask[] {
134
144
  const taskIdMatch = line.match(/^- task_id:\s*(.+)$/);
135
145
  if (taskIdMatch) {
136
146
  flush();
137
- current = { id: taskIdMatch[1].trim(), successCriteria: [], artifacts: [], scope: [] };
147
+ current = { id: taskIdMatch[1].trim(), successCriteria: [], artifacts: [], scope: [], preferredNodes: [], excludeNodes: [] };
138
148
  currentArrayKey = null;
139
149
  continue;
140
150
  }
@@ -285,6 +295,27 @@ export function parseTasksMarkdown(content: string): ParsedTask[] {
285
295
  current.scope = [];
286
296
  currentArrayKey = "scope";
287
297
  break;
298
+ // Collab routing fields
299
+ case "collaboration":
300
+ try {
301
+ current.collaboration = value ? JSON.parse(value) : null;
302
+ } catch {
303
+ current.collaboration = null;
304
+ }
305
+ currentArrayKey = null;
306
+ break;
307
+ case "preferred_nodes":
308
+ current.preferredNodes = [];
309
+ currentArrayKey = "preferredNodes";
310
+ break;
311
+ case "exclude_nodes":
312
+ current.excludeNodes = [];
313
+ currentArrayKey = "excludeNodes";
314
+ break;
315
+ case "cluster_id":
316
+ current.clusterId = value || null;
317
+ currentArrayKey = null;
318
+ break;
288
319
  case "updated_at":
289
320
  current.updatedAt = value;
290
321
  currentArrayKey = null;
@@ -450,6 +481,25 @@ export function serializeTasksMarkdown(tasks: ParsedTask[]): string {
450
481
  lines.push(` - ${s}`);
451
482
  }
452
483
  }
484
+ // Collab routing fields
485
+ if (t.collaboration) {
486
+ lines.push(` collaboration: ${JSON.stringify(t.collaboration)}`);
487
+ }
488
+ if (t.preferredNodes && t.preferredNodes.length > 0) {
489
+ lines.push(" preferred_nodes:");
490
+ for (const n of t.preferredNodes) {
491
+ lines.push(` - ${n}`);
492
+ }
493
+ }
494
+ if (t.excludeNodes && t.excludeNodes.length > 0) {
495
+ lines.push(" exclude_nodes:");
496
+ for (const n of t.excludeNodes) {
497
+ lines.push(` - ${n}`);
498
+ }
499
+ }
500
+ if (t.clusterId) {
501
+ lines.push(` cluster_id: ${t.clusterId}`);
502
+ }
453
503
  lines.push(` updated_at: ${t.updatedAt}`);
454
504
 
455
505
  return lines.join("\n");
@@ -15,10 +15,10 @@ export interface TranscriptEvent {
15
15
  }
16
16
 
17
17
  /**
18
- * Encode a workspace path to Claude's project directory format.
18
+ * Encode a workspace path to the agent CLI's project directory format.
19
19
  * Same algorithm as agent-activity.js: strip leading /, replace / and . with -
20
20
  */
21
- function toClaudeProjectPath(workspacePath: string): string {
21
+ function toAgentProjectPath(workspacePath: string): string {
22
22
  return workspacePath.replace(/\\/g, "/").replace(/:/g, "").replace(/[/.]/g, "-");
23
23
  }
24
24
 
@@ -29,8 +29,8 @@ const WORKSPACE = process.env.WORKSPACE_ROOT || path.join(HOME, ".openclaw/works
29
29
  * Transcript source directories — scans ALL frontend transcript stores.
30
30
  */
31
31
  const TRANSCRIPT_DIRS = [
32
- // Claude Code workspace sessions (cross-platform path encoding)
33
- path.join(HOME, ".claude/projects", toClaudeProjectPath(WORKSPACE)),
32
+ // Agent CLI workspace sessions (cross-platform path encoding)
33
+ path.join(HOME, ".claude/projects", toAgentProjectPath(WORKSPACE)),
34
34
  // OpenClaw Gateway sessions (Discord, Telegram, etc.)
35
35
  path.join(HOME, ".openclaw/agents/main/sessions"),
36
36
  ];
@@ -1,4 +1,4 @@
1
- import { eq, and, ne, or, lte, like, desc } from "drizzle-orm";
1
+ import { eq, and, ne, or, lte, like, desc, isNull } from "drizzle-orm";
2
2
  import { CronExpressionParser } from "cron-parser";
3
3
  import { writeFileSync, mkdirSync } from "fs";
4
4
  import path from "path";
@@ -7,7 +7,7 @@ import { tasks, dependencies } from "./db/schema";
7
7
  import { statusToKanban } from "./parsers/task-markdown";
8
8
  import { syncTasksToMarkdown } from "./sync/tasks";
9
9
  import { logActivity } from "./activity";
10
- import { WORKSPACE_ROOT } from "./config";
10
+ import { WORKSPACE_ROOT, AGENT_NAME, DISPATCH_SIGNAL_FILE } from "./config";
11
11
  import { gatewayNotify } from "./gateway-notify";
12
12
 
13
13
 
@@ -238,23 +238,23 @@ export function schedulerTick(): TickResult {
238
238
  }
239
239
 
240
240
  // --- Phase 2: Single-task dispatch ---
241
- // Rule: ONE auto-start task at a time. Daedalus owns it and works autonomously.
241
+ // Rule: ONE auto-start task at a time. The agent owns it and works autonomously.
242
242
  // Next task only dispatched when current is done, blocked, or waiting-user.
243
243
 
244
- // Check if Daedalus already has an active auto-dispatched task
245
- const daedalusRunning = db
244
+ // Check if the agent already has an active auto-dispatched task
245
+ const agentRunning = db
246
246
  .select()
247
247
  .from(tasks)
248
248
  .where(
249
249
  and(
250
250
  eq(tasks.status, "running"),
251
- eq(tasks.owner, "Daedalus"),
251
+ eq(tasks.owner, AGENT_NAME),
252
252
  ne(tasks.id, "__LIVE_SESSION__")
253
253
  )
254
254
  )
255
255
  .all();
256
256
 
257
- const hasActiveTask = daedalusRunning.length > 0;
257
+ const hasActiveTask = agentRunning.length > 0;
258
258
 
259
259
  // Dispatchable: needs_approval=0 AND (status="ready" OR (status="queued" AND trigger_kind="none"))
260
260
  const dispatchable = db
@@ -263,6 +263,7 @@ export function schedulerTick(): TickResult {
263
263
  .where(
264
264
  and(
265
265
  eq(tasks.needsApproval, 0),
266
+ or(isNull(tasks.execution), ne(tasks.execution, "mesh")),
266
267
  or(
267
268
  eq(tasks.status, "ready"),
268
269
  and(eq(tasks.status, "queued"), eq(tasks.triggerKind, "none"))
@@ -308,7 +309,7 @@ export function schedulerTick(): TickResult {
308
309
  .set({
309
310
  status: "running",
310
311
  kanbanColumn: statusToKanban("running"),
311
- owner: "Daedalus",
312
+ owner: AGENT_NAME,
312
313
  updatedAt: now.toISOString(),
313
314
  })
314
315
  .where(eq(tasks.id, next.id))
@@ -322,7 +323,7 @@ export function schedulerTick(): TickResult {
322
323
  );
323
324
 
324
325
  // Notify via gateway message (TUI chat)
325
- const notifyText = `MC-Kanban: NEW AUTOTASK FOR DAEDALUS READY: "${next.title}"`;
326
+ const notifyText = `MC-Kanban: NEW AUTOTASK FOR ${AGENT_NAME.toUpperCase()} READY: "${next.title}"`;
326
327
  gatewayNotify(notifyText).catch((err) => {
327
328
  console.error("MC gateway notify failed:", err);
328
329
  });
@@ -332,7 +333,7 @@ export function schedulerTick(): TickResult {
332
333
  const signalDir = path.join(WORKSPACE_ROOT, ".tmp");
333
334
  mkdirSync(signalDir, { recursive: true });
334
335
  writeFileSync(
335
- path.join(signalDir, "daedalus-dispatch.json"),
336
+ path.join(signalDir, DISPATCH_SIGNAL_FILE),
336
337
  JSON.stringify({
337
338
  taskId: next.id,
338
339
  title: next.title,
@@ -350,7 +351,7 @@ export function schedulerTick(): TickResult {
350
351
  result.skipped.push(t.id);
351
352
  }
352
353
  } else {
353
- // All eligible tasks stay in backlog (Daedalus busy or nothing to dispatch)
354
+ // All eligible tasks stay in backlog (agent busy or nothing to dispatch)
354
355
  for (const t of eligible) {
355
356
  result.skipped.push(t.id);
356
357
  }