svharness 0.13.3 → 0.13.5

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 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 长期记忆
@@ -276,7 +276,7 @@ svharness build \
276
276
  | `--convert-max-file-mb <n>` | 可选 | build 自动 convert 单文件大小上限(MB) | `50` |
277
277
  | `--convert-timeout-sec <n>` | 可选 | build 自动 convert 请求超时秒数 | `120` |
278
278
  | `--convert-force` | 可选 flag | build 自动 convert 覆盖同名 `.md`(否则按 `-1/-2` 追加) | `false` |
279
- | `--force` | 可选 flag | 覆写已存在的 harness 目录;同时覆写项目根 `AGENTS.md` / `CLAUDE.md`(若存在) | `false` |
279
+ | `--force` | 可选 flag | 覆写已存在的 harness 目录;同时允许覆盖已注入的 build skills/rules 与项目根 `AGENTS.md` / `CLAUDE.md`(默认遇到已存在文件会跳过) | `false` |
280
280
  | `-y, --yes` | 可选 flag | 跳过所有提示,采用默认值 | `false` |
281
281
  | `--verbose` | 可选 flag | 打印每个生成文件 | `false` |
282
282
 
@@ -333,8 +333,10 @@ svharness apply --harness ../my-app-harness --target ./to-apply-project --clone
333
333
  此外,`apply` 会同步注入运行期资产:
334
334
 
335
335
  - `<harness>/agent-env/rules/` → `<target>/<adapter.rulesDir>/`(若该 agent 声明了 `rulesDir`)
336
- - `<harness>/agent-env/skills/` → `<target>/<adapter.skillsDir>/`(全量注入;`harness-apply-skills-main` 由模板写入为薄入口)
337
- - 目标 `.gitignore` 会追加注入路径(幂等去重)
336
+ - `<harness>/agent-env/skills/` → `<target>/<adapter.skillsDir>/`(默认跳过同名已存在文件;`--force` 才覆盖。`harness-apply-skills-main` 由模板写入为薄入口)
337
+ - `--include-build-assets` 开启时:同步生成 `<target>/build-agent-env/skills` 与 `<target>/build-agent-env/rules`,用于二次 agent 驱动修改
338
+ - `--inject-build-main-bridge` 开启时:在 `<target>/<adapter.skillsDir>/harness-build-skills-bridge/` 写入桥接 skill(仅用于构建流二次改造)
339
+ - 目标 `.gitignore` 会在文件尾部 append 注入路径(幂等去重)
338
340
 
339
341
  #### references 内容引用 → apply_skill_registry(S60,由 Agent 写入)
340
342
 
@@ -348,7 +350,7 @@ S60(`harness-build-skill-references-intake`)属于 **build 阶段**。若某
348
350
  #### build / apply 分工
349
351
 
350
352
  - **build 阶段**:生成 harness 本体资产(`AGENTS_APPLY.md`、`agent-env/skills`、`agent-env/rules`、`specs`、`baseline` 等)
351
- - **apply 阶段**:复制 harness 到目标项目、注入 skills/rules、生成入口文件、写 `.gitignore`、校验注入文件引用关系
353
+ - **apply 阶段**:复制 harness 到目标项目、注入 skills/rules、(可选)拷贝构建期 assets、生成入口文件、写 `.gitignore`、输出一致性检查报告
352
354
 
353
355
  #### 全部参数
354
356
 
@@ -356,8 +358,11 @@ S60(`harness-build-skill-references-intake`)属于 **build 阶段**。若某
356
358
  |------|----------|------|--------|
357
359
  | `--harness <path>` | ✅ 必填 | 已构建好的 harness 目录(形如 `./my-app-harness`) | — |
358
360
  | `--target <path>` | 可选 | 目标项目根目录 | **当前工作目录(cwd)** |
359
- | `--agent <agent>` | 可选 | 目标 Agent:`codechat` / `qoder` / `cursor` / `claude-code` / `opencode` / `generic` | **从 `<harness>/.harness-build-state.yaml` 读取**;缺失则报错 |
361
+ | `--agent <agent>` | 可选 | 目标 Agent:`codechat` / `qoder` / `cursor` / `claude-code` / `opencode` / `generic` | `codechat` |
360
362
  | `--clone` | 可选 flag | 兼容参数;当前实现下行为与默认一致(都会拷贝 harness) | `false` |
363
+ | `--include-build-assets` | 可选 flag | 显式拷贝构建期 skills/rules 到 `build-agent-env/`(用于二次改造) | `false` |
364
+ | `--inject-build-main-bridge` | 可选 flag | 向运行期 skills 注入 `harness-build-skills-bridge`(需 `--include-build-assets`) | `false` |
365
+ | `--consistency <mode>` | 可选 | 一致性检查级别:`basic` / `strict`(`strict` 当前为预留模式) | `basic` |
361
366
  | `--force` | 可选 flag | 覆盖已存在注入目录与入口文件;同时允许重拷贝 harness | `false` |
362
367
  | `-y, --yes` | 可选 flag | 跳过交互确认 | `false` |
363
368
  | `--verbose` | 可选 flag | 显示详细日志 | `false` |
@@ -371,10 +376,20 @@ S60(`harness-build-skill-references-intake`)属于 **build 阶段**。若某
371
376
  ├── <adapter.skillsDir>/<runtime-skill>/SKILL.{md|mdc} # 运行期 skills 注入
372
377
  ├── <adapter.skillsDir>/harness-apply-skills-main/
373
378
  │ └── SKILL.{md|mdc} # 薄入口 skill(仅提示/索引,不做二级调度)
379
+ ├── <adapter.skillsDir>/harness-build-skills-bridge/
380
+ │ └── SKILL.{md|mdc} # 可选:--inject-build-main-bridge(build 流桥接,不是默认入口)
381
+ ├── build-agent-env/ # 可选:--include-build-assets 时生成
382
+ │ ├── skills/ # 构建期 skills 镜像(可二次改造)
383
+ │ └── rules/ # 构建期 rules 镜像(可二次改造)
374
384
  └── <name>-harness/ # apply 默认复制后的 harness
375
385
  ```
376
386
 
377
- > 注入完成后,CLI 会校验入口文件、薄入口 skill、注入 skills/rules 内的路径引用是否可达;失败会报错中止。
387
+ > 注入完成后,CLI 会输出一致性检查摘要(目录/文件存在性、frontmatter 关键字段、引用可达性),发现问题会给出警告或错误明细供修复。
388
+
389
+ #### 回滚(bridge 最小清理)
390
+
391
+ - 软回滚:后续 `apply` 不再传 `--inject-build-main-bridge`。
392
+ - 硬回滚:删除 `<adapter.skillsDir>/harness-build-skills-bridge/` 即可;`build-agent-env/` 可保留用于后续构建流改造。
378
393
 
379
394
  ### `convert` —— 文档 → Markdown 预处理(对接 S20_collect_inputs)
380
395
 
@@ -742,7 +757,7 @@ svharness build --harness-name demo-unknown --arch python --agent codechat `
742
757
  - [x] `build` —— 骨架 + 元文件 + 状态文件 + skill 注入
743
758
  - [x] 多架构模板(`android-compose` / `android-xml` / `cpp` / `web-react` / `python`)
744
759
  - [x] `_shared/` + `<arch>/` 两层叠拷合并
745
- - [x] 每个架构自带 3-5 条规则文件(`.mdc` 格式,`always_on` 加载)
760
+ - [x] 每个架构自带 3-5 条规则文件(`.mdc` 格式,`alwaysApply: true` 默认启用)
746
761
  - [x] `apply` —— 把已构建好的 harness 绑定到目标项目(默认复制模式,`--clone` 兼容保留)
747
762
  - [x] 构建辅助资源统一命名(`build-skills/harness-build-skill-*`、`build-rules/harness-build-rule-*`)
748
763
  - [ ] `detach` —— 从目标项目解绑并清理 dispatcher skill
@@ -14,6 +14,7 @@ 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
16
  const DISPATCHER_SKILL_NAME = 'harness-apply-skills-main';
