clawspec 1.0.16 → 1.0.20

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 (40) hide show
  1. package/README.md +16 -0
  2. package/README.zh-CN.md +16 -0
  3. package/package.json +2 -3
  4. package/src/acp/client.ts +17 -1
  5. package/src/orchestrator/helpers.ts +1 -0
  6. package/src/orchestrator/service.ts +143 -13
  7. package/src/watchers/manager.ts +89 -7
  8. package/src/worker/io-helper.ts +6 -5
  9. package/test/acp-client.test.ts +0 -309
  10. package/test/acpx-dependency.test.ts +0 -133
  11. package/test/assistant-journal.test.ts +0 -203
  12. package/test/command-surface.test.ts +0 -23
  13. package/test/config.test.ts +0 -77
  14. package/test/detach-attach.test.ts +0 -98
  15. package/test/file-lock.test.ts +0 -88
  16. package/test/fs-utils.test.ts +0 -22
  17. package/test/helpers/harness.ts +0 -301
  18. package/test/helpers.test.ts +0 -108
  19. package/test/keywords.test.ts +0 -92
  20. package/test/notifier.test.ts +0 -29
  21. package/test/openspec-dependency.test.ts +0 -68
  22. package/test/paths-utils.test.ts +0 -30
  23. package/test/pause-cancel.test.ts +0 -55
  24. package/test/planning-journal.test.ts +0 -155
  25. package/test/plugin-registration.test.ts +0 -35
  26. package/test/project-memory.test.ts +0 -42
  27. package/test/proposal.test.ts +0 -24
  28. package/test/queue-planning.test.ts +0 -322
  29. package/test/queue-work.test.ts +0 -220
  30. package/test/recovery.test.ts +0 -576
  31. package/test/service-archive.test.ts +0 -87
  32. package/test/shell-command.test.ts +0 -48
  33. package/test/state-store.test.ts +0 -74
  34. package/test/tasks-and-checkpoint.test.ts +0 -60
  35. package/test/use-project.test.ts +0 -67
  36. package/test/watcher-planning.test.ts +0 -504
  37. package/test/watcher-work.test.ts +0 -1741
  38. package/test/worker-command.test.ts +0 -66
  39. package/test/worker-io-helper.test.ts +0 -97
  40. package/test/worker-skills.test.ts +0 -12
