clawspec 1.0.15 → 1.0.19
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 +16 -0
- package/README.zh-CN.md +16 -0
- package/package.json +2 -2
- package/src/acp/client.ts +17 -1
- package/src/control/keywords.ts +18 -2
- package/src/orchestrator/helpers.ts +1 -0
- package/src/orchestrator/service.ts +143 -33
- package/src/watchers/manager.ts +20 -6
- package/src/watchers/notifier.ts +1 -0
- package/src/worker/io-helper.ts +6 -5
- package/test/command-surface.test.ts +1 -0
- package/test/doctor.test.ts +142 -0
- package/test/helpers/harness.ts +6 -2
- package/test/recovery.test.ts +52 -25
- package/test/watcher-planning.test.ts +47 -18
- package/test/watcher-work.test.ts +83 -53
- package/test/worker-io-helper.test.ts +1 -1
- package/src/utils/debug-log.ts +0 -14
package/README.md
CHANGED
|
@@ -552,6 +552,8 @@ That loop is the normal operating model:
|
|
|
552
552
|
| `/clawspec status` | Any time you want a reconciled project snapshot | Renders current workspace, project, change, lifecycle, task counts, journal status, and next step |
|
|
553
553
|
| `/clawspec archive` | After all tasks are complete and you want to close the change | Validates and archives the finished OpenSpec change, then clears active change state |
|
|
554
554
|
| `/clawspec cancel` | When you want to abandon the active change | Restores tracked files from snapshots, removes the change, stops execution state, and clears the active change |
|
|
555
|
+
| `/clawspec doctor` | When something isn't working as expected | Runs automated diagnostics on your environment and configuration, reports detected issues with suggested fixes |
|
|
556
|
+
| `/clawspec doctor fix` | After `/clawspec doctor` reports a fixable issue | Automatically applies safe fixes for all detected issues that support auto-repair |
|
|
555
557
|
|
|
556
558
|
Auxiliary host CLI:
|
|
557
559
|
|
|
@@ -858,6 +860,20 @@ Use one of:
|
|
|
858
860
|
|
|
859
861
|
That is by design. Cancel restores tracked files from the ClawSpec snapshot baseline for the active change. It is intentionally narrower than a blanket Git restore.
|
|
860
862
|
|
|
863
|
+
### Worker fails with "stdin is not a terminal" or session creation returns no result
|
|
864
|
+
|
|
865
|
+
Cause:
|
|
866
|
+
|
|
867
|
+
- `~/.acpx/config.json` has a custom agent entry (e.g. `agents.codex` pointing to a local CLI binary)
|
|
868
|
+
- acpx cannot spawn the agent in a non-interactive environment when it is configured as a raw CLI command
|
|
869
|
+
|
|
870
|
+
What to do:
|
|
871
|
+
|
|
872
|
+
1. Run `/clawspec doctor` to check for known configuration issues.
|
|
873
|
+
2. If `/clawspec doctor` reports custom agent entries, run `/clawspec doctor fix` to clear them automatically.
|
|
874
|
+
3. Alternatively, edit `~/.acpx/config.json` manually and set `"agents": {}`.
|
|
875
|
+
4. Retry `cs-work` or `/clawspec continue`.
|
|
876
|
+
|
|
861
877
|
## Development And Validation
|
|
862
878
|
|
|
863
879
|
Useful checks while editing the plugin:
|
package/README.zh-CN.md
CHANGED
|
@@ -556,6 +556,8 @@ cs-work
|
|
|
556
556
|
| `/clawspec status` | 任何时候想看一次统一状态快照时 | 输出当前 workspace、project、change、生命周期、任务计数、journal 状态和下一步建议 |
|
|
557
557
|
| `/clawspec archive` | 所有任务完成,准备收尾时 | 校验并归档当前 OpenSpec change,同时清理活动 change 状态 |
|
|
558
558
|
| `/clawspec cancel` | 想放弃当前 change 时 | 按快照恢复已跟踪文件、删除 change、停止执行状态,并清掉活动 change |
|
|
559
|
+
| `/clawspec doctor` | 遇到异常行为时 | 对环境和配置进行自动化诊断,报告检测到的问题并给出修复建议 |
|
|
560
|
+
| `/clawspec doctor fix` | `/clawspec doctor` 检测到可修复问题后 | 自动应用所有支持自动修复的安全修复 |
|
|
559
561
|
|
|
560
562
|
辅助 host CLI:
|
|
561
563
|
|
|
@@ -869,6 +871,20 @@ cs-detach
|
|
|
869
871
|
|
|
870
872
|
这是设计如此。Cancel 只会恢复 ClawSpec 为当前 change 跟踪的文件快照,而不是做整仓库级别的 Git 还原。
|
|
871
873
|
|
|
874
|
+
### Worker 报错 "stdin is not a terminal" 或 session 创建无结果
|
|
875
|
+
|
|
876
|
+
原因:
|
|
877
|
+
|
|
878
|
+
- `~/.acpx/config.json` 中的 `agents` 字段存在自定义配置(例如 `agents.codex` 指向了本地 CLI 路径)
|
|
879
|
+
- acpx 在非交互式环境下无法通过 raw CLI 方式启动 agent
|
|
880
|
+
|
|
881
|
+
解决方法:
|
|
882
|
+
|
|
883
|
+
1. 运行 `/clawspec doctor` 检查配置问题。
|
|
884
|
+
2. 如果 doctor 报告了自定义 agent 配置,运行 `/clawspec doctor fix` 自动修复。
|
|
885
|
+
3. 也可以手动编辑 `~/.acpx/config.json`,将 `"agents"` 设置为 `{}`。
|
|
886
|
+
4. 然后重新运行 `cs-work` 或 `/clawspec continue`。
|
|
887
|
+
|
|
872
888
|
## 开发与验证
|
|
873
889
|
|
|
874
890
|
开发时常用检查:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clawspec",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.19",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenClaw plugin that orchestrates OpenSpec project workflows with visible main-agent execution.",
|
|
6
6
|
"keywords": [
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"scripts": {
|
|
47
47
|
"check": "node --experimental-strip-types -e \"import('./index.ts')\"",
|
|
48
48
|
"test": "node --experimental-strip-types --test --test-reporter spec test/*.test.ts",
|
|
49
|
-
"test:fast": "node --experimental-strip-types --test --test-reporter spec test/acp-client.test.ts test/acpx-dependency.test.ts test/assistant-journal.test.ts test/command-surface.test.ts test/config.test.ts test/detach-attach.test.ts test/file-lock.test.ts test/fs-utils.test.ts test/helpers.test.ts test/keywords.test.ts test/notifier.test.ts test/openspec-dependency.test.ts test/paths-utils.test.ts test/pause-cancel.test.ts test/planning-journal.test.ts test/plugin-registration.test.ts test/project-memory.test.ts test/proposal.test.ts test/queue-planning.test.ts test/queue-work.test.ts test/service-archive.test.ts test/shell-command.test.ts test/state-store.test.ts test/tasks-and-checkpoint.test.ts test/use-project.test.ts test/worker-command.test.ts test/worker-io-helper.test.ts test/worker-skills.test.ts",
|
|
49
|
+
"test:fast": "node --experimental-strip-types --test --test-reporter spec test/acp-client.test.ts test/acpx-dependency.test.ts test/assistant-journal.test.ts test/command-surface.test.ts test/config.test.ts test/detach-attach.test.ts test/doctor.test.ts test/file-lock.test.ts test/fs-utils.test.ts test/helpers.test.ts test/keywords.test.ts test/notifier.test.ts test/openspec-dependency.test.ts test/paths-utils.test.ts test/pause-cancel.test.ts test/planning-journal.test.ts test/plugin-registration.test.ts test/project-memory.test.ts test/proposal.test.ts test/queue-planning.test.ts test/queue-work.test.ts test/service-archive.test.ts test/shell-command.test.ts test/state-store.test.ts test/tasks-and-checkpoint.test.ts test/use-project.test.ts test/worker-command.test.ts test/worker-io-helper.test.ts test/worker-skills.test.ts",
|
|
50
50
|
"test:slow": "node --experimental-strip-types --test --test-reporter spec test/watcher-planning.test.ts test/watcher-work.test.ts test/recovery.test.ts"
|
|
51
51
|
},
|
|
52
52
|
"engines": {
|
package/src/acp/client.ts
CHANGED
|
@@ -141,7 +141,7 @@ export class AcpWorkerClient {
|
|
|
141
141
|
allowErrorCodes: ["NO_SESSION"],
|
|
142
142
|
});
|
|
143
143
|
|
|
144
|
-
if (events.some((event) => toAcpxErrorEvent(event)?.code === "NO_SESSION")) {
|
|
144
|
+
if (events.some((event) => toAcpxErrorEvent(event)?.code === "NO_SESSION") || events.length === 0) {
|
|
145
145
|
events = await this.runControlCommand({
|
|
146
146
|
agentId: descriptor.agentId,
|
|
147
147
|
cwd: descriptor.cwd,
|
|
@@ -149,6 +149,14 @@ export class AcpWorkerClient {
|
|
|
149
149
|
});
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
if (events.length === 0 || !extractSessionIdentifiers(events).backendSessionId) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
`Failed to create acpx session for agent "${descriptor.agentId}".\n`
|
|
155
|
+
+ `The session command returned no result. This usually means the agent is misconfigured.\n\n`
|
|
156
|
+
+ `Run \`/clawspec doctor\` to diagnose and fix common acpx configuration issues.`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
152
160
|
const identifiers = extractSessionIdentifiers(events);
|
|
153
161
|
const handle: AcpWorkerHandle = {
|
|
154
162
|
sessionKey: descriptor.sessionKey,
|
|
@@ -186,6 +194,13 @@ export class AcpWorkerClient {
|
|
|
186
194
|
cwd: descriptor.cwd,
|
|
187
195
|
env: this.env,
|
|
188
196
|
});
|
|
197
|
+
|
|
198
|
+
const args = this.buildPromptArgs({
|
|
199
|
+
agentId: descriptor.agentId,
|
|
200
|
+
cwd: descriptor.cwd,
|
|
201
|
+
sessionKey: descriptor.sessionKey,
|
|
202
|
+
});
|
|
203
|
+
|
|
189
204
|
child.stdout.setEncoding("utf8");
|
|
190
205
|
child.stderr.setEncoding("utf8");
|
|
191
206
|
child.stdin.end(params.text);
|
|
@@ -250,6 +265,7 @@ export class AcpWorkerClient {
|
|
|
250
265
|
|
|
251
266
|
const exit = await waitForExit(child);
|
|
252
267
|
this.recordSessionExit(params.sessionKey, descriptor, child.pid, exit.code, exit.signal, stderr);
|
|
268
|
+
|
|
253
269
|
if (exit.error) {
|
|
254
270
|
throw exit.error;
|
|
255
271
|
}
|
package/src/control/keywords.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
|
|
1
2
|
export type ClawSpecKeywordKind =
|
|
2
3
|
| "plan"
|
|
3
4
|
| "work"
|
|
@@ -59,11 +60,26 @@ export function extractEmbeddedClawSpecKeyword(text: string): ClawSpecKeywordInt
|
|
|
59
60
|
return direct;
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
const lines = text.split(/\r?\n/);
|
|
64
|
+
for (const line of lines) {
|
|
65
|
+
const trimmed = line.trim();
|
|
66
|
+
if (!trimmed) continue;
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
// Try parsing the whole line first
|
|
70
|
+
const parsed = parseClawSpecKeyword(trimmed);
|
|
64
71
|
if (parsed) {
|
|
65
72
|
return parsed;
|
|
66
73
|
}
|
|
74
|
+
|
|
75
|
+
// Try each word in the line
|
|
76
|
+
const words = trimmed.split(/\s+/);
|
|
77
|
+
for (const word of words) {
|
|
78
|
+
const wordParsed = parseClawSpecKeyword(word);
|
|
79
|
+
if (wordParsed) {
|
|
80
|
+
return wordParsed;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
67
83
|
}
|
|
68
84
|
|
|
69
85
|
return null;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import os from "node:os";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import type {
|
|
3
4
|
OpenClawConfig,
|
|
@@ -6,7 +7,6 @@ import type {
|
|
|
6
7
|
PluginCommandResult,
|
|
7
8
|
PluginLogger,
|
|
8
9
|
} from "openclaw/plugin-sdk";
|
|
9
|
-
import { debugLog } from "../utils/debug-log.ts";
|
|
10
10
|
import { ProjectMemoryStore } from "../memory/store.ts";
|
|
11
11
|
import { OpenSpecClient, OpenSpecCommandError } from "../openspec/cli.ts";
|
|
12
12
|
import { parseTasksFile } from "../openspec/tasks.ts";
|
|
@@ -237,6 +237,8 @@ export class ClawSpecService {
|
|
|
237
237
|
return await this.archiveProject(channelKey);
|
|
238
238
|
case "cancel":
|
|
239
239
|
return await this.cancelProject(channelKey);
|
|
240
|
+
case "doctor":
|
|
241
|
+
return await this.runDoctor(rest);
|
|
240
242
|
default:
|
|
241
243
|
return errorReply(`Unknown subcommand \`${subcommand}\`.\n\n${buildHelpText()}`);
|
|
242
244
|
}
|
|
@@ -250,9 +252,7 @@ export class ClawSpecService {
|
|
|
250
252
|
event: PromptBuildEvent,
|
|
251
253
|
ctx: PromptBuildContext,
|
|
252
254
|
): Promise<{ prependContext?: string; prependSystemContext?: string } | void> {
|
|
253
|
-
debugLog(`[handleBeforePromptBuild] prompt="${event.prompt}", trigger=${ctx.trigger}`);
|
|
254
255
|
if (!shouldHandleUserVisiblePrompt(ctx.trigger)) {
|
|
255
|
-
debugLog(`[handleBeforePromptBuild] Skipped: non-user trigger`);
|
|
256
256
|
this.logger.debug?.(
|
|
257
257
|
`[clawspec] skipping prompt injection for non-user trigger "${ctx.trigger ?? "unknown"}".`,
|
|
258
258
|
);
|
|
@@ -260,7 +260,6 @@ export class ClawSpecService {
|
|
|
260
260
|
}
|
|
261
261
|
|
|
262
262
|
const keyword = extractEmbeddedClawSpecKeyword(event.prompt);
|
|
263
|
-
debugLog(`[handleBeforePromptBuild] keyword=${keyword?.command}`);
|
|
264
263
|
if (keyword) {
|
|
265
264
|
this.logger.debug?.(
|
|
266
265
|
`[clawspec] detected control keyword "${keyword.command}" for prompt build (session=${ctx.sessionKey ?? "unknown"}).`,
|
|
@@ -320,21 +319,16 @@ export class ClawSpecService {
|
|
|
320
319
|
|
|
321
320
|
switch (keyword.kind) {
|
|
322
321
|
case "plan": {
|
|
323
|
-
debugLog(`[cs-plan] Triggered for project: ${match?.project.changeName}`);
|
|
324
322
|
if (!match?.project.repoPath || !match.project.changeName) {
|
|
325
|
-
debugLog(`[cs-plan] Missing repoPath or changeName`);
|
|
326
323
|
return this.buildPluginReplyInjection(
|
|
327
324
|
event.prompt,
|
|
328
325
|
"Select a project and create a change first with `/clawspec use <project-name>` and `/clawspec proposal <change-name> [description]`.",
|
|
329
326
|
);
|
|
330
327
|
}
|
|
331
|
-
debugLog(`[cs-plan] Calling startVisiblePlanningSync`);
|
|
332
328
|
const planningSync = await this.startVisiblePlanningSync(match.channelKey, match.project, ctx, event.prompt, "apply");
|
|
333
329
|
if ("prependContext" in planningSync || "prependSystemContext" in planningSync) {
|
|
334
|
-
debugLog(`[cs-plan] Returning prompt injection`);
|
|
335
330
|
return planningSync;
|
|
336
331
|
}
|
|
337
|
-
debugLog(`[cs-plan] Returning plugin reply`);
|
|
338
332
|
return this.buildPluginReplyInjection(event.prompt, planningSync.text ?? "");
|
|
339
333
|
}
|
|
340
334
|
case "work":
|
|
@@ -684,13 +678,6 @@ export class ClawSpecService {
|
|
|
684
678
|
repoStatePaths.planningJournalSnapshotFile,
|
|
685
679
|
project.planningJournal?.lastSyncedAt,
|
|
686
680
|
);
|
|
687
|
-
const snapshot = await journalStore.readSnapshot(repoStatePaths.planningJournalSnapshotFile);
|
|
688
|
-
const digest = await journalStore.digest(project.changeName);
|
|
689
|
-
this.logger.info(`[clawspec] cs-work check for ${project.changeName}:`);
|
|
690
|
-
this.logger.info(` - hasUnsyncedChanges: ${hasUnsyncedChanges}`);
|
|
691
|
-
this.logger.info(` - Snapshot: entryCount=${snapshot?.entryCount}, lastEntryAt=${snapshot?.lastEntryAt}, hash=${snapshot?.contentHash?.slice(0, 8)}`);
|
|
692
|
-
this.logger.info(` - Current digest: entryCount=${digest.entryCount}, lastEntryAt=${digest.lastEntryAt}, hash=${digest.contentHash.slice(0, 8)}`);
|
|
693
|
-
this.logger.info(` - fallbackLastSyncedAt: ${project.planningJournal?.lastSyncedAt}`);
|
|
694
681
|
if (!hasUnsyncedChanges) {
|
|
695
682
|
const isDetached = !isProjectContextAttached(project);
|
|
696
683
|
if (isDetached) {
|
|
@@ -767,19 +754,15 @@ export class ClawSpecService {
|
|
|
767
754
|
userPrompt: string,
|
|
768
755
|
mode: ExecutionMode,
|
|
769
756
|
): Promise<{ prependContext?: string; prependSystemContext?: string } | PluginCommandResult> {
|
|
770
|
-
debugLog(`[startVisiblePlanningSync] Called for ${project.changeName}`);
|
|
771
|
-
this.logger.info(`[clawspec] startVisiblePlanningSync called for ${project.changeName}`);
|
|
772
757
|
void project;
|
|
773
758
|
void mode;
|
|
774
759
|
|
|
775
760
|
const prepared = await this.preparePlanningSync(channelKey);
|
|
776
761
|
if ("result" in prepared) {
|
|
777
|
-
debugLog(`[startVisiblePlanningSync] preparePlanningSync failed`);
|
|
778
762
|
this.logger.warn(`[clawspec] preparePlanningSync returned error: ${prepared.result.text}`);
|
|
779
763
|
return prepared.result;
|
|
780
764
|
}
|
|
781
765
|
|
|
782
|
-
debugLog(`[startVisiblePlanningSync] Updating state to planning`);
|
|
783
766
|
const startedAt = new Date().toISOString();
|
|
784
767
|
const runningProject = await this.stateStore.updateProject(channelKey, (current) => ({
|
|
785
768
|
...current,
|
|
@@ -793,8 +776,6 @@ export class ClawSpecService {
|
|
|
793
776
|
execution: undefined,
|
|
794
777
|
lastExecutionAt: startedAt,
|
|
795
778
|
}));
|
|
796
|
-
debugLog(`[startVisiblePlanningSync] State updated: status=${runningProject.status}, phase=${runningProject.phase}, sessionKey=${runningProject.boundSessionKey}`);
|
|
797
|
-
this.logger.info(`[clawspec] Project state updated: status=planning, phase=planning_sync, boundSessionKey=${runningProject.boundSessionKey}`);
|
|
798
779
|
|
|
799
780
|
return await this.buildPlanningSyncInjection(runningProject, userPrompt, prepared.instructionResults);
|
|
800
781
|
}
|
|
@@ -912,7 +893,6 @@ export class ClawSpecService {
|
|
|
912
893
|
}
|
|
913
894
|
|
|
914
895
|
async handleAgentEnd(event: AgentEndEvent, ctx: PromptBuildContext): Promise<void> {
|
|
915
|
-
debugLog(`[agent_end] Called, sessionKey=${ctx.sessionKey}`);
|
|
916
896
|
const runningProject = await this.findRunningProjectBySessionKey(ctx.sessionKey);
|
|
917
897
|
if (runningProject?.repoPath && runningProject.changeName) {
|
|
918
898
|
const project = runningProject;
|
|
@@ -977,10 +957,7 @@ export class ClawSpecService {
|
|
|
977
957
|
|
|
978
958
|
const planningProject = await this.findPlanningProjectBySessionKey(ctx.sessionKey)
|
|
979
959
|
?? await this.findPlanningProjectByContext(ctx);
|
|
980
|
-
debugLog(`[agent_end] Planning project: ${planningProject?.changeName}, status=${planningProject?.status}, phase=${planningProject?.phase}`);
|
|
981
960
|
if (planningProject) {
|
|
982
|
-
debugLog(`[agent_end] Calling finalizePlanningTurn`);
|
|
983
|
-
this.logger.info(`[clawspec] agent_end: found planning project ${planningProject.changeName}, calling finalizePlanningTurn`);
|
|
984
961
|
await this.finalizePlanningTurn(planningProject, event);
|
|
985
962
|
return;
|
|
986
963
|
}
|
|
@@ -1930,6 +1907,11 @@ export class ClawSpecService {
|
|
|
1930
1907
|
);
|
|
1931
1908
|
}
|
|
1932
1909
|
|
|
1910
|
+
async runDoctor(rawArgs?: string): Promise<PluginCommandResult> {
|
|
1911
|
+
const acpxConfigPath = path.join(os.homedir(), ".acpx", "config.json");
|
|
1912
|
+
return await runDoctorCommand(acpxConfigPath, rawArgs);
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1933
1915
|
private async captureIncomingMessage(channelKey: string, project: ProjectState, text: string): Promise<ProjectState> {
|
|
1934
1916
|
const trimmed = sanitizePlanningMessageText(text).trim();
|
|
1935
1917
|
if (!trimmed || trimmed.startsWith("/clawspec") || !project.repoPath || !project.changeName) {
|
|
@@ -2428,10 +2410,7 @@ export class ClawSpecService {
|
|
|
2428
2410
|
}
|
|
2429
2411
|
|
|
2430
2412
|
private async finalizePlanningTurn(project: ProjectState, event: AgentEndEvent): Promise<void> {
|
|
2431
|
-
debugLog(`[finalizePlanningTurn] Called for ${project.changeName}, success=${event.success}`);
|
|
2432
|
-
this.logger.info(`[clawspec] finalizePlanningTurn called for ${project.changeName}, success=${event.success}`);
|
|
2433
2413
|
if (!project.repoPath || !project.changeName) {
|
|
2434
|
-
debugLog(`[finalizePlanningTurn] Skipped: missing repoPath or changeName`);
|
|
2435
2414
|
this.logger.warn(`[clawspec] finalizePlanningTurn skipped: missing repoPath or changeName`);
|
|
2436
2415
|
return;
|
|
2437
2416
|
}
|
|
@@ -2491,11 +2470,7 @@ export class ClawSpecService {
|
|
|
2491
2470
|
}
|
|
2492
2471
|
}
|
|
2493
2472
|
|
|
2494
|
-
debugLog(`[finalizePlanningTurn] Writing snapshot`);
|
|
2495
2473
|
const snapshot = await journalStore.writeSnapshot(repoStatePaths.planningJournalSnapshotFile, project.changeName, timestamp);
|
|
2496
|
-
debugLog(`[finalizePlanningTurn] Snapshot written: entryCount=${snapshot.entryCount}, lastEntryAt=${snapshot.lastEntryAt}`);
|
|
2497
|
-
this.logger.info(`[clawspec] Planning snapshot written for ${project.changeName}: entryCount=${snapshot.entryCount}, lastEntryAt=${snapshot.lastEntryAt}`);
|
|
2498
|
-
this.logger.info(`[clawspec] Updating project state: status=${status}, phase=${phase}, dirty=${journalDirty}`);
|
|
2499
2474
|
await this.writeLatestSummary(repoStatePaths, latestSummary);
|
|
2500
2475
|
|
|
2501
2476
|
const finalized = await this.stateStore.updateProject(project.channelKey, (current) => ({
|
|
@@ -3323,3 +3298,138 @@ function parseOptionalNumber(value: string): number | undefined {
|
|
|
3323
3298
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
3324
3299
|
return typeof value === "object" && value !== null;
|
|
3325
3300
|
}
|
|
3301
|
+
|
|
3302
|
+
type DoctorIssue = {
|
|
3303
|
+
id: string;
|
|
3304
|
+
message: string;
|
|
3305
|
+
fix: string;
|
|
3306
|
+
};
|
|
3307
|
+
|
|
3308
|
+
type DoctorContext = {
|
|
3309
|
+
acpxConfigPath: string;
|
|
3310
|
+
acpxConfig: Record<string, unknown> | null;
|
|
3311
|
+
acpxConfigError: boolean;
|
|
3312
|
+
};
|
|
3313
|
+
|
|
3314
|
+
type DoctorCheck = {
|
|
3315
|
+
id: string;
|
|
3316
|
+
check: (ctx: DoctorContext) => DoctorIssue | undefined;
|
|
3317
|
+
applyFix?: (ctx: DoctorContext) => Promise<void>;
|
|
3318
|
+
};
|
|
3319
|
+
|
|
3320
|
+
const DOCTOR_CHECKS: DoctorCheck[] = [
|
|
3321
|
+
{
|
|
3322
|
+
id: "acpx-config-json",
|
|
3323
|
+
check: (ctx) => {
|
|
3324
|
+
if (ctx.acpxConfigError) {
|
|
3325
|
+
return {
|
|
3326
|
+
id: "acpx-config-json",
|
|
3327
|
+
message: `\`${ctx.acpxConfigPath}\` contains invalid JSON. acpx may fail to start.`,
|
|
3328
|
+
fix: `Fix the JSON syntax in \`${ctx.acpxConfigPath}\`, or delete the file to use defaults.`,
|
|
3329
|
+
};
|
|
3330
|
+
}
|
|
3331
|
+
return undefined;
|
|
3332
|
+
},
|
|
3333
|
+
},
|
|
3334
|
+
{
|
|
3335
|
+
id: "acpx-custom-agents",
|
|
3336
|
+
check: (ctx) => {
|
|
3337
|
+
if (!ctx.acpxConfig) return undefined;
|
|
3338
|
+
const agents = ctx.acpxConfig.agents;
|
|
3339
|
+
if (agents && typeof agents === "object" && Object.keys(agents as object).length > 0) {
|
|
3340
|
+
const agentKeys = Object.keys(agents as object).map((k) => `\`${k}\``).join(", ");
|
|
3341
|
+
return {
|
|
3342
|
+
id: "acpx-custom-agents",
|
|
3343
|
+
message:
|
|
3344
|
+
`\`${ctx.acpxConfigPath}\` has custom agent entries: ${agentKeys}.\n`
|
|
3345
|
+
+ ` Custom agent configurations (e.g. pointing to a local CLI) can cause session creation to fail with "stdin is not a terminal".`,
|
|
3346
|
+
fix:
|
|
3347
|
+
`Set \`"agents": {}\` in \`${ctx.acpxConfigPath}\` to use the default agent launcher.\n`
|
|
3348
|
+
+ ` Run \`/clawspec doctor fix\` to apply this fix automatically.`,
|
|
3349
|
+
};
|
|
3350
|
+
}
|
|
3351
|
+
return undefined;
|
|
3352
|
+
},
|
|
3353
|
+
applyFix: async (ctx) => {
|
|
3354
|
+
if (ctx.acpxConfig) {
|
|
3355
|
+
ctx.acpxConfig.agents = {};
|
|
3356
|
+
await writeJsonFile(ctx.acpxConfigPath, ctx.acpxConfig);
|
|
3357
|
+
}
|
|
3358
|
+
},
|
|
3359
|
+
},
|
|
3360
|
+
];
|
|
3361
|
+
|
|
3362
|
+
export async function runDoctorCommand(acpxConfigPath: string, rawArgs?: string): Promise<PluginCommandResult> {
|
|
3363
|
+
const action = rawArgs?.trim().toLowerCase();
|
|
3364
|
+
|
|
3365
|
+
let acpxConfig: Record<string, unknown> | null = null;
|
|
3366
|
+
let acpxConfigError = false;
|
|
3367
|
+
const raw = await tryReadUtf8(acpxConfigPath);
|
|
3368
|
+
if (raw !== undefined && raw.trim() !== "") {
|
|
3369
|
+
try {
|
|
3370
|
+
acpxConfig = JSON.parse(raw) as Record<string, unknown>;
|
|
3371
|
+
} catch {
|
|
3372
|
+
acpxConfigError = true;
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
|
|
3376
|
+
const ctx: DoctorContext = { acpxConfigPath, acpxConfig, acpxConfigError };
|
|
3377
|
+
const issues: DoctorIssue[] = [];
|
|
3378
|
+
for (const check of DOCTOR_CHECKS) {
|
|
3379
|
+
const issue = check.check(ctx);
|
|
3380
|
+
if (issue) {
|
|
3381
|
+
issues.push(issue);
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
3384
|
+
|
|
3385
|
+
if (action === "fix") {
|
|
3386
|
+
if (issues.length === 0) {
|
|
3387
|
+
return okReply("Nothing to fix. acpx configuration is already clean.");
|
|
3388
|
+
}
|
|
3389
|
+
const fixable = DOCTOR_CHECKS.filter((c) => c.applyFix && issues.some((i) => i.id === c.id));
|
|
3390
|
+
if (fixable.length === 0) {
|
|
3391
|
+
return errorReply(
|
|
3392
|
+
"Cannot auto-fix the detected issues. Please fix them manually:\n\n"
|
|
3393
|
+
+ issues.map((i) => `- ${i.fix}`).join("\n"),
|
|
3394
|
+
);
|
|
3395
|
+
}
|
|
3396
|
+
for (const check of fixable) {
|
|
3397
|
+
await check.applyFix!(ctx);
|
|
3398
|
+
}
|
|
3399
|
+
return okReply(
|
|
3400
|
+
[
|
|
3401
|
+
heading("Doctor Fix Applied"),
|
|
3402
|
+
"",
|
|
3403
|
+
...fixable.map((c) => {
|
|
3404
|
+
const issue = issues.find((i) => i.id === c.id);
|
|
3405
|
+
return `Fixed: ${issue?.message.split("\n")[0] ?? c.id}`;
|
|
3406
|
+
}),
|
|
3407
|
+
"",
|
|
3408
|
+
"Please retry your previous command.",
|
|
3409
|
+
].join("\n"),
|
|
3410
|
+
);
|
|
3411
|
+
}
|
|
3412
|
+
|
|
3413
|
+
if (issues.length === 0) {
|
|
3414
|
+
return okReply(
|
|
3415
|
+
[
|
|
3416
|
+
heading("Doctor"),
|
|
3417
|
+
"",
|
|
3418
|
+
"No issues found. acpx configuration looks healthy.",
|
|
3419
|
+
].join("\n"),
|
|
3420
|
+
);
|
|
3421
|
+
}
|
|
3422
|
+
|
|
3423
|
+
return okReply(
|
|
3424
|
+
[
|
|
3425
|
+
heading("Doctor"),
|
|
3426
|
+
"",
|
|
3427
|
+
`Found ${issues.length} issue${issues.length > 1 ? "s" : ""}:`,
|
|
3428
|
+
"",
|
|
3429
|
+
...issues.flatMap((issue, i) => [`**${i + 1}.** ${issue.message}`, ""]),
|
|
3430
|
+
"Suggested fixes:",
|
|
3431
|
+
"",
|
|
3432
|
+
...issues.flatMap((issue, i) => [`**${i + 1}.** ${issue.fix}`, ""]),
|
|
3433
|
+
].join("\n"),
|
|
3434
|
+
);
|
|
3435
|
+
}
|
package/src/watchers/manager.ts
CHANGED
|
@@ -823,7 +823,7 @@ class ExecutionWatcher {
|
|
|
823
823
|
},
|
|
824
824
|
};
|
|
825
825
|
});
|
|
826
|
-
await this.writeExecutionControl(
|
|
826
|
+
await this.writeExecutionControl(queued);
|
|
827
827
|
const nextPlanningStep = nextForcedArtifactId
|
|
828
828
|
? (nextPlanningArtifactId(forcedArtifactIds, selectedArtifactId) ?? await this.describeNextPlanningStep(queued))
|
|
829
829
|
: await this.describeNextPlanningStep(queued);
|
|
@@ -906,6 +906,7 @@ class ExecutionWatcher {
|
|
|
906
906
|
await writeUtf8(repoStatePaths.workerProgressFile, "");
|
|
907
907
|
|
|
908
908
|
const importedSkills = await loadClawSpecSkillBundle(["apply"]);
|
|
909
|
+
|
|
909
910
|
({ runError } = await this.runAcpTurnWithTracking(
|
|
910
911
|
runningProject,
|
|
911
912
|
repoStatePaths,
|
|
@@ -1023,7 +1024,7 @@ class ExecutionWatcher {
|
|
|
1023
1024
|
},
|
|
1024
1025
|
};
|
|
1025
1026
|
});
|
|
1026
|
-
await this.writeExecutionControl(
|
|
1027
|
+
await this.writeExecutionControl(queued);
|
|
1027
1028
|
return true;
|
|
1028
1029
|
}
|
|
1029
1030
|
|
|
@@ -1205,6 +1206,17 @@ class ExecutionWatcher {
|
|
|
1205
1206
|
await flushProgress();
|
|
1206
1207
|
return { runError };
|
|
1207
1208
|
}
|
|
1209
|
+
|
|
1210
|
+
this.logger.debug?.(
|
|
1211
|
+
`[clawspec] calling acpClient.runTurn: ${JSON.stringify({
|
|
1212
|
+
sessionKey,
|
|
1213
|
+
cwd: project.repoPath,
|
|
1214
|
+
agentId: workerAgentId,
|
|
1215
|
+
promptLength: prompt.length,
|
|
1216
|
+
acpxCommand: this.acpClient.command,
|
|
1217
|
+
})}`,
|
|
1218
|
+
);
|
|
1219
|
+
|
|
1208
1220
|
const runTurnPromise = this.acpClient.runTurn({
|
|
1209
1221
|
sessionKey,
|
|
1210
1222
|
cwd: project.repoPath!,
|
|
@@ -1637,7 +1649,7 @@ class ExecutionWatcher {
|
|
|
1637
1649
|
},
|
|
1638
1650
|
}));
|
|
1639
1651
|
|
|
1640
|
-
await this.writeExecutionControl(
|
|
1652
|
+
await this.writeExecutionControl(recovered);
|
|
1641
1653
|
await this.notify(
|
|
1642
1654
|
recovered,
|
|
1643
1655
|
buildWatcherStatusMessage(
|
|
@@ -1698,7 +1710,7 @@ class ExecutionWatcher {
|
|
|
1698
1710
|
lastFailure: current.execution?.lastFailure,
|
|
1699
1711
|
},
|
|
1700
1712
|
}));
|
|
1701
|
-
await this.writeExecutionControl(
|
|
1713
|
+
await this.writeExecutionControl(updated);
|
|
1702
1714
|
return updated;
|
|
1703
1715
|
}
|
|
1704
1716
|
|
|
@@ -1942,8 +1954,10 @@ class ExecutionWatcher {
|
|
|
1942
1954
|
};
|
|
1943
1955
|
}
|
|
1944
1956
|
|
|
1945
|
-
private async writeExecutionControl(
|
|
1946
|
-
const project =
|
|
1957
|
+
private async writeExecutionControl(projectOrChannelKey: string | ProjectState): Promise<void> {
|
|
1958
|
+
const project = typeof projectOrChannelKey === "string"
|
|
1959
|
+
? await this.stateStore.getActiveProject(projectOrChannelKey)
|
|
1960
|
+
: projectOrChannelKey;
|
|
1947
1961
|
if (!project?.repoPath || !project.changeName || !project.execution) {
|
|
1948
1962
|
return;
|
|
1949
1963
|
}
|
package/src/watchers/notifier.ts
CHANGED
package/src/worker/io-helper.ts
CHANGED
|
@@ -118,9 +118,10 @@ export async function ensureWorkerIoHelper(repoStatePaths: RepoStatePaths): Prom
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
export function buildWorkerIoEventCommandPrefix(repoStatePaths: RepoStatePaths): string {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
121
|
+
const ioPath = repoStatePaths.workerIoFile;
|
|
122
|
+
// Use single quotes on POSIX, double quotes on Windows for cross-platform compatibility
|
|
123
|
+
if (process.platform === "win32") {
|
|
124
|
+
return `node "${ioPath.replace(/"/g, '""')}" event`;
|
|
125
|
+
}
|
|
126
|
+
return `node '${ioPath.replace(/'/g, `'\\''`)}' event`;
|
|
126
127
|
}
|
|
@@ -8,6 +8,7 @@ test("help text only advertises the clawspec command surface", () => {
|
|
|
8
8
|
assert.match(help, /\/clawspec workspace/);
|
|
9
9
|
assert.match(help, /\/clawspec proposal <change-name> \[description\]/);
|
|
10
10
|
assert.match(help, /\/clawspec continue/);
|
|
11
|
+
assert.match(help, /\/clawspec doctor/);
|
|
11
12
|
assert.match(help, /\/clawspec detach/);
|
|
12
13
|
assert.match(help, /cs-detach/);
|
|
13
14
|
assert.match(help, /legacy aliases/);
|