17
+ const BUILD_MAIN_BRIDGE_SKILL_NAME = 'harness-build-skills-bridge';
17
18
  const BUILD_SKILL_TEMPLATE_FILES = [
18
19
  'harness-build-skills-main.md',
19
20
  'harness-build-skill-orchestrator.md',
@@ -23,6 +24,16 @@ const BUILD_SKILL_TEMPLATE_FILES = [
23
24
  'harness-build-skill-knowledge-builder.md',
24
25
  'harness-build-skill-wiki-writer.md',
25
26
  ];
27
+ const BUILD_RULE_TEMPLATE_FILES = [
28
+ 'harness-build-rule-agent-agnostic.md',
29
+ 'harness-build-rule-chinese-only.md',
30
+ 'harness-build-rule-convert-check.md',
31
+ 'harness-build-rule-memory-write.md',
32
+ 'harness-build-rule-orchestrator-flow.md',
33
+ 'harness-build-rule-skills-tasks-output.md',
34
+ 'harness-build-rule-specs-schema.md',
35
+ 'harness-build-rule-user-interaction.md',
36
+ ];
26
37
  const PATH_KEYS = [
27
38
  'agent-env/',
28
39
  'baseline/',
@@ -278,6 +289,10 @@ async function copyRuntimeSkills(input) {
278
289
  const content = input.adapter.transform
279
290
  ? input.adapter.transform(rewritten, node_path_1.default.basename(srcSkillFile))
280
291
  : rewritten;
292
+ const syncResult = syncSkillFrontmatterName(content, skillName);
293
+ if (syncResult.warning) {
294
+ logger_1.logger.warn(`运行期 skill frontmatter.name 同步失败(保留原文):${skillName} - ${syncResult.warning}`);
295
+ }
281
296
  const dstSkillDir = node_path_1.default.join(dstDir, skillName);
282
297
  const dstSkillFile = node_path_1.default.join(dstSkillDir, 'SKILL' + input.adapter.skillExt);
283
298
  if ((await fs_extra_1.default.pathExists(dstSkillFile)) && !input.force) {
@@ -285,7 +300,7 @@ async function copyRuntimeSkills(input) {
285
300
  continue;
286
301
  }
287
302
  await fs_extra_1.default.ensureDir(dstSkillDir);
288
- await fs_extra_1.default.outputFile(dstSkillFile, content, 'utf8');
303
+ await fs_extra_1.default.outputFile(dstSkillFile, syncResult.content, 'utf8');
289
304
  written.push(node_path_1.default.relative(input.targetRoot, dstSkillFile));
290
305
  }
291
306
  return written;
@@ -314,6 +329,32 @@ async function injectThinMainSkill(input) {
314
329
  await fs_extra_1.default.outputFile(dstFile, content, 'utf8');
315
330
  return dstFile;
316
331
  }
332
+ async function injectBuildMainBridgeSkill(input) {
333
+ const templatePath = node_path_1.default.join(input.templatesRoot, '_shared', 'apply-skills', `${BUILD_MAIN_BRIDGE_SKILL_NAME}.md`);
334
+ if (!(await fs_extra_1.default.pathExists(templatePath))) {
335
+ throw new Error(`build-main bridge 模板缺失:${templatePath}`);
336
+ }
337
+ const dstDir = node_path_1.default.join(input.targetRoot, input.adapter.skillsDir, BUILD_MAIN_BRIDGE_SKILL_NAME);
338
+ const dstFile = node_path_1.default.join(dstDir, 'SKILL' + input.adapter.skillExt);
339
+ if (await fs_extra_1.default.pathExists(dstDir)) {
340
+ if (!input.force) {
341
+ throw new Error(`目标 bridge skill 目录已存在:${dstDir}\n 使用 --force 覆盖,或先清理目标目录。`);
342
+ }
343
+ await fs_extra_1.default.remove(dstDir);
344
+ }
345
+ const raw = await fs_extra_1.default.readFile(templatePath, 'utf8');
346
+ const rendered = raw
347
+ .replace(/__HARNESS_ROOT_REL__/g, `./${input.harnessDirName}`)
348
+ .replace(/__BUILD_SKILLS_DIR__/g, './build-agent-env/skills')
349
+ .replace(/__BUILD_RULES_DIR__/g, './build-agent-env/rules')
350
+ .replace(/__APPLY_MAIN_SKILL__/g, DISPATCHER_SKILL_NAME);
351
+ const content = input.adapter.transform
352
+ ? input.adapter.transform(rendered, `${BUILD_MAIN_BRIDGE_SKILL_NAME}.md`)
353
+ : rendered;
354
+ await fs_extra_1.default.ensureDir(dstDir);
355
+ await fs_extra_1.default.outputFile(dstFile, content, 'utf8');
356
+ return dstFile;
357
+ }
317
358
  async function updateGitIgnore(input) {
318
359
  const gitignorePath = node_path_1.default.join(input.targetRoot, '.gitignore');
319
360
  const raw = (await fs_extra_1.default.pathExists(gitignorePath))
@@ -338,9 +379,10 @@ async function updateGitIgnore(input) {
338
379
  }
339
380
  if (added.length === 0)
340
381
  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');
382
+ const prefix = raw.length === 0 ? '' : raw.endsWith('\n') ? '\n' : '\n\n';
383
+ const appendix = `${prefix}# svharness apply injected assets\n` + added.map((line) => `${line}\n`).join('');
384
+ await fs_extra_1.default.ensureDir(node_path_1.default.dirname(gitignorePath));
385
+ await fs_extra_1.default.appendFile(gitignorePath, appendix, 'utf8');
344
386
  return added;
345
387
  }
346
388
  async function validateInjectedReferences(input) {
@@ -365,6 +407,107 @@ async function validateInjectedReferences(input) {
365
407
  }
366
408
  return failures;
367
409
  }
410
+ function extractFrontmatter(content) {
411
+ const normalized = content.replace(/\r\n/g, '\n');
412
+ if (!normalized.startsWith('---\n'))
413
+ return undefined;
414
+ const endIdx = normalized.indexOf('\n---\n', 4);
415
+ if (endIdx < 0)
416
+ return undefined;
417
+ return {
418
+ block: normalized.slice(4, endIdx),
419
+ normalized,
420
+ endIdx,
421
+ };
422
+ }
423
+ function extractFrontmatterBlock(content) {
424
+ return extractFrontmatter(content)?.block;
425
+ }
426
+ function syncSkillFrontmatterName(content, expectedName) {
427
+ const frontmatter = extractFrontmatter(content);
428
+ if (!frontmatter) {
429
+ return { content, changed: false, warning: '缺少 frontmatter' };
430
+ }
431
+ let parsed;
432
+ try {
433
+ parsed = js_yaml_1.default.load(frontmatter.block);
434
+ }
435
+ catch (err) {
436
+ return { content, changed: false, warning: `frontmatter 解析失败:${err.message}` };
437
+ }
438
+ if (!parsed || typeof parsed !== 'object') {
439
+ return { content, changed: false, warning: 'frontmatter 解析为空' };
440
+ }
441
+ const currentName = typeof parsed.name === 'string' ? parsed.name.trim() : '';
442
+ if (currentName === expectedName) {
443
+ return { content, changed: false };
444
+ }
445
+ parsed.name = expectedName;
446
+ const dumped = js_yaml_1.default.dump(parsed, { lineWidth: 120, noRefs: true }).trimEnd();
447
+ const rewrittenNormalized = `---\n${dumped}\n---\n` + frontmatter.normalized.slice(frontmatter.endIdx + 5);
448
+ const hasCrlf = content.includes('\r\n');
449
+ return {
450
+ content: hasCrlf ? rewrittenNormalized.replace(/\n/g, '\r\n') : rewrittenNormalized,
451
+ changed: true,
452
+ };
453
+ }
454
+ async function validateFrontmatterFields(input) {
455
+ const warnings = [];
456
+ if (!(await fs_extra_1.default.pathExists(input.file))) {
457
+ warnings.push(`${node_path_1.default.relative(input.targetRoot, input.file)} 缺失,无法校验 frontmatter`);
458
+ return warnings;
459
+ }
460
+ const content = await fs_extra_1.default.readFile(input.file, 'utf8');
461
+ const block = extractFrontmatterBlock(content);
462
+ if (!block) {
463
+ warnings.push(`${node_path_1.default.relative(input.targetRoot, input.file)} 缺少 frontmatter`);
464
+ return warnings;
465
+ }
466
+ try {
467
+ const parsed = js_yaml_1.default.load(block);
468
+ if (!parsed || typeof parsed !== 'object') {
469
+ warnings.push(`${node_path_1.default.relative(input.targetRoot, input.file)} frontmatter 解析为空`);
470
+ return warnings;
471
+ }
472
+ for (const key of input.requiredKeys) {
473
+ if (!(key in parsed)) {
474
+ warnings.push(`${node_path_1.default.relative(input.targetRoot, input.file)} frontmatter 缺少字段:${key}`);
475
+ }
476
+ }
477
+ if ('alwaysApply' in parsed && typeof parsed.alwaysApply !== 'boolean') {
478
+ warnings.push(`${node_path_1.default.relative(input.targetRoot, input.file)} frontmatter alwaysApply 应为 boolean`);
479
+ }
480
+ }
481
+ catch (err) {
482
+ warnings.push(`${node_path_1.default.relative(input.targetRoot, input.file)} frontmatter 解析失败:${err.message}`);
483
+ }
484
+ return warnings;
485
+ }
486
+ async function validateSkillNameMatchesDir(input) {
487
+ const warnings = [];
488
+ if (!(await fs_extra_1.default.pathExists(input.file)))
489
+ return warnings;
490
+ const content = await fs_extra_1.default.readFile(input.file, 'utf8');
491
+ const block = extractFrontmatterBlock(content);
492
+ if (!block)
493
+ return warnings;
494
+ try {
495
+ const parsed = js_yaml_1.default.load(block);
496
+ if (!parsed || typeof parsed !== 'object')
497
+ return warnings;
498
+ if (typeof parsed.name !== 'string')
499
+ return warnings;
500
+ const expected = node_path_1.default.basename(node_path_1.default.dirname(input.file));
501
+ const actual = parsed.name.trim();
502
+ if (actual && actual !== expected) {
503
+ warnings.push(`${node_path_1.default.relative(input.targetRoot, input.file)} frontmatter.name(${actual}) 与目录名(${expected}) 不一致`);
504
+ }
505
+ }
506
+ catch {
507
+ // parse failures are already reported by validateFrontmatterFields
508
+ }
509
+ return warnings;
510
+ }
368
511
  async function saveBuildSkillsToBuildAgentEnv(input) {
369
512
  const destRoot = node_path_1.default.join(input.targetRoot, 'build-agent-env', 'skills');
370
513
  await fs_extra_1.default.ensureDir(destRoot);
@@ -424,19 +567,70 @@ async function saveBuildSkillsToBuildAgentEnv(input) {
424
567
  }
425
568
  return written;
426
569
  }
570
+ async function resolveRuleSourceFile(rulesRoot, ruleName, ruleExt) {
571
+ const candidates = [
572
+ node_path_1.default.join(rulesRoot, `${ruleName}${ruleExt}`),
573
+ node_path_1.default.join(rulesRoot, `${ruleName}.md`),
574
+ node_path_1.default.join(rulesRoot, `${ruleName}.mdc`),
575
+ ];
576
+ for (const candidate of candidates) {
577
+ if (await fs_extra_1.default.pathExists(candidate))
578
+ return candidate;
579
+ }
580
+ return undefined;
581
+ }
582
+ async function saveBuildRulesToBuildAgentEnv(input) {
583
+ if (!input.adapter.rulesDir) {
584
+ logger_1.logger.info(`agent ${input.adapter.name} 未声明 rulesDir,跳过 build-agent-env/rules 生成`);
585
+ return [];
586
+ }
587
+ const destRoot = node_path_1.default.join(input.targetRoot, 'build-agent-env', 'rules');
588
+ await fs_extra_1.default.ensureDir(destRoot);
589
+ const sourceRulesRoot = node_path_1.default.join(node_path_1.default.dirname(input.harnessRoot), input.adapter.rulesDir);
590
+ const ruleExt = input.adapter.ruleExt ?? '.md';
591
+ const written = [];
592
+ for (const templateName of BUILD_RULE_TEMPLATE_FILES) {
593
+ const ruleName = templateName.replace(/\.md$/i, '');
594
+ const dstRuleFile = node_path_1.default.join(destRoot, `${ruleName}${ruleExt}`);
595
+ if ((await fs_extra_1.default.pathExists(dstRuleFile)) && !input.force) {
596
+ logger_1.logger.warn(`build rule 已存在,跳过:${node_path_1.default.relative(input.targetRoot, dstRuleFile)}`);
597
+ continue;
598
+ }
599
+ let raw;
600
+ if (await fs_extra_1.default.pathExists(sourceRulesRoot)) {
601
+ const sourceRuleFile = await resolveRuleSourceFile(sourceRulesRoot, ruleName, ruleExt);
602
+ if (sourceRuleFile) {
603
+ raw = await fs_extra_1.default.readFile(sourceRuleFile, 'utf8');
604
+ }
605
+ }
606
+ if (!raw) {
607
+ const templatePath = node_path_1.default.join(input.templatesRoot, '_shared', 'build-rules', templateName);
608
+ if (!(await fs_extra_1.default.pathExists(templatePath))) {
609
+ logger_1.logger.warn(`build rule 模板缺失,跳过:${templateName}`);
610
+ continue;
611
+ }
612
+ raw = await fs_extra_1.default.readFile(templatePath, 'utf8');
613
+ }
614
+ const rewritten = rewriteHarnessReferences(raw, input.harnessDirName);
615
+ const content = input.adapter.ruleTransform
616
+ ? input.adapter.ruleTransform(rewritten, templateName)
617
+ : rewritten;
618
+ await fs_extra_1.default.outputFile(dstRuleFile, content, 'utf8');
619
+ written.push(node_path_1.default.relative(input.targetRoot, dstRuleFile));
620
+ }
621
+ return written;
622
+ }
427
623
  /**
428
624
  * Entry point for `svharnessbuild apply`.
429
625
  *
430
626
  * Minimum-invasion binding: copies a single dispatcher skill
431
627
  * (`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).
628
+ * agent-native skills directory as a thin entry. Runtime rules/skills are
629
+ * copied into adapter directories, and the project-root AI entry file
630
+ * (`AGENTS.md` / `CLAUDE.md`) is generated from `<harness>/AGENTS_APPLY.md`.
436
631
  *
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.
632
+ * Runtime behavior is path-rewrite based (`./<name>-harness/...`), not
633
+ * dispatcher-embedded binding metadata parsing.
440
634
  */
441
635
  async function runApply(opts) {
442
636
  (0, logger_1.setVerbose)(!!opts.verbose);
@@ -479,7 +673,8 @@ async function runApply(opts) {
479
673
  else {
480
674
  effectiveHarnessRoot = cloneDest;
481
675
  }
482
- // 3. Resolve agent (explicit > state file).
676
+ // 3. Resolve agent (explicit > state file > codechat default).
677
+ const DEFAULT_AGENT = 'codechat';
483
678
  let agent = opts.agent;
484
679
  if (!agent) {
485
680
  agent = await readAgentFromState(harnessRoot);
@@ -488,16 +683,14 @@ async function runApply(opts) {
488
683
  }
489
684
  }
490
685
  if (!agent) {
491
- throw new Error('无法推断目标 agent。请显式传入 --agent <' +
492
- (0, validate_args_1.listSupportedAgents)().join('|') +
493
- '>,或确认 harness 下存在 .harness-build-state.yaml。');
686
+ agent = DEFAULT_AGENT;
687
+ logger_1.logger.info(`未指定 agent,使用默认值:${agent}`);
494
688
  }
495
689
  const adapter = (0, adapters_1.getAdapter)(agent);
496
690
  // 4. Compute injection target.
497
691
  const skillsBaseDir = node_path_1.default.join(targetRoot, adapter.skillsDir);
498
692
  const dispatcherDir = node_path_1.default.join(skillsBaseDir, DISPATCHER_SKILL_NAME);
499
693
  const skillFile = node_path_1.default.join(dispatcherDir, 'SKILL' + adapter.skillExt);
500
- const bindingFile = node_path_1.default.join(dispatcherDir, 'references', 'binding.yaml');
501
694
  // 5. Load dispatcher skill template.
502
695
  const templatePath = node_path_1.default.join(resolveTemplatesRoot(), '_shared', 'apply-skills', `${DISPATCHER_SKILL_NAME}.md`);
503
696
  if (!(await fs_extra_1.default.pathExists(templatePath))) {
@@ -513,6 +706,10 @@ async function runApply(opts) {
513
706
  const harnessRootRel = node_path_1.default.relative(targetRoot, effectiveHarnessRoot).replace(/\\/g, '/') || '.';
514
707
  const cliVersion = (0, version_1.getCliVersion)();
515
708
  const appliedAt = new Date().toISOString();
709
+ const includeBuildAssets = !!opts.includeBuildAssets;
710
+ const requestedBuildBridge = !!opts.injectBuildMainBridge;
711
+ const injectBuildMainBridge = includeBuildAssets && requestedBuildBridge;
712
+ const consistencyMode = opts.consistency === 'strict' ? 'strict' : 'basic';
516
713
  // 6. Confirm (unless --yes).
517
714
  const configRows = [
518
715
  { label: 'harness 源路径', value: harnessRoot },
@@ -525,7 +722,7 @@ async function runApply(opts) {
525
722
  if (cloneMode && effectiveHarnessRoot !== harnessRoot) {
526
723
  configRows.push({ label: '克隆目标', value: effectiveHarnessRoot });
527
724
  }
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}` });
725
+ 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
726
  logger_1.logger.configBox('apply 配置确认', configRows);
530
727
  if (!opts.yes) {
531
728
  const { ok } = await (0, prompts_1.default)({
@@ -539,6 +736,12 @@ async function runApply(opts) {
539
736
  return;
540
737
  }
541
738
  }
739
+ if (consistencyMode === 'strict') {
740
+ logger_1.logger.warn('strict 一致性检查尚在规划中,当前先执行 basic 检查。');
741
+ }
742
+ if (requestedBuildBridge && !includeBuildAssets) {
743
+ logger_1.logger.warn('`--inject-build-main-bridge` 依赖 `--include-build-assets`;当前跳过 bridge 注入。');
744
+ }
542
745
  // 7. Guard against overwrite.
543
746
  if (await fs_extra_1.default.pathExists(dispatcherDir)) {
544
747
  if (!opts.force) {
@@ -549,8 +752,11 @@ async function runApply(opts) {
549
752
  }
550
753
  // 7b. Clone step — copy the whole harness into the target project first,
551
754
  // so subsequent injections resolve to the local copy.
552
- const totalSteps = 5;
755
+ const totalSteps = includeBuildAssets ? 6 : 5;
553
756
  let stepNo = 1;
757
+ let buildSkills = [];
758
+ let buildRules = [];
759
+ let bridgeSkillPath;
554
760
  if (cloneMode && effectiveHarnessRoot !== harnessRoot) {
555
761
  logger_1.logger.section(`步骤 ${stepNo}/${totalSteps} - 拷贝 harness 到目标项目`);
556
762
  if (await fs_extra_1.default.pathExists(cloneDest)) {
@@ -594,12 +800,53 @@ async function runApply(opts) {
594
800
  logger_1.logger.success(`已注入运行期 rules ${runtimeRules.length} 条`);
595
801
  logger_1.logger.success(`已注入运行期 skills ${runtimeSkills.length} 条`);
596
802
  stepNo++;
803
+ if (includeBuildAssets) {
804
+ logger_1.logger.section(`步骤 ${stepNo}/${totalSteps} - 拷贝构建期 skills/rules`);
805
+ buildSkills = await saveBuildSkillsToBuildAgentEnv({
806
+ harnessRoot: effectiveHarnessRoot,
807
+ targetRoot,
808
+ adapter,
809
+ harnessRootRel,
810
+ harnessName,
811
+ harnessVersion,
812
+ harnessArch,
813
+ agent,
814
+ force: !!opts.force,
815
+ cliVersion,
816
+ generatedAt: appliedAt,
817
+ templatesRoot,
818
+ });
819
+ buildRules = await saveBuildRulesToBuildAgentEnv({
820
+ harnessRoot: effectiveHarnessRoot,
821
+ targetRoot,
822
+ adapter,
823
+ harnessDirName,
824
+ force: !!opts.force,
825
+ templatesRoot,
826
+ });
827
+ logger_1.logger.success(`已注入构建期 skills ${buildSkills.length} 条`);
828
+ logger_1.logger.success(`已注入构建期 rules ${buildRules.length} 条`);
829
+ if (injectBuildMainBridge) {
830
+ logger_1.logger.info(`build-main bridge 已启用,目标目录:${adapter.skillsDir}/${BUILD_MAIN_BRIDGE_SKILL_NAME}`);
831
+ bridgeSkillPath = await injectBuildMainBridgeSkill({
832
+ templatesRoot,
833
+ targetRoot,
834
+ adapter,
835
+ harnessDirName,
836
+ force: !!opts.force,
837
+ });
838
+ logger_1.logger.success(`已写入 build-main bridge:${node_path_1.default.relative(targetRoot, bridgeSkillPath)}`);
839
+ }
840
+ stepNo++;
841
+ }
597
842
  logger_1.logger.section(`步骤 ${stepNo}/${totalSteps} - 写入项目根 AI 入口`);
598
843
  await (0, apply_project_entry_1.writeApplyProjectEntry)({
599
844
  projectRoot: targetRoot,
600
845
  harnessRoot: effectiveHarnessRoot,
601
846
  harnessDirName,
602
847
  adapter,
848
+ includeBuildBridge: injectBuildMainBridge,
849
+ buildBridgeSkillName: BUILD_MAIN_BRIDGE_SKILL_NAME,
603
850
  force: !!opts.force,
604
851
  });
605
852
  const entryFile = node_path_1.default.join(targetRoot, adapter.projectEntryFile);
@@ -614,25 +861,84 @@ async function runApply(opts) {
614
861
  logger_1.logger.info('.gitignore 无新增条目(已是最新)');
615
862
  }
616
863
  else {
617
- logger_1.logger.success(`.gitignore 已新增 ${ignoreAdded.length} 条注入路径`);
864
+ logger_1.logger.success(`.gitignore append ${ignoreAdded.length} 条注入路径`);
618
865
  }
619
866
  stepNo++;
620
867
  logger_1.logger.section(`步骤 ${stepNo}/${totalSteps} - 校验注入文件引用关系`);
868
+ const runtimeRuleFiles = runtimeRules.map((rel) => node_path_1.default.join(targetRoot, rel));
869
+ const runtimeSkillFiles = runtimeSkills.map((rel) => node_path_1.default.join(targetRoot, rel));
870
+ const buildRuleFiles = buildRules.map((rel) => node_path_1.default.join(targetRoot, rel));
871
+ const buildSkillFiles = buildSkills.map((rel) => node_path_1.default.join(targetRoot, rel));
621
872
  const filesToCheck = [
622
873
  entryFile,
623
874
  thinMainPath,
624
- ...runtimeRules.map((rel) => node_path_1.default.join(targetRoot, rel)),
625
- ...runtimeSkills.map((rel) => node_path_1.default.join(targetRoot, rel)),
875
+ ...(bridgeSkillPath ? [bridgeSkillPath] : []),
876
+ ...runtimeRuleFiles,
877
+ ...runtimeSkillFiles,
878
+ ...buildRuleFiles,
879
+ ...buildSkillFiles,
880
+ ];
881
+ const consistencyErrors = [];
882
+ const consistencyWarnings = [];
883
+ const requiredDirs = [
884
+ node_path_1.default.join(targetRoot, adapter.skillsDir),
885
+ ...(adapter.rulesDir ? [node_path_1.default.join(targetRoot, adapter.rulesDir)] : []),
886
+ ...(includeBuildAssets
887
+ ? [node_path_1.default.join(targetRoot, 'build-agent-env', 'skills'), node_path_1.default.join(targetRoot, 'build-agent-env', 'rules')]
888
+ : []),
626
889
  ];
890
+ for (const dir of requiredDirs) {
891
+ if (!(await fs_extra_1.default.pathExists(dir))) {
892
+ consistencyErrors.push(`目录缺失:${node_path_1.default.relative(targetRoot, dir)}`);
893
+ }
894
+ }
895
+ for (const file of filesToCheck) {
896
+ if (!(await fs_extra_1.default.pathExists(file))) {
897
+ consistencyErrors.push(`文件缺失:${node_path_1.default.relative(targetRoot, file)}`);
898
+ }
899
+ }
900
+ const skillFrontmatterChecks = [...runtimeSkillFiles, ...buildSkillFiles];
901
+ if (bridgeSkillPath)
902
+ skillFrontmatterChecks.push(bridgeSkillPath);
903
+ for (const file of skillFrontmatterChecks) {
904
+ consistencyWarnings.push(...(await validateFrontmatterFields({
905
+ targetRoot,
906
+ file,
907
+ requiredKeys: ['name', 'description'],
908
+ })));
909
+ consistencyWarnings.push(...(await validateSkillNameMatchesDir({
910
+ targetRoot,
911
+ file,
912
+ })));
913
+ }
914
+ const ruleFrontmatterChecks = [...runtimeRuleFiles, ...buildRuleFiles];
915
+ for (const file of ruleFrontmatterChecks) {
916
+ consistencyWarnings.push(...(await validateFrontmatterFields({
917
+ targetRoot,
918
+ file,
919
+ requiredKeys: ['description'],
920
+ })));
921
+ }
627
922
  const refFailures = await validateInjectedReferences({
628
923
  targetRoot,
629
924
  harnessDirName,
630
925
  files: filesToCheck,
631
926
  });
927
+ consistencyWarnings.push(...refFailures.map((line) => `引用缺失:${line}`));
928
+ const okCount = filesToCheck.length + requiredDirs.length - consistencyErrors.length;
929
+ logger_1.logger.info(`一致性检查(basic)结果:通过 ${Math.max(okCount, 0)} 项,警告 ${consistencyWarnings.length} 项,错误 ${consistencyErrors.length} 项`);
930
+ if (consistencyErrors.length > 0) {
931
+ const preview = consistencyErrors.slice(0, 20).join('\n');
932
+ logger_1.logger.warn(`一致性检查发现 ${consistencyErrors.length} 条错误(请先修复):\n${preview}\n` +
933
+ (consistencyErrors.length > 20 ? '...(其余省略)' : ''));
934
+ }
935
+ if (consistencyWarnings.length > 0) {
936
+ const preview = consistencyWarnings.slice(0, 20).join('\n');
937
+ logger_1.logger.warn(`一致性检查发现 ${consistencyWarnings.length} 条警告:\n${preview}\n` +
938
+ (consistencyWarnings.length > 20 ? '...(其余省略)' : ''));
939
+ }
632
940
  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 ? '...(其余省略)' : ''));
941
+ logger_1.logger.warn(`引用校验发现 ${refFailures.length} 条潜在问题(已并入一致性警告)`);
636
942
  }
637
943
  else {
638
944
  logger_1.logger.success('引用关系校验通过');
@@ -642,8 +948,14 @@ async function runApply(opts) {
642
948
  logger_1.logger.plain('✨ harness 已绑定到当前项目');
643
949
  logger_1.logger.plain('');
644
950
  logger_1.logger.plain(' 下一步:在 agent 中输入');
645
- logger_1.logger.plain(` 应用 ${DISPATCHER_SKILL_NAME} 完成 <你的功能> 功能开发`);
646
- logger_1.logger.plain(` 或阅读 ${adapter.projectEntryFile} 了解完整的 harness 应用工作流`);
951
+ logger_1.logger.plain(` 应用 ${DISPATCHER_SKILL_NAME} 完成 <你的功能> 功能开发(薄入口)`);
952
+ logger_1.logger.plain(` 或阅读 ${adapter.projectEntryFile}(项目根入口)了解完整的 harness 应用工作流`);
953
+ if (includeBuildAssets) {
954
+ logger_1.logger.plain(' 构建期二次改造入口:build-agent-env/skills + build-agent-env/rules');
955
+ if (injectBuildMainBridge) {
956
+ logger_1.logger.plain(` 可选桥接入口:${adapter.skillsDir}/${BUILD_MAIN_BRIDGE_SKILL_NAME}/SKILL${adapter.skillExt}`);
957
+ }
958
+ }
647
959
  logger_1.logger.plain('');
648
960
  if (effectiveHarnessRoot !== harnessRoot) {
649
961
  logger_1.logger.plain(` harness 内容已拷贝至:${node_path_1.default.relative(targetRoot, effectiveHarnessRoot)}/(已纳入目标项目)`);
@@ -286,10 +286,10 @@ async function runInit(opts) {
286
286
  cliVersion,
287
287
  generatedAt: createdAt,
288
288
  };
289
- const injected = await (0, agent_injector_1.injectSkillsLayered)(buildSkillsSrcs, targetRoot, adapter, cwd, buildSkillsBinding);
289
+ const injected = await (0, agent_injector_1.injectSkillsLayered)(buildSkillsSrcs, targetRoot, adapter, cwd, buildSkillsBinding, !!opts.force);
290
290
  logger_1.logger.success(`已向 ${node_path_1.default.join(cwd, adapter.skillsDir)}/ 注入 skill ${injected.length} 个`);
291
291
  logger_1.logger.section(`步骤 4/${totalSteps} - 注入 harness 构建规则`);
292
- const rules = await (0, agent_injector_1.injectRulesLayered)(buildRulesSrcs, adapter, cwd);
292
+ const rules = await (0, agent_injector_1.injectRulesLayered)(buildRulesSrcs, adapter, cwd, !!opts.force);
293
293
  if (adapter.rulesDir) {
294
294
  logger_1.logger.success(`已向 ${node_path_1.default.join(cwd, adapter.rulesDir)}/ 注入 rule ${rules.length} 个`);
295
295
  }
@@ -93,6 +93,9 @@ function mergeApplyOptions(cli, configSection, defaults, cmd) {
93
93
  agent: pickString('agent', cli.agent, cfg.agent, cmd),
94
94
  force: pickBool('force', cli.force, cfg.force, defaults?.force, cmd),
95
95
  clone: pickBool('clone', cli.clone, cfg.clone, undefined, cmd),
96
+ includeBuildAssets: pickBool('includeBuildAssets', cli.includeBuildAssets, cfg.includeBuildAssets, undefined, cmd),
97
+ injectBuildMainBridge: pickBool('injectBuildMainBridge', cli.injectBuildMainBridge, cfg.injectBuildMainBridge, undefined, cmd),
98
+ consistency: pickString('consistency', cli.consistency, cfg.consistency, cmd),
96
99
  yes: pickBool('yes', cli.yes, cfg.yes, defaults?.yes, cmd),
97
100
  verbose: pickBool('verbose', cli.verbose, cfg.verbose, defaults?.verbose, cmd),
98
101
  };
@@ -20,6 +20,19 @@ const BUILD_SKILLS = [
20
20
  'harness-build-skill-knowledge-builder.md',
21
21
  'harness-build-skill-wiki-writer.md',
22
22
  ];
23
+ async function writeInjectedFile(input) {
24
+ const exists = await fs_extra_1.default.pathExists(input.dstPath);
25
+ const relPath = node_path_1.default.relative(input.baseRoot, input.dstPath);
26
+ if (exists && !input.overwrite) {
27
+ logger_1.logger.warn(`${input.kind} 已存在,跳过:${relPath}`);
28
+ return 'skipped';
29
+ }
30
+ await fs_extra_1.default.outputFile(input.dstPath, input.content, 'utf8');
31
+ if (exists && input.overwrite) {
32
+ logger_1.logger.warn(`${input.kind} 已存在,--force 覆盖:${relPath}`);
33
+ }
34
+ return exists ? 'overwritten' : 'written';
35
+ }
23
36
  function buildSkillsBindingYaml(input, adapter) {
24
37
  const harness_root_rel = `./${input.harnessDirName.replace(/\\/g, '/')}/`;
25
38
  const obj = {
@@ -67,7 +80,7 @@ function applyBuildSkillReplacements(raw, adapter, harnessPrefix, opts) {
67
80
  * beside the harness folder instead of inside it.
68
81
  * @param binding Metadata for `harness-build-skills-main` (inlined YAML).
69
82
  */
70
- async function injectSkills(skillsSrcDir, targetRoot, adapter, skillsRoot, binding) {
83
+ async function injectSkills(skillsSrcDir, targetRoot, adapter, skillsRoot, binding, force = false) {
71
84
  if (!(await fs_extra_1.default.pathExists(skillsSrcDir))) {
72
85
  throw new Error(`build-skills source directory missing: ${skillsSrcDir}`);
73
86
  }
@@ -100,9 +113,18 @@ async function injectSkills(skillsSrcDir, targetRoot, adapter, skillsRoot, bindi
100
113
  const skillDir = node_path_1.default.join(dstDir, baseName);
101
114
  await fs_extra_1.default.ensureDir(skillDir);
102
115
  const dstPath = node_path_1.default.join(skillDir, 'SKILL' + adapter.skillExt);
103
- await fs_extra_1.default.outputFile(dstPath, content, 'utf8');
116
+ const writeMode = await writeInjectedFile({
117
+ dstPath,
118
+ content,
119
+ overwrite: force,
120
+ kind: 'skill',
121
+ baseRoot: base,
122
+ });
123
+ if (writeMode === 'skipped') {
124
+ continue;
125
+ }
104
126
  injected.push(node_path_1.default.relative(base, dstPath));
105
- logger_1.logger.debug(`injected skill: ${dstPath}`);
127
+ logger_1.logger.debug(`${writeMode === 'overwritten' ? 'overwrote' : 'injected'} skill: ${dstPath}`);
106
128
  }
107
129
  return injected;
108
130
  }
@@ -127,7 +149,7 @@ async function injectSkills(skillsSrcDir, targetRoot, adapter, skillsRoot, bindi
127
149
  * @param binding Metadata for `__HARNESS_ROOT__` and inlined YAML in
128
150
  * `harness-build-skills-main` (`harnessDirName` inside binding).
129
151
  */
130
- async function injectSkillsLayered(skillsSrcDirs, targetRoot, adapter, skillsRoot, binding) {
152
+ async function injectSkillsLayered(skillsSrcDirs, targetRoot, adapter, skillsRoot, binding, force = false) {
131
153
  const existing = [];
132
154
  for (const d of skillsSrcDirs) {
133
155
  if (await fs_extra_1.default.pathExists(d))
@@ -168,9 +190,18 @@ async function injectSkillsLayered(skillsSrcDirs, targetRoot, adapter, skillsRoo
168
190
  const skillDir = node_path_1.default.join(dstDir, baseName);
169
191
  await fs_extra_1.default.ensureDir(skillDir);
170
192
  const dstPath = node_path_1.default.join(skillDir, 'SKILL' + adapter.skillExt);
171
- await fs_extra_1.default.outputFile(dstPath, content, 'utf8');
193
+ const writeMode = await writeInjectedFile({
194
+ dstPath,
195
+ content,
196
+ overwrite: force,
197
+ kind: 'skill',
198
+ baseRoot: base,
199
+ });
200
+ if (writeMode === 'skipped') {
201
+ continue;
202
+ }
172
203
  injected.push(node_path_1.default.relative(base, dstPath));
173
- logger_1.logger.debug(`injected skill: ${dstPath} (from ${picked})`);
204
+ logger_1.logger.debug(`${writeMode === 'overwritten' ? 'overwrote' : 'injected'} skill: ${dstPath} (from ${picked})`);
174
205
  }
175
206
  return injected;
176
207
  }
@@ -188,7 +219,7 @@ async function injectSkillsLayered(skillsSrcDirs, targetRoot, adapter, skillsRoo
188
219
  * @param adapter Agent-specific configuration
189
220
  * @param rulesRoot Base directory for the agent rules folder, typically cwd.
190
221
  */
191
- async function injectRulesLayered(rulesSrcDirs, adapter, rulesRoot) {
222
+ async function injectRulesLayered(rulesSrcDirs, adapter, rulesRoot, force = false) {
192
223
  if (!adapter.rulesDir) {
193
224
  logger_1.logger.info(`agent ${adapter.name} 未声明 rulesDir,跳过 rules 注入`);
194
225
  return [];
@@ -233,9 +264,18 @@ async function injectRulesLayered(rulesSrcDirs, adapter, rulesRoot) {
233
264
  : raw;
234
265
  const baseName = srcName.replace(/\.md$/i, '');
235
266
  const dstPath = node_path_1.default.join(dstDir, baseName + ruleExt);
236
- await fs_extra_1.default.outputFile(dstPath, content, 'utf8');
267
+ const writeMode = await writeInjectedFile({
268
+ dstPath,
269
+ content,
270
+ overwrite: force,
271
+ kind: 'rule',
272
+ baseRoot: rulesRoot,
273
+ });
274
+ if (writeMode === 'skipped') {
275
+ continue;
276
+ }
237
277
  injected.push(node_path_1.default.relative(rulesRoot, dstPath));
238
- logger_1.logger.debug(`injected rule: ${dstPath} (from ${picked})`);
278
+ logger_1.logger.debug(`${writeMode === 'overwritten' ? 'overwrote' : 'injected'} rule: ${dstPath} (from ${picked})`);
239
279
  }
240
280
  return injected;
241
281
  }
@@ -38,6 +38,18 @@ function rewriteEntryReferences(content, harnessDirName) {
38
38
  return `\`${toHarnessScopedRef(token, harnessDirName)}\``;
39
39
  });
40
40
  }
41
+ function renderBuildBridgeHint(input) {
42
+ if (!input.includeBuildBridge)
43
+ return '';
44
+ return [
45
+ '> 高级模式(仅构建流二次改造场景)',
46
+ '>',
47
+ `> - 已启用 bridge skill:\`${input.bridgeSkillName}\`(路径:\`${input.adapter.skillsDir}/${input.bridgeSkillName}/\`)`,
48
+ '> - 该入口仅用于修改 build-agent-env 下的构建期技能/规则,不作为默认业务开发入口',
49
+ '> - 普通开发仍以 `harness-apply-skills-main` 为主入口',
50
+ '',
51
+ ].join('\n');
52
+ }
41
53
  /**
42
54
  * Write `AGENTS.md` or `CLAUDE.md` at the target project root by copying
43
55
  * `<harness>/AGENTS_APPLY.md` and renaming it to the adapter's entry file.
@@ -59,7 +71,17 @@ async function writeApplyProjectEntry(input) {
59
71
  }
60
72
  }
61
73
  const raw = await fs_extra_1.default.readFile(src, 'utf8');
62
- const rewritten = rewriteEntryReferences(raw, input.harnessDirName);
74
+ const bridgeHint = renderBuildBridgeHint({
75
+ includeBuildBridge: !!input.includeBuildBridge,
76
+ adapter: input.adapter,
77
+ bridgeSkillName: input.buildBridgeSkillName ?? 'harness-build-skills-bridge',
78
+ });
79
+ const withBridgeHint = raw.includes('__BUILD_MAIN_BRIDGE_HINT__')
80
+ ? raw.replace(/__BUILD_MAIN_BRIDGE_HINT__/g, bridgeHint)
81
+ : bridgeHint.length > 0
82
+ ? `${raw.trimEnd()}\n\n${bridgeHint}`
83
+ : raw;
84
+ const rewritten = rewriteEntryReferences(withBridgeHint, input.harnessDirName);
63
85
  await fs_extra_1.default.outputFile(dest, rewritten, 'utf8');
64
86
  logger_1.logger.success(`已写入项目根 AI 入口:${rel}(由 AGENTS_APPLY.md 重命名)`);
65
87
  return rel;
@@ -231,9 +231,16 @@ function buildSuggestedName(sourcePath, detectedType) {
231
231
  .replace(/--+/g, '-');
232
232
  if (!normalized)
233
233
  return undefined;
234
+ const skillPrefix = 'harness-apply-skills-';
235
+ const rulePrefix = 'harness-apply-rules-';
236
+ const stripped = normalized
237
+ .replace(new RegExp(`^${skillPrefix}`), '')
238
+ .replace(new RegExp(`^${rulePrefix}`), '');
239
+ if (!stripped)
240
+ return undefined;
234
241
  if (detectedType === 'skill')
235
- return `harness-apply-skills-${normalized}`;
236
- return `harness-apply-rules-${normalized}`;
242
+ return `${skillPrefix}${stripped}`;
243
+ return `${rulePrefix}${stripped}`;
237
244
  }
238
245
  async function allocateName(targetDir, baseName, used) {
239
246
  if (!used.has(baseName) && !(await fs_extra_1.default.pathExists(node_path_1.default.join(targetDir, baseName)))) {
@@ -46,8 +46,9 @@ async function appendToIgnoreFile(filePath, entries, marker, createNew) {
46
46
  }
47
47
  if (added.length === 0)
48
48
  return [];
49
- const appendix = `\n${marker}\n` + added.map((line) => `${line}\n`).join('');
50
- const next = raw.endsWith('\n') || raw.length === 0 ? raw + appendix : raw + '\n' + appendix;
51
- await fs_extra_1.default.outputFile(filePath, next, 'utf8');
49
+ const prefix = raw.length === 0 ? '' : raw.endsWith('\n') ? '\n' : '\n\n';
50
+ const appendix = `${prefix}${marker}\n${added.map((line) => `${line}\n`).join('')}`;
51
+ await fs_extra_1.default.ensureDir(node_path_1.default.dirname(filePath));
52
+ await fs_extra_1.default.appendFile(filePath, appendix, 'utf8');
52
53
  return added;
53
54
  }
package/dist/index.js CHANGED
@@ -273,11 +273,12 @@ function main() {
273
273
  .option('--config <path>', `从配置文件读取 apply 节(默认 ${config_1.DEFAULT_CONFIG_FILENAME})`)
274
274
  .option('--harness <path>', '已构建好的 harness 目录路径(可在配置文件的 apply.harness 中提供)')
275
275
  .option('--target <path>', '目标项目根目录(默认:当前工作目录)')
276
- .option('--agent <agent>', '目标 Agent:' +
277
- (0, validate_args_1.listSupportedAgents)().join(' | ') +
278
- '(省略时从 harness 状态文件读取)')
276
+ .option('--agent <agent>', '目标 Agent:' + (0, validate_args_1.listSupportedAgents)().join(' | '), DEFAULT_AGENT)
279
277
  .option('--force', '覆盖已存在的 dispatcher skill 目录')
280
278
  .option('--clone', '拷贝整个 harness 到目标项目(默认 bind-only)')
279
+ .option('--include-build-assets', '显式拷贝构建期 skills/rules 到 build-agent-env/')
280
+ .option('--inject-build-main-bridge', '在运行期 skills 中注入 build 主入口桥接 skill(需 include-build-assets)')
281
+ .option('--consistency <mode>', '一致性检查级别:basic | strict(strict 预留)', 'basic')
281
282
  .option('-y, --yes', '跳过交互确认')
282
283
  .option('--verbose', '显示详细日志')
283
284
  .action(async (opts, cmd) => {
@@ -287,12 +288,23 @@ function main() {
287
288
  if (!merged.harness) {
288
289
  throw new Error('--harness <path> 是必填参数(可在配置文件的 apply.harness 中提供)');
289
290
  }
291
+ const agent = (merged.agent ?? DEFAULT_AGENT);
292
+ if (!(0, validate_args_1.listSupportedAgents)().includes(agent)) {
293
+ throw new Error(`--agent "${merged.agent}" is not supported. Allowed: ${(0, validate_args_1.listSupportedAgents)().join(', ')}`);
294
+ }
295
+ const consistency = merged.consistency ?? 'basic';
296
+ if (consistency !== 'basic' && consistency !== 'strict') {
297
+ throw new Error(`--consistency 仅支持 basic | strict,收到:${consistency}`);
298
+ }
290
299
  await (0, apply_1.runApply)({
291
300
  harness: merged.harness,
292
301
  target: merged.target,
293
- agent: merged.agent,
302
+ agent,
294
303
  force: !!merged.force,
295
304
  clone: !!merged.clone,
305
+ includeBuildAssets: !!merged.includeBuildAssets,
306
+ injectBuildMainBridge: !!merged.injectBuildMainBridge,
307
+ consistency,
296
308
  yes: !!merged.yes,
297
309
  verbose: !!merged.verbose,
298
310
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svharness",
3
- "version": "0.13.3",
3
+ "version": "0.13.5",
4
4
  "description": "CLI scaffolder for SDD-Driven Agent-Agnostic Coding Framework (harness)",
5
5
  "bin": {
6
6
  "svharness": "./bin/cli.js",
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  name: harness-apply-skills-main
3
3
  description: >
4
- harness 应用入口(薄索引模式)。提示用户当前项目已注入 runtime skills/rules
5
- 优先直接调用已注入的 `harness-apply-skills-*` skill;如无匹配,再按普通开发流程执行。
4
+ harness 应用入口(薄索引模式)。提示用户当前项目已注入 runtime rules/skills,
5
+ 优先遵守 `alwaysApply: true` 规则,匹配最相关 skill;如无匹配,再按普通开发流程执行。
6
6
  ---
7
7
 
8
8
  # harness-apply-skills-main(harness 应用入口)
@@ -16,9 +16,10 @@ description: >
16
16
  ## 使用原则
17
17
 
18
18
  1. 优先枚举当前项目已注入的 `harness-apply-skills-*` 子 skill,并调用最匹配者完成任务。
19
- 2. 每次任务必须遵守已注入的 rules(always_on 优先)。
19
+ 2. 每次任务必须遵守已注入 rules 中 `alwaysApply: true` 的硬约束。
20
20
  3. 若没有匹配 skill,按普通实现流程完成,但仍需参考 `__HARNESS_ROOT_REL__/specs/`、`__HARNESS_ROOT_REL__/baseline/`。
21
21
  4. 不修改 `__HARNESS_ROOT_REL__` 下的任何文件;harness 作为只读知识源。
22
+ 5. 入口链路以项目根 AI 入口文档为准:`AGENTS.md` 或 `CLAUDE.md`。
22
23
 
23
24
  ## 典型触发
24
25
 
@@ -29,4 +30,5 @@ description: >
29
30
  ## 注意
30
31
 
31
32
  - 本 skill 不负责二级调度与 binding 解析。
33
+ - 本 skill 不替代项目根应用指南;仅做入口提示与路由索引。
32
34
  - 如发现路径不一致,请重新执行 `svharness apply --force`。
@@ -0,0 +1,29 @@
1
+ ---
2
+ name: harness-build-skills-bridge
3
+ description: >
4
+ build 流程桥接入口。仅用于已启用 include-build-assets 的场景,
5
+ 指引 Agent 进入 build-agent-env 下的构建期技能与规则,不替代 apply 主入口。
6
+ ---
7
+
8
+ # harness-build-skills-bridge(build 流程桥接入口)
9
+
10
+ 你是一个 **构建流桥接提示器**。此入口仅用于“二次改造 harness 构建流程”。
11
+
12
+ - harness 根目录:`__HARNESS_ROOT_REL__`
13
+ - 构建期 skills:`__BUILD_SKILLS_DIR__`
14
+ - 构建期 rules:`__BUILD_RULES_DIR__`
15
+ - 运行期默认入口:`__APPLY_MAIN_SKILL__`
16
+
17
+ ## 使用边界
18
+
19
+ 1. 普通业务开发默认走 `__APPLY_MAIN_SKILL__`,不要把本桥接当作默认入口。
20
+ 2. 仅当用户明确要求“修改构建流 / 调整 build 规则 / 演进 build skill”时,才使用本桥接。
21
+ 3. 构建期改造优先在 `__BUILD_SKILLS_DIR__` 与 `__BUILD_RULES_DIR__` 中进行。
22
+ 4. 不直接把整套 build skills/rules 注入为运行期默认能力。
23
+ 5. 产出后需执行一致性自检(frontmatter 字段、引用可达、目录存在性)。
24
+
25
+ ## 典型触发
26
+
27
+ - 使用 build bridge 调整 harness 构建流程
28
+ - 修改 build-agent-env 下的构建期技能
29
+ - 对构建期规则做最小侵入修订
@@ -49,6 +49,8 @@ description: >
49
49
  - 额外 skill 目录建议:`harness-apply-skills-<topic>`
50
50
  - 额外 rule 文件建议:`harness-apply-rules-<topic>.md` / `.mdc`
51
51
  - 命名不合规时必须先给重命名建议,不得直接写入。
52
+ - 若 skill 目录被重命名,必须同步修改该目录下 `SKILL.md` 的 frontmatter `name` 字段为新目录名。
53
+ - 若 skill 文档一级标题显式包含旧 skill 名,也必须同步改为新名,避免目录名与文档语义漂移。
52
54
 
53
55
  ## 状态更新
54
56
 
@@ -4,8 +4,8 @@
4
4
  > 与 `AGENTS_BUILD.md`(构建指南)互补:`AGENTS_BUILD.md` 驱动 harness 本体的构建与演进,
5
5
  > **本文件驱动 harness 知识向目标项目的落地执行**。
6
6
  >
7
- > 如果你是通过 `svharnessbuild apply` 注入的 `harness-apply-skills-main` skill
8
- > 进入的,请先执行该 skill 的调度流程,再参照本文件的守则补充上下文。
7
+ > 若你通过 `svharness apply` 触发了 `harness-apply-skills-main`,它是薄入口提示器;
8
+ > **实际开发流程以本文件为准**。
9
9
 
10
10
  ## 1. 前置条件
11
11
 
@@ -20,23 +20,45 @@
20
20
  > 若 harness 尚未完成构建(如 `S70_runtime_assets` 仍为 `PENDING`),
21
21
  > 建议先回到 harness 本体执行构建流程,而非直接应用。
22
22
 
23
- ## 2. 加载约束与能力
23
+ ## 2. apply 注入布局(先识别执行面)
24
24
 
25
- ### 2.1 强约束(必须遵守)
25
+ `svharness apply` 完成后,运行时通常存在三类路径:
26
+
27
+ 1. 项目根入口:`AGENTS.md` 或 `CLAUDE.md`(由本文件重命名写入)
28
+ 2. 运行时注入目录(以 adapter 为准):
29
+ - rules:`<target>/<adapter.rulesDir>/`
30
+ - skills:`<target>/<adapter.skillsDir>/`
31
+ 3. harness 只读知识库:`./<name>-harness/`
32
+
33
+ 若 `svharness apply` 使用了 `--include-build-assets`,还会存在:
34
+
35
+ 4. 构建期二次改造目录:`build-agent-env/skills/` + `build-agent-env/rules/`
36
+
37
+ __BUILD_MAIN_BRIDGE_HINT__
38
+
39
+ 执行建议:
40
+
41
+ - 编码阶段优先遵守 **注入到 adapter 目录** 的 rules/skills(这是 agent 直接可见层)
42
+ - 规格、基线、references 从 `./<name>-harness/` 读取(只读)
43
+ - 需要重构 harness 工作流时,优先在 `build-agent-env/` 下调整构建期 skill/rule,再通过一致性检查确认可用
44
+
45
+ ## 3. 加载约束与能力
46
+
47
+ ### 3.1 强约束(必须遵守)
26
48
 
27
49
  枚举并读取 `agent-env/rules/` 下所有规则文件:
28
50
 
29
- - `always_on` 规则 —— **每次任务都必须遵守**,不得跳过
30
- - `model_decision` 规则 —— 按本次任务语义挑选适用规则
51
+ - frontmatter 中 `alwaysApply: true` 的规则 —— **每次任务都必须遵守**,不得跳过
52
+ - frontmatter 中非 `alwaysApply: true` 的规则 —— 按本次任务语义挑选适用规则
31
53
 
32
- 规则冲突时以 `always_on` 规则为准。典型约束包括但不限于:
54
+ 规则冲突时以 `alwaysApply: true` 规则为准。典型约束包括但不限于:
33
55
 
34
56
  - `harness-compose-mandatory.mdc` —— Compose UI 强制规范
35
57
  - `harness-mvi-layering.mdc` —— MVI 分层约束
36
58
  - `harness-hilt-injection.mdc` —— 依赖注入规范
37
59
  - `harness-coroutines-scope.mdc` —— 协程作用域规范
38
60
 
39
- ### 2.2 子能力(按需调用)
61
+ ### 3.2 子能力(按需调用)
40
62
 
41
63
  枚举 `agent-env/skills/*/SKILL.md`,提取 `name` + `description` 建立路由表。
42
64
  不要整目录朗读,按需要再深读。典型子能力包括:
@@ -48,19 +70,29 @@
48
70
  - `harness-kotlin-coroutines` —— 协程最佳实践
49
71
  - `harness-android-cli` —— ADB / Gradle 命令
50
72
 
51
- ## 3. 开发工作流
73
+ ### 3.3 references 路由(内容引用)
74
+
75
+ 若任务命中 `references/apply-skills-registry.yaml` 中登记项:
76
+
77
+ - 先读取对应 `reference_md` 原文(位于 `references/md/`)
78
+ - 按原文流程与 entry 指令执行
79
+ - 仍需同时遵守 rules 与 specs
80
+
81
+ 若未命中登记项,再回到普通 skill 路由。
82
+
83
+ ## 4. 开发工作流
52
84
 
53
85
  ```
54
86
  用户需求 → 加载约束 → 语义路由 → 查阅基线/规格 → 编码 → 校验 → 交付
55
87
  ```
56
88
 
57
- ### 3.1 理解需求
89
+ ### 4.1 理解需求
58
90
 
59
91
  - 明确功能边界:是新增、修改还是重构?
60
92
  - 若存在 `specs/` 中对应的规格文件,**必须先对齐 schema** 再编码
61
93
  - 若需求与现有规格冲突,先提规格变更,再写代码
62
94
 
63
- ### 3.2 查阅基线与规格
95
+ ### 4.2 查阅基线与规格
64
96
 
65
97
  按需参考以下目录(均在 harness 根下):
66
98
 
@@ -74,22 +106,27 @@
74
106
  | `specs/interfaces/` | 接口规格 | 只读 |
75
107
  | `tasks/templates/` | 任务模板 | 只读 |
76
108
 
77
- > **禁止引用 `baseline/code/` 之外的源码**作为"参考实现"。
109
+ > `baseline/code/` 是“参考实现”的权威来源。
110
+ > 你仍需阅读并修改目标项目业务源码来完成交付,但不得把 `baseline/code/` 之外的路径当作规范样本来源。
78
111
 
79
- ### 3.3 编码执行
112
+ ### 4.3 编码执行
80
113
 
81
114
  - **所有代码写在目标项目下**,不得修改 harness 目录内的文件
82
115
  - 引用 harness 文件时使用相对路径
83
116
  - 严格遵守 `agent-env/rules/` 中的所有约束
84
117
  - 产出必须同时满足 `specs/`(做什么)与 `agent-env/rules/`(怎么做)
85
118
 
86
- ### 3.4 交付校验
119
+ ### 4.4 交付校验
87
120
 
88
- - 涉及 specs 的任务,产物必须通过 `specs/*/schema.json` 校验
121
+ - 涉及 specs 的任务,产物需与 `specs/*/schema.json` 一致(工具可用时执行自动校验)
89
122
  - 代码改动不得违反 `agent-env/rules/` 中的规则
90
123
  - 在 PR 描述中记录每一条外部假设
124
+ - 若存在 `build-agent-env/`,额外执行一致性自检:
125
+ - 目录存在性:`build-agent-env/skills`、`build-agent-env/rules`
126
+ - frontmatter 关键字段:skill 至少包含 `name`、`description`;rule 至少包含 `description`
127
+ - 引用可达性:文档中的相对路径需能解析到实际文件
91
128
 
92
- ## 4. 记忆沉淀
129
+ ## 5. 记忆沉淀
93
130
 
94
131
  若本次开发产生了可复用经验:
95
132
 
@@ -97,41 +134,41 @@
97
134
  - 经人工审核后转入 `agent-env/memory/categories/`
98
135
  - 不确定时停住询问,不得擅自归档
99
136
 
100
- ## 5. 守则(不得违反)
137
+ ## 6. 守则(不得违反)
101
138
 
102
139
  - **不得修改 harness 目录下的文件**(rules / skills / specs / baseline / VERSION)
103
140
  harness 是只读知识源,演进由构建流程负责
104
141
  - **不得绕过绑定元数据**猜测 harness 路径;路径错误须让用户重新绑定
105
- - **不得把 `always_on` 规则当成"建议"**——它们是硬约束
142
+ - **不得把 `alwaysApply: true` 规则当成"建议"**——它们是硬约束
106
143
  - **不得引用 `baseline/code/` 之外的源码**作为参考实现
107
144
  - 所有面向用户的说明使用**中文**;字段名、类名、路径保留英文
108
145
  - 多个子 skill 有冲突结论时,显式告知用户并请其裁决
109
146
 
110
- ## 6. 典型应用示例
147
+ ## 7. 典型应用示例
111
148
 
112
- ### 示例 1:新增设置项页面
149
+ ### 示例 1:新增配置项功能
113
150
 
114
151
  ```
115
- 用户:基于 harness 新增"显示设置"Fragment
152
+ 用户:基于 harness 新增“配置项 A”
116
153
  Agent:
117
- 1. 加载 harness-compose-mandatory.mdc + harness-mvi-layering.mdc
118
- 2. 路由到 harness-compose-ui skill
119
- 3. 查阅 specs/ui/ 中是否有显示设置相关规格
120
- 4. 参考 baseline/code/ 中的 Fragment 样本
121
- 5. 在目标项目 src/ 下编写 Compose UI + ViewModel + Repository
122
- 6. 校验规格一致性
154
+ 1. 读取 `alwaysApply: true` 规则并装载
155
+ 2. 语义匹配最相关子 skill(或 references 登记项)
156
+ 3. 查阅对应 specsui/behavior/interfaces)
157
+ 4. 参考 baseline/code/ 中同类实现样本
158
+ 5. 在目标项目源码目录完成实现
159
+ 6. 校验与 schema / rules 一致
123
160
  ```
124
161
 
125
- ### 示例 2:新增协议信号
162
+ ### 示例 2:新增协议字段解析
126
163
 
127
164
  ```
128
165
  用户:按 harness 规范实现 CMDID 0xA3 信号解析
129
166
  Agent:
130
- 1. 加载 harness-mvi-layering.mdc
167
+ 1. 加载 `alwaysApply: true` 规则
131
168
  2. 查阅 specs/signals/ 中的信号规格
132
- 3. 参考 baseline/code/ 中的现有信号解析代码
133
- 4. 编写信号解析 + 状态层
134
- 5. 通过 specs/signals/schema.json 校验
169
+ 3. 参考 baseline/code/ 中同类型解析实现
170
+ 4. 在目标项目实现解析与状态更新
171
+ 5. 对齐 signals schema 并完成校验
135
172
  ```
136
173
 
137
174
  ---
@@ -28,7 +28,11 @@ build:
28
28
  apply:
29
29
  harness: ./my-app-harness
30
30
  target: .
31
+ agent: codechat
31
32
  clone: false
33
+ includeBuildAssets: false
34
+ injectBuildMainBridge: false
35
+ consistency: basic
32
36
  yes: true
33
37
 
34
38
  # convert:output 无 harness.yaml 时写入该目录;有 harness.yaml 时写入 <output>/<type>/md/