clawspec 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +908 -0
- package/README.zh-CN.md +914 -0
- package/index.ts +3 -0
- package/openclaw.plugin.json +129 -0
- package/package.json +52 -0
- package/skills/openspec-apply-change.md +146 -0
- package/skills/openspec-explore.md +75 -0
- package/skills/openspec-propose.md +102 -0
- package/src/acp/client.ts +693 -0
- package/src/config.ts +220 -0
- package/src/control/keywords.ts +72 -0
- package/src/dependencies/acpx.ts +221 -0
- package/src/dependencies/openspec.ts +148 -0
- package/src/execution/session.ts +56 -0
- package/src/execution/state.ts +125 -0
- package/src/index.ts +179 -0
- package/src/memory/store.ts +118 -0
- package/src/openspec/cli.ts +279 -0
- package/src/openspec/tasks.ts +40 -0
- package/src/orchestrator/helpers.ts +312 -0
- package/src/orchestrator/service.ts +2971 -0
- package/src/planning/journal.ts +118 -0
- package/src/rollback/store.ts +173 -0
- package/src/state/locks.ts +133 -0
- package/src/state/store.ts +527 -0
- package/src/types.ts +301 -0
- package/src/utils/args.ts +88 -0
- package/src/utils/channel-key.ts +66 -0
- package/src/utils/env-path.ts +31 -0
- package/src/utils/fs.ts +218 -0
- package/src/utils/markdown.ts +136 -0
- package/src/utils/messages.ts +5 -0
- package/src/utils/paths.ts +127 -0
- package/src/utils/shell-command.ts +227 -0
- package/src/utils/slug.ts +50 -0
- package/src/watchers/manager.ts +3042 -0
- package/src/watchers/notifier.ts +69 -0
- package/src/worker/prompts.ts +484 -0
- package/src/worker/skills.ts +52 -0
- package/src/workspace/store.ts +140 -0
- package/test/acp-client.test.ts +234 -0
- package/test/acpx-dependency.test.ts +112 -0
- package/test/assistant-journal.test.ts +136 -0
- package/test/command-surface.test.ts +23 -0
- package/test/config.test.ts +77 -0
- package/test/detach-attach.test.ts +98 -0
- package/test/file-lock.test.ts +78 -0
- package/test/fs-utils.test.ts +22 -0
- package/test/helpers/harness.ts +241 -0
- package/test/helpers.test.ts +108 -0
- package/test/keywords.test.ts +80 -0
- package/test/notifier.test.ts +29 -0
- package/test/openspec-dependency.test.ts +67 -0
- package/test/pause-cancel.test.ts +55 -0
- package/test/planning-journal.test.ts +69 -0
- package/test/plugin-registration.test.ts +35 -0
- package/test/project-memory.test.ts +42 -0
- package/test/proposal.test.ts +24 -0
- package/test/queue-planning.test.ts +247 -0
- package/test/queue-work.test.ts +110 -0
- package/test/recovery.test.ts +576 -0
- package/test/service-archive.test.ts +82 -0
- package/test/shell-command.test.ts +48 -0
- package/test/state-store.test.ts +74 -0
- package/test/tasks-and-checkpoint.test.ts +60 -0
- package/test/use-project.test.ts +19 -0
- package/test/watcher-planning.test.ts +504 -0
- package/test/watcher-work.test.ts +1741 -0
- package/test/worker-command.test.ts +66 -0
- package/test/worker-skills.test.ts +12 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,82 @@
|
|
|
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 { ProjectMemoryStore } from "../src/memory/store.ts";
|
|
7
|
+
import { ClawSpecService } from "../src/orchestrator/service.ts";
|
|
8
|
+
import { ProjectStateStore } from "../src/state/store.ts";
|
|
9
|
+
import { pathExists, writeUtf8 } from "../src/utils/fs.ts";
|
|
10
|
+
import { WorkspaceStore } from "../src/workspace/store.ts";
|
|
11
|
+
|
|
12
|
+
test("service writes archive bundles from visible-execution state", async () => {
|
|
13
|
+
const tempRoot = await mkdtemp(path.join(os.tmpdir(), "clawspec-service-"));
|
|
14
|
+
const repoPath = path.join(tempRoot, "repo");
|
|
15
|
+
const workspacePath = path.join(tempRoot, "workspace");
|
|
16
|
+
const changeName = "demo-change";
|
|
17
|
+
await mkdir(path.join(repoPath, "openspec", "changes", changeName), { recursive: true });
|
|
18
|
+
await mkdir(workspacePath, { recursive: true });
|
|
19
|
+
|
|
20
|
+
await writeUtf8(
|
|
21
|
+
path.join(repoPath, "openspec", "changes", changeName, "tasks.md"),
|
|
22
|
+
[
|
|
23
|
+
"## 1. Setup",
|
|
24
|
+
"",
|
|
25
|
+
"- [x] 1.1 Create plugin",
|
|
26
|
+
"- [ ] 1.2 Add lifecycle store",
|
|
27
|
+
"",
|
|
28
|
+
].join("\n"),
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const stateStore = new ProjectStateStore(tempRoot, "archives");
|
|
32
|
+
const memoryStore = new ProjectMemoryStore(path.join(tempRoot, "memory.json"));
|
|
33
|
+
const workspaceStore = new WorkspaceStore(path.join(tempRoot, "workspace-state.json"), workspacePath);
|
|
34
|
+
await stateStore.initialize();
|
|
35
|
+
await memoryStore.initialize();
|
|
36
|
+
await workspaceStore.initialize();
|
|
37
|
+
|
|
38
|
+
let project = await stateStore.createProject("channel:demo");
|
|
39
|
+
project = await stateStore.updateProject("channel:demo", (current) => ({
|
|
40
|
+
...current,
|
|
41
|
+
workspacePath,
|
|
42
|
+
repoPath,
|
|
43
|
+
projectName: "repo",
|
|
44
|
+
changeName,
|
|
45
|
+
changeDir: path.join(repoPath, "openspec", "changes", changeName),
|
|
46
|
+
latestSummary: "Latest summary text",
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
const fakeApi = {
|
|
50
|
+
logger: {
|
|
51
|
+
info: () => undefined,
|
|
52
|
+
warn: () => undefined,
|
|
53
|
+
error: () => undefined,
|
|
54
|
+
debug: () => undefined,
|
|
55
|
+
},
|
|
56
|
+
} as any;
|
|
57
|
+
|
|
58
|
+
const service = new ClawSpecService({
|
|
59
|
+
api: fakeApi,
|
|
60
|
+
config: {} as any,
|
|
61
|
+
logger: fakeApi.logger,
|
|
62
|
+
stateStore,
|
|
63
|
+
memoryStore,
|
|
64
|
+
openSpec: {} as any,
|
|
65
|
+
archiveDirName: "archives",
|
|
66
|
+
defaultWorkspace: workspacePath,
|
|
67
|
+
defaultWorkerAgentId: "codex",
|
|
68
|
+
workspaceStore,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
await (service as any).ensureProjectSupportFiles(project);
|
|
72
|
+
const counts = await (service as any).loadTaskCounts(project);
|
|
73
|
+
assert.equal(counts.complete, 1);
|
|
74
|
+
assert.equal(counts.remaining, 1);
|
|
75
|
+
|
|
76
|
+
const archivePath = await (service as any).writeArchiveBundle(project, counts);
|
|
77
|
+
assert.equal(await pathExists(path.join(archivePath, "resume-context.md")), true);
|
|
78
|
+
assert.equal(await pathExists(path.join(archivePath, "session-summary.md")), true);
|
|
79
|
+
assert.equal(await pathExists(path.join(archivePath, "changed-files.md")), true);
|
|
80
|
+
assert.equal(await pathExists(path.join(archivePath, "decision-log.md")), true);
|
|
81
|
+
assert.equal(await pathExists(path.join(archivePath, "run-metadata.json")), true);
|
|
82
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
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("useProject initializes repo and selects project", async () => {
|
|
8
|
+
const harness = await createServiceHarness("clawspec-use-project-");
|
|
9
|
+
const { service, stateStore, workspacePath } = harness;
|
|
10
|
+
const channelKey = "discord:use-project:default:main";
|
|
11
|
+
|
|
12
|
+
await service.startProject(channelKey);
|
|
13
|
+
const result = await service.useProject(channelKey, "demo-app");
|
|
14
|
+
const project = await stateStore.getActiveProject(channelKey);
|
|
15
|
+
|
|
16
|
+
assert.match(result.text ?? "", /Project Selected/);
|
|
17
|
+
assert.equal(project?.repoPath, path.join(workspacePath, "demo-app"));
|
|
18
|
+
assert.equal(await pathExists(path.join(workspacePath, "demo-app", "openspec", "config.yaml")), true);
|
|
19
|
+
});
|