svharness 0.13.3 → 0.14.1

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 (42) hide show
  1. package/README.md +53 -11
  2. package/dist/commands/apply.js +268 -26
  3. package/dist/commands/doctor/check-bootstrap.js +32 -0
  4. package/dist/commands/doctor/check-chinese-heuristic.js +64 -0
  5. package/dist/commands/doctor/check-convert-pairing.js +54 -0
  6. package/dist/commands/doctor/check-empty-dirs.js +51 -0
  7. package/dist/commands/doctor/check-incoming.js +47 -0
  8. package/dist/commands/doctor/check-memory.js +23 -0
  9. package/dist/commands/doctor/check-policy-grep.js +44 -0
  10. package/dist/commands/doctor/check-references.js +78 -0
  11. package/dist/commands/doctor/check-requirements.js +69 -0
  12. package/dist/commands/doctor/check-skills-tasks.js +103 -0
  13. package/dist/commands/doctor/check-specs.js +83 -0
  14. package/dist/commands/doctor/check-state-phases.js +93 -0
  15. package/dist/commands/doctor/check-wiki.js +52 -0
  16. package/dist/commands/doctor/index.js +101 -0
  17. package/dist/commands/doctor/report.js +79 -0
  18. package/dist/commands/doctor/types.js +2 -0
  19. package/dist/commands/doctor/utils.js +130 -0
  20. package/dist/commands/doctor.js +51 -0
  21. package/dist/commands/init.js +2 -2
  22. package/dist/config/index.js +2 -1
  23. package/dist/config/merge-options.js +16 -0
  24. package/dist/core/agent-injector.js +50 -9
  25. package/dist/core/apply-project-entry.js +23 -1
  26. package/dist/core/extra-assets-intake.js +9 -2
  27. package/dist/core/project-ignore.js +4 -3
  28. package/dist/core/state.js +7 -1
  29. package/dist/index.js +57 -4
  30. package/dist/utils/skill-md-validate.js +85 -0
  31. package/package.json +3 -1
  32. package/templates/_shared/apply-skills/harness-apply-skills-main.md +5 -3
  33. package/templates/_shared/apply-skills/harness-build-skills-bridge.md +29 -0
  34. package/templates/_shared/build-rules/harness-build-rule-orchestrator-flow.md +8 -1
  35. package/templates/_shared/build-rules/harness-build-rule-pre-seal-review.md +39 -0
  36. package/templates/_shared/build-skills/harness-build-skill-agent-env-merge.md +2 -0
  37. package/templates/_shared/build-skills/harness-build-skill-orchestrator.md +7 -1
  38. package/templates/_shared/build-skills/harness-build-skill-pre-seal-review.md +51 -0
  39. package/templates/_shared/build-skills/harness-build-skills-main.md +1 -0
  40. package/templates/_shared/meta/AGENTS_APPLY.md.ejs +69 -32
  41. package/templates/_shared/meta/README.md.ejs +2 -1
  42. package/templates/svharness.config.example.yaml +12 -0
package/README.md CHANGED
@@ -119,7 +119,7 @@ Harness 是一个 **项目本地的知识层**,由两大部分组成:
119
119
  └── wiki/ # 架构 wiki
120
120
 
121
121
  ├── agent-env/ # Agent 运行期环境
122
- │ ├── rules/ # 编码规则(.mdc,always_on 加载)
122
+ │ ├── rules/ # 编码规则(.mdc,`alwaysApply: true` 为硬约束)
123
123
  │ ├── skills/ # 运行期技能(SKILL.md + references/ + scripts/)
124
124
  │ ├── tools/ # 项目本地脚本
125
125
  │ └── memory/ # Agent 长期记忆
@@ -150,7 +150,7 @@ S40/S50 -> harness-build-skill-spec-builder
150
150
  S60 -> harness-build-skill-references-intake
151
151
  S61/S65 -> harness-build-skill-agent-env-merge
152
152
  S70 -> harness-build-skill-knowledge-builder
