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,309 +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 { spawn } from "node:child_process";
6
- import { chmod, mkdtemp, writeFile } from "node:fs/promises";
7
- import { AcpWorkerClient } from "../src/acp/client.ts";
8
- import { terminateChildProcess } from "../src/utils/shell-command.ts";
9
-
10
- test("AcpWorkerClient tracks active worker lifecycle through acpx CLI", async () => {
11
- const tempRoot = await mkdtemp(path.join(os.tmpdir(), "clawspec-acpx-client-"));
12
- const fake = await createFakeAcpx(tempRoot);
13
- const client = new AcpWorkerClient({
14
- agentId: "codex",
15
- logger: createLogger(),
16
- command: fake.command,
17
- env: fake.env,
18
- });
19
-
20
- const events: Array<{ type: string; text?: string }> = [];
21
- const runPromise = client.runTurn({
22
- sessionKey: "session-1",
23
- cwd: tempRoot,
24
- text: "fix tests",
25
- onEvent: async (event) => {
26
- events.push(event);
27
- },
28
- });
29
-
30
- await waitFor(async () => {
31
- const status = await client.getSessionStatus({
32
- sessionKey: "session-1",
33
- cwd: tempRoot,
34
- agentId: "codex",
35
- });
36
- return status?.details?.status === "alive";
37
- });
38
-
39
- await runPromise;
40
-
41
- const finalStatus = await client.getSessionStatus({
42
- sessionKey: "session-1",
43
- cwd: tempRoot,
44
- agentId: "codex",
45
- });
46
-
47
- assert.equal(events.some((event) => event.type === "text_delta" && event.text?.includes("Working on fix tests")), true);
48
- assert.match(finalStatus?.summary ?? "", /status=dead/);
49
- });
50
-
51
- test("AcpWorkerClient stops the worker when the gateway process disappears", async () => {
52
- const tempRoot = await mkdtemp(path.join(os.tmpdir(), "clawspec-acpx-watchdog-"));
53
- const fake = await createFakeAcpx(tempRoot);
54
- const gateway = spawn(process.execPath, ["-e", "setInterval(() => {}, 1000)"], {
55
- stdio: "ignore",
56
- windowsHide: true,
57
- detached: true,
58
- });
59
- gateway.unref();
60
- const client = new AcpWorkerClient({
61
- agentId: "codex",
62
- logger: createLogger(),
63
- command: fake.command,
64
- env: fake.env,
65
- gatewayPid: gateway.pid,
66
- gatewayWatchdogPollMs: 50,
67
- });
68
-
69
- const startedAt = Date.now();
70
- let sawStarted = false;
71
- const runPromise = client.runTurn({
72
- sessionKey: "session-watchdog",
73
- cwd: tempRoot,
74
- text: "stay alive",
75
- onEvent: async (event) => {
76
- if (event.type === "text_delta" && event.text?.includes("Working on stay alive")) {
77
- sawStarted = true;
78
- }
79
- },
80
- });
81
- const observedRunPromise = runPromise.catch((error) => {
82
- throw error;
83
- });
84
-
85
- try {
86
- await waitFor(async () => sawStarted === true);
87
-
88
- terminateChildProcess(gateway, { force: true });
89
-
90
- await assert.rejects(
91
- observedRunPromise,
92
- /acpx exited with code|signal=SIGTERM|terminated/i,
93
- );
94
- } finally {
95
- terminateChildProcess(gateway, { force: true });
96
- await observedRunPromise.catch(() => undefined);
97
- }
98
-
99
- assert.ok(Date.now() - startedAt < 4_000, "watchdog should stop the worker quickly");
100
- const finalStatus = await client.getSessionStatus({
101
- sessionKey: "session-watchdog",
102
- cwd: tempRoot,
103
- agentId: "codex",
104
- });
105
- assert.match(finalStatus?.summary ?? "", /status=dead/);
106
- });
107
-
108
- async function createFakeAcpx(tempRoot: string): Promise<{ command: string; env: NodeJS.ProcessEnv }> {
109
- const scriptPath = path.join(tempRoot, "fake-acpx.js");
110
- const wrapperPath = path.join(tempRoot, process.platform === "win32" ? "fake-acpx.cmd" : "fake-acpx");
111
-
112
- await writeFile(scriptPath, `
113
- const fs = require("node:fs/promises");
114
- const path = require("node:path");
115
-
116
- const args = process.argv.slice(2);
117
- const stateDir = process.env.FAKE_ACPX_STATE;
118
-
119
- function consumeGlobals(argv) {
120
- const out = [...argv];
121
- const result = [];
122
- while (out.length > 0) {
123
- const head = out[0];
124
- if (head === "--format" || head === "--cwd" || head === "--ttl") {
125
- out.shift();
126
- out.shift();
127
- continue;
128
- }
129
- if (head === "--json-strict" || head === "--approve-all" || head === "--approve-reads" || head === "--deny-all") {
130
- out.shift();
131
- continue;
132
- }
133
- result.push(...out);
134
- break;
135
- }
136
- return result;
137
- }
138
-
139
- function flagValue(argv, name) {
140
- const index = argv.indexOf(name);
141
- if (index >= 0 && index + 1 < argv.length) {
142
- return argv[index + 1];
143
- }
144
- return undefined;
145
- }
146
-
147
- async function readStdin() {
148
- const chunks = [];
149
- for await (const chunk of process.stdin) {
150
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
151
- }
152
- return Buffer.concat(chunks).toString("utf8");
153
- }
154
-
155
- function isAlive(pid) {
156
- if (!Number.isFinite(pid) || pid <= 0) {
157
- return false;
158
- }
159
- try {
160
- process.kill(pid, 0);
161
- return true;
162
- } catch (error) {
163
- return error && typeof error === "object" && error.code === "EPERM";
164
- }
165
- }
166
-
167
- async function writeJsonLine(value) {
168
- process.stdout.write(JSON.stringify(value) + "\\n");
169
- }
170
-
171
- async function main() {
172
- if (args.includes("--version")) {
173
- process.stdout.write("0.3.1\\n");
174
- return;
175
- }
176
-
177
- const rest = consumeGlobals(args);
178
- const agent = rest[0];
179
- const verb = rest[1];
180
- const tail = rest.slice(2);
181
- const sessionName = flagValue(tail, "--session") || flagValue(tail, "--name") || tail[1];
182
- const sessionFile = sessionName ? path.join(stateDir, sessionName + ".json") : "";
183
- const runningFile = sessionName ? path.join(stateDir, sessionName + ".running") : "";
184
-
185
- async function sessionExists() {
186
- try {
187
- await fs.access(sessionFile);
188
- return true;
189
- } catch {
190
- return false;
191
- }
192
- }
193
-
194
- async function runningExists() {
195
- try {
196
- const payload = JSON.parse(await fs.readFile(runningFile, "utf8"));
197
- return isAlive(payload && payload.pid);
198
- } catch {
199
- return false;
200
- }
201
- }
202
-
203
- if (verb === "sessions" && tail[0] === "ensure") {
204
- if (await sessionExists()) {
205
- await writeJsonLine({ acpxRecordId: "record-" + sessionName, acpxSessionId: "backend-" + sessionName, agentSessionId: "agent-" + sessionName, agent });
206
- return;
207
- }
208
- await writeJsonLine({ type: "error", code: "NO_SESSION", message: "missing session" });
209
- return;
210
- }
211
-
212
- if (verb === "sessions" && tail[0] === "new") {
213
- await fs.writeFile(sessionFile, JSON.stringify({ sessionName, agent }), "utf8");
214
- await writeJsonLine({ acpxRecordId: "record-" + sessionName, acpxSessionId: "backend-" + sessionName, agentSessionId: "agent-" + sessionName, agent });
215
- return;
216
- }
217
-
218
- if (verb === "status") {
219
- if (!(await sessionExists())) {
220
- await writeJsonLine({ type: "error", code: "NO_SESSION", message: "missing session" });
221
- return;
222
- }
223
- await writeJsonLine({
224
- status: await runningExists() ? "alive" : "dead",
225
- acpxRecordId: "record-" + sessionName,
226
- acpxSessionId: "backend-" + sessionName,
227
- agentSessionId: "agent-" + sessionName,
228
- pid: 1234,
229
- });
230
- return;
231
- }
232
-
233
- if (verb === "cancel") {
234
- await fs.rm(runningFile, { force: true });
235
- await writeJsonLine({ ok: true });
236
- return;
237
- }
238
-
239
- if (verb === "prompt") {
240
- const text = (await readStdin()).trim();
241
- await fs.writeFile(runningFile, JSON.stringify({ pid: process.pid }), "utf8");
242
- process.on("SIGTERM", async () => {
243
- await fs.rm(runningFile, { force: true });
244
- process.exit(143);
245
- });
246
- await new Promise((resolve) => setTimeout(resolve, 60));
247
- await writeJsonLine({ text: "Working on " + text });
248
- if (sessionName === "session-watchdog" || text.includes("stay alive")) {
249
- setInterval(() => {}, 1_000);
250
- return;
251
- }
252
- await new Promise((resolve) => setTimeout(resolve, 80));
253
- await fs.rm(runningFile, { force: true });
254
- await writeJsonLine({ type: "done" });
255
- return;
256
- }
257
-
258
- if (verb === "sessions" && tail[0] === "close") {
259
- await fs.rm(sessionFile, { force: true });
260
- await fs.rm(runningFile, { force: true });
261
- await writeJsonLine({ ok: true });
262
- return;
263
- }
264
-
265
- process.stderr.write("unexpected args: " + JSON.stringify(args));
266
- process.exit(1);
267
- }
268
-
269
- main().catch((error) => {
270
- process.stderr.write(String(error && error.stack ? error.stack : error));
271
- process.exit(1);
272
- });
273
- `, "utf8");
274
-
275
- if (process.platform === "win32") {
276
- await writeFile(wrapperPath, `@echo off\r\nnode "${scriptPath}" %*\r\n`, "utf8");
277
- } else {
278
- await writeFile(wrapperPath, `#!/usr/bin/env bash\nnode "${scriptPath}" "$@"\n`, "utf8");
279
- await chmod(wrapperPath, 0o755);
280
- }
281
-
282
- return {
283
- command: wrapperPath,
284
- env: {
285
- ...process.env,
286
- FAKE_ACPX_STATE: tempRoot,
287
- },
288
- };
289
- }
290
-
291
- function createLogger() {
292
- return {
293
- info() {},
294
- warn() {},
295
- error() {},
296
- debug() {},
297
- };
298
- }
299
-
300
- async function waitFor(predicate: () => Promise<boolean>, timeoutMs = 2_000): Promise<void> {
301
- const deadline = Date.now() + timeoutMs;
302
- while (Date.now() < deadline) {
303
- if (await predicate()) {
304
- return;
305
- }
306
- await new Promise((resolve) => setTimeout(resolve, 25));
307
- }
308
- throw new Error("timed out waiting for condition");
309
- }
@@ -1,133 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
- import path from "node:path";
4
- import {
5
- ACPX_EXPECTED_VERSION,
6
- ACPX_PACKAGE_NAME,
7
- ensureAcpxCli,
8
- } from "../src/dependencies/acpx.ts";
9
-
10
- const ROOT_PREFIX = process.platform === "win32" ? "C:\\clawspec-test" : "/tmp/clawspec-test";
11
- const OPENCLAW_PREFIX = process.platform === "win32" ? "C:\\openclaw-test" : "/opt/openclaw";
12
-
13
- const LOCAL_COMMAND = path.join(
14
- ROOT_PREFIX,
15
- "node_modules",
16
- ".bin",
17
- process.platform === "win32" ? "acpx.cmd" : "acpx",
18
- );
19
- const BUILTIN_COMMAND = path.join(
20
- OPENCLAW_PREFIX,
21
- "dist",
22
- "extensions",
23
- "acpx",
24
- "node_modules",
25
- ".bin",
26
- process.platform === "win32" ? "acpx.cmd" : "acpx",
27
- );
28
- const OPENCLAW_RUNTIME_ENTRYPOINT = path.join(
29
- OPENCLAW_PREFIX,
30
- "dist",
31
- "index.js",
32
- );
33
-
34
- test("ensureAcpxCli uses the global acpx command when available", async () => {
35
- const calls: Array<{ command: string; args: string[] }> = [];
36
- const result = await ensureAcpxCli({
37
- pluginRoot: ROOT_PREFIX,
38
- runner: async ({ command, args }) => {
39
- calls.push({ command, args });
40
- if (command === LOCAL_COMMAND) {
41
- return { code: 1, stdout: "", stderr: "not found" };
42
- }
43
- if (command === "acpx") {
44
- return { code: 0, stdout: `${ACPX_EXPECTED_VERSION}\n`, stderr: "" };
45
- }
46
- return { code: 1, stdout: "", stderr: "unexpected command" };
47
- },
48
- });
49
-
50
- assert.equal(result.source, "global");
51
- assert.equal(result.version, ACPX_EXPECTED_VERSION);
52
- assert.equal(calls.some((call) => call.command === "npm"), false);
53
- });
54
-
55
- test("ensureAcpxCli accepts a newer global acpx version", async () => {
56
- const calls: Array<{ command: string; args: string[] }> = [];
57
- const result = await ensureAcpxCli({
58
- pluginRoot: ROOT_PREFIX,
59
- runner: async ({ command, args }) => {
60
- calls.push({ command, args });
61
- if (command === LOCAL_COMMAND) {
62
- return { code: 1, stdout: "", stderr: "not found" };
63
- }
64
- if (command === "acpx") {
65
- return { code: 0, stdout: "0.3.2\n", stderr: "" };
66
- }
67
- return { code: 1, stdout: "", stderr: "unexpected command" };
68
- },
69
- });
70
-
71
- assert.equal(result.source, "global");
72
- assert.equal(result.version, "0.3.2");
73
- assert.equal(calls.some((call) => call.command === "npm"), false);
74
- });
75
-
76
- test("ensureAcpxCli prefers the OpenClaw builtin acpx over an incompatible PATH acpx", async () => {
77
- const calls: Array<{ command: string; args: string[] }> = [];
78
- const result = await ensureAcpxCli({
79
- pluginRoot: ROOT_PREFIX,
80
- runtimeEntrypoint: OPENCLAW_RUNTIME_ENTRYPOINT,
81
- runner: async ({ command, args }) => {
82
- calls.push({ command, args });
83
- if (command === LOCAL_COMMAND) {
84
- return { code: 1, stdout: "", stderr: "not found" };
85
- }
86
- if (command === BUILTIN_COMMAND) {
87
- return { code: 0, stdout: `${ACPX_EXPECTED_VERSION}\n`, stderr: "" };
88
- }
89
- if (command === "acpx") {
90
- return { code: 0, stdout: "0.1.15\n", stderr: "" };
91
- }
92
- return { code: 1, stdout: "", stderr: "unexpected command" };
93
- },
94
- });
95
-
96
- assert.equal(result.source, "builtin");
97
- assert.equal(result.version, ACPX_EXPECTED_VERSION);
98
- assert.equal(result.command, BUILTIN_COMMAND);
99
- assert.equal(calls.some((call) => call.command === "npm"), false);
100
- });
101
-
102
- test("ensureAcpxCli installs a plugin-local acpx when none is available", async () => {
103
- const calls: Array<{ command: string; args: string[] }> = [];
104
- let localCheckCount = 0;
105
-
106
- const result = await ensureAcpxCli({
107
- pluginRoot: ROOT_PREFIX,
108
- runner: async ({ command, args }) => {
109
- calls.push({ command, args });
110
- if (command === LOCAL_COMMAND) {
111
- localCheckCount += 1;
112
- if (localCheckCount === 1) {
113
- return { code: 1, stdout: "", stderr: "not found" };
114
- }
115
- return { code: 0, stdout: `${ACPX_EXPECTED_VERSION}\n`, stderr: "" };
116
- }
117
- if (command === "acpx") {
118
- return { code: 1, stdout: "", stderr: "not found" };
119
- }
120
- if (command === "npm") {
121
- return { code: 0, stdout: "installed\n", stderr: "" };
122
- }
123
- return { code: 1, stdout: "", stderr: "unexpected command" };
124
- },
125
- });
126
-
127
- assert.equal(result.source, "local");
128
- assert.equal(result.version, ACPX_EXPECTED_VERSION);
129
- assert.equal(
130
- calls.some((call) => call.command === "npm" && call.args.includes(`${ACPX_PACKAGE_NAME}@${ACPX_EXPECTED_VERSION}`)),
131
- true,
132
- );
133
- });
@@ -1,203 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
- import { PlanningJournalStore } from "../src/planning/journal.ts";
4
- import { getRepoStatePaths } from "../src/utils/paths.ts";
5
- import { createServiceHarness } from "./helpers/harness.ts";
6
-
7
- function createPromptContext(channelId: string) {
8
- return {
9
- trigger: "user",
10
- channel: "discord",
11
- channelId,
12
- accountId: "default",
13
- conversationId: "main",
14
- sessionKey: `agent:main:discord:channel:${channelId}`,
15
- };
16
- }
17
-
18
- test("assistant planning suggestions are appended to the journal for attached discussion turns", async () => {
19
- const harness = await createServiceHarness("clawspec-assistant-journal-");
20
- const { service, stateStore, repoPath } = harness;
21
- const channelKey = "discord:assistant-journal:default:main";
22
- const promptContext = createPromptContext("assistant-journal");
23
- const userPrompt = "Add JWT auth, refresh tokens, and forgot-password support.";
24
-
25
- await service.startProject(channelKey);
26
- await service.useProject(channelKey, "demo-app");
27
- await service.proposalProject(channelKey, "demo-change Demo change");
28
- await service.recordPlanningMessageFromContext(promptContext, userPrompt);
29
- await service.handleBeforePromptBuild(
30
- { prompt: userPrompt, messages: [] },
31
- promptContext,
32
- );
33
-
34
- await service.handleAgentEnd(
35
- {
36
- success: true,
37
- messages: [
38
- { role: "user", content: userPrompt },
39
- {
40
- role: "assistant",
41
- content: [
42
- {
43
- type: "text",
44
- text: "We should keep short-lived access tokens, add a refresh endpoint, and split forgot-password into request and confirm steps.",
45
- },
46
- ],
47
- },
48
- ],
49
- },
50
- promptContext,
51
- );
52
-
53
- const repoStatePaths = getRepoStatePaths(repoPath, "archives");
54
- const journalStore = new PlanningJournalStore(repoStatePaths.planningJournalFile);
55
- const entries = await journalStore.list("demo-change");
56
- const project = await stateStore.getActiveProject(channelKey);
57
-
58
- assert.equal(entries.length, 2);
59
- assert.equal(entries[0]?.role, "user");
60
- assert.equal(entries[1]?.role, "assistant");
61
- assert.match(entries[1]?.text ?? "", /refresh endpoint/i);
62
- assert.equal(project?.planningJournal?.dirty, true);
63
- assert.equal(project?.planningJournal?.entryCount, 2);
64
- });
65
-
66
- test("passive assistant control replies are not appended to the planning journal", async () => {
67
- const harness = await createServiceHarness("clawspec-passive-assistant-journal-");
68
- const { service, repoPath } = harness;
69
- const channelKey = "discord:passive-assistant-journal:default:main";
70
- const promptContext = createPromptContext("passive-assistant-journal");
71
- const userPrompt = "Add a help endpoint for the API catalog.";
72
-
73
- await service.startProject(channelKey);
74
- await service.useProject(channelKey, "demo-app");
75
- await service.proposalProject(channelKey, "demo-change Demo change");
76
- await service.recordPlanningMessageFromContext(promptContext, userPrompt);
77
- await service.handleBeforePromptBuild(
78
- { prompt: userPrompt, messages: [] },
79
- promptContext,
80
- );
81
-
82
- await service.handleAgentEnd(
83
- {
84
- success: true,
85
- messages: [
86
- { role: "user", content: userPrompt },
87
- {
88
- role: "assistant",
89
- content: "Continue describing requirements if needed.\nNext step: run `cs-plan`.",
90
- },
91
- ],
92
- },
93
- promptContext,
94
- );
95
-
96
- const repoStatePaths = getRepoStatePaths(repoPath, "archives");
97
- const journalStore = new PlanningJournalStore(repoStatePaths.planningJournalFile);
98
- const entries = await journalStore.list("demo-change");
99
-
100
- assert.equal(entries.length, 1);
101
- assert.equal(entries[0]?.role, "user");
102
- });
103
-
104
- test("assistant discussion replies are not journaled after the chat is detached", async () => {
105
- const harness = await createServiceHarness("clawspec-detached-assistant-journal-");
106
- const { service, repoPath } = harness;
107
- const channelKey = "discord:detached-assistant-journal:default:main";
108
- const promptContext = createPromptContext("detached-assistant-journal");
109
- const userPrompt = "Add a city weather endpoint.";
110
-
111
- await service.startProject(channelKey);
112
- await service.useProject(channelKey, "demo-app");
113
- await service.proposalProject(channelKey, "demo-change Demo change");
114
- await service.handleBeforePromptBuild(
115
- { prompt: userPrompt, messages: [] },
116
- promptContext,
117
- );
118
- await service.detachProject(channelKey);
119
-
120
- await service.handleAgentEnd(
121
- {
122
- success: true,
123
- messages: [
124
- { role: "user", content: userPrompt },
125
- { role: "assistant", content: "We can resolve the city name first, then fetch weather details." },
126
- ],
127
- },
128
- promptContext,
129
- );
130
-
131
- const repoStatePaths = getRepoStatePaths(repoPath, "archives");
132
- const journalStore = new PlanningJournalStore(repoStatePaths.planningJournalFile);
133
- const entries = await journalStore.list("demo-change");
134
-
135
- assert.equal(entries.length, 0);
136
- });
137
-
138
- test("inbound bot/system messages are ignored by planning journal capture", async () => {
139
- const harness = await createServiceHarness("clawspec-ignore-bot-inbound-");
140
- const { service, stateStore, repoPath } = harness;
141
- const channelKey = "discord:ignore-bot-inbound:default:main";
142
- const promptContext = createPromptContext("ignore-bot-inbound");
143
-
144
- await service.startProject(channelKey);
145
- await service.useProject(channelKey, "demo-app");
146
- await service.proposalProject(channelKey, "demo-change Demo change");
147
-
148
- await service.recordPlanningMessageFromContext(
149
- {
150
- ...promptContext,
151
- from: "assistant",
152
- metadata: {
153
- role: "assistant",
154
- fromSelf: true,
155
- },
156
- },
157
- "Planning ready. Next: run `cs-work` to start implementation.",
158
- );
159
-
160
- const repoStatePaths = getRepoStatePaths(repoPath, "archives");
161
- const journalStore = new PlanningJournalStore(repoStatePaths.planningJournalFile);
162
- const entries = await journalStore.list("demo-change");
163
- const project = await stateStore.getActiveProject(channelKey);
164
-
165
- assert.equal(entries.length, 0);
166
- assert.equal(project?.planningJournal?.dirty, false);
167
- assert.equal(project?.planningJournal?.entryCount, 0);
168
- });
169
-
170
- test("heartbeat assistant replies are not appended to the planning journal", async () => {
171
- const harness = await createServiceHarness("clawspec-ignore-heartbeat-journal-");
172
- const { service, repoPath } = harness;
173
- const channelKey = "discord:ignore-heartbeat-journal:default:main";
174
- const promptContext = createPromptContext("ignore-heartbeat-journal");
175
- const userPrompt = "Add one more API endpoint.";
176
-
177
- await service.startProject(channelKey);
178
- await service.useProject(channelKey, "demo-app");
179
- await service.proposalProject(channelKey, "demo-change Demo change");
180
- await service.recordPlanningMessageFromContext(promptContext, userPrompt);
181
- await service.handleBeforePromptBuild(
182
- { prompt: userPrompt, messages: [] },
183
- promptContext,
184
- );
185
-
186
- await service.handleAgentEnd(
187
- {
188
- success: true,
189
- messages: [
190
- { role: "user", content: userPrompt },
191
- { role: "assistant", content: "HEARTBEAT_OK" },
192
- ],
193
- },
194
- promptContext,
195
- );
196
-
197
- const repoStatePaths = getRepoStatePaths(repoPath, "archives");
198
- const journalStore = new PlanningJournalStore(repoStatePaths.planningJournalFile);
199
- const entries = await journalStore.list("demo-change");
200
-
201
- assert.equal(entries.length, 1);
202
- assert.equal(entries[0]?.role, "user");
203
- });
@@ -1,24 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
- import { buildHelpText } from "../src/orchestrator/helpers.ts";
4
-
5
- test("help text only advertises the clawspec command surface", () => {
6
- const help = buildHelpText();
7
-
8
- assert.match(help, /\/clawspec workspace/);
9
- assert.match(help, /\/clawspec proposal <change-name> \[description\]/);
10
- assert.match(help, /\/clawspec continue/);
11
- assert.match(help, /\/clawspec doctor/);
12
- assert.match(help, /\/clawspec detach/);
13
- assert.match(help, /cs-detach/);
14
- assert.match(help, /legacy aliases/);
15
- assert.match(help, /cs-plan/);
16
- assert.match(help, /cs-work/);
17
-
18
- assert.doesNotMatch(help, /`\/project\b/);
19
- assert.doesNotMatch(help, /\/clawspec apply\b/);
20
- assert.doesNotMatch(help, /cs-proposal\b/);
21
- assert.doesNotMatch(help, /cs-propose\b/);
22
- assert.doesNotMatch(help, /cs-pop\b/);
23
- assert.doesNotMatch(help, /cs-push\b/);
24
- });