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,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,70 @@ 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
|
+
/** Validates that a URL path parameter is safe for use in file paths. */
|
|
69
|
+
export function validatePathParam(param: string): string {
|
|
70
|
+
const cleaned = param.trim();
|
|
71
|
+
if (!cleaned || !/^[\w][\w.-]{0,127}$/.test(cleaned)) {
|
|
72
|
+
throw new Error(`Invalid path parameter: "${param.slice(0, 40)}"`);
|
|
73
|
+
}
|
|
74
|
+
return cleaned;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── Node Identity (Distributed MC) ──
|
|
78
|
+
import { hostname } from "os";
|
|
79
|
+
|
|
80
|
+
/** This node's unique identifier in the mesh */
|
|
81
|
+
export const NODE_ID = process.env.OPENCLAW_NODE_ID || hostname();
|
|
82
|
+
|
|
83
|
+
/** This node's role: "lead" (full authority) or "worker" (read + propose) */
|
|
84
|
+
export const NODE_ROLE: "lead" | "worker" =
|
|
85
|
+
(process.env.OPENCLAW_NODE_ROLE as "lead" | "worker") || "lead";
|
|
86
|
+
|
|
87
|
+
/** Platform identifier for display */
|
|
88
|
+
export const NODE_PLATFORM = process.platform === "darwin" ? "macOS" : "Linux";
|
|
@@ -2,7 +2,7 @@ import Database from "better-sqlite3";
|
|
|
2
2
|
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
3
3
|
import * as schema from "./schema";
|
|
4
4
|
import { DB_PATH } from "../config";
|
|
5
|
-
import fs from "fs";
|
|
5
|
+
import fs, { chmodSync, existsSync } from "fs";
|
|
6
6
|
import path from "path";
|
|
7
7
|
|
|
8
8
|
let _db: ReturnType<typeof drizzle<typeof schema>> | null = null;
|
|
@@ -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() {
|
|
@@ -419,6 +488,21 @@ export function getDb() {
|
|
|
419
488
|
|
|
420
489
|
runMigrations(_sqlite);
|
|
421
490
|
|
|
491
|
+
// Lock down DB file permissions — owner read/write only
|
|
492
|
+
try {
|
|
493
|
+
if (existsSync(DB_PATH)) {
|
|
494
|
+
chmodSync(DB_PATH, 0o600);
|
|
495
|
+
}
|
|
496
|
+
const walPath = DB_PATH + "-wal";
|
|
497
|
+
if (existsSync(walPath)) {
|
|
498
|
+
chmodSync(walPath, 0o600);
|
|
499
|
+
}
|
|
500
|
+
const journalPath = DB_PATH + "-journal";
|
|
501
|
+
if (existsSync(journalPath)) {
|
|
502
|
+
chmodSync(journalPath, 0o600);
|
|
503
|
+
}
|
|
504
|
+
} catch {}
|
|
505
|
+
|
|
422
506
|
_db = drizzle(_sqlite, { schema });
|
|
423
507
|
return _db;
|
|
424
508
|
}
|
|
@@ -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=
|
|
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;
|
|
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
|
|
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;
|