svharness 0.15.11 → 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 (43) hide show
  1. package/README.md +106 -2
  2. package/auto/auto-harness-detail-design.md +136 -4
  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 +1 -1
  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 +16 -1
  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/index.js +77 -5
  19. package/dist/lib/acp-agent-cli-resolver.js +193 -0
  20. package/dist/lib/acp-client.js +4 -2
  21. package/dist/lib/agent-launcher.js +9 -4
  22. package/dist/lib/auto-assets.js +1 -0
  23. package/dist/lib/auto-optimize.js +23 -3
  24. package/dist/lib/auto-orchestrator.js +53 -20
  25. package/dist/lib/auto-pack-req-context.js +88 -0
  26. package/dist/lib/auto-pack-req-finalize.js +28 -0
  27. package/dist/lib/auto-req-pack-review.js +161 -0
  28. package/dist/lib/auto-sample-trace.js +144 -0
  29. package/dist/lib/auto-state.js +124 -15
  30. package/dist/lib/gate-checker.js +20 -10
  31. package/dist/lib/phase-runner.js +13 -2
  32. package/dist/lib/ps-codechat-alias.js +29 -16
  33. package/dist/lib/review-parser.js +14 -0
  34. package/dist/lib/score-aggregator.js +21 -0
  35. package/dist/lib/win-registry.js +1 -1
  36. package/dist/utils/validate-args.js +2 -1
  37. package/package.json +3 -2
  38. package/templates/codechat/Start-CodeChat.ps1 +176 -135
  39. package/templates/codechat/bash.exe.stackdump +29 -0
  40. package/templates/codechat/docs/project-structure.md +59 -0
  41. package/templates/svharness.config.example.yaml +2 -0
  42. package/dist/lib/launch-script-path.js +0 -24
  43. package/templates/codechat/.claude/.env +0 -59
@@ -12,6 +12,7 @@ const logger_1 = require("../utils/logger");
12
12
  const harness_resolver_1 = require("../lib/harness-resolver");
13
13
  const auto_orchestrator_1 = require("../lib/auto-orchestrator");
14
14
  const dashboard_1 = require("../dashboard");
