svharness 0.14.8 → 0.14.11

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 (60) hide show
  1. package/README.md +124 -20
  2. package/bin/launch-codechat-cli.js +18 -0
  3. package/dist/commands/convert.js +129 -22
  4. package/dist/commands/doctor/check-requirements-coverage.js +82 -59
  5. package/dist/commands/doctor/check-requirements-fidelity.js +2 -2
  6. package/dist/commands/doctor/check-source-inventory.js +133 -0
  7. package/dist/commands/doctor/check-state-phases.js +33 -1
  8. package/dist/commands/doctor/index.js +33 -19
  9. package/dist/commands/doctor.js +2 -2
  10. package/dist/commands/init.js +62 -5
  11. package/dist/commands/requirements.js +96 -0
  12. package/dist/commands/shell-integration.js +138 -0
  13. package/dist/commands/start-agent.js +38 -0
  14. package/dist/config/merge-options.js +3 -0
  15. package/dist/core/cli-input-declarations.js +187 -0
  16. package/dist/core/markitdown-client.js +80 -33
  17. package/dist/core/next-steps.js +22 -3
  18. package/dist/core/repomix-apply-hint.js +2 -2
  19. package/dist/core/repomix-pack-path.js +18 -0
  20. package/dist/core/repomix-pack.js +20 -12
  21. package/dist/core/scaffold.js +30 -0
  22. package/dist/core/state.js +9 -1
  23. package/dist/index.js +122 -5
  24. package/dist/lib/agent-launcher.js +171 -0
  25. package/dist/lib/requirements/parse-blocks.js +137 -0
  26. package/dist/lib/requirements/paths.js +71 -0
  27. package/dist/lib/requirements/structure-scan.js +154 -0
  28. package/dist/lib/requirements/types.js +7 -0
  29. package/dist/lib/requirements/verify.js +341 -0
  30. package/dist/lib/win-registry.js +52 -0
  31. package/dist/utils/validate-args.js +32 -10
  32. package/docs/agent-launcher-design.md +274 -0
  33. package/package.json +11 -2
  34. package/scripts/postinstall.js +28 -0
  35. package/scripts/preuninstall.js +17 -0
  36. package/templates/_shared/build-rules/harness-build-rule-orchestrator-flow.md +19 -16
  37. package/templates/_shared/build-rules/harness-build-rule-requirements-extraction.md +7 -26
  38. package/templates/_shared/build-rules/harness-build-rule-user-interaction.md +38 -4
  39. package/templates/_shared/build-skills/harness-build-skill-agent-env-merge.md +6 -0
  40. package/templates/_shared/build-skills/harness-build-skill-knowledge-builder.md +5 -4
  41. package/templates/_shared/build-skills/harness-build-skill-orchestrator.md +22 -15
  42. package/templates/_shared/build-skills/harness-build-skill-references-intake.md +4 -0
  43. package/templates/_shared/build-skills/harness-build-skill-spec-builder.md +28 -38
  44. package/templates/_shared/meta/task_list.md.ejs +4 -0
  45. package/templates/_shared/skeleton/references/md/README.md +5 -0
  46. package/templates/_shared/skeleton/references/raw/README.md +5 -0
  47. package/templates/_shared/skeleton/references/yaml/README.md +5 -0
  48. package/templates/_shared/skeleton/requirements/_work/.gitkeep +0 -0
  49. package/templates/_shared/skeleton/requirements/_work/source-inventory.schema.json +89 -0
  50. package/templates/_shared/skeleton/requirements/coverage-report.schema.json +36 -0
  51. package/templates/_shared/skeleton/requirements/yaml/schema.json +15 -0
  52. package/templates/_shared/skeleton/tasks/base_tasks/README.md +22 -0
  53. package/templates/_shared/skeleton/tasks/base_tasks/_pack-template/00-AGENT-ENTRY.md +52 -0
  54. package/templates/_shared/skeleton/tasks/base_tasks/_pack-template/EXECUTION-STATUS.yaml +56 -0
  55. package/templates/_shared/skeleton/tasks/base_tasks/_pack-template/VERIFY-CASES.yaml +19 -0
  56. package/templates/_shared/skeleton/tasks/base_tasks/_pack-template/evidence/README.md +5 -0
  57. package/templates/_shared/skeleton/tasks/base_tasks/index.yaml +6 -0
  58. package/templates/_shared/tasks/templates/README.md +3 -1
  59. package/templates/_shared/tasks/templates/TEMPLATE-GUIDE.md +3 -3
  60. package/templates/svharness.config.example.yaml +2 -0
package/README.md CHANGED
@@ -40,6 +40,10 @@ svharness build --harness-name my-app --arch android-compose --agent qoder --yes
40
40
 
41
41
  # 把构建完成的 harness 应用到其它项目(绑定式,不拷贝)
42
42
  svharness apply --harness ./my-app-harness --target ../other-project --yes