@@ -1,322 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
- import { pathExists, readJsonFile, writeUtf8 } from "../src/utils/fs.ts";
4
- import { PlanningJournalStore } from "../src/planning/journal.ts";
5
- import { getRepoStatePaths } from "../src/utils/paths.ts";
6
- import { createServiceHarness, seedPlanningProject } from "./helpers/harness.ts";
7
-
8
- test("apply prepares visible planning", async () => {
9
- const harness = await createServiceHarness("clawspec-apply-queue-");
10
- const { service, stateStore, watcherManager, repoPath, workspacePath, changeDir } = harness;
11
- const channelKey = "discord:apply-queue:default:main";
12
-
13
- await seedPlanningProject(stateStore, channelKey, {
14
- workspacePath,
15
- repoPath,
16
- projectName: "demo-app",
17
- changeName: "weather-plan",
18
- changeDir,
19
- phase: "proposal",
20
- status: "ready",
21
- planningDirty: true,
22
- });
23
-
24
- const repoStatePaths = getRepoStatePaths(repoPath, "archives");
25
- await writeUtf8(repoStatePaths.executionControlFile, "{\"state\":\"running\"}\n");
26
- await writeUtf8(repoStatePaths.executionResultFile, "{\"status\":\"running\"}\n");
27
- await writeUtf8(repoStatePaths.workerProgressFile, "{\"kind\":\"task_start\"}\n");
28
-
29
- const result = await service.queuePlanningProject(channelKey, "apply");
30
- const project = await stateStore.getActiveProject(channelKey);
31
-
32
- assert.match(result.text ?? "", /Planning Ready/);
33
- assert.equal(project?.status, "ready");
34
- assert.equal(project?.execution, undefined);
35
- assert.deepEqual(watcherManager.wakeCalls, []);
36
- assert.equal(await pathExists(repoStatePaths.executionControlFile), false);
37
- assert.equal(await pathExists(repoStatePaths.executionResultFile), false);
38
- assert.equal(await pathExists(repoStatePaths.workerProgressFile), false);
39
- });
40
-
41
- test("continue routes back to planning when dirty", async () => {
42
- const harness = await createServiceHarness("clawspec-continue-planning-");
43
- const { service, stateStore, watcherManager, repoPath, workspacePath, changeDir } = harness;
44
- const channelKey = "discord:continue-planning:default:main";
45
-
46
- await seedPlanningProject(stateStore, channelKey, {
47
- workspacePath,
48
- repoPath,
49
- projectName: "demo-app",
50
- changeName: "dirty-plan",
51
- changeDir,
52
- phase: "tasks",
53
- status: "paused",
54
- planningDirty: true,
55
- });
56
-
57
- const result = await service.continueProject(channelKey);
58
- const project = await stateStore.getActiveProject(channelKey);
59
-
60
- assert.match(result.text ?? "", /Planning Ready/);
61
- assert.equal(project?.status, "ready");
62
- assert.equal(project?.execution, undefined);
63
- assert.deepEqual(watcherManager.wakeCalls, []);
64
- });
65
-
66
- test("apply still prepares visible planning review when attached and journal matches the last snapshot", async () => {
67
- const harness = await createServiceHarness("clawspec-apply-no-new-plan-");
68
- const { service, stateStore, watcherManager, repoPath, workspacePath, changeDir } = harness;
69
- const channelKey = "discord:apply-no-new-plan:default:main";
70
-
71
- await seedPlanningProject(stateStore, channelKey, {
72
- workspacePath,
73
- repoPath,
74
- projectName: "demo-app",
75
- changeName: "weather-plan",
76
- changeDir,
77
- phase: "tasks",
78
- status: "ready",
79
- planningDirty: true,
80
- });
81
-
82
- const repoStatePaths = getRepoStatePaths(repoPath, "archives");
83
- const journalStore = new PlanningJournalStore(repoStatePaths.planningJournalFile);
84
- await journalStore.append({
85
- timestamp: new Date(Date.now() - 60_000).toISOString(),
86
- changeName: "weather-plan",
87
- role: "user",
88
- text: "keep the same two API endpoints",
89
- });
90
- await journalStore.writeSnapshot(repoStatePaths.planningJournalSnapshotFile, "weather-plan");
91
-
92
- const result = await service.queuePlanningProject(channelKey, "apply");
93
- const project = await stateStore.getActiveProject(channelKey);
94
-
95
- assert.match(result.text ?? "", /Planning Ready/);
96
- assert.equal(project?.status, "ready");
97
- assert.match(project?.latestSummary ?? "", /Waiting for cs-plan in chat/);
98
- assert.deepEqual(watcherManager.wakeCalls, []);
99
- });
100
-
101
- test("apply reports no new planning notes when the chat context is detached", async () => {
102
- const harness = await createServiceHarness("clawspec-apply-detached-no-new-plan-");
103
- const { service, stateStore, watcherManager, repoPath, workspacePath, changeDir } = harness;
104
- const channelKey = "discord:apply-detached-no-new-plan:default:main";
105
-
106
- await seedPlanningProject(stateStore, channelKey, {
107
- workspacePath,
108
- repoPath,
109
- projectName: "demo-app",
110
- changeName: "weather-plan",
111
- changeDir,
112
- phase: "tasks",
113
- status: "ready",
114
- planningDirty: true,
115
- });
116
-
117
- await stateStore.updateProject(channelKey, (current) => ({
118
- ...current,
119
- contextMode: "detached",
120
- }));
121
-
122
- const repoStatePaths = getRepoStatePaths(repoPath, "archives");
123
- const journalStore = new PlanningJournalStore(repoStatePaths.planningJournalFile);
124
- await journalStore.writeSnapshot(repoStatePaths.planningJournalSnapshotFile, "weather-plan");
125
-
126
- const result = await service.queuePlanningProject(channelKey, "apply");
127
- const project = await stateStore.getActiveProject(channelKey);
128
-
129
- assert.match(result.text ?? "", /No New Planning Notes/);
130
- assert.equal(project?.planningJournal?.dirty, false);
131
- assert.deepEqual(watcherManager.wakeCalls, []);
132
- });
133
-
134
- test("cs-plan runs visible planning sync and writes a fresh snapshot", async () => {
135
- const harness = await createServiceHarness("clawspec-visible-plan-");
136
- const { service, stateStore, repoPath, openSpec, sentMessages } = harness;
137
- const channelKey = "discord:visible-plan:default:main";
138
- const promptContext = {
139
- trigger: "user",
140
- channel: "discord",
141
- channelId: "visible-plan",
142
- accountId: "default",
143
- conversationId: "main",
144
- sessionKey: "agent:main:discord:channel:visible-plan",
145
- };
146
-
147
- await service.startProject(channelKey);
148
- await service.useProject(channelKey, "demo-app");
149
- await service.proposalProject(channelKey, "demo-change Demo change");
150
- await service.recordPlanningMessageFromContext(promptContext, "add another API endpoint");
151
-
152
- const instructionCalls: string[] = [];
153
- const originalInstructionsArtifact = openSpec.instructionsArtifact;
154
- openSpec.instructionsArtifact = async (...args: unknown[]) => {
155
- instructionCalls.push(String(args[1]));
156
- return await originalInstructionsArtifact(...args);
157
- };
158
-
159
- const injected = await service.handleBeforePromptBuild(
160
- { prompt: "cs-plan", messages: [] },
161
- promptContext,
162
- );
163
- const runningProject = await stateStore.getActiveProject(channelKey);
164
-
165
- assert.match(injected?.prependContext ?? "", /ClawSpec planning sync is active for this turn/);
166
- assert.match(injected?.prependContext ?? "", /Prefetched OpenSpec instructions for this turn/);
167
- assert.match(injected?.prependContext ?? "", /OpenSpec commands already executed by the plugin before this turn/);
168
- assert.match(injected?.prependContext ?? "", /openspec instructions proposal --change demo-change --json/);
169
- assert.match(injected?.prependContext ?? "", /planning-instructions[\\/]+proposal\.json/);
170
- assert.deepEqual(instructionCalls, ["proposal", "specs", "design", "tasks"]);
171
- assert.match(injected?.prependContext ?? "", /allowed planning scope is limited/i);
172
- assert.match(injected?.prependContext ?? "", /Do not invent endpoints, features, constraints, files, acceptance criteria, test scenarios, or architecture details/i);
173
- assert.match(injected?.prependContext ?? "", /mandatory final line exactly in this shape/i);
174
- assert.equal(runningProject?.status, "planning");
175
- assert.equal(runningProject?.phase, "planning_sync");
176
- assert.deepEqual(
177
- sentMessages.map((entry) => entry.to),
178
- [
179
- "channel:visible-plan",
180
- "channel:visible-plan",
181
- "channel:visible-plan",
182
- "channel:visible-plan",
183
- ],
184
- );
185
- assert.match(sentMessages[0]?.text ?? "", /openspec instructions proposal --change demo-change --json/);
186
- assert.match(sentMessages[0]?.text ?? "", /planning-instructions\/proposal\.json/);
187
- assert.match(sentMessages[1]?.text ?? "", /openspec instructions specs --change demo-change --json/);
188
- assert.match(sentMessages[1]?.text ?? "", /planning-instructions\/specs\.json/);
189
- assert.match(sentMessages[2]?.text ?? "", /openspec instructions design --change demo-change --json/);
190
- assert.match(sentMessages[2]?.text ?? "", /planning-instructions\/design\.json/);
191
- assert.match(sentMessages[3]?.text ?? "", /openspec instructions tasks --change demo-change --json/);
192
- assert.match(sentMessages[3]?.text ?? "", /planning-instructions\/tasks\.json/);
193
-
194
- await service.handleAgentEnd(
195
- { messages: [], success: true, durationMs: 10 },
196
- promptContext,
197
- );
198
-
199
- const finalized = await stateStore.getActiveProject(channelKey);
200
- const repoStatePaths = getRepoStatePaths(repoPath, "archives");
201
- const snapshotExists = await pathExists(repoStatePaths.planningJournalSnapshotFile);
202
- const snapshot = await readJsonFile<any>(repoStatePaths.planningJournalSnapshotFile, null);
203
-
204
- assert.equal(finalized?.status, "ready");
205
- assert.equal(finalized?.phase, "tasks");
206
- assert.equal(finalized?.planningJournal?.dirty, false);
207
- assert.match(finalized?.latestSummary ?? "", /Say `cs-work` to start implementation/);
208
- assert.equal(snapshotExists, true);
209
- assert.equal(snapshot?.changeName, "demo-change");
210
- });
211
-
212
- test("cs-plan finalizes and writes a fresh snapshot even if agent_end sessionKey changes", async () => {
213
- const harness = await createServiceHarness("clawspec-visible-plan-fallback-");
214
- const { service, stateStore, repoPath } = harness;
215
- const channelKey = "discord:visible-plan-fallback:default:main";
216
- const promptContext = {
217
- trigger: "user",
218
- channel: "discord",
219
- channelId: "visible-plan-fallback",
220
- accountId: "default",
221
- conversationId: "main",
222
- sessionKey: "agent:main:discord:channel:visible-plan-fallback",
223
- };
224
-
225
- await service.startProject(channelKey);
226
- await service.useProject(channelKey, "demo-app");
227
- await service.proposalProject(channelKey, "demo-change Demo change");
228
- await service.recordPlanningMessageFromContext(promptContext, "add another API endpoint");
229
-
230
- await service.handleBeforePromptBuild(
231
- { prompt: "cs-plan", messages: [] },
232
- promptContext,
233
- );
234
-
235
- await service.handleAgentEnd(
236
- { messages: [], success: true, durationMs: 10 },
237
- {
238
- ...promptContext,
239
- sessionKey: "agent:main:discord:channel:visible-plan-fallback:other",
240
- },
241
- );
242
-
243
- const finalized = await stateStore.getActiveProject(channelKey);
244
- const repoStatePaths = getRepoStatePaths(repoPath, "archives");
245
- const snapshot = await readJsonFile<any>(repoStatePaths.planningJournalSnapshotFile, null);
246
-
247
- assert.equal(finalized?.status, "ready");
248
- assert.equal(finalized?.phase, "tasks");
249
- assert.equal(finalized?.planningJournal?.dirty, false);
250
- assert.equal(snapshot?.changeName, "demo-change");
251
- assert.equal(snapshot?.entryCount, 1);
252
- });
253
-
254
- test("cs-plan clears stale execution control artifacts from earlier worker runs", async () => {
255
- const harness = await createServiceHarness("clawspec-visible-plan-cleanup-");
256
- const { service, stateStore, repoPath } = harness;
257
- const channelKey = "discord:visible-plan-cleanup:default:main";
258
- const promptContext = {
259
- trigger: "user",
260
- channel: "discord",
261
- channelId: "visible-plan-cleanup",
262
- accountId: "default",
263
- conversationId: "main",
264
- sessionKey: "agent:main:discord:channel:visible-plan-cleanup",
265
- };
266
-
267
- await service.startProject(channelKey);
268
- await service.useProject(channelKey, "demo-app");
269
- await service.proposalProject(channelKey, "demo-change Demo change");
270
- await service.recordPlanningMessageFromContext(promptContext, "add another API endpoint");
271
-
272
- const repoStatePaths = getRepoStatePaths(repoPath, "archives");
273
- await writeUtf8(repoStatePaths.executionControlFile, "{\"state\":\"running\"}\n");
274
- await writeUtf8(repoStatePaths.executionResultFile, "{\"status\":\"running\"}\n");
275
- await writeUtf8(repoStatePaths.workerProgressFile, "{\"kind\":\"task_start\"}\n");
276
-
277
- await service.handleBeforePromptBuild(
278
- { prompt: "cs-plan", messages: [] },
279
- promptContext,
280
- );
281
-
282
- await service.handleAgentEnd(
283
- { messages: [], success: true, durationMs: 10 },
284
- promptContext,
285
- );
286
-
287
- assert.equal(await pathExists(repoStatePaths.executionControlFile), false);
288
- assert.equal(await pathExists(repoStatePaths.executionResultFile), false);
289
- assert.equal(await pathExists(repoStatePaths.workerProgressFile), false);
290
- });
291
-
292
- test("ordinary planning discussion does not preload planning artifacts or propose skill", async () => {
293
- const harness = await createServiceHarness("clawspec-discussion-guard-");
294
- const { service } = harness;
295
- const channelKey = "discord:discussion-guard:default:main";
296
- const promptContext = {
297
- trigger: "user",
298
- channel: "discord",
299
- channelId: "discussion-guard",
300
- accountId: "default",
301
- conversationId: "main",
302
- sessionKey: "agent:main:discord:channel:discussion-guard",
303
- };
304
-
305
- await service.startProject(channelKey);
306
- await service.useProject(channelKey, "demo-app");
307
- await service.proposalProject(channelKey, "demo-change Demo change");
308
-
309
- const injected = await service.handleBeforePromptBuild(
310
- { prompt: "再增加一个接口", messages: [] },
311
- promptContext,
312
- );
313
-
314
- assert.match(injected?.prependContext ?? "", /ClawSpec planning discussion mode is active/);
315
- assert.doesNotMatch(injected?.prependContext ?? "", /openspec[\\/].*proposal\.md/);
316
- assert.doesNotMatch(injected?.prependContext ?? "", /openspec[\\/].*design\.md/);
317
- assert.doesNotMatch(injected?.prependContext ?? "", /openspec[\\/].*tasks\.md/);
318
- assert.match(injected?.prependContext ?? "", /Do not say planning has started, queued, refreshed, synced, or completed/);
319
- assert.match(injected?.prependContext ?? "", /explicitly tell the user that `cs-plan` is the next step before any further implementation/);
320
- assert.match(injected?.prependContext ?? "", /do not say the next step is `cs-work`/);
321
- assert.doesNotMatch(injected?.prependSystemContext ?? "", /openspec-propose/i);
322
- });
@@ -1,220 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
- import path from "node:path";
4
- import { buildWorkerSessionKey, createWorkerSessionKey } from "../src/execution/session.ts";
5
- import { writeUtf8 } from "../src/utils/fs.ts";
6
- import { createServiceHarness, seedPlanningProject } from "./helpers/harness.ts";
7
-
8
- test("work queues background implementation", async () => {
9
- const harness = await createServiceHarness("clawspec-work-queue-");
10
- const { service, stateStore, watcherManager, repoPath, workspacePath, changeDir } = harness;
11
- const channelKey = "discord:work-queue:default:main";
12
- const tasksPath = path.join(changeDir, "tasks.md");
13
- await writeUtf8(tasksPath, "- [ ] 1.1 Build the demo endpoint\n");
14
-
15
- harness.openSpec.instructionsApply = async (cwd: string, changeName: string) => ({
16
- command: `openspec instructions apply --change ${changeName} --json`,
17
- cwd,
18
- stdout: "{}",
19
- stderr: "",
20
- durationMs: 1,
21
- parsed: {
22
- changeName,
23
- changeDir,
24
- schemaName: "spec-driven",
25
- contextFiles: { tasks: tasksPath },
26
- progress: { total: 1, complete: 0, remaining: 1 },
27
- tasks: [{ id: "1.1", description: "Build the demo endpoint", done: false }],
28
- state: "ready",
29
- instruction: "Implement the remaining task.",
30
- },
31
- });
32
-
33
- await seedPlanningProject(stateStore, channelKey, {
34
- workspacePath,
35
- repoPath,
36
- projectName: "demo-app",
37
- changeName: "queue-work",
38
- changeDir,
39
- phase: "tasks",
40
- status: "ready",
41
- planningDirty: false,
42
- });
43
- await stateStore.updateProject(channelKey, (current) => ({
44
- ...current,
45
- boundSessionKey: "agent:main:discord:channel:work-queue",
46
- }));
47
-
48
- const result = await service.queueWorkProject(channelKey, "apply");
49
- const project = await stateStore.getActiveProject(channelKey);
50
-
51
- assert.match(result.text ?? "", /Execution Queued/);
52
- assert.equal(project?.status, "armed");
53
- assert.equal(project?.execution?.action, "work");
54
- assert.equal(project?.currentTask, "1.1 Build the demo endpoint");
55
- assert.equal(
56
- project?.execution?.sessionKey,
57
- createWorkerSessionKey(project!, {
58
- workerSlot: "primary",
59
- workerAgentId: "codex",
60
- attemptKey: project?.execution?.armedAt,
61
- }),
62
- );
63
- assert.match(project?.execution?.sessionKey ?? "", new RegExp(`^${buildWorkerSessionKey(project!, "primary", "codex")}:`));
64
- assert.notEqual(project?.execution?.sessionKey, project?.boundSessionKey);
65
- assert.deepEqual(watcherManager.wakeCalls, [channelKey]);
66
- });
67
-
68
- test("before_dispatch intercepts cs-work and queues background implementation directly", async () => {
69
- const harness = await createServiceHarness("clawspec-work-dispatch-");
70
- const { service, stateStore, watcherManager, repoPath, workspacePath, changeDir } = harness;
71
- const channelKey = "discord:work-dispatch:default:main";
72
- const tasksPath = path.join(changeDir, "tasks.md");
73
- await writeUtf8(tasksPath, "- [ ] 1.1 Build the demo endpoint\n");
74
-
75
- harness.openSpec.instructionsApply = async (cwd: string, changeName: string) => ({
76
- command: `openspec instructions apply --change ${changeName} --json`,
77
- cwd,
78
- stdout: "{}",
79
- stderr: "",
80
- durationMs: 1,
81
- parsed: {
82
- changeName,
83
- changeDir,
84
- schemaName: "spec-driven",
85
- contextFiles: { tasks: tasksPath },
86
- progress: { total: 1, complete: 0, remaining: 1 },
87
- tasks: [{ id: "1.1", description: "Build the demo endpoint", done: false }],
88
- state: "ready",
89
- instruction: "Implement the remaining task.",
90
- },
91
- });
92
-
93
- await seedPlanningProject(stateStore, channelKey, {
94
- workspacePath,
95
- repoPath,
96
- projectName: "demo-app",
97
- changeName: "queue-work",
98
- changeDir,
99
- phase: "tasks",
100
- status: "ready",
101
- planningDirty: false,
102
- });
103
- await stateStore.updateProject(channelKey, (current) => ({
104
- ...current,
105
- boundSessionKey: "agent:main:discord:channel:work-dispatch",
106
- }));
107
-
108
- const result = await service.handleBeforeDispatch(
109
- { content: "cs-work", channel: "discord" },
110
- {
111
- channelId: "work-dispatch",
112
- accountId: "default",
113
- conversationId: "main",
114
- sessionKey: "agent:main:discord:channel:work-dispatch",
115
- },
116
- );
117
- const project = await stateStore.getActiveProject(channelKey);
118
-
119
- assert.equal(result?.handled, true);
120
- assert.match(result?.text ?? "", /Execution Queued/);
121
- assert.equal(project?.status, "armed");
122
- assert.equal(project?.execution?.action, "work");
123
- assert.deepEqual(watcherManager.wakeCalls, [channelKey]);
124
- });
125
-
126
- test("main chat agent end does not clear a background worker run", async () => {
127
- const harness = await createServiceHarness("clawspec-work-session-");
128
- const { service, stateStore, repoPath, workspacePath, changeDir } = harness;
129
- const channelKey = "discord:work-session:default:main";
130
- const workerSessionKey = "clawspec:worker-session";
131
- const boundSessionKey = "agent:main:discord:channel:work-session";
132
-
133
- await seedPlanningProject(stateStore, channelKey, {
134
- workspacePath,
135
- repoPath,
136
- projectName: "demo-app",
137
- changeName: "queue-work",
138
- changeDir,
139
- phase: "implementing",
140
- status: "running",
141
- planningDirty: false,
142
- execution: { action: "work", state: "running", mode: "apply" },
143
- });
144
- await stateStore.updateProject(channelKey, (current) => ({
145
- ...current,
146
- boundSessionKey,
147
- latestSummary: "Worker is running in the background.",
148
- execution: current.execution
149
- ? {
150
- ...current.execution,
151
- sessionKey: workerSessionKey,
152
- workerAgentId: "codex",
153
- workerSlot: "primary",
154
- }
155
- : current.execution,
156
- }));
157
-
158
- await service.handleAgentEnd(
159
- { messages: [], success: true },
160
- { sessionKey: boundSessionKey, trigger: "user" },
161
- );
162
-
163
- const project = await stateStore.getActiveProject(channelKey);
164
- assert.equal(project?.status, "running");
165
- assert.equal(project?.execution?.state, "running");
166
- assert.equal(project?.execution?.sessionKey, workerSessionKey);
167
- assert.equal(project?.latestSummary, "Worker is running in the background.");
168
- });
169
-
170
- test("work requires OpenClaw ACP default agent when no project override is set", async () => {
171
- const harness = await createServiceHarness("clawspec-work-missing-acp-");
172
- const { service, stateStore, watcherManager, repoPath, workspacePath, changeDir } = harness;
173
- const channelKey = "discord:work-missing-acp:default:main";
174
- const tasksPath = path.join(changeDir, "tasks.md");
175
- await writeUtf8(tasksPath, "- [ ] 1.1 Build the demo endpoint\n");
176
-
177
- (service as any).config = {
178
- acp: {
179
- backend: "acpx",
180
- },
181
- };
182
-
183
- harness.openSpec.instructionsApply = async (cwd: string, changeName: string) => ({
184
- command: `openspec instructions apply --change ${changeName} --json`,
185
- cwd,
186
- stdout: "{}",
187
- stderr: "",
188
- durationMs: 1,
189
- parsed: {
190
- changeName,
191
- changeDir,
192
- schemaName: "spec-driven",
193
- contextFiles: { tasks: tasksPath },
194
- progress: { total: 1, complete: 0, remaining: 1 },
195
- tasks: [{ id: "1.1", description: "Build the demo endpoint", done: false }],
196
- state: "ready",
197
- instruction: "Implement the remaining task.",
198
- },
199
- });
200
-
201
- await seedPlanningProject(stateStore, channelKey, {
202
- workspacePath,
203
- repoPath,
204
- projectName: "demo-app",
205
- changeName: "queue-work",
206
- changeDir,
207
- phase: "tasks",
208
- status: "ready",
209
- planningDirty: false,
210
- });
211
-
212
- const result = await service.queueWorkProject(channelKey, "apply");
213
- const project = await stateStore.getActiveProject(channelKey);
214
-
215
- assert.match(result.text ?? "", /Worker Setup Required/);
216
- assert.match(result.text ?? "", /openclaw config set acp\.backend acpx/);
217
- assert.match(result.text ?? "", /openclaw config set acp\.defaultAgent codex/);
218
- assert.equal(project?.status, "ready");
219
- assert.deepEqual(watcherManager.wakeCalls, []);
220
- });