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,48 +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, writeFile } from "node:fs/promises";
6
- import { runShellCommand } from "../src/utils/shell-command.ts";
7
-
8
- test("runShellCommand handles cwd and script paths that contain spaces", async () => {
9
- const tempRoot = await mkdtemp(path.join(os.tmpdir(), "clawspec-shell-"));
10
- const spaceDir = path.join(tempRoot, "space dir");
11
- const scriptPath = path.join(spaceDir, "echo script.js");
12
- await mkdir(spaceDir, { recursive: true });
13
- await writeFile(scriptPath, `
14
- console.log(process.cwd());
15
- console.log(process.argv[2]);
16
- `, "utf8");
17
-
18
- const result = await runShellCommand({
19
- command: process.execPath,
20
- args: [scriptPath, "hello world"],
21
- cwd: spaceDir,
22
- timeoutMs: 2_000,
23
- });
24
-
25
- assert.equal(result.error, undefined);
26
- assert.equal(result.code, 0);
27
- assert.match(result.stdout, /space dir/);
28
- assert.match(result.stdout, /hello world/);
29
- });
30
-
31
- test("runShellCommand surfaces timeout errors", async () => {
32
- const tempRoot = await mkdtemp(path.join(os.tmpdir(), "clawspec-shell-timeout-"));
33
- const scriptPath = path.join(tempRoot, "sleep.js");
34
- await writeFile(scriptPath, `
35
- setTimeout(() => {
36
- console.log("finished");
37
- }, 5_000);
38
- `, "utf8");
39
-
40
- const result = await runShellCommand({
41
- command: process.execPath,
42
- args: [scriptPath],
43
- cwd: tempRoot,
44
- timeoutMs: 100,
45
- });
46
-
47
- assert.match(result.error?.message ?? "", /timed out/i);
48
- });
@@ -1,74 +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 { ActiveProjectConflictError, ProjectStateStore } from "../src/state/store.ts";
7
- import { readJsonFile, removeIfExists, writeJsonFile } from "../src/utils/fs.ts";
8
- import { getActiveProjectMapPath, getPluginStateRoot } from "../src/utils/paths.ts";
9
- import { withFileLock } from "../src/state/locks.ts";
10
-
11
- test("state store enforces one active project per channel and migrates to repo-local state", async () => {
12
- const tempRoot = await mkdtemp(path.join(os.tmpdir(), "clawspec-state-"));
13
- const repoPath = path.join(tempRoot, "repo");
14
- await mkdir(repoPath, { recursive: true });
15
-
16
- const store = new ProjectStateStore(tempRoot, "archives");
17
- await store.initialize();
18
-
19
- const project = await store.createProject("channel:demo");
20
- assert.equal(project.status, "idle");
21
-
22
- await assert.rejects(() => store.createProject("channel:demo"), ActiveProjectConflictError);
23
-
24
- const updated = await store.setRepoPath("channel:demo", repoPath, "demo");
25
- assert.equal(updated.repoPath, repoPath);
26
- assert.match(updated.storagePath, /\.openclaw[\\\/]clawspec[\\\/]state\.json$/);
27
-
28
- const described = await store.setDescription(
29
- "channel:demo",
30
- "Build the project orchestrator",
31
- "Build the project orchestrator",
32
- "build-project-orchestrator",
33
- );
34
- assert.equal(described.changeName, "build-project-orchestrator");
35
- assert.equal((await store.getActiveProject("channel:demo"))?.repoPath, repoPath);
36
- assert.equal((await store.listActiveProjects()).length, 1);
37
- });
38
-
39
- test("updateProject waits for the active map lock instead of failing on a transiently missing map file", async () => {
40
- const tempRoot = await mkdtemp(path.join(os.tmpdir(), "clawspec-state-lock-"));
41
- const store = new ProjectStateStore(tempRoot, "archives");
42
- await store.initialize();
43
- await store.createProject("channel:demo");
44
-
45
- const activeMapPath = getActiveProjectMapPath(tempRoot);
46
- const activeMapLockPath = path.join(getPluginStateRoot(tempRoot), "locks", "active-projects.lock");
47
- const originalMap = await readJsonFile(activeMapPath, { version: 1, channels: {} as Record<string, unknown> });
48
-
49
- let settled = false;
50
- let updatePromise: Promise<unknown> | undefined;
51
-
52
- await withFileLock(activeMapLockPath, async () => {
53
- await removeIfExists(activeMapPath);
54
- updatePromise = store.updateProject("channel:demo", (current) => ({
55
- ...current,
56
- latestSummary: "updated after lock release",
57
- }));
58
- updatePromise.finally(() => {
59
- settled = true;
60
- });
61
-
62
- await delay(50);
63
- assert.equal(settled, false);
64
-
65
- await writeJsonFile(activeMapPath, originalMap);
66
- });
67
-
68
- const updated = await updatePromise;
69
- assert.equal((updated as { latestSummary?: string }).latestSummary, "updated after lock release");
70
- });
71
-
72
- function delay(ms: number): Promise<void> {
73
- return new Promise((resolve) => setTimeout(resolve, ms));
74
- }
@@ -1,60 +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 { getNextIncompleteTask, parseTasksMarkdown } from "../src/openspec/tasks.ts";
7
- import { readExecutionControl, readExecutionResult, isExecutionTriggerText } from "../src/execution/state.ts";
8
- import { writeJsonFile } from "../src/utils/fs.ts";
9
-
10
- test("tasks parser keeps checkbox order and selects the next unchecked task", () => {
11
- const taskList = parseTasksMarkdown(`
12
- ## 1. Setup
13
-
14
- - [x] 1.1 Create package
15
- - [ ] 1.2 Add state store
16
- - [ ] 1.3 Add docs
17
- `);
18
-
19
- assert.equal(taskList.counts.total, 3);
20
- assert.equal(taskList.counts.complete, 1);
21
- assert.equal(taskList.counts.remaining, 2);
22
- assert.equal(getNextIncompleteTask(taskList)?.taskId, "1.2");
23
- });
24
-
25
- test("execution helpers read structured control and result files", async () => {
26
- const tempRoot = await mkdtemp(path.join(os.tmpdir(), "clawspec-execution-"));
27
- const controlPath = path.join(tempRoot, "execution-control.json");
28
- const resultPath = path.join(tempRoot, "execution-result.json");
29
-
30
- await writeJsonFile(controlPath, {
31
- version: 1,
32
- changeName: "demo-change",
33
- mode: "apply",
34
- state: "armed",
35
- armedAt: new Date().toISOString(),
36
- pauseRequested: false,
37
- cancelRequested: false,
38
- });
39
- await writeJsonFile(resultPath, {
40
- version: 1,
41
- changeName: "demo-change",
42
- mode: "apply",
43
- status: "paused",
44
- timestamp: new Date().toISOString(),
45
- summary: "Paused cleanly.",
46
- progressMade: true,
47
- changedFiles: ["src/index.ts"],
48
- notes: ["wrote execution result"],
49
- });
50
-
51
- const control = await readExecutionControl(controlPath);
52
- const result = await readExecutionResult(resultPath);
53
-
54
- assert.equal(control?.changeName, "demo-change");
55
- assert.equal(control?.state, "armed");
56
- assert.equal(result?.status, "paused");
57
- assert.deepEqual(result?.changedFiles, ["src/index.ts"]);
58
- assert.equal(isExecutionTriggerText("continue"), true);
59
- assert.equal(isExecutionTriggerText("Need one more requirement"), false);
60
- });
@@ -1,67 +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 { pathExists } from "../src/utils/fs.ts";
6
- import { createServiceHarness } from "./helpers/harness.ts";
7
-
8
- test("useProject initializes repo and selects project", async () => {
9
- const harness = await createServiceHarness("clawspec-use-project-");
10
- const { service, stateStore, workspacePath } = harness;
11
- const channelKey = "discord:use-project:default:main";
12
-
13
- await service.startProject(channelKey);
14
- const result = await service.useProject(channelKey, "demo-app");
15
- const project = await stateStore.getActiveProject(channelKey);
16
-
17
- assert.match(result.text ?? "", /Project Selected/);
18
- assert.equal(project?.repoPath, path.join(workspacePath, "demo-app"));
19
- assert.equal(await pathExists(path.join(workspacePath, "demo-app", "openspec", "config.yaml")), true);
20
- });
21
-
22
- test("workspaceProject resolves quoted home-relative path without nesting into default workspace", async () => {
23
- const harness = await createServiceHarness("clawspec-workspace-home-");
24
- const { service, stateStore, workspacePath } = harness;
25
- const channelKey = "discord:workspace-home:default:main";
26
-
27
- await service.startProject(channelKey);
28
- const result = await service.workspaceProject(channelKey, "\"~/Desktop/workspace/ai_workspacce\"");
29
- const project = await stateStore.getActiveProject(channelKey);
30
- const expected = path.join(os.homedir(), "Desktop", "workspace", "ai_workspacce");
31
-
32
- assert.match(result.text ?? "", /Workspace switched/);
33
- assert.equal(project?.workspacePath, expected);
34
- assert.equal(project?.workspacePath?.includes(workspacePath), false);
35
- });
36
-
37
- test("workspaceProject keeps absolute paths as absolute targets", async () => {
38
- const harness = await createServiceHarness("clawspec-workspace-abs-");
39
- const { service, stateStore } = harness;
40
- const channelKey = "discord:workspace-abs:default:main";
41
-
42
- await service.startProject(channelKey);
43
-
44
- const unixAbsolute = process.platform === "win32" ? "/var/tmp/clawspec-abs" : "/tmp/clawspec-abs";
45
- await service.workspaceProject(channelKey, unixAbsolute);
46
- const projectAfterUnix = await stateStore.getActiveProject(channelKey);
47
- assert.equal(projectAfterUnix?.workspacePath, path.normalize(unixAbsolute));
48
-
49
- const driveAbsolute = "C:\\Users\\dev\\workspace\\clawspec-abs";
50
- await service.workspaceProject(channelKey, driveAbsolute);
51
- const projectAfterDrive = await stateStore.getActiveProject(channelKey);
52
- assert.equal(projectAfterDrive?.workspacePath, path.normalize(driveAbsolute));
53
- });
54
-
55
- test("useProject accepts quoted project names with spaces", async () => {
56
- const harness = await createServiceHarness("clawspec-use-project-space-");
57
- const { service, stateStore, workspacePath } = harness;
58
- const channelKey = "discord:use-project-space:default:main";
59
-
60
- await service.startProject(channelKey);
61
- const result = await service.useProject(channelKey, "\"team app\"");
62
- const project = await stateStore.getActiveProject(channelKey);
63
-
64
- assert.match(result.text ?? "", /Project Selected/);
65
- assert.equal(project?.projectName, "team app");
66
- assert.equal(project?.repoPath, path.join(workspacePath, "team app"));
67
- });