43
+
44
+ # build 完成后启动 CodeChat Agent 填充 harness(当前目录)
45
+ svharness start-agent --work-dir .
46
+ # 或全局快捷命令: launch_codechat_cli
43
47
  ```
44
48
 
45
49
  ### 配置文件与向导(v0.10+)
@@ -179,7 +183,7 @@ harness-build-{skill|rule}-<semantic-name>
179
183
 
180
184
  ### 构建阶段
181
185
 
182
- `build` 只生成骨架。各阶段的**实际内容**由 Agent 借助注入的 skill 逐步填充:
186
+ `build` 生成目录骨架,并在 **S00** 预置默认 `tasks/templates/`;其余各阶段的**项目相关内容**由 Agent 借助注入的 skill 逐步填充:
183
187
 
184
188
  | 阶段 | 动作 | 产物 |
185
189
  |------|------|------|
@@ -191,17 +195,15 @@ harness-build-{skill|rule}-<semantic-name>
191
195
  | **S60_process_references** | references 处理(`svharness convert` + 结构化索引 + 用户确认落地) | `references/md/` |
192
196
  | **S61_confirm_baseline_extraction** | 确认是否自动从 baseline 提取 skills/rules(表单确认) | `.harness-build-state.yaml`(`phases.S61_confirm_baseline_extraction.baseline_auto_extract`) |
193
197
  | **S65_customize_agent_env** | agent-env 定制(extra-skills/extra-rules 冲突建议与重命名建议 → 用户确认 → 写入) | `agent-env/rules/` + `agent-env/skills/` |
194
- | **S70_runtime_assets** | 运行期 Skills & tasks 索引(skill 执行 task) | `agent-env/skills/` + `tasks/templates/` |
198
+ | **S70_runtime_assets** | 运行期 Skills 定制 + tasks 模板按项目裁剪/增补(build 已预置默认模板) | `agent-env/skills/` + `tasks/templates/` |
195
199
  | **S80_seed_memory** | Memory 初始化 | `agent-env/memory/categories/` |
196
200
  | **S85_pre_seal_validation** | 封存前校验 | `svharness doctor` + 全面审查报告 |
197
201
  | **S90_finalize** | 封板 | 版本 bump + CHANGELOG + `bootstrap_mode: false` |
198
202
 
199
203
  > 反复调用 orchestrator 总是从"第一个非 DONE 的阶段"继续。
200
204
  > **S10_wiki 仅在有 baseline 时出现;无 baseline 则不构建 wiki,流程从 S20_collect_inputs 开始。**
201
- > **S20_collect_inputs 必须等待用户放入需求文档,且需显式征询 references 与额外运行期资源输入(`--extra-skills`,可混放 skills/rules);Agent 不得自行生成占位文件。若 build 传入了 `--requirements/--references`,CLI 会在拷贝到 raw 后自动尝试 convert 到 md。**
202
- > **S60_process_references 专注 references:必须执行 `svharness convert`,并产出结构化索引(规制约束 / skills 候选 / signals / manuals),经表单确认后再进入后续阶段。**
203
- > **S61_confirm_baseline_extraction 必须显式确认是否启用 baseline 自动提取 skills/rules;未经确认不得在 S65 自动提取。**
204
- > **S65_customize_agent_env 专注 extra-skills/extra-rules:来源可来自 `--extra-skills` 导入后的 `_incoming/manifest.yaml`,先冲突识别与重命名建议,再表单确认写入;推荐命名:`harness-apply-skills-<topic>` 与 `harness-apply-rules-<topic>.md/.mdc`。**
205
+ > **S20_collect_inputs**:`requirements/raw/` 须有真实文档。未传 `--references` / `--extra-skills` CLI 预声明 `input_declarations` 并可能预 DONE S20(当 `--requirements` 已注入);Agent 不得再对已为 `none` 的可选项弹表单。`--strict-input-confirm` 可恢复旧行为。
206
+ > **S60/S61/S65/S80**:未提供对应可选输入时,build 可预标记 `source: cli-default`、`outcome: none` DONE;增量添加时说「reopen S60」等。S61 默认 `baseline_auto_extract: DISABLED`;需提取时传 `--enable-baseline-extract`。
205
207
  > **S40/S50/S85 的 DONE 都带覆盖率门禁:未闭环 gap(或未备案 waiver)不得推进。**
206
208
 
207
209
  ---
@@ -276,13 +278,15 @@ svharness build \
276
278
  | `--baseline-branch <name>` | 可选 | git 基线的分支名(仅 git 模式有效) | `main` |
277
279
  | `--baseline-max-file-kb <kb>` | 可选 | 基线拷贝单文件大小上限(KB) | `1024` |
278
280
  | `--repomix` | 可选 flag | 将 `baseline/code` 打成**单文件** Repomix XML;**须同时提供 `--baseline`**。适用场景见下文 [Repomix](#repomix-repomix) | `false` |
279
- | `--convert-endpoint <url>` | 可选 | build 自动 convert 使用的 markitdown 服务基址;省略时读环境变量 `SVHARNESS_MARKITDOWN_ENDPOINT` | `http://markitdown.desaysz.site` |
281
+ | `--convert-endpoint <url>` | 可选 | build 自动 convert 使用的 markitdown 服务基址(支持逗号分隔 failover);省略时读 `SVHARNESS_MARKITDOWN_ENDPOINT` | `http://markitdown.desaysz.site` |
280
282
  | `--convert-concurrency <n>` | 可选 | build 自动 convert 并发上传数 | `3` |
