clawspec 1.0.16 → 1.0.20
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 -3
- package/src/acp/client.ts +17 -1
- package/src/orchestrator/helpers.ts +1 -0
- package/src/orchestrator/service.ts +143 -13
- package/src/watchers/manager.ts +89 -7
- package/src/worker/io-helper.ts +6 -5
- package/test/acp-client.test.ts +0 -309
- package/test/acpx-dependency.test.ts +0 -133
- package/test/assistant-journal.test.ts +0 -203
- package/test/command-surface.test.ts +0 -23
- package/test/config.test.ts +0 -77
- package/test/detach-attach.test.ts +0 -98
- package/test/file-lock.test.ts +0 -88
- package/test/fs-utils.test.ts +0 -22
- package/test/helpers/harness.ts +0 -301
- package/test/helpers.test.ts +0 -108
- package/test/keywords.test.ts +0 -92
- package/test/notifier.test.ts +0 -29
- package/test/openspec-dependency.test.ts +0 -68
- package/test/paths-utils.test.ts +0 -30
- package/test/pause-cancel.test.ts +0 -55
- package/test/planning-journal.test.ts +0 -155
- package/test/plugin-registration.test.ts +0 -35
- package/test/project-memory.test.ts +0 -42
- package/test/proposal.test.ts +0 -24
- package/test/queue-planning.test.ts +0 -322
- package/test/queue-work.test.ts +0 -220
- package/test/recovery.test.ts +0 -576
- package/test/service-archive.test.ts +0 -87
- package/test/shell-command.test.ts +0 -48
- package/test/state-store.test.ts +0 -74
- package/test/tasks-and-checkpoint.test.ts +0 -60
- package/test/use-project.test.ts +0 -67
- package/test/watcher-planning.test.ts +0 -504
- package/test/watcher-work.test.ts +0 -1741
- package/test/worker-command.test.ts +0 -66
- package/test/worker-io-helper.test.ts +0 -97
- package/test/worker-skills.test.ts +0 -12
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.20",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenClaw plugin that orchestrates OpenSpec project workflows with visible main-agent execution.",
|
|
6
6
|
"keywords": [
|
|
@@ -23,7 +23,6 @@
|
|
|
23
23
|
"index.ts",
|
|
24
24
|
"src",
|
|
25
25
|
"skills",
|
|
26
|
-
"test",
|
|
27
26
|
"README.md",
|
|
28
27
|
"README.zh-CN.md",
|
|
29
28
|
"openclaw.plugin.json",
|
|
@@ -46,7 +45,7 @@
|
|
|
46
45
|
"scripts": {
|
|
47
46
|
"check": "node --experimental-strip-types -e \"import('./index.ts')\"",
|
|
48
47
|
"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",
|
|
48
|
+
"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
49
|
"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
50
|
},
|
|
52
51
|
"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
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import os from "node:os";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import type {
|
|
3
4
|
OpenClawConfig,
|
|
@@ -236,6 +237,8 @@ export class ClawSpecService {
|
|
|
236
237
|
return await this.archiveProject(channelKey);
|
|
237
238
|
case "cancel":
|
|
238
239
|
return await this.cancelProject(channelKey);
|
|
240
|
+
case "doctor":
|
|
241
|
+
return await this.runDoctor(rest);
|
|
239
242
|
default:
|
|
240
243
|
return errorReply(`Unknown subcommand \`${subcommand}\`.\n\n${buildHelpText()}`);
|
|
241
244
|
}
|
|
@@ -675,13 +678,6 @@ export class ClawSpecService {
|
|
|
675
678
|
repoStatePaths.planningJournalSnapshotFile,
|
|
676
679
|
project.planningJournal?.lastSyncedAt,
|
|
677
680
|
);
|
|
678
|
-
const snapshot = await journalStore.readSnapshot(repoStatePaths.planningJournalSnapshotFile);
|
|
679
|
-
const digest = await journalStore.digest(project.changeName);
|
|
680
|
-
this.logger.info(`[clawspec] cs-work check for ${project.changeName}:`);
|
|
681
|
-
this.logger.info(` - hasUnsyncedChanges: ${hasUnsyncedChanges}`);
|
|
682
|
-
this.logger.info(` - Snapshot: entryCount=${snapshot?.entryCount}, lastEntryAt=${snapshot?.lastEntryAt}, hash=${snapshot?.contentHash?.slice(0, 8)}`);
|
|
683
|
-
this.logger.info(` - Current digest: entryCount=${digest.entryCount}, lastEntryAt=${digest.lastEntryAt}, hash=${digest.contentHash.slice(0, 8)}`);
|
|
684
|
-
this.logger.info(` - fallbackLastSyncedAt: ${project.planningJournal?.lastSyncedAt}`);
|
|
685
681
|
if (!hasUnsyncedChanges) {
|
|
686
682
|
const isDetached = !isProjectContextAttached(project);
|
|
687
683
|
if (isDetached) {
|
|
@@ -758,7 +754,6 @@ export class ClawSpecService {
|
|
|
758
754
|
userPrompt: string,
|
|
759
755
|
mode: ExecutionMode,
|
|
760
756
|
): Promise<{ prependContext?: string; prependSystemContext?: string } | PluginCommandResult> {
|
|
761
|
-
this.logger.info(`[clawspec] startVisiblePlanningSync called for ${project.changeName}`);
|
|
762
757
|
void project;
|
|
763
758
|
void mode;
|
|
764
759
|
|
|
@@ -781,7 +776,6 @@ export class ClawSpecService {
|
|
|
781
776
|
execution: undefined,
|
|
782
777
|
lastExecutionAt: startedAt,
|
|
783
778
|
}));
|
|
784
|
-
this.logger.info(`[clawspec] Project state updated: status=planning, phase=planning_sync, boundSessionKey=${runningProject.boundSessionKey}`);
|
|
785
779
|
|
|
786
780
|
return await this.buildPlanningSyncInjection(runningProject, userPrompt, prepared.instructionResults);
|
|
787
781
|
}
|
|
@@ -964,7 +958,6 @@ export class ClawSpecService {
|
|
|
964
958
|
const planningProject = await this.findPlanningProjectBySessionKey(ctx.sessionKey)
|
|
965
959
|
?? await this.findPlanningProjectByContext(ctx);
|
|
966
960
|
if (planningProject) {
|
|
967
|
-
this.logger.info(`[clawspec] agent_end: found planning project ${planningProject.changeName}, calling finalizePlanningTurn`);
|
|
968
961
|
await this.finalizePlanningTurn(planningProject, event);
|
|
969
962
|
return;
|
|
970
963
|
}
|
|
@@ -1914,6 +1907,11 @@ export class ClawSpecService {
|
|
|
1914
1907
|
);
|
|
1915
1908
|
}
|
|
1916
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
|
+
|
|
1917
1915
|
private async captureIncomingMessage(channelKey: string, project: ProjectState, text: string): Promise<ProjectState> {
|
|
1918
1916
|
const trimmed = sanitizePlanningMessageText(text).trim();
|
|
1919
1917
|
if (!trimmed || trimmed.startsWith("/clawspec") || !project.repoPath || !project.changeName) {
|
|
@@ -2412,7 +2410,6 @@ export class ClawSpecService {
|
|
|
2412
2410
|
}
|
|
2413
2411
|
|
|
2414
2412
|
private async finalizePlanningTurn(project: ProjectState, event: AgentEndEvent): Promise<void> {
|
|
2415
|
-
this.logger.info(`[clawspec] finalizePlanningTurn called for ${project.changeName}, success=${event.success}`);
|
|
2416
2413
|
if (!project.repoPath || !project.changeName) {
|
|
2417
2414
|
this.logger.warn(`[clawspec] finalizePlanningTurn skipped: missing repoPath or changeName`);
|
|
2418
2415
|
return;
|
|
@@ -2474,8 +2471,6 @@ export class ClawSpecService {
|
|
|
2474
2471
|
}
|
|
2475
2472
|
|
|
2476
2473
|
const snapshot = await journalStore.writeSnapshot(repoStatePaths.planningJournalSnapshotFile, project.changeName, timestamp);
|
|
2477
|
-
this.logger.info(`[clawspec] Planning snapshot written for ${project.changeName}: entryCount=${snapshot.entryCount}, lastEntryAt=${snapshot.lastEntryAt}`);
|
|
2478
|
-
this.logger.info(`[clawspec] Updating project state: status=${status}, phase=${phase}, dirty=${journalDirty}`);
|
|
2479
2474
|
await this.writeLatestSummary(repoStatePaths, latestSummary);
|
|
2480
2475
|
|
|
2481
2476
|
const finalized = await this.stateStore.updateProject(project.channelKey, (current) => ({
|
|
@@ -3303,3 +3298,138 @@ function parseOptionalNumber(value: string): number | undefined {
|
|
|
3303
3298
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
3304
3299
|
return typeof value === "object" && value !== null;
|
|
3305
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
|
}
|
|
@@ -2781,7 +2795,7 @@ function parseWorkerProgressEvent(line: string): WorkerProgressEvent | undefined
|
|
|
2781
2795
|
|
|
2782
2796
|
function formatWorkerProgressMessage(project: ProjectState, event: WorkerProgressEvent): string | undefined {
|
|
2783
2797
|
const rawMessage = typeof event.message === "string" ? event.message : "";
|
|
2784
|
-
const message = shortenActivityText(rawMessage, 120);
|
|
2798
|
+
const message = shortenActivityText(compactWorkerProgressDisplayPaths(project, rawMessage), 120);
|
|
2785
2799
|
if (!message) {
|
|
2786
2800
|
return undefined;
|
|
2787
2801
|
}
|
|
@@ -2902,6 +2916,74 @@ function compactProjectLabel(project: ProjectState): string {
|
|
|
2902
2916
|
return `${projectName}-${changeName}`;
|
|
2903
2917
|
}
|
|
2904
2918
|
|
|
2919
|
+
function compactWorkerProgressDisplayPaths(project: ProjectState, text: string): string {
|
|
2920
|
+
try {
|
|
2921
|
+
const compactRoot = compactWorkerProgressRoot(project);
|
|
2922
|
+
if (!compactRoot) {
|
|
2923
|
+
return text;
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
let compacted = text;
|
|
2927
|
+
if (project.changeDir) {
|
|
2928
|
+
compacted = replaceDisplayPathPrefix(compacted, project.changeDir, `${compactRoot}:`);
|
|
2929
|
+
}
|
|
2930
|
+
if (project.repoPath) {
|
|
2931
|
+
compacted = replaceDisplayPathPrefix(compacted, project.repoPath, `${compactRoot}:`);
|
|
2932
|
+
}
|
|
2933
|
+
compacted = normalizeCompactedDisplayPaths(compacted, compactRoot);
|
|
2934
|
+
return compacted.length < text.length ? compacted : text;
|
|
2935
|
+
} catch {
|
|
2936
|
+
return text;
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2940
|
+
function compactWorkerProgressRoot(project: ProjectState): string | undefined {
|
|
2941
|
+
const changeName = project.changeName?.trim();
|
|
2942
|
+
if (!changeName) {
|
|
2943
|
+
return undefined;
|
|
2944
|
+
}
|
|
2945
|
+
const projectName = project.projectName?.trim()
|
|
2946
|
+
|| (project.repoPath ? path.basename(project.repoPath) : undefined)
|
|
2947
|
+
|| "project";
|
|
2948
|
+
return `${projectName}@${changeName}`;
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2951
|
+
function replaceDisplayPathPrefix(text: string, targetPath: string, replacement: string): string {
|
|
2952
|
+
const pattern = buildDisplayPathPrefixPattern(targetPath);
|
|
2953
|
+
if (!pattern) {
|
|
2954
|
+
return text;
|
|
2955
|
+
}
|
|
2956
|
+
return text.replace(pattern, replacement);
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
function normalizeCompactedDisplayPaths(text: string, compactRoot: string): string {
|
|
2960
|
+
const prefix = escapeRegExp(`${compactRoot}:`);
|
|
2961
|
+
const pattern = new RegExp("(" + prefix + ")([^\\s\"'`,)\\]}]+)", "g");
|
|
2962
|
+
return text.replace(pattern, (_match, prefix: string, suffix: string) => `${prefix}${suffix.replace(/\\/g, "/")}`);
|
|
2963
|
+
}
|
|
2964
|
+
|
|
2965
|
+
function buildDisplayPathPrefixPattern(targetPath: string): RegExp | undefined {
|
|
2966
|
+
const normalized = normalizeSlashes(targetPath).replace(/\/+$/, "");
|
|
2967
|
+
if (!normalized) {
|
|
2968
|
+
return undefined;
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
const escaped = normalized
|
|
2972
|
+
.split("/")
|
|
2973
|
+
.map((segment) => escapeRegExp(segment))
|
|
2974
|
+
.join("[/\\\\]+");
|
|
2975
|
+
if (!escaped) {
|
|
2976
|
+
return undefined;
|
|
2977
|
+
}
|
|
2978
|
+
|
|
2979
|
+
const flags = /^[A-Za-z]:/.test(normalized) ? "gi" : "g";
|
|
2980
|
+
return new RegExp(`${escaped}(?:[/\\\\]+)?`, flags);
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
function escapeRegExp(value: string): string {
|
|
2984
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2905
2987
|
function compactProgressMarker(current?: number, total?: number): string {
|
|
2906
2988
|
if (!total || total <= 0 || !current || current <= 0) {
|
|
2907
2989
|
return "";
|
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
|
}
|