153
- S80/S90 -> harness-build-skill-orchestrator
153
+ S80/S85/S90 -> harness-build-skill-orchestrator (+ S85 委派 pre-seal-review)
154
154
  ```
155
155
 
156
156
  `.harness-build-state.yaml` 跟踪进度,任何阶段可中断再续。
@@ -192,7 +192,8 @@ harness-build-{skill|rule}-<semantic-name>
192
192
  | **S65_customize_agent_env** | agent-env 定制(extra-skills/extra-rules 冲突建议与重命名建议 → 用户确认 → 写入) | `agent-env/rules/` + `agent-env/skills/` |
193
193
  | **S70_runtime_assets** | 运行期 Skills & tasks 索引(skill 执行 task) | `agent-env/skills/` + `tasks/templates/` |
194
194
  | **S80_seed_memory** | Memory 初始化 | `agent-env/memory/categories/` |
195
- | **S90_finalize** | 封板 | 版本 bump + CHANGELOG + 终审 |
195
+ | **S85_pre_seal_validation** | 封存前校验 | `svharness doctor` + 全面审查报告 |
196
+ | **S90_finalize** | 封板 | 版本 bump + CHANGELOG + `bootstrap_mode: false` |
196
197
 
197
198
  > 反复调用 orchestrator 总是从"第一个非 DONE 的阶段"继续。
198
199
  > **S10_wiki 仅在有 baseline 时出现;无 baseline 则不构建 wiki,流程从 S20_collect_inputs 开始。**
@@ -222,6 +223,7 @@ svharness wizard
222
223
  | `build` | 对应 `svharness build` |
223
224
  | `apply` | 对应 `svharness apply` |
224
225
  | `convert` | 对应 `svharness convert` |
226
+ | `doctor` | 对应 `svharness doctor` |
225
227
 
226
228
  `build` 节除路径外,可写人类可读说明(仅写入配置文件,供团队阅读):
227
229
 
@@ -276,7 +278,7 @@ svharness build \
276
278
  | `--convert-max-file-mb <n>` | 可选 | build 自动 convert 单文件大小上限(MB) | `50` |
277
279
  | `--convert-timeout-sec <n>` | 可选 | build 自动 convert 请求超时秒数 | `120` |
278
280
  | `--convert-force` | 可选 flag | build 自动 convert 覆盖同名 `.md`(否则按 `-1/-2` 追加) | `false` |
279
- | `--force` | 可选 flag | 覆写已存在的 harness 目录;同时覆写项目根 `AGENTS.md` / `CLAUDE.md`(若存在) | `false` |
281
+ | `--force` | 可选 flag | 覆写已存在的 harness 目录;同时允许覆盖已注入的 build skills/rules 与项目根 `AGENTS.md` / `CLAUDE.md`(默认遇到已存在文件会跳过) | `false` |
280
282
  | `-y, --yes` | 可选 flag | 跳过所有提示,采用默认值 | `false` |
281
283
  | `--verbose` | 可选 flag | 打印每个生成文件 | `false` |
282
284
 
@@ -333,8 +335,10 @@ svharness apply --harness ../my-app-harness --target ./to-apply-project --clone
333
335
  此外,`apply` 会同步注入运行期资产:
334
336
 
335
337
  - `<harness>/agent-env/rules/` → `<target>/<adapter.rulesDir>/`(若该 agent 声明了 `rulesDir`)
336
- - `<harness>/agent-env/skills/` → `<target>/<adapter.skillsDir>/`(全量注入;`harness-apply-skills-main` 由模板写入为薄入口)
337
- - 目标 `.gitignore` 会追加注入路径(幂等去重)
338
+ - `<harness>/agent-env/skills/` → `<target>/<adapter.skillsDir>/`(默认跳过同名已存在文件;`--force` 才覆盖。`harness-apply-skills-main` 由模板写入为薄入口)
339
+ - `--include-build-assets` 开启时:同步生成 `<target>/build-agent-env/skills` 与 `<target>/build-agent-env/rules`,用于二次 agent 驱动修改
340
+ - `--inject-build-main-bridge` 开启时:在 `<target>/<adapter.skillsDir>/harness-build-skills-bridge/` 写入桥接 skill(仅用于构建流二次改造)
341
+ - 目标 `.gitignore` 会在文件尾部 append 注入路径(幂等去重)
338
342
 
339
343
  #### references 内容引用 → apply_skill_registry(S60,由 Agent 写入)
340
344
 
@@ -348,7 +352,7 @@ S60(`harness-build-skill-references-intake`)属于 **build 阶段**。若某
348
352
  #### build / apply 分工
349
353
 
350
354
  - **build 阶段**:生成 harness 本体资产(`AGENTS_APPLY.md`、`agent-env/skills`、`agent-env/rules`、`specs`、`baseline` 等)
351
- - **apply 阶段**:复制 harness 到目标项目、注入 skills/rules、生成入口文件、写 `.gitignore`、校验注入文件引用关系
355
+ - **apply 阶段**:复制 harness 到目标项目、注入 skills/rules、(可选)拷贝构建期 assets、生成入口文件、写 `.gitignore`、输出一致性检查报告
352
356
 
353
357
  #### 全部参数
354
358
 
@@ -356,8 +360,11 @@ S60(`harness-build-skill-references-intake`)属于 **build 阶段**。若某
356
360
  |------|----------|------|--------|
357
361
  | `--harness <path>` | ✅ 必填 | 已构建好的 harness 目录(形如 `./my-app-harness`) | — |
358
362
  | `--target <path>` | 可选 | 目标项目根目录 | **当前工作目录(cwd)** |
359
- | `--agent <agent>` | 可选 | 目标 Agent:`codechat` / `qoder` / `cursor` / `claude-code` / `opencode` / `generic` | **从 `<harness>/.harness-build-state.yaml` 读取**;缺失则报错 |
363
+ | `--agent <agent>` | 可选 | 目标 Agent:`codechat` / `qoder` / `cursor` / `claude-code` / `opencode` / `generic` | `codechat` |
360
364
  | `--clone` | 可选 flag | 兼容参数;当前实现下行为与默认一致(都会拷贝 harness) | `false` |
365
+ | `--include-build-assets` | 可选 flag | 显式拷贝构建期 skills/rules 到 `build-agent-env/`(用于二次改造) | `false` |
366
+ | `--inject-build-main-bridge` | 可选 flag | 向运行期 skills 注入 `harness-build-skills-bridge`(需 `--include-build-assets`) | `false` |
367
+ | `--consistency <mode>` | 可选 | 一致性检查级别:`basic` / `strict`(`strict` 当前为预留模式) | `basic` |
361
368
  | `--force` | 可选 flag | 覆盖已存在注入目录与入口文件;同时允许重拷贝 harness | `false` |
362
369
  | `-y, --yes` | 可选 flag | 跳过交互确认 | `false` |
363
370
  | `--verbose` | 可选 flag | 显示详细日志 | `false` |
@@ -371,10 +378,45 @@ S60(`harness-build-skill-references-intake`)属于 **build 阶段**。若某
371
378
  ├── <adapter.skillsDir>/<runtime-skill>/SKILL.{md|mdc} # 运行期 skills 注入
372
379
  ├── <adapter.skillsDir>/harness-apply-skills-main/
373
380
  │ └── SKILL.{md|mdc} # 薄入口 skill(仅提示/索引,不做二级调度)
381
+ ├── <adapter.skillsDir>/harness-build-skills-bridge/
382
+ │ └── SKILL.{md|mdc} # 可选:--inject-build-main-bridge(build 流桥接,不是默认入口)
383
+ ├── build-agent-env/ # 可选:--include-build-assets 时生成
384
+ │ ├── skills/ # 构建期 skills 镜像(可二次改造)
385
+ │ └── rules/ # 构建期 rules 镜像(可二次改造)
374
386
  └── <name>-harness/ # apply 默认复制后的 harness
375
387
  ```