281
283
  | `--convert-max-file-mb <n>` | 可选 | build 自动 convert 单文件大小上限(MB) | `50` |
282
284
  | `--convert-timeout-sec <n>` | 可选 | build 自动 convert 请求超时秒数 | `120` |
283
285
  | `--convert-force` | 可选 flag | build 自动 convert 覆盖同名 `.md`(否则按 `-1/-2` 追加) | `false` |
284
286
  | `--force` | 可选 flag | 覆写已存在的 harness 目录;同时允许覆盖已注入的 build skills/rules 与项目根 `AGENTS.md` / `CLAUDE.md`(默认遇到已存在文件会跳过) | `false` |
285
287
  | `-y, --yes` | 可选 flag | 跳过所有提示,采用默认值 | `false` |
288
+ | `--enable-baseline-extract` | 可选 flag | 启用 baseline 自动提取 skills/rules(S61 ENABLED) | `false` |
289
+ | `--strict-input-confirm` | 可选 flag | 禁用 CLI 预声明,恢复 S60/S61/S65 等 Agent 表单确认 | `false` |
286
290
  | `--verbose` | 可选 flag | 打印每个生成文件 | `false` |
287
291
 
288
292
  #### Wiki 参数
@@ -446,7 +450,7 @@ svharness doctor --harness ./my-app-harness --mode pre-seal --format json --repo
446
450
  | 选项 | 说明 |
447
451
  |------|------|
448
452
  | `--harness` | harness 根目录(必填;可写在 `svharness.config.yaml` 的 `doctor.harness`) |
449
- | `--mode` | `pre-seal`(默认)或 `health` |
453
+ | `--mode` | `pre-seal`(默认)、`health` 或 **`requirements`**(S40 专用) |
450
454
  | `--format` | `text`(默认)或 `json` |
451
455
  | `--report` | JSON 报告路径(默认 `<harness>/doctor-report.json`) |
452
456
  | `--strict` | 将 warning 视为 error |
@@ -459,15 +463,34 @@ svharness doctor --harness ./my-app-harness --mode pre-seal --format json --repo
459
463
  - **Part B**:按 `specs.layers` 与 `agent-env/review-profiles/<arch>.yaml` 做深度评分
460
464
  - **门禁**:`overall_score >= 5.0`(C 级)、`critical_count == 0`、报告 frontmatter `gate_pass: true`、用户表单确认
461
465
 
462
- `doctor` 另含 `check-review-report`(warning):S85 已 DONE 时校验报告 frontmatter 与 state 中 `review_gate_pass` 一致。数量覆盖率阻断仍由 S40/S50 表单门禁负责。
466
+ `doctor` 另含 `check-review-report`(warning):S85 已 DONE 时校验报告 frontmatter 与 state 中 `review_gate_pass` 一致。**S40 数量/结构覆盖率**由 `svharness requirements verify` + `doctor --mode requirements` 负责(非表单门禁)。
463
467
  同时新增:
464
468
 
465
469
  - `check-requirements-fidelity`:检查 `source_excerpt` 缺失、`description` 缩略与占位语义。
466
470
  - `check-specs-depth`:检查 index-only 信号、enum 缺失 `enum_values`、behavior 空 `guard/action`。
467
471
 
468
- ### `convert` —— 文档 Markdown 预处理(对接 S20_collect_inputs)
472
+ - `check-structure-coverage` / `check-source-inventory`:结构块与 Locator 摘录校验(`requirements` 模式)。
473
+
474
+ ### `requirements` —— S40 条目化结构扫描与 verify 门禁
475
+
476
+ `structure-scan` 在 **`svharness convert`(requirements)与 `svharness build` 注入需求后自动执行**;也可手动增量重扫。
469
477
 
470
- 把本地原始需求文档(`.pdf / .docx / .pptx / .xlsx / .html / .epub / .txt / .csv / .json / ...`)通过**云端部署**的 `markitdown_serve`(FastAPI + Microsoft MarkItDown)批量转为 Markdown,产物统一落到 `<harness>/<type>/md/`(`type` 为 `requirements` 或 `references`),直接喂给下游 `S40_extract_requirements` 条目化流程,显著提升 specs 生成质量的稳定性。
478
+ ```bash
479
+ # 增量重扫(docx→md 与 raw/*.md 双路径)
480
+ svharness requirements structure-scan --harness ./my-app-harness
481
+
482
+ # S40 机器门禁(生成 coverage-report + verify-gaps.json)
483
+ svharness requirements verify --harness ./my-app-harness --strict
484
+
485
+ # S40 专用 doctor
486
+ svharness doctor --harness ./my-app-harness --mode requirements --strict
487
+ ```
488
+
489
+ `svharness convert ... --no-structure-scan` 可跳过 convert 后的自动 structure-scan。
490
+
491
+ ### `convert` —— 文档 → Markdown 预处理(对接 S30_convert_docs)
492
+
493
+ 把本地原始需求文档(`.pdf / .docx / .pptx / .xlsx / .html / .epub / .txt / .csv / .json / ...`)通过**云端部署**的 `markitdown_serve`(FastAPI + Microsoft MarkItDown + **多引擎 fallback chain**)批量转为 Markdown,产物统一落到 `<harness>/<type>/md/`(`type` 为 `requirements` 或 `references`),直接喂给下游 `S40_extract_requirements` 条目化流程。
471
494
 
