svharness 0.13.2 → 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 +23 -8
- package/dist/commands/apply.js +338 -26
- package/dist/commands/init.js +2 -2
- package/dist/config/merge-options.js +3 -0
- package/dist/core/agent-injector.js +49 -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/index.js +16 -4
- package/package.json +1 -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-convert-check.md +6 -4
- package/templates/_shared/build-skills/harness-build-skill-agent-env-merge.md +2 -0
- package/templates/_shared/build-skills/harness-build-skill-orchestrator.md +6 -6
- package/templates/_shared/meta/AGENTS_APPLY.md.ejs +69 -32
- package/templates/svharness.config.example.yaml +4 -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 长期记忆
|
|
@@ -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
|
|
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
|
|
337
|
-
-
|
|
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` |
|
|
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
|
|
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` 格式,`
|
|
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
|
package/dist/commands/apply.js
CHANGED
|
@@ -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
|
|
342
|
-
const
|
|
343
|
-
await fs_extra_1.default.
|
|
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
|
|
433
|
-
*
|
|
434
|
-
*
|
|
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
|
-
*
|
|
438
|
-
*
|
|
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
|
-
|
|
492
|
-
|
|
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
|
|
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
|
-
...
|
|
625
|
-
...
|
|
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
|
-
|
|
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}
|
|
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)}/(已纳入目标项目)`);
|
package/dist/commands/init.js
CHANGED
|
@@ -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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
236
|
-
return
|
|
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
|
|
50
|
-
const
|
|
51
|
-
await fs_extra_1.default.
|
|
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
|
|
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,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: harness-apply-skills-main
|
|
3
3
|
description: >
|
|
4
|
-
harness 应用入口(薄索引模式)。提示用户当前项目已注入 runtime skills
|
|
5
|
-
|
|
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.
|
|
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
|
+
- 对构建期规则做最小侵入修订
|
|
@@ -11,11 +11,13 @@ Agent 在进入 S30_convert_docs 阶段后必须逐一检查以下两个目录
|
|
|
11
11
|
|
|
12
12
|
## 转换前置检查
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
对上述两个目录执行以下检查(**必须同时读取 `raw/` 与 `md/`,不得只扫 raw**):
|
|
15
15
|
|
|
16
|
-
1.
|
|
17
|
-
2.
|
|
18
|
-
3.
|
|
16
|
+
1. **列出原始文件**:扫描 `raw/`,识别所有非 `.md`、非 `.gitkeep` 的文件(如 `.pdf`、`.docx`、`.pptx`、`.xlsx`、`.html`、`.txt`、`.csv` 等)。
|
|
17
|
+
2. **列出已有产物**:扫描对应 `md/`,列出已有 `.md`(忽略 `.gitkeep`)。
|
|
18
|
+
3. **同基名配对**:对每个原始文件,检查 `md/` 中是否存在同基名的 `.md`(如 `requirements/raw/需求文档.pdf` → `requirements/md/需求文档.md`)。`svharness build` 自动 convert 或用户手工放入的产物均算已完成。
|
|
19
|
+
4. **判断是否需要转换**:仅当 `md/` 中缺少同基名文件时才转换;**`raw/` 中仍保留 pdf/docx 不代表未转换**(源文件不得删除)。
|
|
20
|
+
5. **常见误判**:勿将 `raw/` 内已是 `.md` 的文件计入待转换;勿忽略 `md/` 已有文件;若产物在 `raw/**/converted_md/`,应移至对应 `md/` 后再判定。
|
|
19
21
|
|
|
20
22
|
## 转换方式(按优先级)
|
|
21
23
|
|
|
@@ -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
|
|
|
@@ -43,12 +43,12 @@ description: >
|
|
|
43
43
|
5. 类型确认:若输入包含规则文件或无法识别类型的资源,提示将由 S65 根据 `__HARNESS_ROOT__agent-env/_incoming/manifest.yaml` 做分流确认(skill→`agent-env/skills/`,rule→`agent-env/rules/`)。
|
|
44
44
|
**关键约束**:用户选择"不新增"仅表示不再添加新文件,**不等于**跳过本阶段;仍须完成第 1 步盘点并基于 `raw/` 现有文件做确认后方可退出。
|
|
45
45
|
**退出条件**:已完成第 1 步盘点;`requirements/raw/` 存在真实文档;其余项完成"已提供/无新增"确认。严禁自动生成占位文档。
|
|
46
|
-
- **S30_convert_docs** — 将 `raw/` 中的非 Markdown 原始文档统一转换为 Markdown
|
|
47
|
-
1.
|
|
48
|
-
2.
|
|
49
|
-
3.
|
|
50
|
-
4.
|
|
51
|
-
|
|
46
|
+
- **S30_convert_docs** — 将 `raw/` 中的非 Markdown 原始文档统一转换为 Markdown(**必须遵守 `harness-build-rule-convert-check`,同时盘点 raw 与 md 并按同基名配对**):
|
|
47
|
+
1. **四目录盘点(不可跳过)**:分别列出 `__HARNESS_ROOT__requirements/raw/`、`requirements/md/`、`references/raw/`、`references/md/` 的文件清单。不得仅因 `raw/` 中仍存在 `.pdf`/`.docx` 等就判定“待转换”——转换后原始文件**故意保留**在 `raw/`。
|
|
48
|
+
2. **配对判定**:对每个 `raw/` 中的非 `.md`、非 `.gitkeep` 源文件,检查对应 `md/` 是否已有**同基名** `.md`(如 `foo.pdf` → `md/foo.md`;`清单.csv` → `md/清单.md`)。仅当 `md/` 缺失时才计入待处理。
|
|
49
|
+
3. **状态卡片写法**:`已完成` 只写已配对的 requirements/references 项;`待处理` **只列** md 仍缺失的源文件。禁止把“raw 里有多少个非 md 文件”直接等同于待转换数量。
|
|
50
|
+
4. 对待处理项:优先调用 `svharness convert`(或表单提示用户运行 CLI);若 `md/` 已有同基名 `.md` 则**跳过转换**。
|
|
51
|
+
5. 全部配对完成后标记 DONE 并推进到 S40_extract_requirements。
|
|
52
52
|
- **S40_extract_requirements** — 委派给 `harness-build-skill-spec-builder`(需求条目化提取到 `__HARNESS_ROOT__requirements/yaml/`)。
|
|
53
53
|
- **S50_generate_specs** — 委派给 `harness-build-skill-spec-builder`(生成受 schema 校验的 YAML)。
|
|
54
54
|
- **S60_process_references**(references 处理)— 仅处理 references:执行 `svharness convert`,产出结构化索引(规制约束 / skills 候选 / signals / manuals),并通过表单确认落地策略;委派给 `harness-build-skill-references-intake`。
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
> 与 `AGENTS_BUILD.md`(构建指南)互补:`AGENTS_BUILD.md` 驱动 harness 本体的构建与演进,
|
|
5
5
|
> **本文件驱动 harness 知识向目标项目的落地执行**。
|
|
6
6
|
>
|
|
7
|
-
>
|
|
8
|
-
>
|
|
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
|
-
|
|
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
|
-
- `
|
|
30
|
-
- `
|
|
51
|
+
- frontmatter 中 `alwaysApply: true` 的规则 —— **每次任务都必须遵守**,不得跳过
|
|
52
|
+
- frontmatter 中非 `alwaysApply: true` 的规则 —— 按本次任务语义挑选适用规则
|
|
31
53
|
|
|
32
|
-
规则冲突时以 `
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
###
|
|
89
|
+
### 4.1 理解需求
|
|
58
90
|
|
|
59
91
|
- 明确功能边界:是新增、修改还是重构?
|
|
60
92
|
- 若存在 `specs/` 中对应的规格文件,**必须先对齐 schema** 再编码
|
|
61
93
|
- 若需求与现有规格冲突,先提规格变更,再写代码
|
|
62
94
|
|
|
63
|
-
###
|
|
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
|
-
>
|
|
109
|
+
> `baseline/code/` 是“参考实现”的权威来源。
|
|
110
|
+
> 你仍需阅读并修改目标项目业务源码来完成交付,但不得把 `baseline/code/` 之外的路径当作规范样本来源。
|
|
78
111
|
|
|
79
|
-
###
|
|
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
|
-
###
|
|
119
|
+
### 4.4 交付校验
|
|
87
120
|
|
|
88
|
-
- 涉及 specs
|
|
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
|
-
##
|
|
129
|
+
## 5. 记忆沉淀
|
|
93
130
|
|
|
94
131
|
若本次开发产生了可复用经验:
|
|
95
132
|
|
|
@@ -97,41 +134,41 @@
|
|
|
97
134
|
- 经人工审核后转入 `agent-env/memory/categories/`
|
|
98
135
|
- 不确定时停住询问,不得擅自归档
|
|
99
136
|
|
|
100
|
-
##
|
|
137
|
+
## 6. 守则(不得违反)
|
|
101
138
|
|
|
102
139
|
- **不得修改 harness 目录下的文件**(rules / skills / specs / baseline / VERSION)
|
|
103
140
|
harness 是只读知识源,演进由构建流程负责
|
|
104
141
|
- **不得绕过绑定元数据**猜测 harness 路径;路径错误须让用户重新绑定
|
|
105
|
-
- **不得把 `
|
|
142
|
+
- **不得把 `alwaysApply: true` 规则当成"建议"**——它们是硬约束
|
|
106
143
|
- **不得引用 `baseline/code/` 之外的源码**作为参考实现
|
|
107
144
|
- 所有面向用户的说明使用**中文**;字段名、类名、路径保留英文
|
|
108
145
|
- 多个子 skill 有冲突结论时,显式告知用户并请其裁决
|
|
109
146
|
|
|
110
|
-
##
|
|
147
|
+
## 7. 典型应用示例
|
|
111
148
|
|
|
112
|
-
### 示例 1
|
|
149
|
+
### 示例 1:新增配置项功能
|
|
113
150
|
|
|
114
151
|
```
|
|
115
|
-
用户:基于 harness
|
|
152
|
+
用户:基于 harness 新增“配置项 A”
|
|
116
153
|
Agent:
|
|
117
|
-
1.
|
|
118
|
-
2.
|
|
119
|
-
3.
|
|
120
|
-
4. 参考 baseline/code/
|
|
121
|
-
5.
|
|
122
|
-
6.
|
|
154
|
+
1. 读取 `alwaysApply: true` 规则并装载
|
|
155
|
+
2. 语义匹配最相关子 skill(或 references 登记项)
|
|
156
|
+
3. 查阅对应 specs(ui/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. 加载
|
|
167
|
+
1. 加载 `alwaysApply: true` 规则
|
|
131
168
|
2. 查阅 specs/signals/ 中的信号规格
|
|
132
|
-
3. 参考 baseline/code/
|
|
133
|
-
4.
|
|
134
|
-
5.
|
|
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/
|