376
388
 
377
- > 注入完成后,CLI 会校验入口文件、薄入口 skill、注入 skills/rules 内的路径引用是否可达;失败会报错中止。
389
+ > 注入完成后,CLI 会输出一致性检查摘要(目录/文件存在性、frontmatter 关键字段、引用可达性),发现问题会给出警告或错误明细供修复。
390
+
391
+ #### 回滚(bridge 最小清理)
392
+
393
+ - 软回滚:后续 `apply` 不再传 `--inject-build-main-bridge`。
394
+ - 硬回滚:删除 `<adapter.skillsDir>/harness-build-skills-bridge/` 即可;`build-agent-env/` 可保留用于后续构建流改造。
395
+
396
+ ### `doctor` —— harness 健康度与封存前硬检查(S85)
397
+
398
+ 在 Agent 进入 **S85_pre_seal_validation** / 封板前,对 harness 目录执行可重复的硬检查:
399
+
400
+ ```bash
401
+ # 封存前全量(要求 S00–S80 均为 DONE)
402
+ svharness doctor --harness ./my-app-harness --mode pre-seal
403
+
404
+ # 日常巡检(不强制所有阶段 DONE)
405
+ svharness doctor --harness ./my-app-harness --mode health
406
+
407
+ # Agent 友好:JSON 报告
408
+ svharness doctor --harness ./my-app-harness --mode pre-seal --format json --report ./my-app-harness/doctor-report.json
409
+ ```
410
+
411
+ | 选项 | 说明 |
412
+ |------|------|
413
+ | `--harness` | harness 根目录(必填;可写在 `svharness.config.yaml` 的 `doctor.harness`) |
414
+ | `--mode` | `pre-seal`(默认)或 `health` |
415
+ | `--format` | `text`(默认)或 `json` |
416
+ | `--report` | JSON 报告路径(默认 `<harness>/doctor-report.json`) |
417
+ | `--strict` | 将 warning 视为 error |
418
+
419
+ 退出码:`0` 通过,`1` 存在 error。通过 doctor 后,由 `harness-build-skill-pre-seal-review` 完成语义层全面审查并经用户确认,方可将 S85 标为 DONE 并进入 S90。
378
420
 
379
421
  ### `convert` —— 文档 → Markdown 预处理(对接 S20_collect_inputs)
380
422
 