472
495
  > **表格清洗(xlsx/xls/csv)**:源文件为 `.xlsx`、`.xls` 或 `.csv` 时,转换完成后自动清理 Markdown 表格中的 `NaN`、全空行、以及数据全空且表头为 `Unnamed: N` 的列(CLI 与服务端双端生效)。对已存在的产物 md 可执行:`node scripts/cleanup-spreadsheet-md.js <dirOrFile>`。
473
496
 
@@ -475,6 +498,11 @@ svharness doctor --harness ./my-app-harness --mode pre-seal --format json --repo
475
498
 
476
499
  > **架构约束**:CLI 只是 HTTP 客户端,**不 spawn / 不装 Python / 不管进程**。服务端代码集中在 `svharnessbuild/markitdown_serve/` 便于仓内维护,但**不随 npm 包分发**,部署方式详见 [markitdown_serve/README.md](./markitdown_serve/README.md)。
477
500
 
501
+ > **高可靠转换(2026-06)**:
502
+ > - **服务端 fallback chain**:MarkItDown 失败或输出过短时,按扩展名尝试后备(PDF:pymupdf → pypdf → tesseract OCR;xlsx:openpyxl → pandas)。详见 [markitdown_serve/README.md §详细设计](./markitdown_serve/README.md#详细设计) 与 [详细设计 §4.10](../tmp/svharnessbuild-detail-design.md#410-文档转换convert-命令--markitdown_serve-远端服务)。
503
+ > - **CLI endpoint failover**:`--endpoint` / `SVHARNESS_MARKITDOWN_ENDPOINT` 支持逗号分隔多 URL。
504
+ > - **失败追溯**:有失败时写入 `<outputDir>/convert-failures.json`(含 `trace_id`)。
505
+
478
506
  ```bash
479
507
  # 最简 —— 所有参数均可省略,默认扫描当前目录
480
508
  svharness convert
@@ -485,8 +513,9 @@ svharness convert --input ./docs/*.pdf --output ./docs/md --yes
485
513
  # harness 模式:md 写入 ./my-app-harness/requirements/md/
486
514
  svharness convert --input ./my-app-harness/requirements/raw --output ./my-app-harness --yes
487
515
 
488
- # 显示处理日志
489
- svharness convert --verbose
516
+ # 多节点 failover + 详细引擎日志
517
+ export SVHARNESS_MARKITDOWN_ENDPOINT=https://md-a.example.com,https://md-b.example.com
518
+ svharness convert --verbose --yes
490
519
  ```
491
520
 
492
521
  #### 全部参数
@@ -496,7 +525,7 @@ svharness convert --verbose
496
525
  | `--input <path...>` | 一个或多个源文件 / 目录 / glob,支持 `./a/*.pdf`、`./docs/**/*.docx`、`./{a,b}/*.md` | `.`(当前目录) |
497
526
  | `--harness <path>` | harness 根目录(需 `harness.yaml`);单独使用时写入 `<harness>/<type>/md/`;与 `--output` 同用时 `--output` 为最终 md 目录 | — |
498
527
  | `--output <path>` | **独立模式**:最终 md 输出目录;**harness 模式**:`path` 含 `harness.yaml` 时写入 `<path>/<type>/md/` | `.`(当前目录) |
499
- | `--endpoint <url>` | 云端 `markitdown_serve` 基址,省略时读环境变量 `SVHARNESS_MARKITDOWN_ENDPOINT` | `http://markitdown.desaysz.site` |
528
+ | `--endpoint <url>` | 云端 `markitdown_serve` 基址;**逗号分隔**可配置 failover;省略时读 `SVHARNESS_MARKITDOWN_ENDPOINT` | `http://markitdown.desaysz.site` |
500
529
  | `--concurrency <n>` | 并发上传数 | `3` |
501
530
  | `--max-file-mb <n>` | 单文件大小上限(MB),本地 + 服务端双重限流 | `50` |
502
531
  | `--timeout-sec <n>` | 单请求超时秒数 | `120`(2 分钟) |
@@ -505,12 +534,13 @@ svharness convert --verbose
505
534
  | `--split-sheets-suffix <suffix>` | xlsx/xls 按 sheet 拆分的子目录后缀 | `_split` |
506
535
  | `--no-split-sheets` | flag,不将 xlsx/xls 合并 md 按 `##` 拆分为多文件 | `false`(默认拆分) |
507
536
  | `-y, --yes` | flag,跳过交互确认 | `false` |
508
- | `--verbose` | flag,显示详细日志 | `false` |
537
+ | `--verbose` | flag,显示详细日志(含每文件 `converter` / `fallback_used` / endpoint) | `false` |
509
538
 
510
539
  #### 行为要点
511
540
 
512
- - **健康探针前置**:上传前先 `GET /healthz`,不通直接退出并打印清晰定位指引(endpoint / 环境变量 / 服务端目录三条线索)。
513
- - **单文件失败不中断**:失败单文件计入汇总,其它文件照常完成。结束时打印 `ok / skipped / failed` 计数。
541
+ - **健康探针前置**:上传前先 `GET /healthz`(多 endpoint 时过滤不健康节点),不通直接退出。
542
+ - **单文件失败不中断**:失败计入汇总;有失败时写入 `convert-failures.json` `exit 1`。
543
+ - **Endpoint failover**:5xx / 网络错误在 comma-separated 列表中切换下一节点;415 等 4xx 不切换。
514
544
  - **本地白名单预过滤**:客户端与服务端的扩展名白名单保持一致,非白名单文件本地直接跳过,避免无意义往返。
515
545
  - **两种输出模式**:
516
546
  - **独立**:`--output` 即最终 md 目录(路径下无 `harness.yaml`),例如 `--output ./docs/md`。
@@ -530,16 +560,84 @@ svharness convert --verbose
530
560
  └── md/
531
561
  ├── 参考文档.md
532
562
  └── ...
563
+
564
+ # 有失败时额外产出(与 md 同目录):
565
+ convert-failures.json
533
566
  ```
534
567
 
535
568
  #### 接口契约(CLI ⇄ markitdown_serve)
536
569
 
537
570
  | 端点 | 方法 | 约定 |
538
571
  |---|---|---|
539
- | `/healthz` | GET | 200 `{status, markitdown_version}` |
540
- | `/convert` | POST | multipart/form-data,字段 `file`;200 返回 `{markdown, source_name, mime}`;413/415/500 带结构化 `error` 字段 |
572
+ | `/healthz` | GET | 200 `{status, markitdown_version, converters?}` |
573
+ | `/convert` | POST | multipart/form-data,字段 `file`;200 返回 `{markdown, source_name, mime, converter?, fallback_used?}`;413/415/500 `error` + `trace_id` |
574
+
575
+ 完整契约、fallback chain、Docker 部署见 [markitdown_serve/README.md](./markitdown_serve/README.md);模块设计见 [tmp/svharnessbuild-detail-design.md §4.10](../tmp/svharnessbuild-detail-design.md#410-文档转换convert-命令--markitdown_serve-远端服务)。
576
+
577
+ ### `start-agent` —— 启动 Agent 填充 harness(v0.16+)
578
+
579
+ `build` 生成骨架后,用本命令在**项目根(workdir)**启动 CodeChat CLI,驱动 S10–S90 构建阶段;`apply` 之后亦可在目标项目根启动 Agent 做业务开发。
580
+
581
+ 详细设计见 [`docs/agent-launcher-design.md`](docs/agent-launcher-design.md)。
541
582
 
542
- 完整契约与部署说明见 [markitdown_serve/README.md](./markitdown_serve/README.md)。
583
+ #### 快速示例
584
+
585
+ ```bash
586
+ # 当前目录
587
+ svharness start-agent
588
+
589
+ # 显式 workdir
590
+ svharness start-agent --work-dir /path/to/project
591
+ svharness start-agent codechat ./my-app
592
+ svharness start-agent /path/to/project
593
+
594
+ # 不同步项目 env、保留 CodeChat 权限确认
595
+ svharness start-agent --work-dir . --no-sync-env --no-skip-permissions
596
+ ```
597
+
598
+ #### 全部参数
599
+
600
+ | 参数 | 说明 | 默认 |
601
+ |------|------|------|
602
+ | `[agentOrWorkdir]` | 已知 agent 名 **或** workdir 路径 | — |
603
+ | `[workdir]` | 工作目录(前一参数为 agent 时使用) | — |
604
+ | `--work-dir <path>` | Agent 工作目录(harness 项目根) | 当前目录 |
605
+ | `--agent <agent>` | 目标 Agent(**当前仅 codechat 可实际启动**) | `codechat` |
606
+ | `--no-sync-env` | 不同步 `<workdir>/.claude/.env` → `~/.claude/.env` | false |
607
+ | `--no-skip-permissions` | 不传 `--dangerously-skip-permissions` | 默认传 |
608
+
609
+ #### 平台说明
610
+
611
+ | 平台 | CodeChat runner |
612
+ |------|-----------------|
613
+ | Windows | `~/.codechat/cli_app/run.bat` |
614
+ | Linux | `~/.codechat/cli_app/run.sh` |
615
+
616
+ ### `launch_codechat_cli` —— 全局快捷命令(v0.16+)
617
+
618
+ `npm install -g svharness` 后可用,等价于 `svharness start-agent codechat`(不支持额外 flag)。
619
+
620
+ ```bash
621
+ launch_codechat_cli
622
+ launch_codechat_cli D:\projects\my-app # Windows
623
+ launch_codechat_cli ~/projects/my-app # Linux
624
+ ```
625
+
626
+ ### `shell` —— Windows 右键菜单(v0.16+,仅 win32)
627
+
628
+ 在资源管理器**文件夹空白处**与**文件夹图标**右键增加「在此启动 CodeChat Agent」。
629
+
630
+ ```bash
631
+ svharness shell install # 注册(幂等)
632
+ svharness shell uninstall # 移除
633
+ svharness shell status # 查看 stub / 注册表状态
634
+ ```
635
+
636
+ `npm install -g svharness` 在 Windows 上 **默认自动** `shell install`。跳过:
637
+
638
+ ```bash
639
+ SVHARNESS_SKIP_SHELL=1 npm install -g svharness
640
+ ```
543
641
 
544
642
  ---
545
643
 
@@ -666,10 +764,16 @@ svharness build \
666
764
  ```
667
765
  svharnessbuild/ # 源码目录名沿用旧名(历史包名),CLI 名称已迁至 svharness
668
766
  ├── bin/cli.js # npm bin 入口 → dist/index.js
767
+ ├── bin/launch-codechat-cli.js # 全局快捷命令 launch_codechat_cli
768
+ ├── docs/agent-launcher-design.md # Agent 启动器详细设计
669
769
  ├── src/
670
770
  │ ├── index.ts # CLI 入口(commander)
671
771
  │ ├── types.ts # SupportedArch 枚举
672
772
  │ ├── commands/init.ts # 4 步主管线(双模板根:_shared + <arch>)
773
+ │ ├── commands/start-agent.ts # start-agent 子命令
774
+ │ ├── commands/shell-integration.ts # Windows 右键菜单
775
+ │ ├── lib/agent-launcher.ts # CodeChat 启动核心
776
+ │ ├── lib/win-registry.ts # HKCU 注册表封装
673
777
  │ ├── core/
674
778
  │ │ ├── scaffold.ts # scaffoldLayered: 先 _shared 再 <arch>
675
779
  │ │ ├── render-meta.ts # renderMetaLayered: 同名 .ejs 以 <arch> 为准
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { runStartAgent } = require('../dist/lib/agent-launcher');
5
+
6
+ const positionalWorkdir = process.argv[2];
7
+
8
+ runStartAgent({
9
+ agent: 'codechat',
10
+ positionalWorkdir,
11
+ })
12
+ .then((code) => {
13
+ process.exitCode = code;
14
+ })
15
+ .catch((err) => {
16
+ console.error('[err] ' + err.message);
17
+ process.exitCode = 1;
18
+ });
@@ -27,6 +27,7 @@ const logger_1 = require("../utils/logger");
27
27
  const validate_args_1 = require("../utils/validate-args");
28
28
  const markitdown_client_1 = require("../core/markitdown-client");
29
29
  const doc_intake_paths_1 = require("../core/doc-intake-paths");
30
+ const structure_scan_1 = require("../lib/requirements/structure-scan");
30
31
  const markdown_table_cleanup_1 = require("../core/markdown-table-cleanup");
31
32
  const markdown_sheet_split_1 = require("../core/markdown-sheet-split");
32
33
  /**
@@ -87,7 +88,12 @@ async function runConvert(opts) {
87
88
  configRows.push({ label: 'harness ', value: v.harnessRoot });
88
89
  configRows.push({ label: 'type ', value: v.type });
89
90
  }
90
- configRows.push({ label: 'output dir ', value: outDir }, { label: 'endpoint ', value: v.endpoint }, { label: 'concurrency', value: String(v.concurrency) }, { label: 'max file ', value: `${v.maxFileMB} MB` }, { label: 'timeout ', value: `${v.timeoutSec}s` }, { label: 'force ', value: opts.force ? 'yes' : 'no' }, {
91
+ configRows.push({ label: 'output dir ', value: outDir }, {
92
+ label: 'endpoint ',
93
+ value: v.endpoints.length > 1
94
+ ? `${v.endpoint} (+${v.endpoints.length - 1} failover)`
95
+ : v.endpoint,
96
+ }, { label: 'concurrency', value: String(v.concurrency) }, { label: 'max file ', value: `${v.maxFileMB} MB` }, { label: 'timeout ', value: `${v.timeoutSec}s` }, { label: 'force ', value: opts.force ? 'yes' : 'no' }, {
91
97
  label: 'split sheets',
92
98
  value: opts.splitSheets === false
93
99
  ? 'no'
@@ -130,20 +136,60 @@ async function runConvert(opts) {
130
136
  return;
131
137
  }
132
138
  }
133
- // 4. Probe health endpoint before any upload.
134
- try {
135
- const h = await (0, markitdown_client_1.ping)(v.endpoint);
136
- logger_1.logger.debug(`healthz ok: status=${h.status} markitdown=${h.markitdown_version ?? 'n/a'}`);
139
+ // 4. Probe health endpoint(s) before any upload.
140
+ let activeEndpoints = v.endpoints;
141
+ if (v.endpoints.length > 1) {
142
+ activeEndpoints = await (0, markitdown_client_1.filterHealthyEndpoints)(v.endpoints);
143
+ if (activeEndpoints.length === 0) {
144
+ logger_1.logger.error(`all ${v.endpoints.length} markitdown_serve endpoint(s) unreachable`);
145
+ for (const ep of v.endpoints) {
146
+ logger_1.logger.plain(` - ${ep}`);
147
+ }
148
+ process.exitCode = 1;
149
+ return;
150
+ }
151
+ if (activeEndpoints.length < v.endpoints.length) {
152
+ logger_1.logger.warn(`using ${activeEndpoints.length}/${v.endpoints.length} healthy endpoint(s)`);
153
+ }
154
+ if (opts.verbose) {
155
+ try {
156
+ const h = await (0, markitdown_client_1.ping)(activeEndpoints[0]);
157
+ if (h.converters) {
158
+ const ready = Object.entries(h.converters)
159
+ .filter(([, ok]) => ok)
160
+ .map(([name]) => name)
161
+ .join(', ');
162
+ logger_1.logger.debug(`converters ready: ${ready}`);
163
+ }
164
+ }
165
+ catch {
166
+ /* ignore */
167
+ }
168
+ }
137
169
  }
