clawspec 1.0.19 → 1.0.21

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 (41) hide show
  1. package/README.md +6 -0
  2. package/README.zh-CN.md +6 -0
  3. package/package.json +1 -2
  4. package/src/bootstrap/state.ts +128 -0
  5. package/src/dependencies/acpx.ts +6 -0
  6. package/src/dependencies/openspec.ts +5 -0
  7. package/src/index.ts +125 -43
  8. package/src/watchers/manager.ts +69 -1
  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 -24
  13. package/test/config.test.ts +0 -77
  14. package/test/detach-attach.test.ts +0 -98
  15. package/test/doctor.test.ts +0 -142
  16. package/test/file-lock.test.ts +0 -88
  17. package/test/fs-utils.test.ts +0 -22
  18. package/test/helpers/harness.ts +0 -305
  19. package/test/helpers.test.ts +0 -108
  20. package/test/keywords.test.ts +0 -92
  21. package/test/notifier.test.ts +0 -29
  22. package/test/openspec-dependency.test.ts +0 -68
  23. package/test/paths-utils.test.ts +0 -30
  24. package/test/pause-cancel.test.ts +0 -55
  25. package/test/planning-journal.test.ts +0 -155
  26. package/test/plugin-registration.test.ts +0 -35
  27. package/test/project-memory.test.ts +0 -42
  28. package/test/proposal.test.ts +0 -24
  29. package/test/queue-planning.test.ts +0 -322
  30. package/test/queue-work.test.ts +0 -220
  31. package/test/recovery.test.ts +0 -603
  32. package/test/service-archive.test.ts +0 -87
  33. package/test/shell-command.test.ts +0 -48
  34. package/test/state-store.test.ts +0 -74
  35. package/test/tasks-and-checkpoint.test.ts +0 -60
  36. package/test/use-project.test.ts +0 -67
  37. package/test/watcher-planning.test.ts +0 -533
  38. package/test/watcher-work.test.ts +0 -1771
  39. package/test/worker-command.test.ts +0 -66
  40. package/test/worker-io-helper.test.ts +0 -97
  41. package/test/worker-skills.test.ts +0 -12
