svharness 0.15.10 → 0.15.13

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.
Files changed (49) hide show
  1. package/README.md +106 -2
  2. package/auto/auto-harness-detail-design.md +342 -36
  3. package/auto/skills/svharness/SKILL.md +44 -1
  4. package/auto/templates/auto-pack-req.config.example.yaml +29 -0
  5. package/auto/templates/auto.config.example.yaml +2 -2
  6. package/auto/templates/phase-prompts/S40-adversarial-review.md +31 -0
  7. package/auto/templates/phase-prompts/S40-review.md +22 -0
  8. package/auto/templates/phase-prompts/S40-sample-trace.md +26 -0
  9. package/auto/templates/requirements-review-rubric.yaml +79 -0
  10. package/dist/commands/auto-pack-req.js +70 -0
  11. package/dist/commands/auto.js +18 -2
  12. package/dist/commands/convert.js +2 -0
  13. package/dist/commands/shell-integration.js +29 -56
  14. package/dist/config/merge-options.js +2 -0
  15. package/dist/config/normalize.js +7 -0
  16. package/dist/constants/convert.js +5 -0
  17. package/dist/core/markitdown-client.js +7 -2
  18. package/dist/dashboard/event-bus.js +3 -0
  19. package/dist/dashboard/index.js +1 -1
  20. package/dist/dashboard/sse-handler.js +3 -0
  21. package/dist/dashboard/static/js/app.js +5 -0
  22. package/dist/index.js +79 -6
  23. package/dist/lib/acp-agent-cli-resolver.js +193 -0
  24. package/dist/lib/acp-client.js +4 -2
  25. package/dist/lib/agent-launcher.js +9 -4
  26. package/dist/lib/auto-assets.js +1 -0
  27. package/dist/lib/auto-optimize.js +23 -3
  28. package/dist/lib/auto-orchestrator.js +79 -20
  29. package/dist/lib/auto-pack-req-context.js +88 -0
  30. package/dist/lib/auto-pack-req-finalize.js +28 -0
  31. package/dist/lib/auto-req-pack-review.js +161 -0
  32. package/dist/lib/auto-sample-trace.js +144 -0
  33. package/dist/lib/auto-state.js +130 -14
  34. package/dist/lib/cost-tracker.js +30 -2
  35. package/dist/lib/gate-checker.js +20 -10
  36. package/dist/lib/phase-prompt-loader.js +4 -0
  37. package/dist/lib/phase-runner.js +234 -71
  38. package/dist/lib/ps-codechat-alias.js +29 -16
  39. package/dist/lib/review-parser.js +14 -0
  40. package/dist/lib/score-aggregator.js +21 -0
  41. package/dist/lib/win-registry.js +1 -1
  42. package/dist/utils/validate-args.js +2 -1
  43. package/package.json +3 -2
  44. package/templates/codechat/Start-CodeChat.ps1 +176 -135
  45. package/templates/codechat/bash.exe.stackdump +29 -0
  46. package/templates/codechat/docs/project-structure.md +59 -0
  47. package/templates/svharness.config.example.yaml +2 -0
  48. package/dist/lib/launch-script-path.js +0 -24
  49. package/templates/codechat/.claude/.env +0 -59
@@ -101,6 +101,9 @@ class SseManager {
101
101
  this.buildSnapshot.score = event.data.finalScore;
102
102
  }
103
103
  break;
104
+ case 'build_failed':
105
+ this.buildSnapshot.status = 'failed';
106
+ break;
104
107
  }
105
108
  }