138
- catch (err) {
139
- const e = err;
140
- logger_1.logger.error(`markitdown_serve unreachable at ${v.endpoint}: ${e.message}`);
141
- logger_1.logger.plain('');
142
- logger_1.logger.plain(' - confirm the service is deployed and reachable from this machine');
143
- logger_1.logger.plain(' - pass --endpoint <url> or set env SVHARNESS_MARKITDOWN_ENDPOINT');
144
- logger_1.logger.plain(' - check the server code under svharnessbuild/markitdown_serve/');
145
- process.exitCode = 1;
146
- return;
170
+ else {
171
+ try {
172
+ const h = await (0, markitdown_client_1.ping)(v.endpoint);
173
+ logger_1.logger.debug(`healthz ok: status=${h.status} markitdown=${h.markitdown_version ?? 'n/a'}`);
174
+ if (opts.verbose && h.converters) {
175
+ const ready = Object.entries(h.converters)
176
+ .filter(([, ok]) => ok)
177
+ .map(([name]) => name)
178
+ .join(', ');
179
+ logger_1.logger.debug(`converters ready: ${ready}`);
180
+ }
181
+ }
182
+ catch (err) {
183
+ const e = err;
184
+ logger_1.logger.error(`markitdown_serve unreachable at ${v.endpoint}: ${e.message}`);
185
+ logger_1.logger.plain('');
186
+ logger_1.logger.plain(' - confirm the service is deployed and reachable from this machine');
187
+ logger_1.logger.plain(' - pass --endpoint <url> or comma-separated URLs for failover');
188
+ logger_1.logger.plain(' - set env SVHARNESS_MARKITDOWN_ENDPOINT (supports comma-separated list)');
189
+ logger_1.logger.plain(' - check the server code under svharnessbuild/markitdown_serve/');
190
+ process.exitCode = 1;
191
+ return;
192
+ }
147
193
  }
148
194
  // 5. Bounded-concurrency upload.
149
195
  const results = skipped.map(planToResult);
@@ -155,7 +201,7 @@ async function runConvert(opts) {
155
201
  const item = queue.shift();
156
202
  if (!item)
157
203
  break;
158
- results.push(await uploadOne(item.source, outDir, used, v, opts));
204
+ results.push(await uploadOne(item.source, outDir, used, v, opts, activeEndpoints));
159
205
  }
160
206
  };
