svharness 0.15.1 → 0.15.8
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/auto/auto-harness-detail-design.md +184 -31
- package/bin/launch-codechat-cli.js +2 -0
- package/dist/commands/auto.js +44 -2
- package/dist/commands/shell-integration.js +12 -2
- package/dist/commands/start-agent.js +1 -0
- package/dist/config/merge-options.js +3 -3
- package/dist/core/build-project-entry.js +4 -0
- package/dist/core/scaffold.js +5 -3
- package/dist/dashboard/event-bus.js +48 -0
- package/dist/dashboard/index.js +69 -0
- package/dist/dashboard/server.js +76 -0
- package/dist/dashboard/sse-handler.js +124 -0
- package/dist/dashboard/static/css/styles.css +265 -0
- package/dist/dashboard/static/index.html +57 -0
- package/dist/dashboard/static/js/app.js +431 -0
- package/dist/index.js +20 -0
- package/dist/lib/acp-client.js +82 -18
- package/dist/lib/agent-launcher.js +79 -9
- package/dist/lib/auto-materials-prep.js +47 -4
- package/dist/lib/auto-optimize.js +3 -2
- package/dist/lib/auto-orchestrator.js +25 -6
- package/dist/lib/auto-state.js +66 -3
- package/dist/lib/cost-tracker.js +2 -0
- package/dist/lib/phase-runner.js +102 -3
- package/dist/lib/ps-codechat-alias.js +6 -2
- package/dist/lib/win-registry.js +27 -0
- package/package.json +2 -2
- package/templates/codechat/.claude/.env +59 -0
- package/templates/codechat/Start-CodeChat.ps1 +101 -10
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
> **文档定位**:`svharness auto` 模式的完整设计书,面向本系统的开发者与维护者。
|
|
4
4
|
> **上级文档**:`svharnessbuild/README.md`(CLI 总体规范)、`svharnessbuild/tmp/svharnessbuild-detail-design.md`(SDD)。
|
|
5
5
|
> **参照工程**:[snarktank/ralph](https://github.com/snarktank/ralph)(外循环思路)。
|
|
6
|
-
> **版本**:v2.
|
|
7
|
-
> **最后更新**:2026-06-
|
|
6
|
+
> **版本**:v2.5(2026-06-16,新增 Web 仪表盘、默认标志优化、优化容错)
|
|
7
|
+
> **最后更新**:2026-06-16
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
@@ -428,6 +428,17 @@ class AutoOrchestrator {
|
|
|
428
428
|
await this.runBuildSkeleton(); // svharness build --yes
|
|
429
429
|
}
|
|
430
430
|
|
|
431
|
+
// == v2.5 新增 ==
|
|
432
|
+
// 启动本地 Web 仪表盘(端口 7879),通过 SSE 实时推送构建进展
|
|
433
|
+
startDashboard(7879);
|
|
434
|
+
|
|
435
|
+
// == v2.4 修改 ==
|
|
436
|
+
// PhaseRunner 必须延迟到 ctx.harnessRoot 就绪后创建,
|
|
437
|
+
// 避免构造函数中根据初始 ctx.harnessRoot=null 创建的 PhaseRunner
|
|
438
|
+
// 持有陈旧引用,导致后续 runBuildPhases() 报"缺少 harnessRoot"。
|
|
439
|
+
// 实际代码:this.phaseRunner = new PhaseRunner(this.ctx, ...);
|
|
440
|
+
this.ensurePhaseRunnerReady();
|
|
441
|
+
|
|
431
442
|
// === AGENT-2: 构建 ===
|
|
432
443
|
await this.runHarnessBuild(); // 驱动 S10→S80 全流程
|
|
433
444
|
|
|
@@ -492,6 +503,11 @@ export class AcpClient {
|
|
|
492
503
|
|
|
493
504
|
#### 策略 A:Claude Code CLI `--print` 模式(与 Ralph 一致,零额外依赖)
|
|
494
505
|
|
|
506
|
+
> **v2.4 修订**:`spawnSync` → 异步 `spawn` + Promise,实时转发子进程 stdout/stderr 到终端。
|
|
507
|
+
> 原版 `spawnSync` 完全缓冲子进程输出,用户只能看到阶段标题后静默等待数分钟。
|
|
508
|
+
> 新版通过 `child.stdout.on('data')` / `child.stderr.on('data')` 实时 tee 输出,
|
|
509
|
+
> 同时收集完整结果供日志和完成标记解析。
|
|
510
|
+
|
|
495
511
|
```typescript
|
|
496
512
|
function parseCompletionSignals(output: string): string | undefined {
|
|
497
513
|
const patterns = [
|
|
@@ -506,25 +522,42 @@ function parseCompletionSignals(output: string): string | undefined {
|
|
|
506
522
|
return undefined;
|
|
507
523
|
}
|
|
508
524
|
|
|
509
|
-
function callViaCliPrint(opts: AcpCallOptions): AcpResult {
|
|
510
|
-
const
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
{
|
|
514
|
-
input: opts.prompt,
|
|
525
|
+
async function callViaCliPrint(opts: AcpCallOptions): Promise<AcpResult> {
|
|
526
|
+
const args = ['--dangerously-skip-permissions', '--print'];
|
|
527
|
+
return new Promise((resolve) => {
|
|
528
|
+
const child = spawn('claude', args, {
|
|
515
529
|
cwd: opts.workDir,
|
|
516
|
-
encoding: 'utf-8',
|
|
517
530
|
timeout: opts.timeoutMs ?? 600_000,
|
|
518
531
|
shell: process.platform === 'win32',
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
532
|
+
});
|
|
533
|
+
let stdout = '', stderr = '', settled = false;
|
|
534
|
+
|
|
535
|
+
child.stdout.on('data', (chunk) => { // ← 实时输出到终端
|
|
536
|
+
process.stdout.write(chunk.toString());
|
|
537
|
+
stdout += chunk;
|
|
538
|
+
});
|
|
539
|
+
child.stderr.on('data', (chunk) => { // ← 实时输出到终端
|
|
540
|
+
process.stderr.write(chunk.toString());
|
|
541
|
+
stderr += chunk;
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
child.stdin.end(opts.prompt);
|
|
545
|
+
|
|
546
|
+
child.on('close', (code) => {
|
|
547
|
+
if (settled) return; settled = true;
|
|
548
|
+
const success = code === 0;
|
|
549
|
+
resolve({
|
|
550
|
+
success,
|
|
551
|
+
output: stdout,
|
|
552
|
+
completionSignal: parseCompletionSignals(stdout),
|
|
553
|
+
error: success ? undefined : stderr || `claude exit ${code}`,
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
child.on('error', (err) => {
|
|
557
|
+
if (settled) return; settled = true;
|
|
558
|
+
resolve({ success: false, output: stdout, error: err.message });
|
|
559
|
+
});
|
|
560
|
+
});
|
|
528
561
|
}
|
|
529
562
|
```
|
|
530
563
|
|
|
@@ -832,9 +865,18 @@ svharness auto --yes
|
|
|
832
865
|
| `--harness-name <name>` | 从 `--goal` 提取关键词,或从 work-dir 目录名派生 | `--harness-name` |
|
|
833
866
|
| `--arch <arch>` | 有 baseline/code 时从代码文件扩展名推断;无 baseline 时自动选 `documents` | `--arch` |
|
|
834
867
|
| `--agent <agent>` | 固定为 `claude-code`(auto 模式专属,不支持其他 agent) | `--agent` |
|
|
835
|
-
| `--force` |
|
|
868
|
+
| `--force` | **默认 true**(v2.5 起)。用户可用 `--no-force` 禁用 | `--no-force` |
|
|
869
|
+
|
|
870
|
+
> **v2.4 修改:`--force` 模式下 `.bak` 备份机制**(`src/core/scaffold.ts` + `src/core/build-project-entry.ts`):
|
|
871
|
+
> - **Harness 目录**:`ensureTargetAvailable()` 不再直接 `fs.remove()`,改为 `fs.move(targetRoot, targetRoot + '.bak', { overwrite: true })`,先重命名备份再重建
|
|
872
|
+
> - **项目根入口文件**(CLAUDE.md/AGENTS.md):`writeBuildProjectEntry()` 中 `force=true` 时先 `fs.copyFile(dest, dest + '.bak')` 记录旧内容,再覆盖写入新内容
|
|
873
|
+
> - 无 `--force` 时零行为变化,仍保持原有跳过/报错逻辑
|
|
874
|
+
>
|
|
875
|
+
> **v2.5 修改**:`--force`、`--yes`、`--auto-approve` 三个标志的默认值从 `undefined` 改为 `true`(仅 `mergeAutoOptions()`),用户无需再手动传参。底层 `pickBool()` 保持优先级:CLI 显式值 > 配置文件 > 默认 true,因此 `--no-force` 仍可覆盖。
|
|
876
|
+
|
|
877
|
+
| `--yes` / `--auto-approve` | **默认 true**(v2.5 起),无人值守直接通过所有确认门禁 | `--no-yes` / `--no-auto-approve` |
|
|
836
878
|
|
|
837
|
-
|
|
879
|
+
**参数推断算法**(v2.5 增强:`deriveHarnessName` 三阶段安全兜底——goal→workDir→`'harness'`,确保中文场景不抛错):
|
|
838
880
|
|
|
839
881
|
```typescript
|
|
840
882
|
interface BuildParams {
|
|
@@ -1122,6 +1164,17 @@ const PHASE_DEFS: PhaseDef[] = [
|
|
|
1122
1164
|
{ key: 'S80_seed_memory', mode: 'acp', promptTemplate: 'S80-seed-memory.md' },
|
|
1123
1165
|
];
|
|
1124
1166
|
|
|
1167
|
+
// == v2.4 新增:阶段中文描述映射,供执行前显示给用户了解当前进展 ==
|
|
1168
|
+
const PHASE_DESC: Record<string, string> = {
|
|
1169
|
+
S10_wiki: '生成 baseline wiki 正文',
|
|
1170
|
+
S40_extract_requirements: '需求条目化与覆盖率检查',
|
|
1171
|
+
S50_generate_specs: '生成四域规格(signals/ui/behavior/interfaces)',
|
|
1172
|
+
S60_process_references: '参考资料结构化索引与注册',
|
|
1173
|
+
S65_customize_agent_env: 'Agent 运行环境定制合并',
|
|
1174
|
+
S70_runtime_assets: '运行期 Skills 与 Tasks 裁剪',
|
|
1175
|
+
S80_seed_memory: 'Memory 初始化播种',
|
|
1176
|
+
};
|
|
1177
|
+
|
|
1125
1178
|
function extractPhaseNumber(phaseName: string): number {
|
|
1126
1179
|
const match = phaseName.match(/^S(\d+)/);
|
|
1127
1180
|
return match ? parseInt(match[1], 10) : Infinity;
|
|
@@ -1166,6 +1219,13 @@ async runHarnessBuild(opts?: { onlyPendingOrOutdated?: boolean }) {
|
|
|
1166
1219
|
await this.waitForUserConfirm(`即将执行 ${phase},请确认后继续`);
|
|
1167
1220
|
}
|
|
1168
1221
|
|
|
1222
|
+
// == v2.4 修改:每个阶段执行前显示中文描述(src/lib/phase-runner.ts)==
|
|
1223
|
+
// PHASE_DESC 映射:S10_wiki→生成 baseline wiki 正文;S40→需求条目化...
|
|
1224
|
+
logger.info(`${phase}:${PHASE_DESC[phase] ?? phase}`);
|
|
1225
|
+
if (def.promptTemplate) {
|
|
1226
|
+
logger.info(`调用 Claude 执行「${def.promptTemplate}」...`);
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1169
1229
|
await this.runMechanicalPre(phase, def.mechanicalSteps);
|
|
1170
1230
|
|
|
1171
1231
|
if (def.mode === 'mechanical' || def.mode === 'skip') continue;
|
|
@@ -1178,9 +1238,11 @@ async runHarnessBuild(opts?: { onlyPendingOrOutdated?: boolean }) {
|
|
|
1178
1238
|
if (stateAfter.phases[phase]?.status !== 'DONE') {
|
|
1179
1239
|
await this.retryPhase(phase, result);
|
|
1180
1240
|
}
|
|
1241
|
+
// == v2.4 修改:阶段完成日志增加 emoji ==
|
|
1242
|
+
const label = afterStatus === 'DONE' ? '✅ 完成' : '⏭️ 跳过';
|
|
1243
|
+
logger.info(`${phase} ${label}`);
|
|
1181
1244
|
await this.runGates(phase);
|
|
1182
1245
|
await this.runSubAgentCheck(phase);
|
|
1183
|
-
logger.info(`✓ ${phase} DONE`);
|
|
1184
1246
|
}
|
|
1185
1247
|
}
|
|
1186
1248
|
|
|
@@ -2102,9 +2164,13 @@ auto:
|
|
|
2102
2164
|
# 控制流
|
|
2103
2165
|
checkpoint: null # 设为阶段名则在该阶段前暂停
|
|
2104
2166
|
maxPhase: null # 调试用:只跑到此阶段
|
|
2105
|
-
autoApprove:
|
|
2167
|
+
autoApprove: true # v2.5 起默认 true,无人值守自动通过所有表单门禁
|
|
2106
2168
|
baselineAutoExtract: explicit # explicit | conservative | disabled(见 §6.2 S61 策略表)
|
|
2107
2169
|
|
|
2170
|
+
# 仪表盘
|
|
2171
|
+
dashboard: true # v2.5 新增:启动本地 Web 仪表盘(端口 7879)
|
|
2172
|
+
dashboardPort: 7879 # 仪表盘 HTTP 端口
|
|
2173
|
+
|
|
2108
2174
|
# 成本控制
|
|
2109
2175
|
maxTotalCalls: 30
|
|
2110
2176
|
budgetUsd: null # null = 不限制
|
|
@@ -2112,7 +2178,7 @@ auto:
|
|
|
2112
2178
|
|
|
2113
2179
|
# ACP 配置
|
|
2114
2180
|
acpMode: auto # auto | sdk | cli-print
|
|
2115
|
-
acpTimeoutMs:
|
|
2181
|
+
acpTimeoutMs: 1800000 # 30 分钟(v2.4 从 10 分钟提升)
|
|
2116
2182
|
maxRetriesPerPhase: 2 # 每阶段最大重试次数
|
|
2117
2183
|
|
|
2118
2184
|
# 构建参数(同 svharness.config.yaml build 节)
|
|
@@ -2211,19 +2277,32 @@ async function main(config) {
|
|
|
2211
2277
|
if (score.composite_score >= config.targetScore && score.gate_pass) {
|
|
2212
2278
|
runFinalize(harnessRoot); break;
|
|
2213
2279
|
}
|
|
2214
|
-
|
|
2215
|
-
|
|
2280
|
+
// == v2.5 修改:优化实施容错 ==
|
|
2281
|
+
// ACP 超时不 throw——状态重置(阶段→PENDING)比 ACP 完成更重要。
|
|
2282
|
+
// 不重置状态则下次重跑仍从原评审点开始,永远无法推进。
|
|
2283
|
+
try {
|
|
2284
|
+
runOptimizePlan(harnessRoot, config, round); // → 第<n>轮修改-plan.md
|
|
2285
|
+
runOptimizeImplement(harnessRoot, config, round); // 按计划实施
|
|
2286
|
+
} catch (err) {
|
|
2287
|
+
logger.warn(`第 ${round} 轮优化失败,状态重置已执行,下一轮继续`);
|
|
2288
|
+
}
|
|
2216
2289
|
}
|
|
2217
2290
|
}
|
|
2218
2291
|
|
|
2219
|
-
function callClaude(workDir, prompt, config) {
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2292
|
+
function callClaude(workDir, prompt, config, onData) {
|
|
2293
|
+
// == v2.4 修改:spawnSync → 异步 spawn ==
|
|
2294
|
+
// spawnSync 完全缓冲子进程输出,用户只能看到阶段标题后静默等待数分钟。
|
|
2295
|
+
// 异步 spawn 通过 child.stdout.on('data') 实时 tee 输出到终端,
|
|
2296
|
+
// 同时收集完整结果供日志和完成标记解析。
|
|
2297
|
+
// == v2.5 新增:onData 回调用于仪表盘 SSE 推送 ==
|
|
2298
|
+
const result = spawnPromise('claude', ['--dangerously-skip-permissions', '--print'], {
|
|
2299
|
+
input: prompt, cwd: workDir,
|
|
2300
|
+
timeout: config.acpTimeoutMs ?? 1_800_000,
|
|
2223
2301
|
shell: process.platform === 'win32',
|
|
2302
|
+
onData, // → dashboardEventBus.emitAcpOutput()
|
|
2224
2303
|
});
|
|
2225
|
-
|
|
2226
|
-
return result
|
|
2304
|
+
// 即使超时也返回部分输出(Claude 可能已写完状态文件),不 throw
|
|
2305
|
+
return result;
|
|
2227
2306
|
}
|
|
2228
2307
|
```
|
|
2229
2308
|
|
|
@@ -2474,3 +2553,77 @@ flowchart TD
|
|
|
2474
2553
|
---
|
|
2475
2554
|
|
|
2476
2555
|
*设计结束(v2.3,已吸收 design-problem2.md 全部 14 项修订)。*
|
|
2556
|
+
|
|
2557
|
+
---
|
|
2558
|
+
|
|
2559
|
+
## 附录:v2.4 实现修订记录(2026-06-16)
|
|
2560
|
+
|
|
2561
|
+
根据实现反馈对详细设计的修订:
|
|
2562
|
+
|
|
2563
|
+
| # | 文件 | 修订内容 | 类型 | 对应 § |
|
|
2564
|
+
|---|------|---------|------|--------|
|
|
2565
|
+
| 1 | `src/lib/auto-orchestrator.ts` | **PhaseRunner 延迟创建**:构造函数不再创建 PhaseRunner,改为 `run()` 方法中 ctx.harnessRoot 就绪后才创建 `new PhaseRunner(this.ctx, ...)`。修复初始 `ctx.harnessRoot=null` 时 PhaseRunner 持有陈旧引用 → 阶段执行报"缺少 harnessRoot"的 Bug | Bug 修复 | §3.3 |
|
|
2566
|
+
| 2 | `src/core/scaffold.ts` | **Harness 目录 .bak 备份**:`ensureTargetAvailable()` 中 `force=true` 且目标目录存在时,不再 `fs.remove()`,改为 `fs.move(targetRoot, targetRoot + '.bak', { overwrite: true })` | 增强 | §5.3.2 |
|
|
2567
|
+
| 3 | `src/core/build-project-entry.ts` | **CLAUDE.md .bak 备份**:`writeBuildProjectEntry()` 中 `force=true` 且文件已存在时,先 `fs.copyFile(dest, dest + '.bak')` 再覆盖写入新内容 | 增强 | §5.3.2 |
|
|
2568
|
+
| 4 | `src/lib/acp-client.ts` | **ACP 实时输出**:`callViaCliPrint()` 从 `spawnSync` 改为异步 `spawn` + Promise,`child.stdout.on('data')` / `child.stderr.on('data')` 实时 tee 输出到终端,同时收集完整结果供日志和完成标记解析 | 增强 | §4.3 |
|
|
2569
|
+
| 5 | `src/lib/phase-runner.ts` | **可见性日志**:执行每个阶段前打印中文描述(PHASE_DESC 映射)+ 提示调用的 prompt 模板名;完成后打印 emoji 标记(`✅ 完成` / `⏭️ 跳过`);重试日志改为"准备重试第 N 次" | 增强 | §6.3 |
|
|
2570
|
+
|
|
2571
|
+
---
|
|
2572
|
+
|
|
2573
|
+
## 附录:v2.5 实现修订记录(2026-06-16)
|
|
2574
|
+
|
|
2575
|
+
根据实现反馈对详细设计的修订:
|
|
2576
|
+
|
|
2577
|
+
| # | 文件 | 修订内容 | 类型 | 对应 § |
|
|
2578
|
+
|---|------|---------|------|--------|
|
|
2579
|
+
| 1 | `src/config/merge-options.ts` | **默认标志优化**:`mergeAutoOptions()` 中 `force`、`yes`、`autoApprove` 三个 boolean 标志的默认值从 `undefined` 改为 `true`。用户无需再手动传 `--force --yes --auto-approve`。底层 `pickBool()` 保持优先级不变(CLI 显式值 > 配置文件 > 默认),因此 `--no-force` 仍可显式覆盖。仅影响 `mergeAutoOptions()`,`mergeBuildOptions()`/`mergeApplyOptions()` 不受影响 | 优化 | §9 |
|
|
2580
|
+
| 2 | `src/dashboard/`(新建模块) | **Web 仪表盘**:创建与 `auto/` 平级的 `src/dashboard/` 模块,解耦自动构建设仪表盘逻辑 | 新增 | §15 |
|
|
2581
|
+
| | `src/dashboard/event-bus.ts` | **DashboardEventBus**:EventEmitter 单例,15 个类型化事件方法(build_started/completed、phase_started/completed/failed、acp_output、cost_updated、score_updated、review/optimize 事件) | 新增 | §15.1 |
|
|
2582
|
+
| | `src/dashboard/sse-handler.ts` | **SSE 广播管理器**:维护 BuildSnapshot(总阶段数、完成数、当前阶段、状态映射、花费、评分等),通过 `text/event-stream` 实时推送事件到浏览器客户端。包含 15 秒心跳保活、自动清理断开客户端 | 新增 | §15.2 |
|
|
2583
|
+
| | `src/dashboard/server.ts` | **HTTP 服务器**:Node.js 内置 `http` 模块(零外部依赖),路由 `GET /` → index.html、`GET /events` → SSE 端点、`GET /api/state` → 构建状态快照、`GET /css/*` / `GET /js/*` → 静态资源 | 新增 | §15.3 |
|
|
2584
|
+
| | `src/dashboard/index.ts` | **startDashboard/stopDashboard 入口**:订阅 EventBus 所有事件转发至 SSE,启动 HTTP 服务器(默认端口 7879),自动打开浏览器,端口被占用时仅日志警告不 crash | 新增 | §15.4 |
|
|
2585
|
+
| | `src/dashboard/static/` | **前端三面板页面**:深色主题仪表盘 | 新增 | §15.5 |
|
|
2586
|
+
| | `src/dashboard/static/index.html` | **页面结构**:顶部状态栏 + 进度条 → 中部 SVG 流程图 → 底部详细日志面板 | 新增 | §15.5 |
|
|
2587
|
+
| | `src/dashboard/static/css/styles.css` | **样式**:深色主题 CSS,SVG 节点颜色编码(灰/蓝/绿/红/黄),脉冲动画 @keyframes | 新增 | §15.5 |
|
|
2588
|
+
| | `src/dashboard/static/js/app.js` | **前端逻辑**:EventSource SSE 客户端、SVG 数据流图渲染(React Flow 风格节点+箭头+状态色码)、log 面板(系统/成功/错误/ACP 输出分类),100ms 批处理渲染节流 | 新增 | §15.5 |
|
|
2589
|
+
| 3 | `src/types/auto-types.ts` | **AcpCallOptions.onData**:增加 `onData?: (text: string) => void` 可选回调,用于 ACP 实时 stdout 逐块推送到仪表盘 | 增强 | §4.3 |
|
|
2590
|
+
| 4 | `src/lib/acp-client.ts` | **onData 转发**:在 `child.stdout.on('data')` 处理器中,收集 stdout 后调用 `opts.onData?.(text)`,将 Claude 实时输出逐块推送给 EventBus → SSE → 前端日志面板 | 增强 | §4.3 |
|
|
2591
|
+
| 5 | `src/lib/phase-runner.ts` | **事件埋点**:引入 `dashboardEventBus`;`runPhase()` 开始处 emit `phase_started`;ACP 调用传入 `onData: (text) => dashboardEventBus.emitAcpOutput()`;完成时 emit `phase_completed`(含耗时);失败时 emit `phase_failed` | 增强 | §6.3 |
|
|
2592
|
+
| 6 | `src/lib/auto-orchestrator.ts` | **事件埋点 + 优化容错**:`run()` 开始 emit `build_started`;review 循环前后 emit `review_started/completed` + `score_updated`;optimize 块 try-catch 包裹(v2.5 关键修复——ACP 超时不 throw),成功后 emit `optimize_started/completed`;最终 emit `build_completed` + `cost_updated` | Bug 修复 | §3.3 / §12 |
|
|
2593
|
+
| 7 | `src/lib/cost-tracker.ts` | **花费事件**:每次 `recordCall()` 末尾 emit `cost_updated`,使仪表盘实时更新花费显示 | 增强 | §9.4 |
|
|
2594
|
+
| 8 | `src/lib/auto-optimize.ts` | **优化实施容错**(根因修复):`runOptimizeImplement()` 中 ACP 调用失败不再 `throw new Error()`,改为 `logger.warn("继续状态重置")`。状态重置(对应阶段→PENDING)是优化流程正确推进的关键——不做状态重置则下次重跑仍从原评审点开始,永远不能推进到 S90 | Bug 修复 | §12 |
|
|
2595
|
+
| 9 | `src/commands/auto.ts` | **Dashboard 集成**:`runAuto()` 中 `startDashboard(7879)` → `orchestrator.run()` → `finally { stopDashboard() }` | 集成 | §14 |
|
|
2596
|
+
| 10 | `package.json` | **构建脚本**:`build` 增加 `cpSync('src/dashboard/static', 'dist/dashboard/static')` 确保前端静态资源随编译产物一同发布 | 增强 | — |
|
|
2597
|
+
|
|
2598
|
+
### v2.5 架构变更示意图
|
|
2599
|
+
|
|
2600
|
+
```
|
|
2601
|
+
svharnessbuild/src/
|
|
2602
|
+
├── dashboard/ ← 新增模块(与 auto/ 平级)
|
|
2603
|
+
│ ├── index.ts ← startDashboard/stopDashboard
|
|
2604
|
+
│ ├── event-bus.ts ← DashboardEventBus 单例
|
|
2605
|
+
│ ├── server.ts ← HTTP 服务器(端口 7879)
|
|
2606
|
+
│ ├── sse-handler.ts ← SSE 广播管理器
|
|
2607
|
+
│ └── static/ ← 前端资源(纯 HTML/CSS/JS)
|
|
2608
|
+
├── lib/
|
|
2609
|
+
│ ├── auto-orchestrator.ts ← +事件埋点 + try-catch 优化容错
|
|
2610
|
+
│ ├── phase-runner.ts ← +事件埋点 + onData 传递
|
|
2611
|
+
│ ├── auto-optimize.ts ← 优化实施不 throw
|
|
2612
|
+
│ ├── cost-tracker.ts ← +cost_updated 事件
|
|
2613
|
+
│ └── acp-client.ts ← +onData 回调
|
|
2614
|
+
├── commands/
|
|
2615
|
+
│ └── auto.ts ← +startDashboard/stopDashboard
|
|
2616
|
+
└── config/
|
|
2617
|
+
└── merge-options.ts ← force/yes/autoApprove 默认 true
|
|
2618
|
+
```
|
|
2619
|
+
|
|
2620
|
+
### v2.5 事件流
|
|
2621
|
+
|
|
2622
|
+
```
|
|
2623
|
+
PhaseRunner/AutoOrchestrator/CostTracker
|
|
2624
|
+
→ dashboardEventBus.emit*(event)
|
|
2625
|
+
→ SseManager.handleDashboardEvent(event)
|
|
2626
|
+
→ update BuildSnapshot state
|
|
2627
|
+
→ broadcast SSE event → 浏览器三面板自动更新
|
|
2628
|
+
```
|
|
2629
|
+
|
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
const { runStartAgent } = require('../dist/lib/agent-launcher');
|
|
5
5
|
|
|
6
6
|
const positionalWorkdir = process.argv[2];
|
|
7
|
+
const extraArgs = process.argv.slice(3);
|
|
7
8
|
|
|
8
9
|
runStartAgent({
|
|
9
10
|
agent: 'codechat',
|
|
10
11
|
positionalWorkdir,
|
|
12
|
+
extraArgs: extraArgs.length > 0 ? extraArgs : undefined,
|
|
11
13
|
})
|
|
12
14
|
.then((code) => {
|
|
13
15
|
process.exitCode = code;
|
package/dist/commands/auto.js
CHANGED
|
@@ -6,17 +6,59 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.runAuto = runAuto;
|
|
7
7
|
exports.resolveAutoContext = resolveAutoContext;
|
|
8
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const node_child_process_1 = require("node:child_process");
|
|
9
10
|
const config_1 = require("../config");
|
|
10
11
|
const logger_1 = require("../utils/logger");
|
|
11
12
|
const harness_resolver_1 = require("../lib/harness-resolver");
|
|
12
13
|
const auto_orchestrator_1 = require("../lib/auto-orchestrator");
|
|
14
|
+
const dashboard_1 = require("../dashboard");
|
|
15
|
+
/**
|
|
16
|
+
* Prevent system sleep on Windows during long-running auto execution.
|
|
17
|
+
* Registers process-level handlers so sleep is restored on any exit path.
|
|
18
|
+
* Non-Windows platforms are a no-op.
|
|
19
|
+
*/
|
|
20
|
+
function preventSleepWindows() {
|
|
21
|
+
if (process.platform !== 'win32')
|
|
22
|
+
return null;
|
|
23
|
+
const restore = () => {
|
|
24
|
+
try {
|
|
25
|
+
(0, node_child_process_1.execSync)('powercfg /change standby-timeout-ac 30', { timeout: 5000 });
|
|
26
|
+
(0, node_child_process_1.execSync)('powercfg /change hibernate-timeout-ac 30', { timeout: 5000 });
|
|
27
|
+
}
|
|
28
|
+
catch { /* best-effort */ }
|
|
29
|
+
};
|
|
30
|
+
// Disable sleep
|
|
31
|
+
try {
|
|
32
|
+
(0, node_child_process_1.execSync)('powercfg /change standby-timeout-ac 0', { timeout: 5000 });
|
|
33
|
+
(0, node_child_process_1.execSync)('powercfg /change hibernate-timeout-ac 0', { timeout: 5000 });
|
|
34
|
+
console.log('💤 已禁止系统休眠,执行完成后自动恢复');
|
|
35
|
+
}
|
|
36
|
+
catch { /* powercfg not available or no permission; skip */ }
|
|
37
|
+
// Register safety net: restore on any process exit
|
|
38
|
+
process.on('exit', restore);
|
|
39
|
+
process.on('SIGINT', () => process.exit(130));
|
|
40
|
+
process.on('SIGTERM', () => process.exit(143));
|
|
41
|
+
process.on('uncaughtExceptionMonitor', restore);
|
|
42
|
+
return restore;
|
|
43
|
+
}
|
|
13
44
|
async function runAuto(positionalWorkdir, opts, cmd) {
|
|
14
45
|
const loaded = (0, config_1.loadConfig)({ configPath: opts.config });
|
|
15
46
|
const merged = (0, config_1.mergeAutoOptions)(opts, loaded?.config.auto, loaded?.config.defaults, cmd);
|
|
16
47
|
const ctx = await resolveAutoContext(positionalWorkdir, merged);
|
|
17
48
|
(0, logger_1.setVerbose)(ctx.verbose);
|
|
49
|
+
// Start dashboard server (non-blocking, best-effort)
|
|
50
|
+
(0, dashboard_1.startDashboard)(7879).catch(() => { });
|
|
51
|
+
// Prevent system sleep during execution; restore on any exit path
|
|
52
|
+
const restoreSleep = preventSleepWindows();
|
|
18
53
|
const orchestrator = new auto_orchestrator_1.AutoOrchestrator(ctx);
|
|
19
|
-
|
|
54
|
+
try {
|
|
55
|
+
await orchestrator.run();
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
// Graceful completion: restore sleep (exit handler also covers crashes)
|
|
59
|
+
restoreSleep?.();
|
|
60
|
+
(0, dashboard_1.stopDashboard)();
|
|
61
|
+
}
|
|
20
62
|
}
|
|
21
63
|
async function resolveAutoContext(positionalWorkdir, opts) {
|
|
22
64
|
const workDir = node_path_1.default.resolve(positionalWorkdir ?? opts.workDir ?? process.cwd());
|
|
@@ -35,7 +77,7 @@ async function resolveAutoContext(positionalWorkdir, opts) {
|
|
|
35
77
|
budgetUsd: opts.budgetUsd ?? null,
|
|
36
78
|
costAlertThreshold: normalizeNumber(opts.costAlertThreshold, 5, '--cost-alert'),
|
|
37
79
|
acpMode: opts.acpMode === 'sdk' || opts.acpMode === 'cli-print' ? opts.acpMode : 'auto',
|
|
38
|
-
acpTimeoutMs: normalizeInteger(opts.acpTimeoutMs,
|
|
80
|
+
acpTimeoutMs: normalizeInteger(opts.acpTimeoutMs, 1_800_000, '--acp-timeout-ms'),
|
|
39
81
|
maxRetriesPerPhase: normalizeInteger(opts.maxRetriesPerPhase, 2, '--max-retries-per-phase'),
|
|
40
82
|
baselineAutoExtract: normalizeBaselineExtract(opts.baselineAutoExtract),
|
|
41
83
|
promptDir: opts.promptDir,
|
|
@@ -117,7 +117,7 @@ try { chcp 65001 | Out-Null } catch {}
|
|
|
117
117
|
$OutputEncoding = [System.Text.UTF8Encoding]::new($false)
|
|
118
118
|
|
|
119
119
|
Set-Location -LiteralPath $WorkDir
|
|
120
|
-
& node '${escapedNodeScript}' $WorkDir
|
|
120
|
+
& node '${escapedNodeScript}' "$WorkDir"
|
|
121
121
|
exit $LASTEXITCODE
|
|
122
122
|
`;
|
|
123
123
|
}
|
|
@@ -137,7 +137,8 @@ function buildMenuCommand(ps1Path, dirPlaceholder, wtPath) {
|
|
|
137
137
|
return `powershell.exe ${psArgs}`;
|
|
138
138
|
}
|
|
139
139
|
function registerMenuKey(menuKey, command, iconValue) {
|
|
140
|
-
|
|
140
|
+
// Use PowerShell-based write for the label (Unicode-safe for Chinese chars)
|
|
141
|
+
(0, win_registry_1.regSetDefaultUnicode)(menuKey, win_registry_1.SHELL_MENU_LABEL);
|
|
141
142
|
(0, win_registry_1.regSetString)(menuKey, 'Icon', iconValue);
|
|
142
143
|
(0, win_registry_1.regSetDefault)(`${menuKey}\\command`, command);
|
|
143
144
|
}
|
|
@@ -176,8 +177,12 @@ async function runShellInstall(opts = {}) {
|
|
|
176
177
|
*/
|
|
177
178
|
async function runShellUninstall(opts = {}) {
|
|
178
179
|
assertWin32();
|
|
180
|
+
// Remove svharness-managed menu
|
|
179
181
|
(0, win_registry_1.regDeleteKey)(win_registry_1.SHELL_MENU_KEY_BG);
|
|
180
182
|
(0, win_registry_1.regDeleteKey)(win_registry_1.SHELL_MENU_KEY_DIR);
|
|
183
|
+
// Also remove standalone script menu (Start-CodeChat.ps1 independent install)
|
|
184
|
+
(0, win_registry_1.regDeleteKey)(STANDALONE_MENU_KEY_BG);
|
|
185
|
+
(0, win_registry_1.regDeleteKey)(STANDALONE_MENU_KEY_DIR);
|
|
181
186
|
if (await fs_extra_1.default.pathExists(STUB_PATH)) {
|
|
182
187
|
await fs_extra_1.default.remove(STUB_PATH);
|
|
183
188
|
}
|
|
@@ -187,6 +192,11 @@ async function runShellUninstall(opts = {}) {
|
|
|
187
192
|
if (await fs_extra_1.default.pathExists(LEGACY_STUB_PATH)) {
|
|
188
193
|
await fs_extra_1.default.remove(LEGACY_STUB_PATH);
|
|
189
194
|
}
|
|
195
|
+
// Clean up standalone script directory (Start-CodeChat.ps1 independent install)
|
|
196
|
+
const standaloneDir = path_1.default.join(process.env.LOCALAPPDATA ?? path_1.default.join(os_1.default.homedir(), 'AppData', 'Local'), 'codechat-standalone');
|
|
197
|
+
if (await fs_extra_1.default.pathExists(standaloneDir)) {
|
|
198
|
+
await fs_extra_1.default.remove(standaloneDir);
|
|
199
|
+
}
|
|
190
200
|
await (0, ps_codechat_alias_1.uninstallCodechatAlias)({ silent: opts.silent });
|
|
191
201
|
logSuccess('Windows 右键菜单与 PowerShell 别名已卸载', opts.silent);
|
|
192
202
|
}
|
|
@@ -67,14 +67,14 @@ function mergeAutoOptions(cli, configSection, defaults, cmd) {
|
|
|
67
67
|
requirements: pickString('requirements', cli.requirements, cfg.requirements, cmd),
|
|
68
68
|
references: pickString('references', cli.references, cfg.references, cmd),
|
|
69
69
|
extraSkills: pickStringArray('extraSkills', cli.extraSkills, cfg.extraSkills, cmd),
|
|
70
|
-
force: pickBool('force', cli.force, cfg.force,
|
|
70
|
+
force: pickBool('force', cli.force, cfg.force, true, cmd),
|
|
71
71
|
targetScore: pickNumber('targetScore', cli.targetScore, cfg.targetScore, cmd),
|
|
72
72
|
maxOptimizeRounds: pickNumber('maxOptimizeRounds', cli.maxOptimizeRounds, cfg.maxOptimizeRounds, cmd),
|
|
73
73
|
checkpoint: pickString('checkpoint', cli.checkpoint, cfg.checkpoint ?? undefined, cmd),
|
|
74
74
|
maxPhase: pickString('maxPhase', cli.maxPhase, cfg.maxPhase ?? undefined, cmd),
|
|
75
75
|
dryRun: pickBool('dryRun', cli.dryRun, cfg.dryRun, undefined, cmd),
|
|
76
|
-
autoApprove: pickBool('autoApprove', cli.autoApprove, cfg.autoApprove,
|
|
77
|
-
yes: pickBool('yes', cli.yes, cfg.yes,
|
|
76
|
+
autoApprove: pickBool('autoApprove', cli.autoApprove, cfg.autoApprove, true, cmd),
|
|
77
|
+
yes: pickBool('yes', cli.yes, cfg.yes, true, cmd),
|
|
78
78
|
verbose: pickBool('verbose', cli.verbose, cfg.verbose, defaults?.verbose, cmd),
|
|
79
79
|
maxTotalCalls: pickNumber('maxTotalCalls', cli.maxTotalCalls, cfg.maxTotalCalls, cmd),
|
|
80
80
|
budgetUsd: pickNumber('budgetUsd', cli.budgetUsd, cfg.budgetUsd ?? undefined, cmd),
|
|
@@ -87,6 +87,10 @@ async function writeBuildProjectEntry(input) {
|
|
|
87
87
|
logger_1.logger.warn(`项目根已存在 ${rel},跳过写入(如需覆盖请对 harness 使用 --force 并确保允许覆盖根入口文件)`);
|
|
88
88
|
return undefined;
|
|
89
89
|
}
|
|
90
|
+
// --force 模式下先备份现有文件为 .bak 再覆盖
|
|
91
|
+
const backupPath = dest + '.bak';
|
|
92
|
+
await fs_extra_1.default.copyFile(dest, backupPath);
|
|
93
|
+
logger_1.logger.info(`已备份 ${rel} 为 ${rel}.bak`);
|
|
90
94
|
}
|
|
91
95
|
const body = renderBuildProjectEntryMarkdown({
|
|
92
96
|
adapter: input.adapter,
|
package/dist/core/scaffold.js
CHANGED
|
@@ -111,13 +111,15 @@ async function rollbackTarget(targetRoot) {
|
|
|
111
111
|
}
|
|
112
112
|
/**
|
|
113
113
|
* Target directory guard. When targetRoot exists and `force` is not set, throws.
|
|
114
|
+
* When `force` is set, backs up the existing directory as `.bak` before removal.
|
|
114
115
|
*/
|
|
115
116
|
async function ensureTargetAvailable(targetRoot, force) {
|
|
116
117
|
if (!(await fs_extra_1.default.pathExists(targetRoot)))
|
|
117
118
|
return;
|
|
118
119
|
if (!force) {
|
|
119
|
-
throw new Error(`target directory already exists: ${targetRoot}\n Use --force to overwrite (will
|
|
120
|
+
throw new Error(`target directory already exists: ${targetRoot}\n Use --force to overwrite (will backup existing content as .bak).`);
|
|
120
121
|
}
|
|
121
|
-
|
|
122
|
-
await fs_extra_1.default.
|
|
122
|
+
const backupPath = targetRoot + '.bak';
|
|
123
|
+
await fs_extra_1.default.move(targetRoot, backupPath, { overwrite: true });
|
|
124
|
+
logger_1.logger.warn(`已将现有 ${node_path_1.default.basename(targetRoot)} 备份为 ${node_path_1.default.basename(backupPath)},重新生成...`);
|
|
123
125
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.dashboardEventBus = exports.DashboardEventBus = void 0;
|
|
4
|
+
const node_events_1 = require("node:events");
|
|
5
|
+
class DashboardEventBus extends node_events_1.EventEmitter {
|
|
6
|
+
emitBuildStarted(totalPhases) {
|
|
7
|
+
this.emitEvent('build_started', { totalPhases });
|
|
8
|
+
}
|
|
9
|
+
emitBuildCompleted(finalScore) {
|
|
10
|
+
this.emitEvent('build_completed', { finalScore });
|
|
11
|
+
}
|
|
12
|
+
emitPhaseStarted(key, short, description, attempt) {
|
|
13
|
+
this.emitEvent('phase_started', { key, short, description, attempt });
|
|
14
|
+
}
|
|
15
|
+
emitPhaseCompleted(key, status, elapsedMs) {
|
|
16
|
+
this.emitEvent('phase_completed', { key, status, elapsedMs });
|
|
17
|
+
}
|
|
18
|
+
emitPhaseFailed(key, error, attempt) {
|
|
19
|
+
this.emitEvent('phase_failed', { key, error, attempt });
|
|
20
|
+
}
|
|
21
|
+
emitAcpOutput(phase, text) {
|
|
22
|
+
this.emitEvent('acp_output', { phase, text });
|
|
23
|
+
}
|
|
24
|
+
emitCostUpdated(totalCost, callCount) {
|
|
25
|
+
this.emitEvent('cost_updated', { totalCost, callCount });
|
|
26
|
+
}
|
|
27
|
+
emitScoreUpdated(score, round) {
|
|
28
|
+
this.emitEvent('score_updated', { score, round });
|
|
29
|
+
}
|
|
30
|
+
emitReviewStarted(round) {
|
|
31
|
+
this.emitEvent('review_started', { round });
|
|
32
|
+
}
|
|
33
|
+
emitReviewCompleted(round, score, criticalCount) {
|
|
34
|
+
this.emitEvent('review_completed', { round, score, criticalCount });
|
|
35
|
+
}
|
|
36
|
+
emitOptimizeStarted(round) {
|
|
37
|
+
this.emitEvent('optimize_started', { round });
|
|
38
|
+
}
|
|
39
|
+
emitOptimizeCompleted(round) {
|
|
40
|
+
this.emitEvent('optimize_completed', { round });
|
|
41
|
+
}
|
|
42
|
+
emitEvent(type, data) {
|
|
43
|
+
const event = { type, data, timestamp: Date.now() };
|
|
44
|
+
this.emit(type, event);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
exports.DashboardEventBus = DashboardEventBus;
|
|
48
|
+
exports.dashboardEventBus = new DashboardEventBus();
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.startDashboard = startDashboard;
|
|
4
|
+
exports.stopDashboard = stopDashboard;
|
|
5
|
+
const event_bus_1 = require("./event-bus");
|
|
6
|
+
const server_1 = require("./server");
|
|
7
|
+
const sse_handler_1 = require("./sse-handler");
|
|
8
|
+
const logger_1 = require("../utils/logger");
|
|
9
|
+
let server = null;
|
|
10
|
+
let sseManager = null;
|
|
11
|
+
/**
|
|
12
|
+
* Start the dashboard HTTP server.
|
|
13
|
+
*
|
|
14
|
+
* @param port - HTTP port (default 7879)
|
|
15
|
+
* @returns Promise that resolves when server is listening
|
|
16
|
+
*/
|
|
17
|
+
function startDashboard(port = 7879) {
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
if (server) {
|
|
20
|
+
resolve();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
sseManager = new sse_handler_1.SseManager();
|
|
24
|
+
server = (0, server_1.createServer)(event_bus_1.dashboardEventBus, sseManager);
|
|
25
|
+
// Subscribe to all events from the bus and forward to SSE clients
|
|
26
|
+
const eventTypes = [
|
|
27
|
+
'build_started', 'build_completed',
|
|
28
|
+
'phase_started', 'phase_completed', 'phase_failed',
|
|
29
|
+
'acp_output',
|
|
30
|
+
'cost_updated', 'score_updated',
|
|
31
|
+
'review_started', 'review_completed',
|
|
32
|
+
'optimize_started', 'optimize_completed',
|
|
33
|
+
];
|
|
34
|
+
for (const type of eventTypes) {
|
|
35
|
+
event_bus_1.dashboardEventBus.on(type, (event) => {
|
|
36
|
+
sseManager?.handleDashboardEvent(event);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
server.on('error', (err) => {
|
|
40
|
+
if (err.code === 'EADDRINUSE') {
|
|
41
|
+
logger_1.logger.warn(`[dashboard] 端口 ${port} 已被占用,跳过仪表盘`);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
logger_1.logger.warn(`[dashboard] 启动失败: ${err.message}`);
|
|
45
|
+
}
|
|
46
|
+
// Don't crash the build, dashboard is optional
|
|
47
|
+
server = null;
|
|
48
|
+
resolve();
|
|
49
|
+
});
|
|
50
|
+
server.listen(port, () => {
|
|
51
|
+
const url = `http://localhost:${port}`;
|
|
52
|
+
logger_1.logger.info(`[dashboard] 仪表盘已启动,请手动访问: ${url}`);
|
|
53
|
+
resolve();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Stop the dashboard server and clean up.
|
|
59
|
+
*/
|
|
60
|
+
function stopDashboard() {
|
|
61
|
+
if (sseManager) {
|
|
62
|
+
sseManager.closeAll();
|
|
63
|
+
sseManager = null;
|
|
64
|
+
}
|
|
65
|
+
if (server) {
|
|
66
|
+
server.close();
|
|
67
|
+
server = null;
|
|
68
|
+
}
|
|
69
|
+
}
|