15
+ const acp_agent_cli_resolver_1 = require("../lib/acp-agent-cli-resolver");
15
16
  /**
16
17
  * Prevent system sleep on Windows during long-running auto execution.
17
18
  * Registers process-level handlers so sleep is restored on any exit path.
@@ -46,6 +47,13 @@ async function runAuto(positionalWorkdir, opts, cmd) {
46
47
  const merged = (0, config_1.mergeAutoOptions)(opts, loaded?.config.auto, loaded?.config.defaults, cmd);
47
48
  const ctx = await resolveAutoContext(positionalWorkdir, merged);
48
49
  (0, logger_1.setVerbose)(ctx.verbose);
50
+ if (!ctx.dryRun) {
51
+ logger_1.logger.section('[auto] 启动前检查:Agent CLI');
52
+ if (!ctx.acpAgentCli) {
53
+ throw new Error('内部错误:ACP Agent CLI 未解析');
54
+ }
55
+ logger_1.logger.success(`已找到 Agent CLI → ${(0, acp_agent_cli_resolver_1.formatAcpAgentCliLog)(ctx.acpAgentCli)}`);
56
+ }
49
57
  // Start dashboard server (non-blocking, best-effort)
50
58
  (0, dashboard_1.startDashboard)(7879).catch(() => { });
51
59
  // Prevent system sleep during execution; restore on any exit path
@@ -63,12 +71,14 @@ async function runAuto(positionalWorkdir, opts, cmd) {
63
71
  async function resolveAutoContext(positionalWorkdir, opts) {
64
72
  const workDir = node_path_1.default.resolve(positionalWorkdir ?? opts.workDir ?? process.cwd());
65
73
  const harnessRoot = await (0, harness_resolver_1.resolveHarnessRoot)(workDir);
74
+ const acpAgentCli = opts.dryRun ? null : (0, acp_agent_cli_resolver_1.ensureAcpAgentCliAvailable)();
66
75
  return {
67
76
  workDir,
68
77
  harnessRoot,
69
78
  harnessExists: !!harnessRoot,
79
+ acpAgentCli,
70
80
  targetScore: normalizeNumber(opts.targetScore, 9.5, '--target-score'),
71
- maxOptimizeRounds: normalizeInteger(opts.maxOptimizeRounds, 5, '--max-optimize-rounds'),
81
+ maxOptimizeRounds: normalizeInteger(opts.maxOptimizeRounds, 10, '--max-optimize-rounds'),
72
82
  dryRun: !!opts.dryRun,
73
83
  autoApprove: !!opts.autoApprove,
74
84
  yes: !!opts.yes,
@@ -80,6 +90,8 @@ async function resolveAutoContext(positionalWorkdir, opts) {
80
90
  acpTimeoutMs: normalizeInteger(opts.acpTimeoutMs, 1_800_000, '--acp-timeout-ms'),
81
91
  maxRetriesPerPhase: normalizeInteger(opts.maxRetriesPerPhase, 2, '--max-retries-per-phase'),
82
92
  baselineAutoExtract: normalizeBaselineExtract(opts.baselineAutoExtract),
93
+ scope: normalizeScope(opts.scope),
94
+ sampleCount: normalizeInteger(opts.sampleCount, 7, '--sample-count'),
83
95
  promptDir: opts.promptDir,
84
96
  checkpoint: opts.checkpoint,
85
97
  maxPhase: opts.maxPhase,
@@ -107,3 +119,6 @@ function normalizeBaselineExtract(value) {
107
119
  }
108
120
  return 'conservative';
109
121
  }
122
+ function normalizeScope(value) {
123
+ return value === 'requirements' ? 'requirements' : 'full';
124
+ }
@@ -182,6 +182,8 @@ async function runConvert(opts) {
182
182
  catch (err) {
183
183
  const e = err;
184
184
  logger_1.logger.error(`markitdown_serve unreachable at ${v.endpoint}: ${e.message}`);
185
+ logger_1.logger.plain(' - if another user is converting a large PDF, the service may still be busy; retry in a minute');
186
+ logger_1.logger.plain(' - production should run uvicorn with --workers 4+ and nginx proxy_read_timeout ≥ 900s');
185
187
  logger_1.logger.plain('');
186
188
  logger_1.logger.plain(' - confirm the service is deployed and reachable from this machine');
187
189
  logger_1.logger.plain(' - pass --endpoint <url> or comma-separated URLs for failover');
@@ -3,7 +3,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.resolveLaunchScriptPath = void 0;
7
6
  exports.resolveShellMenuIconSourcePath = resolveShellMenuIconSourcePath;
8
7
  exports.installShellMenuIcon = installShellMenuIcon;
9
8
  exports.resolveWindowsTerminalPath = resolveWindowsTerminalPath;
@@ -19,11 +18,8 @@ const child_process_1 = require("child_process");
19
18
  const logger_1 = require("../utils/logger");
20
19
  const win_registry_1 = require("../lib/win-registry");
21
20
  const ps_codechat_alias_1 = require("../lib/ps-codechat-alias");
22
- const launch_script_path_1 = require("../lib/launch-script-path");
23
- Object.defineProperty(exports, "resolveLaunchScriptPath", { enumerable: true, get: function () { return launch_script_path_1.resolveLaunchScriptPath; } });
24
21
  const STUB_DIR = path_1.default.join(process.env.LOCALAPPDATA ?? path_1.default.join(os_1.default.homedir(), 'AppData', 'Local'), 'svharness', 'bin');
25
- const STUB_PATH = path_1.default.join(STUB_DIR, 'launch_codechat_cli.ps1');
26
- const LEGACY_STUB_PATH = path_1.default.join(STUB_DIR, 'launch_codechat_cli.cmd');
22
+ const PS1_INSTALL_PATH = path_1.default.join(STUB_DIR, 'Start-CodeChat.ps1');
27
23
  const ICON_FILENAME = 'codechat-agent.ico';
28
24
  const ICON_STUB_PATH = path_1.default.join(STUB_DIR, ICON_FILENAME);
29
25
  const STANDALONE_MENU_KEY_BG = 'HKCU\\Software\\Classes\\Directory\\Background\\shell\\CodeChatStandaloneAgent';
@@ -102,31 +98,24 @@ function resolveWindowsTerminalPath() {
102
98
  }
103
99
  return null;
104
100
  }
105
- function buildStubContent(launchScriptPath) {
106
- const escapedNodeScript = launchScriptPath.replace(/'/g, "''");
107
- // ASCII-only + UTF-8 BOM. Legacy conhost fallback sets UTF-8 before CodeChat TUI.
108
- return `param(
109
- [string]$WorkDir = (Get-Location).Path
110
- )
111
-
112
- $ErrorActionPreference = 'Stop'
113
-
114
- try { chcp 65001 | Out-Null } catch {}
115
- [Console]::InputEncoding = [System.Text.UTF8Encoding]::new($false)
116
- [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
117
- $OutputEncoding = [System.Text.UTF8Encoding]::new($false)
118
-
119
- Set-Location -LiteralPath $WorkDir
120
- & node '${escapedNodeScript}' "$WorkDir"
121
- exit $LASTEXITCODE
122
- `;
123
- }
124
- function writeStubFile(launchScriptPath) {
125
- const content = buildStubContent(launchScriptPath);
126
- return fs_extra_1.default.writeFile(STUB_PATH, `\uFEFF${content}`, 'utf8');
127
- }
128
101
  function buildPsInvokeArgs(ps1Path, dirPlaceholder) {
129
- return `-NoExit -NoProfile -ExecutionPolicy Bypass -File "${ps1Path}" ${dirPlaceholder}`;
102
+ return `-NoExit -NoProfile -ExecutionPolicy Bypass -File "${ps1Path}" -Action Start -WorkDir ${dirPlaceholder}`;
103
+ }
104
+ async function installStartCodeChatPs1() {
105
+ const fromDist = path_1.default.resolve(__dirname, '..', '..', 'templates', 'codechat', 'Start-CodeChat.ps1');
106
+ const fromCwd = path_1.default.resolve(__dirname, '..', '..', '..', 'templates', 'codechat', 'Start-CodeChat.ps1');
107
+ let source = '';
108
+ if (fs_extra_1.default.existsSync(fromDist)) {
109
+ source = fromDist;
110
+ }
111
+ else if (fs_extra_1.default.existsSync(fromCwd)) {
112
+ source = fromCwd;
113
+ }
114
+ else {
115
+ throw new Error(`\u672A\u627E\u5230 Start-CodeChat.ps1\uFF08\u9884\u671F\uFF1A${fromDist}\uFF09`);
116
+ }
117
+ await fs_extra_1.default.copy(source, PS1_INSTALL_PATH, { overwrite: true });
118
+ return PS1_INSTALL_PATH;
130
119
  }
131
120
  function buildMenuCommand(ps1Path, dirPlaceholder, wtPath) {
132
121
  const psArgs = buildPsInvokeArgs(ps1Path, dirPlaceholder);
@@ -150,23 +139,18 @@ async function runShellInstall(opts = {}) {
150
139
  // Avoid duplicate menu entries: overwrite standalone menu if present.
151
140
  (0, win_registry_1.regDeleteKey)(STANDALONE_MENU_KEY_BG);
152
141
  (0, win_registry_1.regDeleteKey)(STANDALONE_MENU_KEY_DIR);
153
- const launchScriptPath = (0, launch_script_path_1.resolveLaunchScriptPath)();
154
142
  await fs_extra_1.default.ensureDir(STUB_DIR);
155
- await writeStubFile(launchScriptPath);
156
- if (await fs_extra_1.default.pathExists(LEGACY_STUB_PATH)) {
157
- await fs_extra_1.default.remove(LEGACY_STUB_PATH);
158
- }
143
+ const ps1Path = await installStartCodeChatPs1();
159
144
  const iconValue = await installShellMenuIcon();
160
145
  const wtPath = resolveWindowsTerminalPath();
161
- const bgCommand = buildMenuCommand(STUB_PATH, '"%V"', wtPath);
162
- const dirCommand = buildMenuCommand(STUB_PATH, '"%1"', wtPath);
146
+ const bgCommand = buildMenuCommand(PS1_INSTALL_PATH, '"%V"', wtPath);
147
+ const dirCommand = buildMenuCommand(PS1_INSTALL_PATH, '"%1"', wtPath);
163
148
  registerMenuKey(win_registry_1.SHELL_MENU_KEY_BG, bgCommand, iconValue);
164
149
  registerMenuKey(win_registry_1.SHELL_MENU_KEY_DIR, dirCommand, iconValue);
165
150
  const via = wtPath ? 'Windows Terminal' : 'PowerShell(UTF-8)';
166
151
  logSuccess(`Windows 右键菜单已注册:「在此启动 CodeChat Agent」(${via})`, opts.silent);
167
- logInfo(` stub: ${STUB_PATH}`, opts.silent);
152
+ logInfo(` 脚本: ${ps1Path}`, opts.silent);
168
153
  logInfo(` icon: ${ICON_STUB_PATH}`, opts.silent);
169
- logInfo(` 脚本: ${launchScriptPath}`, opts.silent);
170
154
  if (wtPath) {
171
155
  logInfo(` wt: ${wtPath}`, opts.silent);
172
156
  }
@@ -183,15 +167,12 @@ async function runShellUninstall(opts = {}) {
183
167
  // Also remove standalone script menu (Start-CodeChat.ps1 independent install)
184
168
  (0, win_registry_1.regDeleteKey)(STANDALONE_MENU_KEY_BG);
185
169
  (0, win_registry_1.regDeleteKey)(STANDALONE_MENU_KEY_DIR);
186
- if (await fs_extra_1.default.pathExists(STUB_PATH)) {
187
- await fs_extra_1.default.remove(STUB_PATH);
170
+ if (await fs_extra_1.default.pathExists(PS1_INSTALL_PATH)) {
171
+ await fs_extra_1.default.remove(PS1_INSTALL_PATH);
188
172
  }
189
173
  if (await fs_extra_1.default.pathExists(ICON_STUB_PATH)) {
190
174
  await fs_extra_1.default.remove(ICON_STUB_PATH);
191
175
  }
192
- if (await fs_extra_1.default.pathExists(LEGACY_STUB_PATH)) {
193
- await fs_extra_1.default.remove(LEGACY_STUB_PATH);
194
- }
195
176
  // Clean up standalone script directory (Start-CodeChat.ps1 independent install)
196
177
  const standaloneDir = path_1.default.join(process.env.LOCALAPPDATA ?? path_1.default.join(os_1.default.homedir(), 'AppData', 'Local'), 'codechat-standalone');
197
178
  if (await fs_extra_1.default.pathExists(standaloneDir)) {
@@ -205,24 +186,17 @@ async function runShellUninstall(opts = {}) {
205
186
  */
