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.
- package/README.md +53 -11
- package/dist/commands/apply.js +268 -26
- package/dist/commands/doctor/check-bootstrap.js +32 -0
- package/dist/commands/doctor/check-chinese-heuristic.js +64 -0
- package/dist/commands/doctor/check-convert-pairing.js +54 -0
- package/dist/commands/doctor/check-empty-dirs.js +51 -0
- package/dist/commands/doctor/check-incoming.js +47 -0
- package/dist/commands/doctor/check-memory.js +23 -0
- package/dist/commands/doctor/check-policy-grep.js +44 -0
- package/dist/commands/doctor/check-references.js +78 -0
- package/dist/commands/doctor/check-requirements.js +69 -0
- package/dist/commands/doctor/check-skills-tasks.js +103 -0
- package/dist/commands/doctor/check-specs.js +83 -0
- package/dist/commands/doctor/check-state-phases.js +93 -0
- package/dist/commands/doctor/check-wiki.js +52 -0
- package/dist/commands/doctor/index.js +101 -0
- package/dist/commands/doctor/report.js +79 -0
- package/dist/commands/doctor/types.js +2 -0
- package/dist/commands/doctor/utils.js +130 -0
- package/dist/commands/doctor.js +51 -0
- package/dist/commands/init.js +2 -2
- package/dist/config/index.js +2 -1
- package/dist/config/merge-options.js +16 -0
- package/dist/core/agent-injector.js +50 -9
- package/dist/core/apply-project-entry.js +23 -1
- package/dist/core/extra-assets-intake.js +9 -2
- package/dist/core/project-ignore.js +4 -3
- package/dist/core/state.js +7 -1
- package/dist/index.js +57 -4
- package/dist/utils/skill-md-validate.js +85 -0
- package/package.json +3 -1
- package/templates/_shared/apply-skills/harness-apply-skills-main.md +5 -3
- package/templates/_shared/apply-skills/harness-build-skills-bridge.md +29 -0
- package/templates/_shared/build-rules/harness-build-rule-orchestrator-flow.md +8 -1
- package/templates/_shared/build-rules/harness-build-rule-pre-seal-review.md +39 -0
- package/templates/_shared/build-skills/harness-build-skill-agent-env-merge.md +2 -0
- package/templates/_shared/build-skills/harness-build-skill-orchestrator.md +7 -1
- package/templates/_shared/build-skills/harness-build-skill-pre-seal-review.md +51 -0
- package/templates/_shared/build-skills/harness-build-skills-main.md +1 -0
- package/templates/_shared/meta/AGENTS_APPLY.md.ejs +69 -32
- package/templates/_shared/meta/README.md.ejs +2 -1
- 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
|
|
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
|
|
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
|
-
| **
|
|
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
|
|
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
|
|
337
|
-
-
|
|
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` |
|
|
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
|
|
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` 格式,`
|
|
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
|
-
- [
|
|
791
|
+
- [x] `doctor` —— 检查既有 harness 的健康度(`svharness doctor`)
|
|
750
792
|
- [ ] `phase` —— 从 CLI 直接运行某个构建阶段
|
|
751
793
|
- [ ] `--arch auto` —— 根据 `--baseline` 自动推断架构
|
|
752
794
|
- [ ] 更多架构模板(`ios-swiftui` / `service-kotlin` / `python-fastapi` 等)
|
package/dist/commands/apply.js
CHANGED
|
@@ -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
|
|
342
|
-
const
|
|
343
|
-
await fs_extra_1.default.
|
|
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
|
|
433
|
-
*
|
|
434
|
-
*
|
|
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
|
-
*
|
|
438
|
-
*
|
|
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
|
-
|
|
492
|
-
|
|
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
|
|
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
|
-
...
|
|
625
|
-
...
|
|
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
|
-
|
|
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}
|
|
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
|
+
}
|