161
207
  for (let i = 0; i < Math.min(v.concurrency, toUpload.length); i++) {
@@ -173,6 +219,18 @@ async function runConvert(opts) {
173
219
  const failed = results.filter((r) => r.status === 'failed');
174
220
  if (failed.length > 0) {
175
221
  process.exitCode = 1;
222
+ await writeFailuresReport(outDir, activeEndpoints, failed, opts.failuresReport);
223
+ }
224
+ if (v.harnessMode && v.harnessRoot && v.type === 'requirements') {
225
+ try {
226
+ await (0, structure_scan_1.maybeRunStructureScanForRequirements)(v.harnessRoot, 'convert', {
227
+ noStructureScan: opts.noStructureScan,
228
+ verbose: opts.verbose,
229
+ });
230
+ }
231
+ catch (err) {
232
+ logger_1.logger.warn(`structure-scan 失败(不中断 convert):${err.message}`);
233
+ }
176
234
  }
177
235
  }
178
236
  /** Walk glob patterns / explicit paths / directories into an absolute file list. */
@@ -324,7 +382,7 @@ async function buildPlan(files, limitBytes) {
324
382
  }
325
383
  return out;
326
384
  }
327
- async function uploadOne(source, outDir, used, v, opts) {
385
+ async function uploadOne(source, outDir, used, v, opts, activeEndpoints) {
328
386
  const basename = node_path_1.default.basename(source, node_path_1.default.extname(source));
329
387
  const outPath = await resolveOutputPath(outDir, basename, used, !!opts.force);
330
388
  used.add(outPath);
@@ -332,7 +390,11 @@ async function uploadOne(source, outDir, used, v, opts) {
332
390
  const splitSuffix = opts.splitSheetsSuffix ?? '_split';
333
391
  logger_1.logger.debug(`upload ${source} -> ${outPath}`);
334
392
  try {
335
- const resp = await (0, markitdown_client_1.postConvertWithRetry)(v.endpoint, source, v.timeoutSec * 1000);
393
+ const resp = await (0, markitdown_client_1.postConvertWithEndpointFailover)(activeEndpoints, source, v.timeoutSec * 1000);
394
+ if (opts.verbose && (resp.converter || resp.fallback_used)) {
395
+ logger_1.logger.debug(`converter ${node_path_1.default.basename(source)}: ${resp.converter ?? 'markitdown'} ` +
396
+ `fallback=${resp.fallback_used ? 'yes' : 'no'} endpoint=${resp.endpointUsed}`);
397
+ }
336
398
  const ext = node_path_1.default.extname(source).toLowerCase();
337
399
  let markdown = resp.markdown;
338
400
  if (markdown_table_cleanup_1.XLSX_CONVERT_EXTS.has(ext)) {
@@ -358,17 +420,37 @@ async function uploadOne(source, outDir, used, v, opts) {
358
420
  outputs.push(...splitWritten);
359
421
  logger_1.logger.debug(`sheet split ${node_path_1.default.basename(source)}: ${split.sections.length} sections -> ${splitDir}`);
360
422
  logger_1.logger.success(`${node_path_1.default.basename(source)} -> ${node_path_1.default.basename(outPath)} + ${split.sections.length} sheets in ${node_path_1.default.basename(splitDir)}/ (${bytes}B)`);
361
- return { source, output: outPath, outputs, status: 'ok', bytes };
423
+ return {
424
+ source,
425
+ output: outPath,
426
+ outputs,
427
+ status: 'ok',
428
+ bytes,
429
+ converter: resp.converter,
430
+ fallbackUsed: resp.fallback_used,
431
+ endpoint: resp.endpointUsed,
432
+ };
362
433
  }
363
434
  }
364
- logger_1.logger.success(`${node_path_1.default.basename(source)} -> ${node_path_1.default.basename(outPath)} (${bytes}B)`);
365
- return { source, output: outPath, outputs, status: 'ok', bytes };
435
+ const convNote = resp.fallback_used && resp.converter ? ` [${resp.converter}]` : '';
436
+ logger_1.logger.success(`${node_path_1.default.basename(source)} -> ${node_path_1.default.basename(outPath)} (${bytes}B)${convNote}`);
437
+ return {
438
+ source,
439
+ output: outPath,
440
+ outputs,
441
+ status: 'ok',
442
+ bytes,
443
+ converter: resp.converter,
444
+ fallbackUsed: resp.fallback_used,
445
+ endpoint: resp.endpointUsed,
446
+ };
366
447
  }
367
448
  catch (err) {
368
449
  const e = err;
369
450
  const reason = e.message;
451
+ const traceId = e instanceof markitdown_client_1.MarkItDownClientError ? (0, markitdown_client_1.extractTraceId)(e.body) : undefined;
370
452
  logger_1.logger.error(`failed: ${node_path_1.default.basename(source)} — ${reason}`);
371
- return { source, status: 'failed', reason };
453
+ return { source, status: 'failed', reason, traceId };
372
454
  }
373
455
  }
374
456
  /**
@@ -393,14 +475,39 @@ function printSummary(results) {
393
475
  const ok = results.filter((r) => r.status === 'ok').length;
394
476
  const failed = results.filter((r) => r.status === 'failed').length;
395
477
  const skipped = results.filter((r) => r.status === 'skipped').length;
478
+ const fallbackOk = results.filter((r) => r.status === 'ok' && r.fallbackUsed).length;
396
479
  logger_1.logger.section('convert summary');
397
480
  logger_1.logger.plain(` ok : ${ok}`);
398
481
  logger_1.logger.plain(` skipped : ${skipped}`);
399
482
  logger_1.logger.plain(` failed : ${failed}`);
483
+ if (fallbackOk > 0) {
484
+ logger_1.logger.plain(` fallback: ${fallbackOk} file(s) used backup converter`);
485
+ }
400
486
  if (failed > 0) {
401
487
  logger_1.logger.plain('');
402
488
  for (const r of results.filter((x) => x.status === 'failed')) {
403
- logger_1.logger.plain(` - ${node_path_1.default.basename(r.source)}: ${r.reason}`);
489
+ const trace = r.traceId ? ` (trace_id=${r.traceId})` : '';
490
+ logger_1.logger.plain(` - ${node_path_1.default.basename(r.source)}: ${r.reason}${trace}`);
404
491
  }
405
492
  }
406
493
  }
494
+ async function writeFailuresReport(outDir, endpoints, failed, customPath) {
495
+ if (customPath === '')
496
+ return;
497
+ const reportPath = customPath && customPath.trim().length > 0
498
+ ? node_path_1.default.isAbsolute(customPath)
499
+ ? customPath
500
+ : node_path_1.default.resolve(outDir, customPath)
501
+ : node_path_1.default.join(outDir, 'convert-failures.json');
502
+ const payload = {
503
+ generated_at: new Date().toISOString(),
504
+ endpoints,
505
+ failures: failed.map((r) => ({
506
+ source: r.source,
507
+ reason: r.reason,
508
+ trace_id: r.traceId,
509
+ })),
510
+ };
511
+ await fs_extra_1.default.writeFile(reportPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
512
+ logger_1.logger.warn(`wrote failure manifest: ${reportPath}`);
513
+ }