@@ -1,155 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
- import os from "node:os";
4
- import path from "node:path";
5
- import { mkdtemp } from "node:fs/promises";
6
- import { PlanningJournalStore } from "../src/planning/journal.ts";
7
-
8
- test("planning journal reports clean when snapshot matches current content", async () => {
9
- const tempRoot = await mkdtemp(path.join(os.tmpdir(), "clawspec-journal-clean-"));
10
- const journalPath = path.join(tempRoot, "planning-journal.jsonl");
11
- const snapshotPath = path.join(tempRoot, "planning-journal.snapshot.json");
12
- const store = new PlanningJournalStore(journalPath);
13
-
14
- await store.append({
15
- timestamp: "2026-03-23T07:00:00.000Z",
16
- changeName: "hello",
17
- role: "user",
18
- text: "keep the two existing endpoints",
19
- });
20
- await store.writeSnapshot(snapshotPath, "hello", "2026-03-23T07:05:00.000Z");
21
-
22
- assert.equal(await store.hasUnsyncedChanges("hello", snapshotPath), false);
23
- });
24
-
25
- test("planning journal reports dirty when a new entry is appended after the snapshot", async () => {
26
- const tempRoot = await mkdtemp(path.join(os.tmpdir(), "clawspec-journal-dirty-"));
27
- const journalPath = path.join(tempRoot, "planning-journal.jsonl");
28
- const snapshotPath = path.join(tempRoot, "planning-journal.snapshot.json");
29
- const store = new PlanningJournalStore(journalPath);
30
-
31
- await store.append({
32
- timestamp: "2026-03-23T07:00:00.000Z",
33
- changeName: "hello",
34
- role: "user",
35
- text: "keep the two existing endpoints",
36
- });
37
- await store.writeSnapshot(snapshotPath, "hello", "2026-03-23T07:05:00.000Z");
38
- await store.append({
39
- timestamp: "2026-03-23T07:10:00.000Z",
40
- changeName: "hello",
41
- role: "user",
42
- text: "add two new interfaces to the same change",
43
- });
44
-
45
- assert.equal(await store.hasUnsyncedChanges("hello", snapshotPath), true);
46
- });
47
-
48
- test("planning journal falls back to lastSyncedAt when no snapshot exists yet", async () => {
49
- const tempRoot = await mkdtemp(path.join(os.tmpdir(), "clawspec-journal-fallback-"));
50
- const journalPath = path.join(tempRoot, "planning-journal.jsonl");
51
- const snapshotPath = path.join(tempRoot, "planning-journal.snapshot.json");
52
- const store = new PlanningJournalStore(journalPath);
53
-
54
- await store.append({
55
- timestamp: "2026-03-23T07:00:00.000Z",
56
- changeName: "hello",
57
- role: "user",
58
- text: "keep the two existing endpoints",
59
- });
60
-
61
- assert.equal(
62
- await store.hasUnsyncedChanges("hello", snapshotPath, "2026-03-23T07:05:00.000Z"),
63
- false,
64
- );
65
- assert.equal(
66
- await store.hasUnsyncedChanges("hello", snapshotPath, "2026-03-23T06:55:00.000Z"),
67
- true,
68
- );
69
- });
70
-
71
- test("snapshot is always written after planning sync regardless of journal dirty state", async () => {
72
- const tempRoot = await mkdtemp(path.join(os.tmpdir(), "clawspec-snapshot-always-"));
73
- const journalPath = path.join(tempRoot, "planning-journal.jsonl");
74
- const snapshotPath = path.join(tempRoot, "planning-journal.snapshot.json");
75
- const store = new PlanningJournalStore(journalPath);
76
-
77
- await store.append({
78
- timestamp: "2026-03-27T03:00:00.000Z",
79
- changeName: "test",
80
- role: "user",
81
- text: "initial requirement",
82
- });
83
-
84
- const snapshot1 = await store.writeSnapshot(snapshotPath, "test", "2026-03-27T03:05:00.000Z");
85
- assert.equal(snapshot1.entryCount, 1);
86
-
87
- await store.append({
88
- timestamp: "2026-03-27T03:10:00.000Z",
89
- changeName: "test",
90
- role: "assistant",
91
- text: "planning sync response",
92
- });
93
-
94
- const snapshot2 = await store.writeSnapshot(snapshotPath, "test", "2026-03-27T03:15:00.000Z");
95
- assert.equal(snapshot2.entryCount, 2);
96
- assert.equal(await store.hasUnsyncedChanges("test", snapshotPath), false);
97
- });
98
-
99
- test("snapshot correctly captures all journal entries including assistant messages", async () => {
100
- const tempRoot = await mkdtemp(path.join(os.tmpdir(), "clawspec-snapshot-complete-"));
101
- const journalPath = path.join(tempRoot, "planning-journal.jsonl");
102
- const snapshotPath = path.join(tempRoot, "planning-journal.snapshot.json");
103
- const store = new PlanningJournalStore(journalPath);
104
-
105
- await store.append({
106
- timestamp: "2026-03-27T03:00:00.000Z",
107
- changeName: "test",
108
- role: "user",
109
- text: "user requirement",
110
- });
111
- await store.append({
112
- timestamp: "2026-03-27T03:05:00.000Z",
113
- changeName: "test",
114
- role: "assistant",
115
- text: "assistant response",
116
- });
117
-
118
- const snapshot = await store.writeSnapshot(snapshotPath, "test");
119
- const digest = await store.digest("test");
120
-
121
- assert.equal(snapshot.entryCount, digest.entryCount);
122
- assert.equal(snapshot.lastEntryAt, digest.lastEntryAt);
123
- assert.equal(snapshot.contentHash, digest.contentHash);
124
- });
125
-
126
- test("snapshot sync after cs-plan, add requirement, cs-plan again", async () => {
127
- const tempRoot = await mkdtemp(path.join(os.tmpdir(), "clawspec-replan-"));
128
- const journalPath = path.join(tempRoot, "planning-journal.jsonl");
129
- const snapshotPath = path.join(tempRoot, "planning-journal.snapshot.json");
130
- const store = new PlanningJournalStore(journalPath);
131
-
132
- await store.append({
133
- timestamp: "2026-03-27T06:00:00.000Z",
134
- changeName: "test",
135
- role: "user",
136
- text: "initial requirement",
137
- });
138
-
139
- const snapshot1 = await store.writeSnapshot(snapshotPath, "test", "2026-03-27T06:05:00.000Z");
140
- assert.equal(snapshot1.entryCount, 1);
141
- assert.equal(await store.hasUnsyncedChanges("test", snapshotPath), false);
142
-
143
- await store.append({
144
- timestamp: "2026-03-27T06:10:00.000Z",
145
- changeName: "test",
146
- role: "user",
147
- text: "additional requirement",
148
- });
149
-
150
- assert.equal(await store.hasUnsyncedChanges("test", snapshotPath), true);
151
-
152
- const snapshot2 = await store.writeSnapshot(snapshotPath, "test", "2026-03-27T06:15:00.000Z");
153
- assert.equal(snapshot2.entryCount, 2);
154
- assert.equal(await store.hasUnsyncedChanges("test", snapshotPath), false);
155
- });
@@ -1,35 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
- import os from "node:os";
4
- import path from "node:path";
5
- import { mkdtemp } from "node:fs/promises";
6
- import plugin from "../src/index.ts";
7
-
8
- test("plugin registers the clawspec command", async () => {
9
- const stateDir = await mkdtemp(path.join(os.tmpdir(), "clawspec-plugin-register-"));
10
- const commandNames: string[] = [];
11
-
12
- plugin.register({
13
- pluginConfig: {},
14
- config: {},
15
- logger: {
16
- info() {},
17
- warn() {},
18
- error() {},
19
- debug() {},
20
- },
21
- runtime: {
22
- state: {
23
- resolveStateDir: () => stateDir,
24
- },
25
- },
26
- registerService() {},
27
- registerCli() {},
28
- registerCommand(command: { name: string }) {
29
- commandNames.push(command.name);
30
- },
31
- on() {},
32
- } as any);
33
-
34
- assert.deepEqual(commandNames, ["clawspec"]);
35
- });
@@ -1,42 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
- import os from "node:os";
4
- import path from "node:path";
5
- import { mkdtemp, mkdir } from "node:fs/promises";
6
- import {
7
- DuplicateRememberedProjectError,
8
- ProjectMemoryStore,
9
- RememberedProjectPathInvalidError,
10
- } from "../src/memory/store.ts";
11
-
12
- test("project memory store remembers, lists, overwrites, and validates paths", async () => {
13
- const tempRoot = await mkdtemp(path.join(os.tmpdir(), "clawspec-memory-"));
14
- const memoryFile = path.join(tempRoot, "project-memory.json");
15
- const repoA = path.join(tempRoot, "repo-a");
16
- const repoB = path.join(tempRoot, "repo-b");
17
- await mkdir(repoA, { recursive: true });
18
- await mkdir(repoB, { recursive: true });
19
-
20
- const store = new ProjectMemoryStore(memoryFile);
21
- await store.initialize();
22
-
23
- const created = await store.remember("demo", repoA);
24
- assert.equal(created.created, true);
25
- assert.equal(created.overwritten, false);
26
-
27
- const entries = await store.list();
28
- assert.equal(entries.length, 1);
29
- assert.equal(entries[0].repoPath, repoA);
30
-
31
- await assert.rejects(() => store.remember("demo", repoB), DuplicateRememberedProjectError);
32
-
33
- const overwritten = await store.remember("demo", repoB, { overwrite: true });
34
- assert.equal(overwritten.overwritten, true);
35
- assert.equal((await store.resolveForUse("demo")).repoPath, repoB);
36
-
37
- const staleFile = path.join(tempRoot, "stale-memory.json");
38
- const staleStore = new ProjectMemoryStore(staleFile);
39
- await staleStore.initialize();
40
- await staleStore.remember("stale", path.join(tempRoot, "missing"));
41
- await assert.rejects(() => staleStore.resolveForUse("stale"), RememberedProjectPathInvalidError);
42
- });
@@ -1,24 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
- import path from "node:path";
4
- import { pathExists } from "../src/utils/fs.ts";
5
- import { createServiceHarness } from "./helpers/harness.ts";
6
-
7
- test("proposal creates scaffold and rollback baseline", async () => {
8
- const harness = await createServiceHarness("clawspec-proposal-");
9
- const { service, stateStore, workspacePath } = harness;
10
- const channelKey = "discord:proposal:default:main";
11
-
12
- await service.startProject(channelKey);
13
- await service.useProject(channelKey, "demo-app");
14
- const result = await service.proposalProject(channelKey, "support-weather Add weather endpoints");
15
- const project = await stateStore.getActiveProject(channelKey);
16
- const changeDir = path.join(workspacePath, "demo-app", "openspec", "changes", "support-weather");
17
-
18
- assert.match(result.text ?? "", /Proposal Ready/);
19
- assert.equal(project?.changeName, "support-weather");
20
- assert.equal(project?.status, "ready");
21
- assert.equal(project?.phase, "proposal");
22
- assert.equal(await pathExists(path.join(changeDir, ".openspec.yaml")), true);
23
- assert.equal(await pathExists(project?.rollback?.manifestPath ?? ""), true);
24
- });
@@ -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
- });