206
187
  function getShellStatus() {
207
188
  assertWin32();
208
- let launchScriptPath = '';
209
- try {
210
- launchScriptPath = (0, launch_script_path_1.resolveLaunchScriptPath)();
211
- }
212
- catch {
213
- launchScriptPath = '(未找到)';
214
- }
189
+ const ps1Installed = fs_extra_1.default.existsSync(PS1_INSTALL_PATH);
215
190
  const backgroundMenu = (0, win_registry_1.regKeyExists)(win_registry_1.SHELL_MENU_KEY_BG);
216
191
  const directoryMenu = (0, win_registry_1.regKeyExists)(win_registry_1.SHELL_MENU_KEY_DIR);
217
- const stubExists = fs_extra_1.default.existsSync(STUB_PATH);
218
192
  const wtPath = resolveWindowsTerminalPath();
219
193
  const aliasStatus = (0, ps_codechat_alias_1.getCodechatAliasStatus)();
220
194
  return {
221
- installed: backgroundMenu && directoryMenu && stubExists,
222
- stubPath: STUB_PATH,
195
+ installed: backgroundMenu && directoryMenu && ps1Installed,
196
+ stubPath: PS1_INSTALL_PATH,
223
197
  backgroundMenu,
224
198
  directoryMenu,
225
- launchScriptPath,
199
+ launchScriptPath: PS1_INSTALL_PATH,
226
200
  viaWindowsTerminal: wtPath !== null,
227
201
  aliasInstalled: aliasStatus.installed,
228
202
  profilePath: aliasStatus.profilePath,
@@ -232,10 +206,9 @@ function printShellStatus() {
232
206
  const status = getShellStatus();
233
207
  logger_1.logger.section('Windows shell 集成状态');
234
208
  logger_1.logger.plain(` 右键菜单完整: ${status.installed ? '是' : '否'}`);
235
- logger_1.logger.plain(` stub 路径: ${status.stubPath}`);
209
+ logger_1.logger.plain(` Start-CodeChat.ps1: ${status.stubPath}`);
236
210
  logger_1.logger.plain(` 空白处菜单: ${status.backgroundMenu ? '已注册' : '未注册'}`);
237
211
  logger_1.logger.plain(` 文件夹菜单: ${status.directoryMenu ? '已注册' : '未注册'}`);
238
- logger_1.logger.plain(` 启动脚本: ${status.launchScriptPath}`);
239
212
  logger_1.logger.plain(` 经 WT 启动: ${status.viaWindowsTerminal ? '是(install 时检测到 wt.exe)' : '否'}`);
240
213
  logger_1.logger.plain(` codechat 别名: ${status.aliasInstalled ? '已安装' : '未安装'}`);
241
214
  logger_1.logger.plain(` profile 路径: ${status.profilePath}`);
@@ -85,6 +85,8 @@ function mergeAutoOptions(cli, configSection, defaults, cmd) {
85
85
  promptDir: pickString('promptDir', cli.promptDir, cfg.promptDir, cmd),
86
86
  baselineAutoExtract: pickString('baselineAutoExtract', cli.baselineAutoExtract, cfg.baselineAutoExtract, cmd),
87
87
  convertEndpoint: pickString('convertEndpoint', cli.convertEndpoint, cfg.convertEndpoint, cmd),
88
+ scope: pickString('scope', cli.scope, cfg.scope, cmd),
89
+ sampleCount: pickNumber('sampleCount', cli.sampleCount, cfg.sampleCount, cmd),
88
90
  };
89
91
  }
90
92
  function mergeBuildOptions(cli, configSection, defaults, cmd) {
@@ -50,6 +50,7 @@ function pickAutoSection(raw) {
50
50
  'costAlertThreshold',
51
51
  'acpTimeoutMs',
52
52
  'maxRetriesPerPhase',
53
+ 'sampleCount',
53
54
  ]) {
54
55
  if (raw[k] !== undefined && raw[k] !== null) {
55
56
  s[k] = Number(raw[k]);
@@ -235,5 +236,11 @@ function normalizeConfig(raw, configPath) {
235
236
  }
236
237
  cfg.auto = pickAutoSection(raw.auto);
237
238
  }
239
+ if (raw.auto_pack_req !== undefined) {
240
+ if (!isPlainObject(raw.auto_pack_req)) {
241
+ throw new Error(`配置 auto_pack_req 必须是对象:${configPath}`);
242
+ }
243
+ cfg.auto_pack_req = pickAutoSection(raw.auto_pack_req);
244
+ }
238
245
  return cfg;
239
246
  }
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_CONVERT_TIMEOUT_SEC = void 0;
4
+ /** Default per-file convert HTTP timeout: 15 minutes. */
5
+ exports.DEFAULT_CONVERT_TIMEOUT_SEC = 900;
@@ -57,7 +57,7 @@ function parseEndpointList(raw) {
57
57
  }
58
58
  return out;
59
59
  }
60
- async function ping(endpoint, timeoutMs = 10_000) {
60
+ async function ping(endpoint, timeoutMs = 30_000) {
61
61
  const controller = new AbortController();
62
62
  const timer = setTimeout(() => controller.abort(), timeoutMs);
63
63
  try {
@@ -80,7 +80,7 @@ async function ping(endpoint, timeoutMs = 10_000) {
80
80
  }
81
81
  }
82
82
  /** Probe endpoints in order; return those that respond to /healthz. */
83
- async function filterHealthyEndpoints(endpoints, timeoutMs = 10_000) {
83
+ async function filterHealthyEndpoints(endpoints, timeoutMs = 30_000) {
84
84
  const healthy = [];
85
85
  for (const endpoint of endpoints) {
86
86
  try {
@@ -186,6 +186,11 @@ function extractTraceId(body) {
186
186
  }
187
187
  function describeHttpError(status, body, endpoint) {
188
188
  const prefix = endpoint ? `[${endpoint}] ` : '';
189
+ if (status === 504) {
190
+ return (`${prefix}gateway timeout (HTTP 504): reverse proxy gave up waiting for markitdown_serve ` +
191
+ `(worker may still be running VLM/OCR). Increase nginx proxy_read_timeout to match ` +
192
+ `--timeout-sec, or reduce MARKITDOWN_OCR_MAX_RENDER_PX / use a shorter PDF export.`);
193
+ }
189
194
  if (body && typeof body === 'object') {
190
195
  const b = body;
191
196
  if (status === 415 && b.error === 'unsupported_format') {
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,7 +254,7 @@ 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')
@@ -277,6 +283,72 @@ function main() {
277
283
  process.exitCode = 1;
278
284
  }
279
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
+ });
280
352
  program
281
353
  .command('wizard')
282
354
  .description('交互式向导:分步填写路径与说明,可保存到 svharness.config.yaml 并/或立即执行 build/apply/convert。')
@@ -299,7 +371,7 @@ function main() {
299
371
  .option('--endpoint <url>', 'markitdown_serve 基址(逗号分隔可配置 failover)')
300
372
  .option('--concurrency <n>', '【默认 3】并发上传数', (v) => Number(v))
301
373
  .option('--max-file-mb <n>', '【默认 50】单文件大小上限(MB)', (v) => Number(v))
302
- .option('--timeout-sec <n>', '【默认 120】超时秒数', (v) => Number(v))
374
+ .option('--timeout-sec <n>', `【默认 ${convert_1.DEFAULT_CONVERT_TIMEOUT_SEC},15 分钟】单文件转换超时秒数`, (v) => Number(v))
303
375
  .option('--type <type>', 'requirements | references')
304
376
  .option('--force', '覆盖已存在同名 .md')
305
377
  .option('--split-sheets-suffix <suffix>', 'xlsx/xls 按 sheet(##)拆分时的子目录后缀', '_split')
@@ -326,7 +398,7 @@ function main() {
326
398
  yes: opts.yes,
327
399
  verbose: opts.verbose,
328
400
  }, loaded?.config.convert, loaded?.config.defaults, cmd);
329
- await (0, convert_1.runConvert)({
401
+ await (0, convert_2.runConvert)({
330
402
  input: merged.input ?? [],
331
403
  harness: merged.harness,
332
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
  }