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
@@ -0,0 +1,188 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { parseTasksMarkdown, serializeTasksMarkdown } from "../parsers/task-markdown";
3
+
4
+ describe("parseTasksMarkdown", () => {
5
+ it("parses a minimal task", () => {
6
+ const md = `# Active Tasks
7
+
8
+ Updated: 2026-03-21
9
+
10
+ ## Live Tasks
11
+
12
+ - task_id: T-20260321-001
13
+ title: Test task
14
+ status: queued
15
+ owner: main
16
+ success_criteria:
17
+ artifacts:
18
+ next_action: do something
19
+ updated_at: 2026-03-21T12:00:00Z
20
+ `;
21
+ const tasks = parseTasksMarkdown(md);
22
+ expect(tasks).toHaveLength(1);
23
+ expect(tasks[0].id).toBe("T-20260321-001");
24
+ expect(tasks[0].title).toBe("Test task");
25
+ expect(tasks[0].status).toBe("queued");
26
+ expect(tasks[0].owner).toBe("main");
27
+ expect(tasks[0].nextAction).toBe("do something");
28
+ });
29
+
30
+ it("parses mesh execution fields", () => {
31
+ const md = `## Live Tasks
32
+
33
+ - task_id: T-20260321-002
34
+ title: Mesh task
35
+ status: running
36
+ owner: daedalus
37
+ success_criteria:
38
+ artifacts:
39
+ next_action: n/a
40
+ execution: mesh
41
+ mesh_task_id: MESH-TEST-001
42
+ mesh_node: calos-ubuntu
43
+ metric: tests pass
44
+ budget_minutes: 60
45
+ scope:
46
+ - src/lib/
47
+ - src/app/
48
+ updated_at: 2026-03-21T12:00:00Z
49
+ `;
50
+ const tasks = parseTasksMarkdown(md);
51
+ expect(tasks).toHaveLength(1);
52
+ expect(tasks[0].execution).toBe("mesh");
53
+ expect(tasks[0].meshTaskId).toBe("MESH-TEST-001");
54
+ expect(tasks[0].meshNode).toBe("calos-ubuntu");
55
+ expect(tasks[0].metric).toBe("tests pass");
56
+ expect(tasks[0].budgetMinutes).toBe(60);
57
+ expect(tasks[0].scope).toEqual(["src/lib/", "src/app/"]);
58
+ });
59
+
60
+ it("parses collaboration fields", () => {
61
+ const md = `## Live Tasks
62
+
63
+ - task_id: T-20260321-003
64
+ title: Collab task
65
+ status: queued
66
+ owner: main
67
+ success_criteria:
68
+ artifacts:
69
+ next_action: n/a
70
+ execution: mesh
71
+ collaboration: {"mode":"parallel","min_nodes":2,"max_nodes":3}
72
+ preferred_nodes:
73
+ - node-a
74
+ - node-b
75
+ cluster_id: security-team
76
+ updated_at: 2026-03-21T12:00:00Z
77
+ `;
78
+ const tasks = parseTasksMarkdown(md);
79
+ expect(tasks).toHaveLength(1);
80
+ expect(tasks[0].collaboration).toEqual({ mode: "parallel", min_nodes: 2, max_nodes: 3 });
81
+ expect(tasks[0].preferredNodes).toEqual(["node-a", "node-b"]);
82
+ expect(tasks[0].clusterId).toBe("security-team");
83
+ });
84
+
85
+ it("parses scheduling fields", () => {
86
+ const md = `## Live Tasks
87
+
88
+ - task_id: T-20260321-004
89
+ title: Scheduled task
90
+ status: queued
91
+ owner: main
92
+ success_criteria:
93
+ artifacts:
94
+ next_action: n/a
95
+ needs_approval: false
96
+ trigger_kind: cron
97
+ trigger_cron: 0 10 * * 1
98
+ trigger_tz: America/New_York
99
+ is_recurring: true
100
+ capacity_class: heavy
101
+ auto_priority: 5
102
+ updated_at: 2026-03-21T12:00:00Z
103
+ `;
104
+ const tasks = parseTasksMarkdown(md);
105
+ expect(tasks).toHaveLength(1);
106
+ expect(tasks[0].needsApproval).toBe(false);
107
+ expect(tasks[0].triggerKind).toBe("cron");
108
+ expect(tasks[0].triggerCron).toBe("0 10 * * 1");
109
+ expect(tasks[0].triggerTz).toBe("America/New_York");
110
+ expect(tasks[0].isRecurring).toBe(true);
111
+ expect(tasks[0].capacityClass).toBe("heavy");
112
+ expect(tasks[0].autoPriority).toBe(5);
113
+ });
114
+
115
+ it("returns empty for markdown without Live Tasks section", () => {
116
+ const md = `# Active Tasks\n\nNo tasks here.`;
117
+ expect(parseTasksMarkdown(md)).toEqual([]);
118
+ });
119
+ });
120
+
121
+ describe("serializeTasksMarkdown round-trip", () => {
122
+ it("round-trips a minimal task", () => {
123
+ const md = `# Active Tasks
124
+
125
+ Updated: 2026-03-21
126
+
127
+ ## Live Tasks
128
+
129
+ - task_id: T-20260321-001
130
+ title: Test task
131
+ status: queued
132
+ owner: main
133
+ success_criteria:
134
+ artifacts:
135
+ next_action: do something
136
+ updated_at: 2026-03-21T12:00:00Z
137
+ `;
138
+ const parsed = parseTasksMarkdown(md);
139
+ const serialized = serializeTasksMarkdown(parsed);
140
+ const reparsed = parseTasksMarkdown(serialized);
141
+
142
+ expect(reparsed).toHaveLength(1);
143
+ expect(reparsed[0].id).toBe(parsed[0].id);
144
+ expect(reparsed[0].title).toBe(parsed[0].title);
145
+ expect(reparsed[0].status).toBe(parsed[0].status);
146
+ expect(reparsed[0].nextAction).toBe(parsed[0].nextAction);
147
+ });
148
+
149
+ it("round-trips mesh + collab fields", () => {
150
+ const md = `## Live Tasks
151
+
152
+ - task_id: MESH-001
153
+ title: Full mesh task
154
+ status: running
155
+ owner: daedalus
156
+ success_criteria:
157
+ - tests pass
158
+ - no regressions
159
+ artifacts:
160
+ - /path/to/output.json
161
+ next_action: wait for results
162
+ execution: mesh
163
+ mesh_task_id: NATS-001
164
+ mesh_node: node-alpha
165
+ metric: all green
166
+ budget_minutes: 45
167
+ scope:
168
+ - src/
169
+ collaboration: {"mode":"review","min_nodes":2}
170
+ preferred_nodes:
171
+ - node-alpha
172
+ - node-beta
173
+ cluster_id: dev-team
174
+ updated_at: 2026-03-21T15:00:00Z
175
+ `;
176
+ const parsed = parseTasksMarkdown(md);
177
+ const serialized = serializeTasksMarkdown(parsed);
178
+ const reparsed = parseTasksMarkdown(serialized);
179
+
180
+ expect(reparsed[0].execution).toBe("mesh");
181
+ expect(reparsed[0].meshTaskId).toBe("NATS-001");
182
+ expect(reparsed[0].scope).toEqual(["src/"]);
183
+ expect(reparsed[0].preferredNodes).toEqual(["node-alpha", "node-beta"]);
184
+ expect(reparsed[0].clusterId).toBe("dev-team");
185
+ expect(reparsed[0].successCriteria).toEqual(["tests pass", "no regressions"]);
186
+ expect(reparsed[0].artifacts).toEqual(["/path/to/output.json"]);
187
+ });
188
+ });
@@ -0,0 +1,175 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ extractWikilinks,
4
+ extractTaskIds,
5
+ extractFileRefs,
6
+ buildResolutionMaps,
7
+ resolveWikilink,
8
+ extractAllReferences,
9
+ } from "../memory/wikilinks";
10
+
11
+ describe("extractWikilinks", () => {
12
+ it("extracts basic wikilinks", () => {
13
+ const result = extractWikilinks("See [[Project Alpha]] and [[Task Beta]]");
14
+ expect(result).toEqual(["Project Alpha", "Task Beta"]);
15
+ });
16
+
17
+ it("handles aliased wikilinks [[target|display]]", () => {
18
+ const result = extractWikilinks("Check [[real-target|Display Name]]");
19
+ expect(result).toEqual(["real-target"]);
20
+ });
21
+
22
+ it("deduplicates wikilinks", () => {
23
+ const result = extractWikilinks("[[Foo]] and [[Foo]] again");
24
+ expect(result).toEqual(["Foo"]);
25
+ });
26
+
27
+ it("ignores wikilinks inside code blocks", () => {
28
+ const result = extractWikilinks("```\n[[Inside Code]]\n```\nOutside [[Real]]");
29
+ expect(result).toEqual(["Real"]);
30
+ });
31
+
32
+ it("ignores wikilinks inside inline code", () => {
33
+ const result = extractWikilinks("Use `[[NotALink]]` but [[ActualLink]]");
34
+ expect(result).toEqual(["ActualLink"]);
35
+ });
36
+
37
+ it("returns empty for no wikilinks", () => {
38
+ expect(extractWikilinks("No links here")).toEqual([]);
39
+ });
40
+
41
+ it("skips empty targets", () => {
42
+ expect(extractWikilinks("[[]]")).toEqual([]);
43
+ });
44
+
45
+ it("trims whitespace from targets", () => {
46
+ const result = extractWikilinks("[[ Padded ]]");
47
+ expect(result).toEqual(["Padded"]);
48
+ });
49
+ });
50
+
51
+ describe("extractTaskIds", () => {
52
+ it("extracts T-YYYYMMDD-NNN format", () => {
53
+ const result = extractTaskIds("Working on T-20260305-001 and T-20260310-042");
54
+ expect(result).toEqual(["T-20260305-001", "T-20260310-042"]);
55
+ });
56
+
57
+ it("extracts ARCANE-style IDs", () => {
58
+ const result = extractTaskIds("ARCANE-M01 and ARCANE-B02-3");
59
+ expect(result).toEqual(["ARCANE-M01", "ARCANE-B02-3"]);
60
+ });
61
+
62
+ it("deduplicates task IDs", () => {
63
+ const result = extractTaskIds("T-20260305-001 mentioned twice: T-20260305-001");
64
+ expect(result).toEqual(["T-20260305-001"]);
65
+ });
66
+
67
+ it("ignores IDs in code blocks", () => {
68
+ const result = extractTaskIds("```\nT-20260101-999\n```");
69
+ expect(result).toEqual([]);
70
+ });
71
+ });
72
+
73
+ describe("extractFileRefs", () => {
74
+ it("extracts markdown file references", () => {
75
+ const result = extractFileRefs("See docs/README.md for details");
76
+ expect(result).toContain("docs/README.md");
77
+ });
78
+
79
+ it("extracts various extensions", () => {
80
+ const result = extractFileRefs("Edit src/app.ts and config.json");
81
+ expect(result).toContain("src/app.ts");
82
+ expect(result).toContain("config.json");
83
+ });
84
+
85
+ it("skips http URLs", () => {
86
+ const result = extractFileRefs("Visit https://example.com/page");
87
+ // http refs are filtered out
88
+ const httpRefs = result.filter((r) => r.includes("http"));
89
+ expect(httpRefs).toEqual([]);
90
+ });
91
+
92
+ it("includes short but valid file refs", () => {
93
+ // "a.md" is 4 chars, passes the >= 4 length check
94
+ const result = extractFileRefs("see a.md for info");
95
+ expect(result).toContain("a.md");
96
+ });
97
+ });
98
+
99
+ describe("buildResolutionMaps", () => {
100
+ it("builds path, basename, and title maps", () => {
101
+ const docs = [
102
+ { filePath: "projects/alpha/README.md", title: "Alpha Project" },
103
+ { filePath: "memory/notes.md", title: "Session Notes" },
104
+ ];
105
+ const maps = buildResolutionMaps(docs);
106
+
107
+ expect(maps.byPath.has("projects/alpha/README.md")).toBe(true);
108
+ expect(maps.byBasename.has("readme")).toBe(true);
109
+ expect(maps.byTitle.has("alpha project")).toBe(true);
110
+ });
111
+
112
+ it("first doc wins for basename conflicts", () => {
113
+ const docs = [
114
+ { filePath: "a/notes.md", title: null },
115
+ { filePath: "b/notes.md", title: null },
116
+ ];
117
+ const maps = buildResolutionMaps(docs);
118
+ expect(maps.byBasename.get("notes")).toBe("a/notes.md");
119
+ });
120
+ });
121
+
122
+ describe("resolveWikilink", () => {
123
+ const maps = buildResolutionMaps([
124
+ { filePath: "projects/alpha.md", title: "Alpha Project" },
125
+ { filePath: "memory/2026-03-01.md", title: "March 1 Log" },
126
+ ]);
127
+
128
+ it("resolves exact path", () => {
129
+ expect(resolveWikilink("projects/alpha.md", maps)).toBe("projects/alpha.md");
130
+ });
131
+
132
+ it("resolves path without .md extension", () => {
133
+ expect(resolveWikilink("projects/alpha", maps)).toBe("projects/alpha.md");
134
+ });
135
+
136
+ it("resolves by basename (case-insensitive)", () => {
137
+ expect(resolveWikilink("Alpha", maps)).toBe("projects/alpha.md");
138
+ });
139
+
140
+ it("resolves by title (case-insensitive)", () => {
141
+ expect(resolveWikilink("alpha project", maps)).toBe("projects/alpha.md");
142
+ });
143
+
144
+ it("returns null for unresolvable target", () => {
145
+ expect(resolveWikilink("nonexistent", maps)).toBeNull();
146
+ });
147
+ });
148
+
149
+ describe("extractAllReferences", () => {
150
+ const maps = buildResolutionMaps([
151
+ { filePath: "projects/alpha.md", title: "Alpha Project" },
152
+ { filePath: "tasks/T-20260305-001.md", title: "T-20260305-001" },
153
+ { filePath: "self.md", title: "Self" },
154
+ ]);
155
+
156
+ it("combines wikilinks, task IDs, and file refs", () => {
157
+ const content = "See [[Alpha Project]] and T-20260305-001 in projects/alpha.md";
158
+ const refs = extractAllReferences(content, maps, "self.md");
159
+ expect(refs).toContain("projects/alpha.md");
160
+ expect(refs).toContain("tasks/T-20260305-001.md");
161
+ });
162
+
163
+ it("excludes self-references", () => {
164
+ const content = "[[Self]] is this document";
165
+ const refs = extractAllReferences(content, maps, "self.md");
166
+ expect(refs).not.toContain("self.md");
167
+ });
168
+
169
+ it("deduplicates across sources", () => {
170
+ const content = "[[Alpha Project]] and projects/alpha.md";
171
+ const refs = extractAllReferences(content, maps, "self.md");
172
+ const alphaCount = refs.filter((r) => r === "projects/alpha.md").length;
173
+ expect(alphaCount).toBe(1);
174
+ });
175
+ });
@@ -19,3 +19,61 @@ export const LORE_DIRS = [
19
19
  path.join(WORKSPACE_ROOT, "projects", "arcane", "lore", "canon"),
20
20
  path.join(WORKSPACE_ROOT, "projects", "arcane", "lore", "drafts"),
21
21
  ];
