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.
@@ -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.3(2026-06-13,依据 `design-problem2.md` 审查修订)
7
- > **最后更新**:2026-06-13
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 result = spawnSync(
511
- 'claude',
512
- ['--dangerously-skip-permissions', '--print'],
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
- const output = result.stdout ?? '';
522
- return {
523
- success: result.status === 0,
524
- output,
525
- completionSignal: parseCompletionSignals(output),
526
- error: result.stderr || result.error?.message,
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` | 检测目标 `<name>-harness/` 目录是否存在,存在则自动加 `--force` | `--no-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: false # true = 自动通过所有表单门禁
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: 600000 # 10 分钟
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
- runOptimizePlan(harnessRoot, config, round); // 第<n>轮修改-plan.md
2215
- runOptimizeImplement(harnessRoot, config, round); // 按计划实施
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
- const result = spawnSync('claude', ['--dangerously-skip-permissions', '--print'], {
2221
- input: prompt, cwd: workDir, encoding: 'utf-8',
2222
- timeout: config.acpTimeoutMs ?? 600_000,
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
- if (result.error) throw new Error(`claude CLI 调用失败: ${result.error.message}`);
2226
- return result.stdout;
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;
@@ -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
- await orchestrator.run();
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, 600_000, '--acp-timeout-ms'),
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
- (0, win_registry_1.regSetDefault)(menuKey, win_registry_1.SHELL_MENU_LABEL);
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
  }
@@ -30,6 +30,7 @@ async function runStartAgentCommand(opts) {
30
30
  positionalWorkdir,
31
31
  syncEnv: !opts.noSyncEnv,
32
32
  skipPermissions: !opts.noSkipPermissions,
33
+ extraArgs: opts.extraArgs,
33
34
  });
34
35
  }
35
36
  function handleStartAgentError(err) {
@@ -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, defaults?.force, cmd),
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, undefined, cmd),
77
- yes: pickBool('yes', cli.yes, cfg.yes, defaults?.yes, cmd),
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,
@@ -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 delete existing content).`);
120
+ throw new Error(`target directory already exists: ${targetRoot}\n Use --force to overwrite (will backup existing content as .bak).`);
120
121
  }
121
- logger_1.logger.warn(`--force specified, removing existing ${targetRoot}`);
122
- await fs_extra_1.default.remove(targetRoot);
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
+ }