@@ -742,11 +784,11 @@ svharness build --harness-name demo-unknown --arch python --agent codechat `
742
784
  - [x] `build` —— 骨架 + 元文件 + 状态文件 + skill 注入
743
785
  - [x] 多架构模板(`android-compose` / `android-xml` / `cpp` / `web-react` / `python`)
744
786
  - [x] `_shared/` + `<arch>/` 两层叠拷合并
745
- - [x] 每个架构自带 3-5 条规则文件(`.mdc` 格式,`always_on` 加载)
787
+ - [x] 每个架构自带 3-5 条规则文件(`.mdc` 格式,`alwaysApply: true` 默认启用)
746
788
  - [x] `apply` —— 把已构建好的 harness 绑定到目标项目(默认复制模式,`--clone` 兼容保留)
747
789
  - [x] 构建辅助资源统一命名(`build-skills/harness-build-skill-*`、`build-rules/harness-build-rule-*`)
748
790
  - [ ] `detach` —— 从目标项目解绑并清理 dispatcher skill
749
- - [ ] `doctor` —— 检查既有 harness 的健康度
791
+ - [x] `doctor` —— 检查既有 harness 的健康度(`svharness doctor`)
750
792
  - [ ] `phase` —— 从 CLI 直接运行某个构建阶段
751
793
  - [ ] `--arch auto` —— 根据 `--baseline` 自动推断架构
752
794
  - [ ] 更多架构模板(`ios-swiftui` / `service-kotlin` / `python-fastapi` 等)
@@ -13,7 +13,9 @@ const apply_project_entry_1 = require("../core/apply-project-entry");
13
13
  const validate_args_1 = require("../utils/validate-args");
14
14
  const logger_1 = require("../utils/logger");
15
15
  const version_1 = require("../utils/version");
16
+ const skill_md_validate_1 = require("../utils/skill-md-validate");
16
17
  const DISPATCHER_SKILL_NAME = 'harness-apply-skills-main';
18
+ const BUILD_MAIN_BRIDGE_SKILL_NAME = 'harness-build-skills-bridge';
17
19
  const BUILD_SKILL_TEMPLATE_FILES = [
18
20
  'harness-build-skills-main.md',
19
21
  'harness-build-skill-orchestrator.md',
@@ -22,6 +24,18 @@ const BUILD_SKILL_TEMPLATE_FILES = [
22
24
  'harness-build-skill-agent-env-merge.md',
23
25
  'harness-build-skill-knowledge-builder.md',
24
26
  'harness-build-skill-wiki-writer.md',
27
+ 'harness-build-skill-pre-seal-review.md',
28
+ ];
29
+ const BUILD_RULE_TEMPLATE_FILES = [
30
+ 'harness-build-rule-agent-agnostic.md',
31
+ 'harness-build-rule-chinese-only.md',
32
+ 'harness-build-rule-convert-check.md',
33
+ 'harness-build-rule-memory-write.md',
34
+ 'harness-build-rule-orchestrator-flow.md',
35
+ 'harness-build-rule-pre-seal-review.md',
36
+ 'harness-build-rule-skills-tasks-output.md',
37
+ 'harness-build-rule-specs-schema.md',
38
+ 'harness-build-rule-user-interaction.md',
25
39
  ];
26
40
  const PATH_KEYS = [
27
41
  'agent-env/',
@@ -278,6 +292,10 @@ async function copyRuntimeSkills(input) {
278
292
  const content = input.adapter.transform
279
293
  ? input.adapter.transform(rewritten, node_path_1.default.basename(srcSkillFile))
280
294
  : rewritten;
295
+ const syncResult = syncSkillFrontmatterName(content, skillName);
296
+ if (syncResult.warning) {
297
+ logger_1.logger.warn(`运行期 skill frontmatter.name 同步失败(保留原文):${skillName} - ${syncResult.warning}`);
298
+ }
281
299
  const dstSkillDir = node_path_1.default.join(dstDir, skillName);
282
300
  const dstSkillFile = node_path_1.default.join(dstSkillDir, 'SKILL' + input.adapter.skillExt);
283
301
  if ((await fs_extra_1.default.pathExists(dstSkillFile)) && !input.force) {
@@ -285,7 +303,7 @@ async function copyRuntimeSkills(input) {
285
303
  continue;
286
304
  }
287
305
  await fs_extra_1.default.ensureDir(dstSkillDir);
288
- await fs_extra_1.default.outputFile(dstSkillFile, content, 'utf8');
306
+ await fs_extra_1.default.outputFile(dstSkillFile, syncResult.content, 'utf8');
289
307
  written.push(node_path_1.default.relative(input.targetRoot, dstSkillFile));
290
308
  }
291
309
  return written;
@@ -314,6 +332,32 @@ async function injectThinMainSkill(input) {
314
332
  await fs_extra_1.default.outputFile(dstFile, content, 'utf8');
315
333
  return dstFile;
316
334
  }
335
+ async function injectBuildMainBridgeSkill(input) {
336
+ const templatePath = node_path_1.default.join(input.templatesRoot, '_shared', 'apply-skills', `${BUILD_MAIN_BRIDGE_SKILL_NAME}.md`);
337
+ if (!(await fs_extra_1.default.pathExists(templatePath))) {
338
+ throw new Error(`build-main bridge 模板缺失:${templatePath}`);
339
+ }
340
+ const dstDir = node_path_1.default.join(input.targetRoot, input.adapter.skillsDir, BUILD_MAIN_BRIDGE_SKILL_NAME);
341
+ const dstFile = node_path_1.default.join(dstDir, 'SKILL' + input.adapter.skillExt);
342
+ if (await fs_extra_1.default.pathExists(dstDir)) {
343
+ if (!input.force) {
344
+ throw new Error(`目标 bridge skill 目录已存在:${dstDir}\n 使用 --force 覆盖,或先清理目标目录。`);
345
+ }
346
+ await fs_extra_1.default.remove(dstDir);
347
+ }
348
+ const raw = await fs_extra_1.default.readFile(templatePath, 'utf8');
349
+ const rendered = raw
350
+ .replace(/__HARNESS_ROOT_REL__/g, `./${input.harnessDirName}`)
351
+ .replace(/__BUILD_SKILLS_DIR__/g, './build-agent-env/skills')
352
+ .replace(/__BUILD_RULES_DIR__/g, './build-agent-env/rules')
353
+ .replace(/__APPLY_MAIN_SKILL__/g, DISPATCHER_SKILL_NAME);
354
+ const content = input.adapter.transform
355
+ ? input.adapter.transform(rendered, `${BUILD_MAIN_BRIDGE_SKILL_NAME}.md`)
356
+ : rendered;
357
+ await fs_extra_1.default.ensureDir(dstDir);
358
+ await fs_extra_1.default.outputFile(dstFile, content, 'utf8');
359
+ return dstFile;
360
+ }
317
361
  async function updateGitIgnore(input) {
318
362
  const gitignorePath = node_path_1.default.join(input.targetRoot, '.gitignore');
319
363
  const raw = (await fs_extra_1.default.pathExists(gitignorePath))
@@ -338,9 +382,10 @@ async function updateGitIgnore(input) {
338
382
  }
339
383
  if (added.length === 0)
340
384
  return [];
341
- const appendix = '\n# svharness apply injected assets\n' + added.map((line) => `${line}\n`).join('');
342
- const next = raw.endsWith('\n') || raw.length === 0 ? raw + appendix : raw + '\n' + appendix;
343
- await fs_extra_1.default.outputFile(gitignorePath, next, 'utf8');
385
+ const prefix = raw.length === 0 ? '' : raw.endsWith('\n') ? '\n' : '\n\n';
386
+ const appendix = `${prefix}# svharness apply injected assets\n` + added.map((line) => `${line}\n`).join('');
387
+ await fs_extra_1.default.ensureDir(node_path_1.default.dirname(gitignorePath));
388
+ await fs_extra_1.default.appendFile(gitignorePath, appendix, 'utf8');
344
389
  return added;
345
390
  }
346
391
  async function validateInjectedReferences(input) {
@@ -365,6 +410,34 @@ async function validateInjectedReferences(input) {
365
410
  }
366
411
  return failures;
367
412
  }