22
+
23
+ // ── LLM-Agnostic Identity & Model Configuration ──
24
+ // Override via env vars to use any agent name, human name, or LLM provider.
25
+
26
+ /** Name of the autonomous agent (shown in kanban, scheduler, notifications) */
27
+ export const AGENT_NAME = process.env.OPENCLAW_AGENT_NAME || "Daedalus";
28
+
29
+ /** Name of the human operator (shown in done-gate, manual task ownership) */
30
+ export const HUMAN_NAME = process.env.OPENCLAW_HUMAN_NAME || "Gui";
31
+
32
+ /** Dispatch signal filename (agent picks up auto-dispatched tasks via this file) */
33
+ export const DISPATCH_SIGNAL_FILE = `${AGENT_NAME.toLowerCase()}-dispatch.json`;
34
+
35
+ // ── Model Capability Tiers ──
36
+ // Maps provider-agnostic capability levels to concrete model names.
37
+ // Override LLM_PROVIDER env var to switch between providers.
38
+
39
+ export type CapabilityTier = "fast" | "standard" | "reasoning";
40
+
41
+ export const LLM_PROVIDER = process.env.LLM_PROVIDER || "anthropic";
42
+
43
+ const MODEL_MAP: Record<string, Record<CapabilityTier, string>> = {
44
+ anthropic: { fast: "haiku", standard: "sonnet", reasoning: "opus" },
45
+ openai: { fast: "gpt-4o-mini", standard: "gpt-4o", reasoning: "o1" },
46
+ google: { fast: "gemini-2.0-flash", standard: "gemini-2.5-pro", reasoning: "gemini-2.5-pro" },
47
+ local: { fast: "llama-3.2-3b", standard: "llama-3.3-70b", reasoning: "deepseek-r1" },
48
+ };
49
+
50
+ /**
51
+ * Resolve a capability tier to a concrete model name for the active provider.
52
+ * Falls back to the tier name itself if provider is unknown.
53
+ */
54
+ export function getModel(tier: CapabilityTier): string {
55
+ return MODEL_MAP[LLM_PROVIDER]?.[tier] ?? tier;
56
+ }
57
+
58
+ /**
59
+ * Get the full model map for a provider (for UI display).
60
+ */
61
+ export function getProviderModels(provider?: string): Record<CapabilityTier, string> | null {
62
+ return MODEL_MAP[provider || LLM_PROVIDER] ?? null;
63
+ }
64
+
65
+ /** All registered provider names */
66
+ export const AVAILABLE_PROVIDERS = Object.keys(MODEL_MAP);
67
+
68
+ // ── Node Identity (Distributed MC) ──
69
+ import { hostname } from "os";
70
+
71
+ /** This node's unique identifier in the mesh */
72
+ export const NODE_ID = process.env.OPENCLAW_NODE_ID || hostname();
73
+
74
+ /** This node's role: "lead" (full authority) or "worker" (read + propose) */
75
+ export const NODE_ROLE: "lead" | "worker" =
76
+ (process.env.OPENCLAW_NODE_ROLE as "lead" | "worker") || "lead";
77
+
78
+ /** Platform identifier for display */
79
+ export const NODE_PLATFORM = process.platform === "darwin" ? "macOS" : "Linux";
@@ -406,6 +406,75 @@ function runMigrations(sqlite: Database.Database) {
406
406
  CREATE INDEX IF NOT EXISTS idx_token_usage_node ON token_usage(node_id);
407
407
  CREATE INDEX IF NOT EXISTS idx_token_usage_ts ON token_usage(timestamp);
408
408
  `);
409
+
410
+ // --- Cowork: Clusters ---
411
+ sqlite.exec(`
412
+ CREATE TABLE IF NOT EXISTS clusters (
413
+ id TEXT PRIMARY KEY,
414
+ name TEXT NOT NULL,
415
+ description TEXT,
416
+ color TEXT,
417
+ default_mode TEXT DEFAULT 'parallel',
418
+ default_convergence TEXT DEFAULT 'unanimous',
419
+ convergence_threshold INTEGER DEFAULT 66,
420
+ max_rounds INTEGER DEFAULT 5,
421
+ status TEXT DEFAULT 'active',
422
+ updated_at TEXT NOT NULL,
423
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
424
+ );
425
+
426
+ CREATE TABLE IF NOT EXISTS cluster_members (
427
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
428
+ cluster_id TEXT NOT NULL,
429
+ node_id TEXT NOT NULL,
430
+ role TEXT NOT NULL DEFAULT 'worker',
431
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
432
+ UNIQUE(cluster_id, node_id)
433
+ );
434
+
435
+ CREATE INDEX IF NOT EXISTS idx_cluster_members_cluster ON cluster_members(cluster_id);
436
+ CREATE INDEX IF NOT EXISTS idx_cluster_members_node ON cluster_members(node_id);
437
+ `);
438
+
439
+ // Migrate cluster_members: add created_at if missing (old table had joined_at)
440
+ const cmCols = sqlite.prepare("PRAGMA table_info(cluster_members)").all() as Array<{ name: string }>;
441
+ const cmColNames = cmCols.map((c) => c.name);
442
+ if (!cmColNames.includes("created_at")) {
443
+ sqlite.exec("ALTER TABLE cluster_members ADD COLUMN created_at TEXT NOT NULL DEFAULT (datetime('now'))");
444
+ if (cmColNames.includes("joined_at")) {
445
+ sqlite.exec("UPDATE cluster_members SET created_at = joined_at WHERE joined_at IS NOT NULL");
446
+ }
447
+ }
448
+
449
+ // Normalize: set execution='local' for pre-existing rows where it's NULL
450
+ sqlite.exec("UPDATE tasks SET execution = 'local' WHERE execution IS NULL");
451
+
452
+ // Add cluster_id to tasks if missing
453
+ if (!colNames.includes("cluster_id")) {
454
+ sqlite.exec("ALTER TABLE tasks ADD COLUMN cluster_id TEXT");
455
+ }
456
+
457
+ // --- Distributed MC: mesh sync tracking columns ---
458
+ if (!colNames.includes("mesh_revision")) {
459
+ sqlite.exec("ALTER TABLE tasks ADD COLUMN mesh_revision INTEGER");
460
+ }
461
+ if (!colNames.includes("mesh_synced_at")) {
462
+ sqlite.exec("ALTER TABLE tasks ADD COLUMN mesh_synced_at TEXT");
463
+ }
464
+ if (!colNames.includes("mesh_origin")) {
465
+ sqlite.exec("ALTER TABLE tasks ADD COLUMN mesh_origin TEXT");
466
+ }
467
+
468
+ // Collab routing columns — bridge reads these from markdown to build NATS payload
469
+ if (!colNames.includes("collaboration")) {
470
+ sqlite.exec("ALTER TABLE tasks ADD COLUMN collaboration TEXT");
471
+ }
472
+ if (!colNames.includes("preferred_nodes")) {
473
+ sqlite.exec("ALTER TABLE tasks ADD COLUMN preferred_nodes TEXT");
474
+ }
475
+ if (!colNames.includes("exclude_nodes")) {
476
+ sqlite.exec("ALTER TABLE tasks ADD COLUMN exclude_nodes TEXT");
477
+ }
409
478
  }
410
479
 
411
480
  export function getDb() {
@@ -21,7 +21,7 @@ export const tasks = sqliteTable("tasks", {
21
21
  endDate: text("end_date"), // ISO date: "2026-04-15" (range end)
22
22
  color: text("color"), // hex color for Gantt bars
23
23
  description: text("description"), // markdown body for projects/phases
24
- needsApproval: integer("needs_approval").default(1), // 0=Daedalus can start, 1=wait for Gui
24
+ needsApproval: integer("needs_approval").default(1), // 0=agent can auto-start, 1=wait for human approval
25
25
  triggerKind: text("trigger_kind").default("none"), // "none" | "at" | "cron"
26
26
  triggerAt: text("trigger_at"), // ISO datetime for one-shot trigger
27
27
  triggerCron: text("trigger_cron"), // cron expression (e.g. "0 10 * * 1")
@@ -30,14 +30,18 @@ export const tasks = sqliteTable("tasks", {
30
30
  capacityClass: text("capacity_class").default("normal"), // "light" | "normal" | "heavy"
31
31
  autoPriority: integer("auto_priority").default(0), // higher = dispatched first
32
32
  // Mesh execution fields (synced from active-tasks.md)
33
- execution: text("execution"), // "mesh" = dispatched via mesh bridge; null = manual/Daedalus
33
+ execution: text("execution"), // "mesh" = dispatched via mesh bridge; "local" = agent executes locally
34
34
  meshTaskId: text("mesh_task_id"), // NATS KV task ID (e.g. "MESH-TEST-003")
35
35
  meshNode: text("mesh_node"), // node that claimed the task (e.g. "calos-ubuntu")
36
36
  metric: text("metric"), // mechanical success check (e.g. "tests pass")
37
37
  budgetMinutes: integer("budget_minutes").default(30), // agent time budget
38
38
  scope: text("scope"), // JSON array of allowed file paths
39
+ collaboration: text("collaboration"), // JSON: { mode, min_nodes, max_nodes, convergence, ... }
40
+ preferredNodes: text("preferred_nodes"), // JSON array of node IDs for mesh routing
41
+ excludeNodes: text("exclude_nodes"), // JSON array of node IDs to avoid
42
+ clusterId: text("cluster_id"), // FK to clusters.id for collab dispatch
39
43
  showInCalendar: integer("show_in_calendar").default(0), // 1=show meta-task in calendar view
40
- acknowledgedAt: text("acknowledged_at"), // ISO datetime — when Daedalus acknowledged auto-dispatch
44
+ acknowledgedAt: text("acknowledged_at"), // ISO datetime — when the agent acknowledged auto-dispatch
41
45
  updatedAt: text("updated_at").notNull(),
42
46
  createdAt: text("created_at")
43
47
  .notNull()
@@ -216,3 +220,57 @@ export type SoulEvolution = typeof soulEvolutionLog.$inferSelect;
216
220
  export type NewSoulEvolution = typeof soulEvolutionLog.$inferInsert;
217
221
  export type SoulSpawn = typeof soulSpawns.$inferSelect;
218
222
  export type NewSoulSpawn = typeof soulSpawns.$inferInsert;
223
+
224
+ // --- Cowork: Clusters ---
225
+
226
+ export const clusters = sqliteTable("clusters", {
227
+ id: text("id").primaryKey(), // slug: "security-team"
228
+ name: text("name").notNull(),
229
+ description: text("description"),
230
+ color: text("color"), // hex: "#6366f1"
231
+ defaultMode: text("default_mode").default("parallel"), // parallel | sequential | review
232
+ defaultConvergence: text("default_convergence").default("unanimous"), // unanimous | majority | coordinator
233
+ convergenceThreshold: integer("convergence_threshold").default(66), // 0-100 (for majority)
234
+ maxRounds: integer("max_rounds").default(5),
235
+ status: text("status").default("active"), // active | archived
236
+ updatedAt: text("updated_at").notNull(),
237
+ createdAt: text("created_at")
238
+ .notNull()
239
+ .default(sql`(datetime('now'))`),
240
+ });
241
+
242
+ export const clusterMembers = sqliteTable("cluster_members", {
243
+ id: integer("id").primaryKey({ autoIncrement: true }),
244
+ clusterId: text("cluster_id").notNull(),
245
+ nodeId: text("node_id").notNull(),
246
+ role: text("role").notNull().default("worker"), // lead | implementer | reviewer | auditor | worker | custom
247
+ createdAt: text("created_at")
248
+ .notNull()
249
+ .default(sql`(datetime('now'))`),
250
+ });
251
+
252
+ export type Cluster = typeof clusters.$inferSelect;
253
+ export type NewCluster = typeof clusters.$inferInsert;
254
+ export type ClusterMember = typeof clusterMembers.$inferSelect;
255
+ export type NewClusterMember = typeof clusterMembers.$inferInsert;
256
+
257
+ // ── HyperAgent Protocol (stub — dashboard views deferred) ──────────
258
+
259
+ export const hyperagentProposals = sqliteTable("hyperagent_proposals", {
260
+ id: integer("id").primaryKey({ autoIncrement: true }),
261
+ externalId: integer("external_id"),
262
+ title: text("title").notNull(),
263
+ proposalType: text("proposal_type").notNull(),
264
+ status: text("status").notNull().default("pending"),
265
+ description: text("description"),
266
+ soulId: text("soul_id"),
267
+ nodeId: text("node_id"),
268
+ reviewedBy: text("reviewed_by"),
269
+ reviewedAt: text("reviewed_at"),
270
+ createdAt: text("created_at")
271
+ .notNull()
272
+ .default(sql`(datetime('now'))`),
273
+ });
274
+
275
+ export type HyperagentProposal = typeof hyperagentProposals.$inferSelect;
276
+ export type NewHyperagentProposal = typeof hyperagentProposals.$inferInsert;