106
109
  getSnapshot() {
@@ -323,6 +323,11 @@ function connectSSE() {
323
323
  addLog({ type: 'success', text: `✅ 构建完成!${data.finalScore != null ? `最终评分: ${data.finalScore}` : ''}`, timestamp: Date.now() });
324
324
  updateStatus();
325
325
  },
326
+ build_failed() {
327
+ state.status = 'failed';
328
+ addLog({ type: 'error', text: '❌ 构建失败!请检查终端日志', timestamp: Date.now() });
329
+ updateStatus();
330
+ },
326
331
  phase_started(data) {
327
332
  state.currentPhase = data.key;
328
333
  state.phaseStatuses[data.key] = 'IN_PROGRESS';
package/dist/index.js CHANGED
@@ -5,13 +5,15 @@ const logger_1 = require("./utils/logger");
5
5
  const version_1 = require("./utils/version");
6
6
  const validate_args_1 = require("./utils/validate-args");
7
7
  const harness_name_1 = require("./utils/harness-name");
8
+ const convert_1 = require("./constants/convert");
8
9
  const init_1 = require("./commands/init");
9
10
  const apply_1 = require("./commands/apply");
10
- const convert_1 = require("./commands/convert");
11
+ const convert_2 = require("./commands/convert");
11
12
  const wizard_1 = require("./commands/wizard");
12
13
  const doctor_1 = require("./commands/doctor");
13
14
  const wiki_1 = require("./commands/wiki");
14
15
  const auto_1 = require("./commands/auto");
16
+ const auto_pack_req_1 = require("./commands/auto-pack-req");
15
17
  const requirements_1 = require("./commands/requirements");
16
18
  const start_agent_1 = require("./commands/start-agent");
17
19
  const shell_integration_1 = require("./commands/shell-integration");
@@ -163,7 +165,7 @@ function attachBuildOptions(cmd) {
163
165
  .option('--convert-endpoint <url>', '【build 自动 convert】markitdown_serve 基址(默认 http://markitdown.desaysz.site)')
164
166
  .option('--convert-concurrency <n>', '【build 自动 convert】并发上传数(默认沿用 convert)', (v) => Number(v))
165
167
  .option('--convert-max-file-mb <n>', '【build 自动 convert】单文件大小上限 MB(默认沿用 convert)', (v) => Number(v))
166
- .option('--convert-timeout-sec <n>', '【build 自动 convert】超时秒数(默认沿用 convert)', (v) => Number(v))
168
+ .option('--convert-timeout-sec <n>', `【build 自动 convert】超时秒数(默认 ${convert_1.DEFAULT_CONVERT_TIMEOUT_SEC},15 分钟)`, (v) => Number(v))
167
169
  .option('--convert-force', '【build 自动 convert】覆盖已存在同名 .md')
168
170
  .option('--force', '强制覆盖已存在的目标目录')
169
171
  .option('-y, --yes', '跳过交互确认,使用默认值')
@@ -218,6 +220,10 @@ function main() {
218
220
  '│ # 方式六:对当前工程生成 wiki 骨架 ',
219
221
  '│ svharness wiki ',
220
222
  '│ ',
223
+ '│ # 方式七:在 requirements 资料目录直接条目化(或指定资料包根) ',
224
+ '│ cd ./requirements && svharness auto_pack_req --yes ',
225
+ '│ svharness auto_pack_req ./req-pack --yes ',
226
+ '│ ',
221
227
  '└──────────────────────────────────────────────────────────────────────────',
222
228
  ' 📚 使用说明文档:https://yesv-desaysv.feishu.cn/docx/OEBwdywTfoncPNxPZ0acClNincg',
223
229
  '',
@@ -248,14 +254,14 @@ function main() {
248
254
  .option('--extra-skills <path...>', '额外运行期资源路径(覆盖自动检测)')
249
255
  .option('--force', '目标 harness 已存在时覆盖')
250
256
  .option('--target-score <score>', '目标综合评分(默认 9.5)', (v) => Number(v))
251
- .option('--max-optimize-rounds <n>', '最大优化轮数(默认 5)', (v) => Number(v))
257
+ .option('--max-optimize-rounds <n>', '最大优化轮数(默认 10)', (v) => Number(v))
252
258
  .option('--checkpoint <phase>', '到达阶段前暂停')
253
259
  .option('--max-phase <phase>', '只运行到此阶段即停止')
254
260
  .option('--dry-run', '打印 prompt 和动作,不调用 Claude')
255
261
  .option('--auto-approve', '自动通过无人值守下的表单门禁')
256
262
  .option('-y, --yes', '跳过交互确认')
257
263
  .option('--verbose', '显示详细日志')
258
- .option('--max-total-calls <n>', 'ACP 调用次数上限(默认 30)', (v) => Number(v))
264
+ .option('--max-total-calls <n>', 'ACP 调用次数上限(默认 100)', (v) => Number(v))
259
265
  .option('--budget-usd <amount>', '费用预算上限(USD)', (v) => Number(v))
260
266
  .option('--cost-alert <amount>', '单次调用费用告警阈值(USD)', (v) => Number(v))
261
267
  .option('--acp-mode <mode>', 'ACP 模式:auto | cli-print | sdk')
@@ -264,6 +270,7 @@ function main() {
264
270
  .option('--prompt-dir <path>', '覆盖 auto phase prompt 模板目录')
265
271
  .option('--baseline-auto-extract <mode>', 'explicit | conservative | disabled')
266
272
  .option('--convert-endpoint <url>', 'markitdown_serve 基址')
273
+ .option('--no-auto-repair', '禁用阶段失败自动修复(默认启用)')
267
274
  .action(async (workdir, opts, cmd) => {
268
275
  try {
269
276
  await (0, auto_1.runAuto)(workdir, {
@@ -276,6 +283,72 @@ function main() {
276
283
  process.exitCode = 1;
277
284
  }
278
285
  });
286
+ program
287
+ .command('auto_pack_req [workdir]')
288
+ .description('需求包全自动条目化(S40 专项):S30+S40 → 机械门禁+对抗验证+抽样验证,综合分须严格大于目标分')
289
+ .addHelpText('after', [
290
+ '',
291
+ '说明:',
292
+ ' 独立于 svharness auto,仅跑 S30_convert_docs + S40_extract_requirements,不执行 S50~S90。',
293
+ ' 综合分 = 机械门禁×0.40 + 对抗验证×0.30 + 抽样验证×0.30',
294
+ ' 通过条件:综合分 > targetScore(默认 9.5)且 verify/doctor 硬门禁全通过。',
295
+ '',
296
+ '工作目录(无参数时默认当前目录):',
297
+ ' · 当前目录是 requirements 资料夹(含 doc/md/pdf 等)→ 视为输入,在上级目录创建 harness',
298
+ ' · 当前目录是资料包根(含 requirements/ 子目录)→ 在目录内创建 harness',
299
+ ' · 当前目录是已有 harness 根 → 重置 S40 后重做条目化',
300
+ '',
301
+ '两种入口:',
302
+ ' · 资料包新建 — 无 harness.yaml,自动 build 后条目化',
303
+ ' · 已有 harness 重做 — 检测到 harness.yaml 后重置 S40=PENDING,仅重跑 S40',
304
+ '',
305
+ '内置默认:targetScore=9.5, maxOptimizeRounds=10, sampleCount=7, autoApprove=yes',
306
+ '配置示例:auto/templates/auto-pack-req.config.example.yaml(auto_pack_req: 节)',
307
+ '详细设计:auto/auto-harness-detail-design.md §2.2.1',
308
+ '',
309
+ '示例:',
310
+ ' cd ./requirements && svharness auto_pack_req --yes',
311
+ ' svharness auto_pack_req ./req-pack --yes',
312
+ ' svharness auto_pack_req ./my-harness --requirements ./docs --yes',
313
+ ' svharness auto_pack_req --dry-run',
314
+ '',
315
+ ].join('\n'))
316
+ .option('--work-dir <path>', '工作目录(资料包根或已有 harness 根)')
317
+ .option('--config <path>', '配置文件路径(支持 auto_pack_req: 节)')
318
+ .option('--harness-name <name>', 'harness 名称(资料包模式可自动推断)')
319
+ .option('--goal <text>', '构建目的描述')
320
+ .option('--arch <arch>', '架构模板:' + (0, validate_args_1.listSupportedArches)().join(' | '))
321
+ .option('--agent <agent>', '目标 Agent(默认 claude-code)')
322
+ .option('--requirements <path>', '需求文档路径(覆盖自动检测)')
323
+ .option('--force', '目标 harness 已存在时覆盖')
324
+ .option('--target-score <score>', '目标综合评分(默认 9.5,须严格大于此值)', (v) => Number(v))
325
+ .option('--max-optimize-rounds <n>', '最大优化轮数(默认 10)', (v) => Number(v))
326
+ .option('--sample-count <n>', '抽样验证片段数(默认 7)', (v) => Number(v))
327
+ .option('--dry-run', '打印 prompt 和动作,不调用 Agent')
328
+ .option('--auto-approve', '自动通过无人值守下的表单门禁')
329
+ .option('-y, --yes', '跳过交互确认')
330
+ .option('--verbose', '显示详细日志')
331
+ .option('--max-total-calls <n>', 'ACP 调用次数上限(默认 100)', (v) => Number(v))
332
+ .option('--budget-usd <amount>', '费用预算上限(USD)', (v) => Number(v))
333
+ .option('--cost-alert <amount>', '单次调用费用告警阈值(USD)', (v) => Number(v))
334
+ .option('--acp-mode <mode>', 'ACP 模式:auto | cli-print | sdk')
335
+ .option('--acp-timeout-ms <n>', '单次 ACP 调用超时毫秒', (v) => Number(v))
336
+ .option('--max-retries-per-phase <n>', '每阶段最大重试次数', (v) => Number(v))
337
+ .option('--prompt-dir <path>', '覆盖 auto phase prompt 模板目录')
338
+ .option('--convert-endpoint <url>', 'markitdown_serve 基址')
339
+ .option('--no-auto-repair', '禁用阶段失败自动修复(默认启用)')
340
+ .action(async (workdir, opts, cmd) => {
341
+ try {
342
+ await (0, auto_pack_req_1.runAutoPackReq)(workdir, {
343
+ ...opts,
344
+ costAlertThreshold: opts.costAlert,
345
+ }, cmd);
346
+ }
347
+ catch (err) {
348
+ logger_1.logger.error(err.message);
349
+ process.exitCode = 1;
350
+ }
351
+ });
279
352
  program
280
353
  .command('wizard')
281
354
  .description('交互式向导:分步填写路径与说明,可保存到 svharness.config.yaml 并/或立即执行 build/apply/convert。')
@@ -298,7 +371,7 @@ function main() {
298
371
  .option('--endpoint <url>', 'markitdown_serve 基址(逗号分隔可配置 failover)')
299
372
  .option('--concurrency <n>', '【默认 3】并发上传数', (v) => Number(v))
300
373
  .option('--max-file-mb <n>', '【默认 50】单文件大小上限(MB)', (v) => Number(v))
301
- .option('--timeout-sec <n>', '【默认 120】超时秒数', (v) => Number(v))
374
+ .option('--timeout-sec <n>', `【默认 ${convert_1.DEFAULT_CONVERT_TIMEOUT_SEC},15 分钟】单文件转换超时秒数`, (v) => Number(v))
302
375
  .option('--type <type>', 'requirements | references')
303
376
  .option('--force', '覆盖已存在同名 .md')
304
377
  .option('--split-sheets-suffix <suffix>', 'xlsx/xls 按 sheet(##)拆分时的子目录后缀', '_split')
@@ -325,7 +398,7 @@ function main() {
325
398
  yes: opts.yes,
326
399
  verbose: opts.verbose,
327
400
  }, loaded?.config.convert, loaded?.config.defaults, cmd);
328
- await (0, convert_1.runConvert)({
401
+ await (0, convert_2.runConvert)({
329
402
  input: merged.input ?? [],
330
403
  harness: merged.harness,
331
404
  output: merged.output,
@@ -0,0 +1,193 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getPreferredLocalClaudePath = getPreferredLocalClaudePath;
7
+ exports.resolveAcpAgentCli = resolveAcpAgentCli;
8
+ exports.ensureAcpAgentCliAvailable = ensureAcpAgentCliAvailable;
9
+ exports.formatAcpAgentCliLog = formatAcpAgentCliLog;
10
+ const fs_extra_1 = __importDefault(require("fs-extra"));
11
+ const node_path_1 = __importDefault(require("node:path"));
12
+ const os_1 = __importDefault(require("os"));
13
+ const node_child_process_1 = require("node:child_process");
14
+ const codechat_runner_resolver_1 = require("./codechat-runner-resolver");
15
+ /** Preferred Claude install: ~/.local/bin/claude(.exe) */
16
+ function getPreferredLocalClaudePath(homedir = os_1.default.homedir(), platform = process.platform) {
17
+ const binName = platform === 'win32' ? 'claude.exe' : 'claude';
18
+ return node_path_1.default.join(homedir, '.local', 'bin', binName);
19
+ }
20
+ function normalizeComparable(p) {
21
+ return node_path_1.default.resolve(p).replace(/\\/g, '/').toLowerCase();
22
+ }
23
+ function canAccessExecutable(filePath) {
24
+ try {
25
+ fs_extra_1.default.accessSync(filePath, fs_extra_1.default.constants.F_OK);
26
+ return true;
27
+ }
28
+ catch {
29
+ return false;
30
+ }
31
+ }
32
+ function findExecutableInPath(name, platform = process.platform) {
33
+ try {
34
+ if (platform === 'win32') {
35
+ const out = (0, node_child_process_1.execSync)(`where.exe ${name}`, {
36
+ encoding: 'utf8',
37
+ stdio: ['pipe', 'pipe', 'ignore'],
38
+ windowsHide: true,
39
+ });
40
+ for (const line of out.split(/\r?\n/)) {
41
+ const trimmed = line.trim();
42
+ if (trimmed && canAccessExecutable(trimmed)) {
43
+ return node_path_1.default.resolve(trimmed);
44
+ }
45
+ }
46
+ return null;
47
+ }
48
+ const out = (0, node_child_process_1.execSync)(`command -v ${name}`, {
49
+ encoding: 'utf8',
50
+ stdio: ['pipe', 'pipe', 'ignore'],
51
+ }).trim();
52
+ if (out && canAccessExecutable(out)) {
53
+ return node_path_1.default.resolve(out);
54
+ }
55
+ }
56
+ catch {
57
+ // not in PATH
58
+ }
59
+ return null;
60
+ }
61
+ function getOtherClaudeCandidates(homedir, platform, excludePath) {
62
+ const exclude = excludePath ? normalizeComparable(excludePath) : undefined;
63
+ const found = [];
64
+ const seen = new Set();
65
+ const push = (candidate) => {
66
+ if (!candidate || !canAccessExecutable(candidate))
67
+ return;
68
+ const key = normalizeComparable(candidate);
69
+ if (exclude && key === exclude)
70
+ return;
71
+ if (seen.has(key))
72
+ return;
73
+ seen.add(key);
74
+ found.push(node_path_1.default.resolve(candidate));
75
+ };
76
+ push(findExecutableInPath('claude', platform));
77
+ if (platform === 'win32') {
78
+ const appData = process.env.APPDATA ?? node_path_1.default.join(homedir, 'AppData', 'Roaming');
79
+ push(node_path_1.default.join(appData, 'npm', 'claude.cmd'));
80
+ push(node_path_1.default.join(appData, 'npm', 'claude'));
81
+ push(node_path_1.default.join(homedir, '.claude', 'local', 'claude.exe'));
82
+ }
83
+ return found;
84
+ }
85
+ const ALTERNATIVE_CLI_NAMES = [
86
+ 'codechat',
87
+ 'qodercli',
88
+ 'traecli',
89
+ 'opencode',
90
+ 'codex',
91
+ ];
92
+ function tryResolveCodechatRunner() {
93
+ try {
94
+ const { runner, source, label } = (0, codechat_runner_resolver_1.resolveCodechatRunner)();
95
+ return {
96
+ command: runner,
97
+ kind: 'codechat-runner',
98
+ source: `codechat-${source}`,
99
+ label,
100
+ };
101
+ }
102
+ catch {
103
+ return null;
104
+ }
105
+ }
106
+ function tryAlternativeClis(platform) {
107
+ for (const name of ALTERNATIVE_CLI_NAMES) {
108
+ const resolved = findExecutableInPath(name, platform);
109
+ if (resolved) {
110
+ return {
111
+ command: resolved,
112
+ kind: 'alternative-cli',
113
+ source: 'path',
114
+ label: name,
115
+ };
116
+ }
117
+ }
118
+ return tryResolveCodechatRunner();
119
+ }
120
+ /**
121
+ * Resolve the CLI used by ACP (`--print` mode).
122
+ *
123
+ * Priority:
124
+ * 1. ~/.local/bin/claude(.exe)
125
+ * 2. Other claude installs (PATH, npm global, etc.)
126
+ * 3. Alternative CLIs (codechat, qodercli, traecli, …) or CodeChat runner
127
+ */
128
+ function resolveAcpAgentCli(options = {}) {
129
+ if (options.testCandidates?.length) {
130
+ return options.testCandidates[0];
131
+ }
132
+ const platform = options.platform ?? process.platform;
133
+ const homedir = options.homedir ?? os_1.default.homedir();
134
+ const localClaude = getPreferredLocalClaudePath(homedir, platform);
135
+ if (canAccessExecutable(localClaude)) {
136
+ return {
137
+ command: node_path_1.default.resolve(localClaude),
138
+ kind: 'claude-local-bin',
139
+ source: 'local-bin',
140
+ label: localClaude,
141
+ };
142
+ }
143
+ if (!options.skipProbe) {
144
+ const otherClaude = getOtherClaudeCandidates(homedir, platform, localClaude);
145
+ if (otherClaude.length > 0) {
146
+ return {
147
+ command: otherClaude[0],
148
+ kind: 'claude-path',
149
+ source: 'path',
150
+ label: otherClaude[0],
151
+ };
152
+ }
153
+ const alt = tryAlternativeClis(platform);
154
+ if (alt)
155
+ return alt;
156
+ }
157
+ return null;
158
+ }
159
+ function buildAgentCliNotFoundError(homedir, platform) {
160
+ const localClaude = getPreferredLocalClaudePath(homedir, platform);
161
+ const altList = ALTERNATIVE_CLI_NAMES.join('、');
162
+ const lines = [
163
+ '未找到可用于 ACP 调用的 Agent CLI(codechat / claude)。已按优先级检查:',
164
+ ` 1. 本地 Claude → ${localClaude}`,
165
+ ' 2. 其他 Claude 安装(PATH / npm global)',
166
+ ` 3. 其他 CLI(${altList})或 CodeChat runner`,
167
+ '',
168
+ '请先安装并配置其中之一,例如:',
169
+ ` - 将 claude.exe 放到 ${node_path_1.default.join(homedir, '.local', 'bin')}`,
170
+ ' - 或安装 CodeChat CLI / IDE 插件(~/.codechat/cli_app/)',
171
+ ` - 或确保 ${altList} 之一在 PATH 中`,
172
+ '',
173
+ '配置完成后再执行:svharness auto',
174
+ ];
175
+ return lines.join('\n');
176
+ }
177
+ /**
178
+ * Ensure an Agent CLI is available before auto mode starts.
179
+ * Throws with a user-facing message when nothing is found.
180
+ */
181
+ function ensureAcpAgentCliAvailable(options = {}) {
182
+ const resolved = resolveAcpAgentCli(options);
183
+ if (!resolved) {
184
+ const homedir = options.homedir ?? os_1.default.homedir();
185
+ const platform = options.platform ?? process.platform;
186
+ throw new Error(buildAgentCliNotFoundError(homedir, platform));
187
+ }
188
+ return resolved;
189
+ }
190
+ function formatAcpAgentCliLog(resolved) {
191
+ const detail = resolved.label ?? resolved.command;
192
+ return `${resolved.command} (${resolved.source}${detail ? `: ${detail}` : ''})`;
193
+ }
@@ -29,8 +29,10 @@ function parseCompletionSignal(output) {
29
29
  */
30
30
  class AcpClient {
31
31
  mode;
32
- constructor(mode = 'auto') {
32
+ agentCommand;
33
+ constructor(mode = 'auto', agentCommand = 'claude') {
33
34
  this.mode = mode;
35
+ this.agentCommand = agentCommand;
34
36
  }
35
37
  async call(opts) {
36
38
  if (this.mode === 'sdk') {
@@ -45,7 +47,7 @@ class AcpClient {
45
47
  const timeoutMs = opts.timeoutMs ?? 1_800_000;
46
48
  const isWin = process.platform === 'win32';
47
49
  return new Promise((resolve) => {
48
- const child = (0, node_child_process_1.spawn)('claude', args, {
50
+ const child = (0, node_child_process_1.spawn)(this.agentCommand, args, {
49
51
  cwd: opts.workDir,
50
52
  shell: isWin,
51
53
  });
@@ -230,10 +230,15 @@ async function ensureUserEnv(workdir, config, syncEnv) {
230
230
  }
231
231
  function buildRunnerArgs(config, workdir, skipPermissions) {
232
232
  const envFile = resolveUserEnvPath(config);
233
- const args = [
234
- formatRunnerArg('--env-file', envFile),
235
- formatRunnerArg('--cwd', workdir),
236
- ];
233
+ const resolvedWorkdir = path_1.default.resolve(workdir);
234
+ const args = [formatRunnerArg('--env-file', envFile)];
235
+ // run.bat → bootstrap.ps1: -cwd <path> must be two args; --cwd=path is dropped via cmd %*.
236
+ if (process.platform === 'win32') {
237
+ args.push('-cwd', resolvedWorkdir);
238
+ }
239
+ else {
240
+ args.push(formatRunnerArg('--cwd', resolvedWorkdir));
241
+ }
237
242
  if (skipPermissions) {
238
243
  args.push('--dangerously-skip-permissions');
239
244
  }
@@ -14,6 +14,7 @@ function resolveAutoAssetPaths(promptDirOverride) {
14
14
  autoRoot,
15
15
  promptDir,
16
16
  reviewRubric: node_path_1.default.join(autoRoot, 'templates', 'review-rubric.yaml'),
17
+ requirementsReviewRubric: node_path_1.default.join(autoRoot, 'templates', 'requirements-review-rubric.yaml'),
17
18
  skillDir: node_path_1.default.join(autoRoot, 'skills', 'svharness'),
18
19
  };
19
20
  }
@@ -11,6 +11,18 @@ const logger_1 = require("../utils/logger");
11
11
  const phase_prompt_loader_1 = require("./phase-prompt-loader");
12
12
  const phase_deps_1 = require("./phase-deps");
13
13
  const auto_state_1 = require("./auto-state");
14
+ const SCOPE_ALLOWED_PHASES = {
15
+ full: new Set([
16
+ 'S10_wiki',
17
+ 'S40_extract_requirements',
18
+ 'S50_generate_specs',
19
+ 'S60_process_references',
20
+ 'S65_customize_agent_env',
21
+ 'S70_runtime_assets',
22
+ 'S80_seed_memory',
23
+ ]),
24
+ requirements: new Set(['S40_extract_requirements']),
25
+ };
14
26
  async function runOptimizePlan(ctx, acp, cost, round, review) {
15
27
  if (!ctx.harnessRoot)
16
28
  throw new Error('缺少 harnessRoot,无法生成优化计划');
@@ -62,22 +74,27 @@ async function runOptimizeImplement(ctx, acp, cost, round, review) {
62
74
  logger_1.logger.warn(`优化实施 ACP 调用失败:${result.error ?? 'unknown error'},继续状态重置`);
63
75
  }
64
76
  const state = await (0, auto_state_1.readBuildState)(ctx.harnessRoot);
65
- const phases = await collectPlanPhases(ctx.harnessRoot, round, review);
77
+ const allowed = SCOPE_ALLOWED_PHASES[ctx.scope];
78
+ const phases = (await collectPlanPhases(ctx.harnessRoot, round, review)).filter((phase) => allowed.has(phase));
66
79
  for (const phase of phases) {
67
80
  if (!state.phases[phase])
68
81
  continue;
69
82
  state.phases[phase].status = 'PENDING';
70
83
  delete state.phases[phase].outdated_reason;
71
- (0, phase_deps_1.invalidateDownstream)(phase, state);
84
+ if (ctx.scope === 'full') {
85
+ (0, phase_deps_1.invalidateDownstream)(phase, state);
86
+ }
72
87
  }
73
88
  for (const phase of (0, phase_deps_1.collectOutdatedKeys)(state)) {
89
+ if (!allowed.has(phase))
90
+ continue;
74
91
  state.phases[phase].status = 'PENDING';
75
92
  delete state.phases[phase].outdated_reason;
76
93
  }
77
94
  await (0, auto_state_1.writeBuildState)(ctx.harnessRoot, state);
78
95
  }
79
96
  async function collectPlanPhases(harnessRoot, round, review) {
80
- const phases = new Set(review.gaps.map((gap) => (0, phase_deps_1.normalizeGapPhase)(gap.phase)));
97
+ const phases = new Set(review.gaps.map((gap) => (0, phase_deps_1.normalizeGapPhase)(gap.phase)).filter((p) => p.includes('_')));
81
98
  const planPath = node_path_1.default.join(harnessRoot, 'auto-review', `第${round}轮修改-plan.md`);
82
99
  if (await fs_extra_1.default.pathExists(planPath)) {
83
100
  const body = await fs_extra_1.default.readFile(planPath, 'utf8');
@@ -85,5 +102,8 @@ async function collectPlanPhases(harnessRoot, round, review) {
85
102
  phases.add((0, phase_deps_1.normalizeGapPhase)(match[0]));
86
103
  }
87
104
  }
105
+ if (phases.size === 0) {
106
+ phases.add('S40_extract_requirements');
107
+ }
88
108
  return [...phases];
89
109
  }
@@ -52,9 +52,9 @@ class AutoOrchestrator {
52
52
  phaseRunner;
53
53
  constructor(ctx) {
54
54
  this.ctx = ctx;
55
- this.acp = new acp_client_1.AcpClient(ctx.acpMode);
55
+ const agentCommand = ctx.acpAgentCli?.command ?? 'claude';
56
+ this.acp = new acp_client_1.AcpClient(ctx.acpMode, agentCommand);
56
57
  this.cost = new cost_tracker_1.CostTracker(ctx.maxTotalCalls, ctx.budgetUsd, ctx.costAlertThreshold);
57
- // PhaseRunner 在 run() 中 ctx 就绪后延迟创建,避免初始 ctx.harnessRoot=null 时 PhaseRunner 持有陈旧引用
58
58
  }
59
59
  async run() {
60
60
  if (!this.ctx.harnessExists) {
@@ -63,28 +63,65 @@ class AutoOrchestrator {
63
63
  if (!this.ctx.harnessRoot) {
64
64
  throw new Error('未解析到 harnessRoot,无法继续 auto');
65
65
  }
66
- // PhaseRunner 延迟创建:此时 ctx.harnessRoot 已就绪(可能在 runMaterialsPrepAndBuild 后才赋值)
67
66
  this.phaseRunner = new phase_runner_1.PhaseRunner(this.ctx, this.acp, this.cost);
68
67
  event_bus_1.dashboardEventBus.emitBuildStarted(phase_runner_1.PHASE_DEFS.length);
69
68
  await this.ensureReviewRubric();
70
- await this.phaseRunner.runBuildPhases();
71
- if (this.ctx.maxPhase && phaseNumber(this.ctx.maxPhase) < 85) {
72
- logger_1.logger.info(`已到达 --max-phase ${this.ctx.maxPhase},跳过评审优化`);
73
- event_bus_1.dashboardEventBus.emitBuildCompleted(undefined);
74
- return;
69
+ try {
70
+ await this.phaseRunner.runBuildPhases();
71
+ }
72
+ catch (err) {
73
+ const msg = err.message;
74
+ logger_1.logger.error(`[auto] 构建阶段执行失败:${msg}`);
75
+ event_bus_1.dashboardEventBus.emitBuildFailed(undefined);
76
+ throw new Error(`构建阶段失败:${msg};auto 构建终止,请在修复后重新执行`);
77
+ }
78
+ if (this.shouldSkipReviewLoop()) {
79
+ logger_1.logger.info(this.ctx.scope === 'requirements'
80
+ ? '[auto_pack_req] 构建阶段完成,进入三维度评审'
81
+ : `已到达 --max-phase ${this.ctx.maxPhase},跳过评审优化`);
82
+ if (this.ctx.scope !== 'requirements') {
83
+ event_bus_1.dashboardEventBus.emitBuildCompleted(undefined);
84
+ return;
85
+ }
75
86
  }
87
+ await this.runReviewOptimizeLoop();
88
+ }
89
+ shouldSkipReviewLoop() {
90
+ if (this.ctx.scope === 'requirements')
91
+ return false;
92
+ return !!(this.ctx.maxPhase && phaseNumber(this.ctx.maxPhase) < 85);
93
+ }
94
+ async runReviewOptimizeLoop() {
76
95
  let previousScore = 0;
77
96
  let finalReview;
78
97
  for (let round = 1; round <= this.ctx.maxOptimizeRounds; round++) {
79
98
  event_bus_1.dashboardEventBus.emitReviewStarted(round);
80
- const review = await this.runHarnessReview(round);
99
+ let review;
100
+ try {
101
+ review = await this.runReview(round);
102
+ }
103
+ catch (err) {
104
+ const msg = err.message;
105
+ if (msg.includes('预算') || msg.includes('调用次数') || msg.includes('ACP 调用')) {
106
+ logger_1.logger.error(`[auto] 第 ${round} 轮评审因资源限制终止:${msg}`);
107
+ event_bus_1.dashboardEventBus.emitBuildFailed(undefined);
108
+ throw err;
109
+ }
110
+ logger_1.logger.warn(`[auto] 第 ${round} 轮评审异常:${msg},继续下一轮`);
111
+ continue;
112
+ }
81
113
  event_bus_1.dashboardEventBus.emitReviewCompleted(round, review.composite_score, review.critical_count);
82
114
  event_bus_1.dashboardEventBus.emitScoreUpdated(review.composite_score, round);
83
- if (review.composite_score >= this.ctx.targetScore &&
84
- review.gate_pass &&
85
- review.critical_count === 0) {
115
+ const passed = this.ctx.scope === 'requirements'
116
+ ? review.composite_score > this.ctx.targetScore &&
117
+ review.gate_pass &&
118
+ review.critical_count === 0
119
+ : review.composite_score >= this.ctx.targetScore &&
120
+ review.gate_pass &&
121
+ review.critical_count === 0;
122
+ if (passed) {
86
123
  finalReview = review;
87
- await this.runFinalize(review);
124
+ await this.runCompletion(review);
88
125
  break;
89
126
  }
90
127
  if (round >= this.ctx.maxOptimizeRounds) {
@@ -103,7 +140,12 @@ class AutoOrchestrator {
103
140
  event_bus_1.dashboardEventBus.emitOptimizeCompleted(round);
104
141
  }
105
142
  catch (err) {
106
- logger_1.logger.warn(`第 ${round} 轮优化执行失败:${err.message},状态重置已完成,下一轮可继续`);
143
+ const msg = err.message;
144
+ if (msg.includes('预算') || msg.includes('次数') || msg.includes('ACP 调用')) {
145
+ logger_1.logger.warn(`[auto] 第 ${round} 轮优化因资源限制终止:${msg}`);
146
+ break;
147
+ }
148
+ logger_1.logger.warn(`第 ${round} 轮优化执行失败:${msg},状态重置已完成,下一轮可继续`);
107
149
  }
108
150
  }
109
151
  event_bus_1.dashboardEventBus.emitCostUpdated(this.cost.report().estimatedCostUsd, this.cost.report().totalCalls);
@@ -118,14 +160,26 @@ class AutoOrchestrator {
118
160
  if (!this.ctx.harnessRoot)
119
161
  return;
120
162
  const assets = (0, auto_assets_1.resolveAutoAssetPaths)();
121
- const dest = node_path_1.default.join(this.ctx.harnessRoot, 'auto', 'review-rubric.yaml');
122
- if (!(await fs_extra_1.default.pathExists(dest))) {
123
- await fs_extra_1.default.ensureDir(node_path_1.default.dirname(dest));
124
- await fs_extra_1.default.copyFile(assets.reviewRubric, dest);
163
+ const autoDir = node_path_1.default.join(this.ctx.harnessRoot, 'auto');
164
+ await fs_extra_1.default.ensureDir(autoDir);
165
+ const rubricDest = node_path_1.default.join(autoDir, 'review-rubric.yaml');
166
+ if (!(await fs_extra_1.default.pathExists(rubricDest))) {
167
+ await fs_extra_1.default.copyFile(assets.reviewRubric, rubricDest);
125
168
  logger_1.logger.success('已注入 auto/review-rubric.yaml');
126
169
  }
170
+ if (this.ctx.scope === 'requirements') {
171
+ const reqRubricDest = node_path_1.default.join(autoDir, 'requirements-review-rubric.yaml');
172
+ if (!(await fs_extra_1.default.pathExists(reqRubricDest))) {
173
+ await fs_extra_1.default.copyFile(assets.requirementsReviewRubric, reqRubricDest);
174
+ logger_1.logger.success('已注入 auto/requirements-review-rubric.yaml');
175
+ }
176
+ }
127
177
  }
128
- async runHarnessReview(round) {
178
+ async runReview(round) {
179
+ if (this.ctx.scope === 'requirements') {
180
+ const { runReqPackReview } = await Promise.resolve().then(() => __importStar(require('./auto-req-pack-review')));
181
+ return runReqPackReview(this.ctx, this.acp, this.cost, round);
182
+ }
129
183
  const { runHarnessReview } = await Promise.resolve().then(() => __importStar(require('./auto-review')));
130
184
  return runHarnessReview(this.ctx, this.acp, this.cost, round);
131
185
  }
@@ -138,7 +192,12 @@ class AutoOrchestrator {
138
192
  await runOptimizeImplement(this.ctx, this.acp, this.cost, round, review);
139
193
  await this.phaseRunner.runBuildPhases({ onlyPendingOrOutdated: true });
140
194
  }
141
- async runFinalize(review) {
195
+ async runCompletion(review) {
196
+ if (this.ctx.scope === 'requirements') {
197
+ const { finalizeReqPack } = await Promise.resolve().then(() => __importStar(require('./auto-pack-req-finalize')));
198
+ await finalizeReqPack(this.ctx, review);
199
+ return;
200
+ }
142
201
  const { finalizeAutoHarness } = await Promise.resolve().then(() => __importStar(require('./auto-finalize')));
143
202
  await finalizeAutoHarness(this.ctx, review);
144
203
  }