413
+ function syncSkillFrontmatterName(content, expectedName) {
414
+ const frontmatter = (0, skill_md_validate_1.extractFrontmatter)(content);
415
+ if (!frontmatter) {
416
+ return { content, changed: false, warning: '缺少 frontmatter' };
417
+ }
418
+ let parsed;
419
+ try {
420
+ parsed = js_yaml_1.default.load(frontmatter.block);
421
+ }
422
+ catch (err) {
423
+ return { content, changed: false, warning: `frontmatter 解析失败:${err.message}` };
424
+ }
425
+ if (!parsed || typeof parsed !== 'object') {
426
+ return { content, changed: false, warning: 'frontmatter 解析为空' };
427
+ }
428
+ const currentName = typeof parsed.name === 'string' ? parsed.name.trim() : '';
429
+ if (currentName === expectedName) {
430
+ return { content, changed: false };
431
+ }
432
+ parsed.name = expectedName;
433
+ const dumped = js_yaml_1.default.dump(parsed, { lineWidth: 120, noRefs: true }).trimEnd();
434
+ const rewrittenNormalized = `---\n${dumped}\n---\n` + frontmatter.normalized.slice(frontmatter.endIdx + 5);
435
+ const hasCrlf = content.includes('\r\n');
436
+ return {
437
+ content: hasCrlf ? rewrittenNormalized.replace(/\n/g, '\r\n') : rewrittenNormalized,
438
+ changed: true,
439
+ };
440
+ }
368
441
  async function saveBuildSkillsToBuildAgentEnv(input) {
369
442
  const destRoot = node_path_1.default.join(input.targetRoot, 'build-agent-env', 'skills');
370
443
  await fs_extra_1.default.ensureDir(destRoot);
@@ -424,19 +497,70 @@ async function saveBuildSkillsToBuildAgentEnv(input) {
424
497
  }
425
498
  return written;
426
499
  }
500
+ async function resolveRuleSourceFile(rulesRoot, ruleName, ruleExt) {
501
+ const candidates = [
502
+ node_path_1.default.join(rulesRoot, `${ruleName}${ruleExt}`),
503
+ node_path_1.default.join(rulesRoot, `${ruleName}.md`),
504
+ node_path_1.default.join(rulesRoot, `${ruleName}.mdc`),
505
+ ];
506
+ for (const candidate of candidates) {
507
+ if (await fs_extra_1.default.pathExists(candidate))
508
+ return candidate;
509
+ }
510
+ return undefined;
511
+ }
512
+ async function saveBuildRulesToBuildAgentEnv(input) {
513
+ if (!input.adapter.rulesDir) {
514
+ logger_1.logger.info(`agent ${input.adapter.name} 未声明 rulesDir,跳过 build-agent-env/rules 生成`);
515
+ return [];
516
+ }
517
+ const destRoot = node_path_1.default.join(input.targetRoot, 'build-agent-env', 'rules');
518
+ await fs_extra_1.default.ensureDir(destRoot);
519
+ const sourceRulesRoot = node_path_1.default.join(node_path_1.default.dirname(input.harnessRoot), input.adapter.rulesDir);
520
+ const ruleExt = input.adapter.ruleExt ?? '.md';
521
+ const written = [];
522
+ for (const templateName of BUILD_RULE_TEMPLATE_FILES) {
523
+ const ruleName = templateName.replace(/\.md$/i, '');
524
+ const dstRuleFile = node_path_1.default.join(destRoot, `${ruleName}${ruleExt}`);
525
+ if ((await fs_extra_1.default.pathExists(dstRuleFile)) && !input.force) {
526
+ logger_1.logger.warn(`build rule 已存在,跳过:${node_path_1.default.relative(input.targetRoot, dstRuleFile)}`);
527
+ continue;
528
+ }
529
+ let raw;
530
+ if (await fs_extra_1.default.pathExists(sourceRulesRoot)) {
531
+ const sourceRuleFile = await resolveRuleSourceFile(sourceRulesRoot, ruleName, ruleExt);
532
+ if (sourceRuleFile) {
533
+ raw = await fs_extra_1.default.readFile(sourceRuleFile, 'utf8');
534
+ }
535
+ }
536
+ if (!raw) {
537
+ const templatePath = node_path_1.default.join(input.templatesRoot, '_shared', 'build-rules', templateName);
538
+ if (!(await fs_extra_1.default.pathExists(templatePath))) {
539
+ logger_1.logger.warn(`build rule 模板缺失,跳过:${templateName}`);
540
+ continue;
541
+ }
542
+ raw = await fs_extra_1.default.readFile(templatePath, 'utf8');
543
+ }
544
+ const rewritten = rewriteHarnessReferences(raw, input.harnessDirName);
545
+ const content = input.adapter.ruleTransform
546
+ ? input.adapter.ruleTransform(rewritten, templateName)
547
+ : rewritten;
548
+ await fs_extra_1.default.outputFile(dstRuleFile, content, 'utf8');
549
+ written.push(node_path_1.default.relative(input.targetRoot, dstRuleFile));
550
+ }
551
+ return written;
552
+ }
427
553
  /**
428
554
  * Entry point for `svharnessbuild apply`.
429
555
  *
430
556
  * Minimum-invasion binding: copies a single dispatcher skill
431
557
  * (`harness-apply-skills-main/SKILL.{md|mdc}`) into the target project's
432
- * agent-native skills directory. The skill carries the harness binding
433
- * metadata **inlined into its own body** (`## 绑定元数据` section), so any
434
- * agent can resolve `harness_root` purely from the skill's loaded content
435
- * without depending on CWD or "same-directory" heuristics (scheme B).
558
+ * agent-native skills directory as a thin entry. Runtime rules/skills are
559
+ * copied into adapter directories, and the project-root AI entry file
560
+ * (`AGENTS.md` / `CLAUDE.md`) is generated from `<harness>/AGENTS_APPLY.md`.
436
561
  *
437
- * `references/binding.yaml` is still written as a redundant copy for
438
- * `svharnessbuild`'s own tooling (status / detach / re-apply detection) —
439
- * it is NOT the runtime source of truth.
562
+ * Runtime behavior is path-rewrite based (`./<name>-harness/...`), not
563
+ * dispatcher-embedded binding metadata parsing.
440
564
  */
441
565
  async function runApply(opts) {
442
566
  (0, logger_1.setVerbose)(!!opts.verbose);
@@ -479,7 +603,8 @@ async function runApply(opts) {
479
603
  else {
480
604
  effectiveHarnessRoot = cloneDest;
481
605
  }
482
- // 3. Resolve agent (explicit > state file).
606
+ // 3. Resolve agent (explicit > state file > codechat default).
607
+ const DEFAULT_AGENT = 'codechat';
483
608
  let agent = opts.agent;
484
609
  if (!agent) {
485
610
  agent = await readAgentFromState(harnessRoot);
@@ -488,16 +613,14 @@ async function runApply(opts) {
488
613
  }
489
614
  }
490
615
  if (!agent) {
491
- throw new Error('无法推断目标 agent。请显式传入 --agent <' +
492
- (0, validate_args_1.listSupportedAgents)().join('|') +
493
- '>,或确认 harness 下存在 .harness-build-state.yaml。');
616
+ agent = DEFAULT_AGENT;
617
+ logger_1.logger.info(`未指定 agent,使用默认值:${agent}`);
494
618
  }
495
619
  const adapter = (0, adapters_1.getAdapter)(agent);
496
620
  // 4. Compute injection target.
497
621
  const skillsBaseDir = node_path_1.default.join(targetRoot, adapter.skillsDir);
498
622
  const dispatcherDir = node_path_1.default.join(skillsBaseDir, DISPATCHER_SKILL_NAME);
499
623
  const skillFile = node_path_1.default.join(dispatcherDir, 'SKILL' + adapter.skillExt);
500
- const bindingFile = node_path_1.default.join(dispatcherDir, 'references', 'binding.yaml');
501
624
  // 5. Load dispatcher skill template.
502
625
  const templatePath = node_path_1.default.join(resolveTemplatesRoot(), '_shared', 'apply-skills', `${DISPATCHER_SKILL_NAME}.md`);
503
626
  if (!(await fs_extra_1.default.pathExists(templatePath))) {
@@ -513,6 +636,10 @@ async function runApply(opts) {
513
636
  const harnessRootRel = node_path_1.default.relative(targetRoot, effectiveHarnessRoot).replace(/\\/g, '/') || '.';
514
637
  const cliVersion = (0, version_1.getCliVersion)();
515
638
  const appliedAt = new Date().toISOString();
639
+ const includeBuildAssets = !!opts.includeBuildAssets;
640
+ const requestedBuildBridge = !!opts.injectBuildMainBridge;
641
+ const injectBuildMainBridge = includeBuildAssets && requestedBuildBridge;
642
+ const consistencyMode = opts.consistency === 'strict' ? 'strict' : 'basic';
516
643
  // 6. Confirm (unless --yes).
517
644
  const configRows = [
518
645
  { label: 'harness 源路径', value: harnessRoot },
@@ -525,7 +652,7 @@ async function runApply(opts) {
525
652
  if (cloneMode && effectiveHarnessRoot !== harnessRoot) {
526
653
  configRows.push({ label: '克隆目标', value: effectiveHarnessRoot });
527
654
  }
528
- configRows.push({ label: '注入 skill', value: node_path_1.default.relative(targetRoot, skillFile) }, { label: '运行期 skills 注入', value: adapter.skillsDir }, { label: '运行期 rules 注入', value: adapter.rulesDir ?? '(agent 未声明 rulesDir)' }, { label: '.gitignore', value: '将追加注入路径(幂等)' }, { label: '项目根 AI 入口', value: `${adapter.projectEntryFile}(由 AGENTS_APPLY.md 重命名)` }, { label: '工具版本', value: `svharness@${cliVersion}` });
655
+ configRows.push({ label: '注入 skill', value: node_path_1.default.relative(targetRoot, skillFile) }, { label: '运行期 skills 注入', value: adapter.skillsDir }, { label: '运行期 rules 注入', value: adapter.rulesDir ?? '(agent 未声明 rulesDir)' }, { label: '构建期 assets 拷贝', value: includeBuildAssets ? '开启(build-agent-env)' : '关闭' }, { label: 'build-main 桥接', value: injectBuildMainBridge ? '开启' : '关闭' }, { label: '一致性检查级别', value: consistencyMode }, { label: '.gitignore', value: '将追加注入路径(幂等)' }, { label: '项目根 AI 入口', value: `${adapter.projectEntryFile}(由 AGENTS_APPLY.md 重命名)` }, { label: '工具版本', value: `svharness@${cliVersion}` });
529
656
  logger_1.logger.configBox('apply 配置确认', configRows);
530
657
  if (!opts.yes) {
531
658
  const { ok } = await (0, prompts_1.default)({
@@ -539,6 +666,12 @@ async function runApply(opts) {
539
666
  return;
540
667
  }
541
668
  }
669
+ if (consistencyMode === 'strict') {
670
+ logger_1.logger.warn('strict 一致性检查尚在规划中,当前先执行 basic 检查。');
671
+ }
672
+ if (requestedBuildBridge && !includeBuildAssets) {
673
+ logger_1.logger.warn('`--inject-build-main-bridge` 依赖 `--include-build-assets`;当前跳过 bridge 注入。');
674
+ }
542
675
  // 7. Guard against overwrite.
543
676
  if (await fs_extra_1.default.pathExists(dispatcherDir)) {
544
677
  if (!opts.force) {
@@ -549,8 +682,11 @@ async function runApply(opts) {
549
682
  }
550
683
  // 7b. Clone step — copy the whole harness into the target project first,
551
684
  // so subsequent injections resolve to the local copy.
552
- const totalSteps = 5;
685
+ const totalSteps = includeBuildAssets ? 6 : 5;
553
686
  let stepNo = 1;
687
+ let buildSkills = [];
688
+ let buildRules = [];
689
+ let bridgeSkillPath;
554
690
  if (cloneMode && effectiveHarnessRoot !== harnessRoot) {
555
691
  logger_1.logger.section(`步骤 ${stepNo}/${totalSteps} - 拷贝 harness 到目标项目`);
556
692
  if (await fs_extra_1.default.pathExists(cloneDest)) {
@@ -594,12 +730,53 @@ async function runApply(opts) {
594
730
  logger_1.logger.success(`已注入运行期 rules ${runtimeRules.length} 条`);
595
731
  logger_1.logger.success(`已注入运行期 skills ${runtimeSkills.length} 条`);
596
732
  stepNo++;
733
+ if (includeBuildAssets) {
734
+ logger_1.logger.section(`步骤 ${stepNo}/${totalSteps} - 拷贝构建期 skills/rules`);
735
+ buildSkills = await saveBuildSkillsToBuildAgentEnv({
736
+ harnessRoot: effectiveHarnessRoot,
737
+ targetRoot,
738
+ adapter,
739
+ harnessRootRel,
740
+ harnessName,
741
+ harnessVersion,
742
+ harnessArch,
743
+ agent,
744
+ force: !!opts.force,
745
+ cliVersion,
746
+ generatedAt: appliedAt,
747
+ templatesRoot,
748
+ });
749
+ buildRules = await saveBuildRulesToBuildAgentEnv({
750
+ harnessRoot: effectiveHarnessRoot,
751
+ targetRoot,
752
+ adapter,
753
+ harnessDirName,
754
+ force: !!opts.force,
755
+ templatesRoot,
756
+ });
757
+ logger_1.logger.success(`已注入构建期 skills ${buildSkills.length} 条`);
758
+ logger_1.logger.success(`已注入构建期 rules ${buildRules.length} 条`);
759
+ if (injectBuildMainBridge) {
760
+ logger_1.logger.info(`build-main bridge 已启用,目标目录:${adapter.skillsDir}/${BUILD_MAIN_BRIDGE_SKILL_NAME}`);
761
+ bridgeSkillPath = await injectBuildMainBridgeSkill({
762
+ templatesRoot,
763
+ targetRoot,
764
+ adapter,
765
+ harnessDirName,
766
+ force: !!opts.force,
767
+ });
768
+ logger_1.logger.success(`已写入 build-main bridge:${node_path_1.default.relative(targetRoot, bridgeSkillPath)}`);
769
+ }
770
+ stepNo++;
771
+ }
597
772
  logger_1.logger.section(`步骤 ${stepNo}/${totalSteps} - 写入项目根 AI 入口`);
598
773
  await (0, apply_project_entry_1.writeApplyProjectEntry)({
599
774
  projectRoot: targetRoot,
600
775
  harnessRoot: effectiveHarnessRoot,
601
776
  harnessDirName,
602
777
  adapter,
778
+ includeBuildBridge: injectBuildMainBridge,
779
+ buildBridgeSkillName: BUILD_MAIN_BRIDGE_SKILL_NAME,
603
780
  force: !!opts.force,
604
781
  });
605
782
  const entryFile = node_path_1.default.join(targetRoot, adapter.projectEntryFile);
@@ -614,25 +791,84 @@ async function runApply(opts) {
614
791
  logger_1.logger.info('.gitignore 无新增条目(已是最新)');
615
792
  }
616
793
  else {
617
- logger_1.logger.success(`.gitignore 已新增 ${ignoreAdded.length} 条注入路径`);
794
+ logger_1.logger.success(`.gitignore append ${ignoreAdded.length} 条注入路径`);
618
795
  }
619
796
  stepNo++;
620
797
  logger_1.logger.section(`步骤 ${stepNo}/${totalSteps} - 校验注入文件引用关系`);
798
+ const runtimeRuleFiles = runtimeRules.map((rel) => node_path_1.default.join(targetRoot, rel));
799
+ const runtimeSkillFiles = runtimeSkills.map((rel) => node_path_1.default.join(targetRoot, rel));
800
+ const buildRuleFiles = buildRules.map((rel) => node_path_1.default.join(targetRoot, rel));
801
+ const buildSkillFiles = buildSkills.map((rel) => node_path_1.default.join(targetRoot, rel));
621
802
  const filesToCheck = [
622
803
  entryFile,
623
804
  thinMainPath,
624
- ...runtimeRules.map((rel) => node_path_1.default.join(targetRoot, rel)),
625
- ...runtimeSkills.map((rel) => node_path_1.default.join(targetRoot, rel)),
805
+ ...(bridgeSkillPath ? [bridgeSkillPath] : []),
806
+ ...runtimeRuleFiles,
807
+ ...runtimeSkillFiles,
808
+ ...buildRuleFiles,
809
+ ...buildSkillFiles,
810
+ ];
811
+ const consistencyErrors = [];
812
+ const consistencyWarnings = [];
813
+ const requiredDirs = [
814
+ node_path_1.default.join(targetRoot, adapter.skillsDir),
815
+ ...(adapter.rulesDir ? [node_path_1.default.join(targetRoot, adapter.rulesDir)] : []),
816
+ ...(includeBuildAssets
817
+ ? [node_path_1.default.join(targetRoot, 'build-agent-env', 'skills'), node_path_1.default.join(targetRoot, 'build-agent-env', 'rules')]
818
+ : []),
626
819
  ];
820
+ for (const dir of requiredDirs) {
821
+ if (!(await fs_extra_1.default.pathExists(dir))) {
822
+ consistencyErrors.push(`目录缺失:${node_path_1.default.relative(targetRoot, dir)}`);
823
+ }
824
+ }
825
+ for (const file of filesToCheck) {
826
+ if (!(await fs_extra_1.default.pathExists(file))) {
827
+ consistencyErrors.push(`文件缺失:${node_path_1.default.relative(targetRoot, file)}`);
828
+ }
829
+ }
830
+ const skillFrontmatterChecks = [...runtimeSkillFiles, ...buildSkillFiles];
831
+ if (bridgeSkillPath)
832
+ skillFrontmatterChecks.push(bridgeSkillPath);
833
+ for (const file of skillFrontmatterChecks) {
834
+ consistencyWarnings.push(...(await (0, skill_md_validate_1.validateFrontmatterFields)({
835
+ targetRoot,
836
+ file,
837
+ requiredKeys: ['name', 'description'],
838
+ })));
839
+ consistencyWarnings.push(...(await (0, skill_md_validate_1.validateSkillNameMatchesDir)({
840
+ targetRoot,
841
+ file,
842
+ })));
843
+ }
844
+ const ruleFrontmatterChecks = [...runtimeRuleFiles, ...buildRuleFiles];
845
+ for (const file of ruleFrontmatterChecks) {
846
+ consistencyWarnings.push(...(await (0, skill_md_validate_1.validateFrontmatterFields)({
847
+ targetRoot,
848
+ file,
849
+ requiredKeys: ['description'],
850
+ })));
851
+ }
627
852
  const refFailures = await validateInjectedReferences({
628
853
  targetRoot,
629
854
  harnessDirName,
630
855
  files: filesToCheck,
631
856
  });
857
+ consistencyWarnings.push(...refFailures.map((line) => `引用缺失:${line}`));
858
+ const okCount = filesToCheck.length + requiredDirs.length - consistencyErrors.length;
859
+ logger_1.logger.info(`一致性检查(basic)结果:通过 ${Math.max(okCount, 0)} 项,警告 ${consistencyWarnings.length} 项,错误 ${consistencyErrors.length} 项`);
860
+ if (consistencyErrors.length > 0) {
861
+ const preview = consistencyErrors.slice(0, 20).join('\n');
862
+ logger_1.logger.warn(`一致性检查发现 ${consistencyErrors.length} 条错误(请先修复):\n${preview}\n` +
863
+ (consistencyErrors.length > 20 ? '...(其余省略)' : ''));
864
+ }
865
+ if (consistencyWarnings.length > 0) {
866
+ const preview = consistencyWarnings.slice(0, 20).join('\n');
867
+ logger_1.logger.warn(`一致性检查发现 ${consistencyWarnings.length} 条警告:\n${preview}\n` +
868
+ (consistencyWarnings.length > 20 ? '...(其余省略)' : ''));
869
+ }
632
870
  if (refFailures.length > 0) {
633
- const preview = refFailures.slice(0, 20).join('\n');
634
- logger_1.logger.warn(`引用校验发现 ${refFailures.length} 条潜在问题(请人工复核):\n${preview}\n` +
635
- (refFailures.length > 20 ? '...(其余省略)' : ''));
871
+ logger_1.logger.warn(`引用校验发现 ${refFailures.length} 条潜在问题(已并入一致性警告)`);
636
872
  }
637
873
  else {
638
874
  logger_1.logger.success('引用关系校验通过');
@@ -642,8 +878,14 @@ async function runApply(opts) {
642
878
  logger_1.logger.plain('✨ harness 已绑定到当前项目');
643
879
  logger_1.logger.plain('');
644
880
  logger_1.logger.plain(' 下一步:在 agent 中输入');
645
- logger_1.logger.plain(` 应用 ${DISPATCHER_SKILL_NAME} 完成 <你的功能> 功能开发`);
646
- logger_1.logger.plain(` 或阅读 ${adapter.projectEntryFile} 了解完整的 harness 应用工作流`);
881
+ logger_1.logger.plain(` 应用 ${DISPATCHER_SKILL_NAME} 完成 <你的功能> 功能开发(薄入口)`);
882
+ logger_1.logger.plain(` 或阅读 ${adapter.projectEntryFile}(项目根入口)了解完整的 harness 应用工作流`);
883
+ if (includeBuildAssets) {
884
+ logger_1.logger.plain(' 构建期二次改造入口:build-agent-env/skills + build-agent-env/rules');
885
+ if (injectBuildMainBridge) {
886
+ logger_1.logger.plain(` 可选桥接入口:${adapter.skillsDir}/${BUILD_MAIN_BRIDGE_SKILL_NAME}/SKILL${adapter.skillExt}`);
887
+ }
888
+ }
647
889
  logger_1.logger.plain('');
648
890
  if (effectiveHarnessRoot !== harnessRoot) {
649
891
  logger_1.logger.plain(` harness 内容已拷贝至:${node_path_1.default.relative(targetRoot, effectiveHarnessRoot)}/(已纳入目标项目)`);
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.checkBootstrap = checkBootstrap;
4
+ const utils_1 = require("./utils");
5
+ async function checkBootstrap(ctx) {
6
+ const id = 'bootstrap';
7
+ const findings = [];
8
+ if (ctx.mode !== 'pre-seal') {
9
+ return { id, title: 'Bootstrap 模式', findings };
10
+ }
11
+ const harness = await (0, utils_1.readHarnessYaml)(ctx.harnessRoot);
12
+ if (!harness) {
13
+ findings.push({
14
+ checkId: id,
15
+ severity: 'error',
16
+ message: '缺少或无法解析 harness.yaml',
17
+ path: 'harness.yaml',
18
+ });
19
+ return { id, title: 'Bootstrap 模式', findings };
20
+ }
21
+ const specs = harness.specs;
22
+ const bootstrapMode = specs?.bootstrap_mode;
23
+ if (bootstrapMode !== true) {
24
+ findings.push({
25
+ checkId: id,
26
+ severity: 'error',
27
+ message: `封存前 specs.bootstrap_mode 应为 true(当前:${String(bootstrapMode)})`,
28
+ path: 'harness.yaml',
29
+ });
30
+ }
31
+ return { id, title: 'Bootstrap 模式', findings };
32
+ }