svharness 0.13.5 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +30 -3
  2. package/dist/commands/apply.js +7 -77
  3. package/dist/commands/doctor/check-bootstrap.js +32 -0
  4. package/dist/commands/doctor/check-chinese-heuristic.js +64 -0
  5. package/dist/commands/doctor/check-convert-pairing.js +54 -0
  6. package/dist/commands/doctor/check-empty-dirs.js +51 -0
  7. package/dist/commands/doctor/check-incoming.js +47 -0
  8. package/dist/commands/doctor/check-memory.js +23 -0
  9. package/dist/commands/doctor/check-policy-grep.js +44 -0
  10. package/dist/commands/doctor/check-references.js +78 -0
  11. package/dist/commands/doctor/check-requirements.js +69 -0
  12. package/dist/commands/doctor/check-skills-tasks.js +103 -0
  13. package/dist/commands/doctor/check-specs.js +83 -0
  14. package/dist/commands/doctor/check-state-phases.js +93 -0
  15. package/dist/commands/doctor/check-wiki.js +52 -0
  16. package/dist/commands/doctor/index.js +101 -0
  17. package/dist/commands/doctor/report.js +79 -0
  18. package/dist/commands/doctor/types.js +2 -0
  19. package/dist/commands/doctor/utils.js +130 -0
  20. package/dist/commands/doctor.js +51 -0
  21. package/dist/config/index.js +2 -1
  22. package/dist/config/merge-options.js +13 -0
  23. package/dist/core/agent-injector.js +1 -0
  24. package/dist/core/state.js +7 -1
  25. package/dist/index.js +41 -0
  26. package/dist/utils/skill-md-validate.js +85 -0
  27. package/package.json +3 -1
  28. package/templates/_shared/build-rules/harness-build-rule-orchestrator-flow.md +8 -1
  29. package/templates/_shared/build-rules/harness-build-rule-pre-seal-review.md +39 -0
  30. package/templates/_shared/build-skills/harness-build-skill-orchestrator.md +7 -1
  31. package/templates/_shared/build-skills/harness-build-skill-pre-seal-review.md +51 -0
  32. package/templates/_shared/build-skills/harness-build-skills-main.md +1 -0
  33. package/templates/_shared/meta/README.md.ejs +2 -1
  34. package/templates/svharness.config.example.yaml +8 -0
package/README.md CHANGED
@@ -150,7 +150,7 @@ S40/S50 -> harness-build-skill-spec-builder
150
150
  S60 -> harness-build-skill-references-intake
151
151
  S61/S65 -> harness-build-skill-agent-env-merge
152
152
  S70 -> harness-build-skill-knowledge-builder
153
- S80/S90 -> harness-build-skill-orchestrator
153
+ S80/S85/S90 -> harness-build-skill-orchestrator (+ S85 委派 pre-seal-review)
154
154
  ```
155
155
 
156
156
  `.harness-build-state.yaml` 跟踪进度,任何阶段可中断再续。
@@ -192,7 +192,8 @@ harness-build-{skill|rule}-<semantic-name>
192
192
  | **S65_customize_agent_env** | agent-env 定制(extra-skills/extra-rules 冲突建议与重命名建议 → 用户确认 → 写入) | `agent-env/rules/` + `agent-env/skills/` |
193
193
  | **S70_runtime_assets** | 运行期 Skills & tasks 索引(skill 执行 task) | `agent-env/skills/` + `tasks/templates/` |
194
194
  | **S80_seed_memory** | Memory 初始化 | `agent-env/memory/categories/` |
195
- | **S90_finalize** | 封板 | 版本 bump + CHANGELOG + 终审 |
195
+ | **S85_pre_seal_validation** | 封存前校验 | `svharness doctor` + 全面审查报告 |
196
+ | **S90_finalize** | 封板 | 版本 bump + CHANGELOG + `bootstrap_mode: false` |
196
197
 
197
198
  > 反复调用 orchestrator 总是从"第一个非 DONE 的阶段"继续。
198
199
  > **S10_wiki 仅在有 baseline 时出现;无 baseline 则不构建 wiki,流程从 S20_collect_inputs 开始。**
@@ -222,6 +223,7 @@ svharness wizard
222
223
  | `build` | 对应 `svharness build` |
223
224
  | `apply` | 对应 `svharness apply` |
224
225
  | `convert` | 对应 `svharness convert` |
226
+ | `doctor` | 对应 `svharness doctor` |
225
227
 
226
228
  `build` 节除路径外,可写人类可读说明(仅写入配置文件,供团队阅读):
227
229
 
@@ -391,6 +393,31 @@ S60(`harness-build-skill-references-intake`)属于 **build 阶段**。若某
391
393
  - 软回滚:后续 `apply` 不再传 `--inject-build-main-bridge`。
392
394
  - 硬回滚:删除 `<adapter.skillsDir>/harness-build-skills-bridge/` 即可;`build-agent-env/` 可保留用于后续构建流改造。
393
395
 
396
+ ### `doctor` —— harness 健康度与封存前硬检查(S85)
397
+
398
+ 在 Agent 进入 **S85_pre_seal_validation** / 封板前,对 harness 目录执行可重复的硬检查:
399
+
400
+ ```bash
401
+ # 封存前全量(要求 S00–S80 均为 DONE)
402
+ svharness doctor --harness ./my-app-harness --mode pre-seal
403
+
404
+ # 日常巡检(不强制所有阶段 DONE)
405
+ svharness doctor --harness ./my-app-harness --mode health
406
+
407
+ # Agent 友好:JSON 报告
408
+ svharness doctor --harness ./my-app-harness --mode pre-seal --format json --report ./my-app-harness/doctor-report.json
409
+ ```
410
+
411
+ | 选项 | 说明 |
412
+ |------|------|
413
+ | `--harness` | harness 根目录(必填;可写在 `svharness.config.yaml` 的 `doctor.harness`) |
414
+ | `--mode` | `pre-seal`(默认)或 `health` |
415
+ | `--format` | `text`(默认)或 `json` |
416
+ | `--report` | JSON 报告路径(默认 `<harness>/doctor-report.json`) |
417
+ | `--strict` | 将 warning 视为 error |
418
+
419
+ 退出码:`0` 通过,`1` 存在 error。通过 doctor 后,由 `harness-build-skill-pre-seal-review` 完成语义层全面审查并经用户确认,方可将 S85 标为 DONE 并进入 S90。
420
+
394
421
  ### `convert` —— 文档 → Markdown 预处理(对接 S20_collect_inputs)
395
422
 
396
423
  把本地原始需求文档(`.pdf / .docx / .pptx / .xlsx / .html / .epub / .txt / .csv / .json / ...`)通过**云端部署**的 `markitdown_serve`(FastAPI + Microsoft MarkItDown)批量转为 Markdown,产物统一落到 `<harness>/<type>/md/`(`type` 为 `requirements` 或 `references`),直接喂给下游 `S40_extract_requirements` 条目化流程,显著提升 specs 生成质量的稳定性。
@@ -761,7 +788,7 @@ svharness build --harness-name demo-unknown --arch python --agent codechat `
761
788
  - [x] `apply` —— 把已构建好的 harness 绑定到目标项目(默认复制模式,`--clone` 兼容保留)
762
789
  - [x] 构建辅助资源统一命名(`build-skills/harness-build-skill-*`、`build-rules/harness-build-rule-*`)
763
790
  - [ ] `detach` —— 从目标项目解绑并清理 dispatcher skill
764
- - [ ] `doctor` —— 检查既有 harness 的健康度
791
+ - [x] `doctor` —— 检查既有 harness 的健康度(`svharness doctor`)
765
792
  - [ ] `phase` —— 从 CLI 直接运行某个构建阶段
766
793
  - [ ] `--arch auto` —— 根据 `--baseline` 自动推断架构
767
794
  - [ ] 更多架构模板(`ios-swiftui` / `service-kotlin` / `python-fastapi` 等)
@@ -13,6 +13,7 @@ const apply_project_entry_1 = require("../core/apply-project-entry");
13
13
  const validate_args_1 = require("../utils/validate-args");
14
14
  const logger_1 = require("../utils/logger");
15
15
  const version_1 = require("../utils/version");
16
+ const skill_md_validate_1 = require("../utils/skill-md-validate");
16
17
  const DISPATCHER_SKILL_NAME = 'harness-apply-skills-main';
17
18
  const BUILD_MAIN_BRIDGE_SKILL_NAME = 'harness-build-skills-bridge';
18
19
  const BUILD_SKILL_TEMPLATE_FILES = [
@@ -23,6 +24,7 @@ const BUILD_SKILL_TEMPLATE_FILES = [
23
24
  'harness-build-skill-agent-env-merge.md',
24
25
  'harness-build-skill-knowledge-builder.md',
25
26
  'harness-build-skill-wiki-writer.md',
27
+ 'harness-build-skill-pre-seal-review.md',
26
28
  ];
27
29
  const BUILD_RULE_TEMPLATE_FILES = [
28
30
  'harness-build-rule-agent-agnostic.md',
@@ -30,6 +32,7 @@ const BUILD_RULE_TEMPLATE_FILES = [
30
32
  'harness-build-rule-convert-check.md',
31
33
  'harness-build-rule-memory-write.md',
32
34
  'harness-build-rule-orchestrator-flow.md',
35
+ 'harness-build-rule-pre-seal-review.md',
33
36
  'harness-build-rule-skills-tasks-output.md',
34
37
  'harness-build-rule-specs-schema.md',
35
38
  'harness-build-rule-user-interaction.md',
@@ -407,24 +410,8 @@ async function validateInjectedReferences(input) {
407
410
  }
408
411
  return failures;
409
412
  }
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
413
  function syncSkillFrontmatterName(content, expectedName) {
427
- const frontmatter = extractFrontmatter(content);
414
+ const frontmatter = (0, skill_md_validate_1.extractFrontmatter)(content);
428
415
  if (!frontmatter) {
429
416
  return { content, changed: false, warning: '缺少 frontmatter' };
430
417
  }
@@ -451,63 +438,6 @@ function syncSkillFrontmatterName(content, expectedName) {
451
438
  changed: true,
452
439
  };
453
440
  }
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
- }
511
441
  async function saveBuildSkillsToBuildAgentEnv(input) {
512
442
  const destRoot = node_path_1.default.join(input.targetRoot, 'build-agent-env', 'skills');
513
443
  await fs_extra_1.default.ensureDir(destRoot);
@@ -901,19 +831,19 @@ async function runApply(opts) {
901
831
  if (bridgeSkillPath)
902
832
  skillFrontmatterChecks.push(bridgeSkillPath);
903
833
  for (const file of skillFrontmatterChecks) {
904
- consistencyWarnings.push(...(await validateFrontmatterFields({
834
+ consistencyWarnings.push(...(await (0, skill_md_validate_1.validateFrontmatterFields)({
905
835
  targetRoot,
906
836
  file,
907
837
  requiredKeys: ['name', 'description'],
908
838
  })));
909
- consistencyWarnings.push(...(await validateSkillNameMatchesDir({
839
+ consistencyWarnings.push(...(await (0, skill_md_validate_1.validateSkillNameMatchesDir)({
910
840
  targetRoot,
911
841
  file,
912
842
  })));
913
843
  }
914
844
  const ruleFrontmatterChecks = [...runtimeRuleFiles, ...buildRuleFiles];
915
845
  for (const file of ruleFrontmatterChecks) {
916
- consistencyWarnings.push(...(await validateFrontmatterFields({
846
+ consistencyWarnings.push(...(await (0, skill_md_validate_1.validateFrontmatterFields)({
917
847
  targetRoot,
918
848
  file,
919
849
  requiredKeys: ['description'],
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.checkBootstrap = checkBootstrap;
4
+ const utils_1 = require("./utils");
5
+ async function checkBootstrap(ctx) {
6
+ const id = 'bootstrap';
7
+ const findings = [];
8
+ if (ctx.mode !== 'pre-seal') {
9
+ return { id, title: 'Bootstrap 模式', findings };
10
+ }
11
+ const harness = await (0, utils_1.readHarnessYaml)(ctx.harnessRoot);
12
+ if (!harness) {
13
+ findings.push({
14
+ checkId: id,
15
+ severity: 'error',
16
+ message: '缺少或无法解析 harness.yaml',
17
+ path: 'harness.yaml',
18
+ });
19
+ return { id, title: 'Bootstrap 模式', findings };
20
+ }
21
+ const specs = harness.specs;
22
+ const bootstrapMode = specs?.bootstrap_mode;
23
+ if (bootstrapMode !== true) {
24
+ findings.push({
25
+ checkId: id,
26
+ severity: 'error',
27
+ message: `封存前 specs.bootstrap_mode 应为 true(当前:${String(bootstrapMode)})`,
28
+ path: 'harness.yaml',
29
+ });
30
+ }
31
+ return { id, title: 'Bootstrap 模式', findings };
32
+ }
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.checkChineseHeuristic = checkChineseHeuristic;
7
+ const fs_extra_1 = __importDefault(require("fs-extra"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const utils_1 = require("./utils");
10
+ const SCAN_PATHS = [
11
+ 'requirements/yaml',
12
+ 'specs',
13
+ 'agent-env/rules',
14
+ 'agent-env/skills',
15
+ 'tasks/templates',
16
+ 'baseline/wiki',
17
+ 'agent-env/memory',
18
+ ];
19
+ const LATIN_RUN_MIN = 24;
20
+ const LATIN_RUN = /[A-Za-z][A-Za-z0-9_\-./]{23,}/g;
21
+ const ALLOWED_IN_LINE = [
22
+ /^CMDID_/,
23
+ /^https?:\/\//,
24
+ /schema\.json/,
25
+ /\.yaml$/,
26
+ /\.md$/,
27
+ /frontmatter/,
28
+ ];
29
+ function lineLooksAllowed(line) {
30
+ const t = line.trim();
31
+ if (!t)
32
+ return true;
33
+ return ALLOWED_IN_LINE.some((re) => re.test(t));
34
+ }
35
+ async function checkChineseHeuristic(ctx) {
36
+ const id = 'chinese-heuristic';
37
+ const findings = [];
38
+ for (const sub of SCAN_PATHS) {
39
+ const root = node_path_1.default.join(ctx.harnessRoot, sub);
40
+ const files = await (0, utils_1.listRealFilesRecursive)(root, { extensions: ['.yaml', '.yml', '.md', '.mdc'] });
41
+ for (const file of files) {
42
+ if (file.endsWith('schema.json'))
43
+ continue;
44
+ const content = await fs_extra_1.default.readFile(file, 'utf8');
45
+ const lines = content.split(/\r?\n/);
46
+ for (let i = 0; i < lines.length; i++) {
47
+ const line = lines[i];
48
+ if (lineLooksAllowed(line))
49
+ continue;
50
+ const matches = line.match(LATIN_RUN);
51
+ if (matches && matches.some((m) => m.length >= LATIN_RUN_MIN)) {
52
+ findings.push({
53
+ checkId: id,
54
+ severity: 'warning',
55
+ message: `第 ${i + 1} 行疑似过长英文描述,请确认已中文化:${matches[0].slice(0, 40)}…`,
56
+ path: (0, utils_1.rel)(ctx.harnessRoot, file),
57
+ });
58
+ break;
59
+ }
60
+ }
61
+ }
62
+ }
63
+ return { id, title: '中文产出启发式', findings };
64
+ }
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.checkConvertPairing = checkConvertPairing;
7
+ const fs_extra_1 = __importDefault(require("fs-extra"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const utils_1 = require("./utils");
10
+ async function checkPairingForSide(ctx, side, findings, checkId) {
11
+ const rawDir = node_path_1.default.join(ctx.harnessRoot, side, 'raw');
12
+ const mdDir = node_path_1.default.join(ctx.harnessRoot, side, 'md');
13
+ if (!(await fs_extra_1.default.pathExists(rawDir)))
14
+ return;
15
+ const rawNames = await (0, utils_1.listDirEntries)(rawDir);
16
+ const mdNames = new Set((await (0, utils_1.listDirEntries)(mdDir)).map((n) => (0, utils_1.basenameWithoutExt)(n)));
17
+ for (const name of rawNames) {
18
+ const rawPath = node_path_1.default.join(rawDir, name);
19
+ const stat = await fs_extra_1.default.stat(rawPath);
20
+ if (!stat.isFile())
21
+ continue;
22
+ const ext = node_path_1.default.extname(name).toLowerCase();
23
+ if (ext === '.md') {
24
+ const base = (0, utils_1.basenameWithoutExt)(name);
25
+ if (!mdNames.has(base)) {
26
+ findings.push({
27
+ checkId,
28
+ severity: 'warning',
29
+ message: `${side}/raw 中 ${name} 为 .md,建议在 ${side}/md/ 保留同基名副本或确认已条目化`,
30
+ path: (0, utils_1.rel)(ctx.harnessRoot, rawPath),
31
+ });
32
+ }
33
+ continue;
34
+ }
35
+ if (!(0, utils_1.isRealFileName)(name))
36
+ continue;
37
+ const base = (0, utils_1.basenameWithoutExt)(name);
38
+ if (!mdNames.has(base)) {
39
+ findings.push({
40
+ checkId,
41
+ severity: 'error',
42
+ message: `${side}/raw/${name} 在 ${side}/md/ 缺少同基名 .md(${base}.md)`,
43
+ path: (0, utils_1.rel)(ctx.harnessRoot, rawPath),
44
+ });
45
+ }
46
+ }
47
+ }
48
+ async function checkConvertPairing(ctx) {
49
+ const id = 'convert-pairing';
50
+ const findings = [];
51
+ await checkPairingForSide(ctx, 'requirements', findings, id);
52
+ await checkPairingForSide(ctx, 'references', findings, id);
53
+ return { id, title: 'raw/md 转换配对', findings };
54
+ }
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.checkEmptyDirs = checkEmptyDirs;
7
+ const fs_extra_1 = __importDefault(require("fs-extra"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const utils_1 = require("./utils");
10
+ async function collectEmptyDirs(harnessRoot, dir, findings, checkId) {
11
+ if (!(await fs_extra_1.default.pathExists(dir)))
12
+ return;
13
+ const entries = await fs_extra_1.default.readdir(dir, { withFileTypes: true });
14
+ for (const ent of entries) {
15
+ if (ent.name.startsWith('.') && ent.name !== '.gitkeep')
16
+ continue;
17
+ const full = node_path_1.default.join(dir, ent.name);
18
+ if (ent.isDirectory()) {
19
+ const relPath = (0, utils_1.rel)(harnessRoot, full);
20
+ if (utils_1.EMPTY_DIR_WHITELIST.has(relPath)) {
21
+ await collectEmptyDirs(harnessRoot, full, findings, checkId);
22
+ continue;
23
+ }
24
+ if (relPath === 'agent-env/_incoming/skills') {
25
+ const state = await (0, utils_1.readBuildState)(harnessRoot);
26
+ const s65 = state?.phases.S65_customize_agent_env;
27
+ if (s65?.status === 'DONE') {
28
+ await collectEmptyDirs(harnessRoot, full, findings, checkId);
29
+ continue;
30
+ }
31
+ }
32
+ if (await (0, utils_1.isEmptyOrGitkeepOnly)(full)) {
33
+ findings.push({
34
+ checkId,
35
+ severity: 'error',
36
+ message: `目录为空或仅含 .gitkeep:${relPath}`,
37
+ path: relPath,
38
+ });
39
+ }
40
+ else {
41
+ await collectEmptyDirs(harnessRoot, full, findings, checkId);
42
+ }
43
+ }
44
+ }
45
+ }
46
+ async function checkEmptyDirs(ctx) {
47
+ const id = 'empty-dirs';
48
+ const findings = [];
49
+ await collectEmptyDirs(ctx.harnessRoot, ctx.harnessRoot, findings, id);
50
+ return { id, title: '空目录检查', findings };
51
+ }
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.checkIncoming = checkIncoming;
7
+ const fs_extra_1 = __importDefault(require("fs-extra"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const utils_1 = require("./utils");
10
+ async function checkIncoming(ctx) {
11
+ const id = 'incoming';
12
+ const findings = [];
13
+ const incomingDir = node_path_1.default.join(ctx.harnessRoot, 'agent-env', '_incoming', 'skills');
14
+ const manifestPath = node_path_1.default.join(ctx.harnessRoot, 'agent-env', '_incoming', 'manifest.yaml');
15
+ if (!(await fs_extra_1.default.pathExists(incomingDir))) {
16
+ return { id, title: 'Incoming 资产', findings };
17
+ }
18
+ const entries = await (0, utils_1.listDirEntries)(incomingDir);
19
+ const fileNames = await Promise.all(entries.map(async (n) => {
20
+ const full = node_path_1.default.join(incomingDir, n);
21
+ const s = await fs_extra_1.default.stat(full);
22
+ return s.isFile() ? n : null;
23
+ }));
24
+ const realFiles = fileNames.filter((n) => !!n && (0, utils_1.isRealFileName)(n));
25
+ if (realFiles.length === 0) {
26
+ return { id, title: 'Incoming 资产', findings };
27
+ }
28
+ const state = await (0, utils_1.readBuildState)(ctx.harnessRoot);
29
+ const s65 = state?.phases.S65_customize_agent_env;
30
+ if (s65?.status === 'DONE') {
31
+ findings.push({
32
+ checkId: id,
33
+ severity: 'error',
34
+ message: `S65 已 DONE 但 _incoming/skills/ 仍有未处理文件:${realFiles.join(', ')}`,
35
+ path: (0, utils_1.rel)(ctx.harnessRoot, incomingDir),
36
+ });
37
+ }
38
+ else if (!(await fs_extra_1.default.pathExists(manifestPath))) {
39
+ findings.push({
40
+ checkId: id,
41
+ severity: 'warning',
42
+ message: '_incoming/skills/ 有文件但缺少 manifest.yaml',
43
+ path: (0, utils_1.rel)(ctx.harnessRoot, manifestPath),
44
+ });
45
+ }
46
+ return { id, title: 'Incoming 资产', findings };
47
+ }
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.checkMemory = checkMemory;
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ const utils_1 = require("./utils");
9
+ async function checkMemory(ctx) {
10
+ const id = 'memory';
11
+ const findings = [];
12
+ const categoriesDir = node_path_1.default.join(ctx.harnessRoot, 'agent-env', 'memory', 'categories');
13
+ const mdFiles = await (0, utils_1.listRealFilesRecursive)(categoriesDir, { extensions: ['.md'] });
14
+ if (mdFiles.length === 0) {
15
+ findings.push({
16
+ checkId: id,
17
+ severity: 'error',
18
+ message: 'agent-env/memory/categories/ 无分类 .md(S80 须播种)',
19
+ path: 'agent-env/memory/categories',
20
+ });
21
+ }
22
+ return { id, title: 'Memory 分类', findings };
23
+ }
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.checkPolicyGrep = checkPolicyGrep;
7
+ const fs_extra_1 = __importDefault(require("fs-extra"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const utils_1 = require("./utils");
10
+ const SCAN_ROOTS = [
11
+ 'specs',
12
+ 'tasks',
13
+ 'agent-env/rules',
14
+ 'agent-env/skills',
15
+ 'agent-env/memory',
16
+ ];
17
+ const SCAN_EXTENSIONS = new Set(['.yaml', '.yml', '.md', '.mdc']);
18
+ async function checkPolicyGrep(ctx) {
19
+ const id = 'policy-grep';
20
+ const findings = [];
21
+ for (const root of SCAN_ROOTS) {
22
+ const absRoot = node_path_1.default.join(ctx.harnessRoot, root);
23
+ const files = await (0, utils_1.listRealFilesRecursive)(absRoot);
24
+ for (const file of files) {
25
+ const ext = node_path_1.default.extname(file).toLowerCase();
26
+ if (!SCAN_EXTENSIONS.has(ext))
27
+ continue;
28
+ const content = (await fs_extra_1.default.readFile(file, 'utf8')).toLowerCase();
29
+ for (const term of utils_1.AGENT_BLACKLIST) {
30
+ const needle = term.toLowerCase();
31
+ if (content.includes(needle)) {
32
+ findings.push({
33
+ checkId: id,
34
+ severity: 'error',
35
+ message: `含 agent 绑定关键词 "${term}"`,
36
+ path: (0, utils_1.rel)(ctx.harnessRoot, file),
37
+ });
38
+ break;
39
+ }
40
+ }
41
+ }
42
+ }
43
+ return { id, title: 'Agent-agnostic 策略', findings };
44
+ }
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.checkReferences = checkReferences;
7
+ const fs_extra_1 = __importDefault(require("fs-extra"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const js_yaml_1 = __importDefault(require("js-yaml"));
10
+ const utils_1 = require("./utils");
11
+ async function checkReferences(ctx) {
12
+ const id = 'references';
13
+ const findings = [];
14
+ const rawDir = node_path_1.default.join(ctx.harnessRoot, 'references', 'raw');
15
+ const mdDir = node_path_1.default.join(ctx.harnessRoot, 'references', 'md');
16
+ const indexPath = node_path_1.default.join(ctx.harnessRoot, 'references', 'yaml', 'index.yaml');
17
+ const rawFiles = await (0, utils_1.listRealFilesRecursive)(rawDir);
18
+ const mdFiles = await (0, utils_1.listRealFilesRecursive)(mdDir, { extensions: ['.md'] });
19
+ if (rawFiles.length === 0 && mdFiles.length === 0) {
20
+ findings.push({
21
+ checkId: id,
22
+ severity: 'info',
23
+ message: 'references 无输入,若 S20 已确认「无新增」可忽略',
24
+ });
25
+ return { id, title: '参考资料', findings };
26
+ }
27
+ if (rawFiles.length > 0 && mdFiles.length === 0) {
28
+ findings.push({
29
+ checkId: id,
30
+ severity: 'error',
31
+ message: 'references/raw 有文件但 references/md/ 无 Markdown 产物',
32
+ path: 'references/md',
33
+ });
34
+ }
35
+ if (rawFiles.length > 0 || mdFiles.length > 0) {
36
+ if (!(await fs_extra_1.default.pathExists(indexPath))) {
37
+ findings.push({
38
+ checkId: id,
39
+ severity: 'error',
40
+ message: '缺少 references/yaml/index.yaml 结构化索引(S60 产出)',
41
+ path: 'references/yaml/index.yaml',
42
+ });
43
+ }
44
+ else {
45
+ try {
46
+ const raw = await fs_extra_1.default.readFile(indexPath, 'utf8');
47
+ if (!raw.trim()) {
48
+ findings.push({
49
+ checkId: id,
50
+ severity: 'error',
51
+ message: 'references/yaml/index.yaml 为空',
52
+ path: 'references/yaml/index.yaml',
53
+ });
54
+ }
55
+ else {
56
+ const doc = js_yaml_1.default.load(raw);
57
+ if (!doc || typeof doc !== 'object') {
58
+ findings.push({
59
+ checkId: id,
60
+ severity: 'error',
61
+ message: 'references/yaml/index.yaml 解析为空',
62
+ path: 'references/yaml/index.yaml',
63
+ });
64
+ }
65
+ }
66
+ }
67
+ catch (err) {
68
+ findings.push({
69
+ checkId: id,
70
+ severity: 'error',
71
+ message: `index.yaml 解析失败:${err.message}`,
72
+ path: 'references/yaml/index.yaml',
73
+ });
74
+ }
75
+ }
76
+ }
77
+ return { id, title: '参考资料', findings };
78
+ }
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.checkRequirements = checkRequirements;
7
+ const fs_extra_1 = __importDefault(require("fs-extra"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const js_yaml_1 = __importDefault(require("js-yaml"));
10
+ const utils_1 = require("./utils");
11
+ async function checkRequirements(ctx) {
12
+ const id = 'requirements';
13
+ const findings = [];
14
+ const rawDir = node_path_1.default.join(ctx.harnessRoot, 'requirements', 'raw');
15
+ const yamlDir = node_path_1.default.join(ctx.harnessRoot, 'requirements', 'yaml');
16
+ const rawFiles = await (0, utils_1.listRealFilesRecursive)(rawDir);
17
+ if (rawFiles.length === 0) {
18
+ findings.push({
19
+ checkId: id,
20
+ severity: 'error',
21
+ message: 'requirements/raw/ 无真实文档(不得仅含 .gitkeep)',
22
+ path: 'requirements/raw',
23
+ });
24
+ }
25
+ const yamlFiles = (await (0, utils_1.listRealFilesRecursive)(yamlDir, { extensions: ['.yaml', '.yml'] })).filter((f) => !f.endsWith('schema.json'));
26
+ if (yamlFiles.length === 0) {
27
+ findings.push({
28
+ checkId: id,
29
+ severity: 'error',
30
+ message: 'requirements/yaml/ 无条目化 YAML 产出',
31
+ path: 'requirements/yaml',
32
+ });
33
+ }
34
+ else {
35
+ for (const file of yamlFiles) {
36
+ try {
37
+ const raw = await fs_extra_1.default.readFile(file, 'utf8');
38
+ if (!raw.trim()) {
39
+ findings.push({
40
+ checkId: id,
41
+ severity: 'error',
42
+ message: 'requirements YAML 文件为空',
43
+ path: (0, utils_1.rel)(ctx.harnessRoot, file),
44
+ });
45
+ continue;
46
+ }
47
+ js_yaml_1.default.load(raw);
48
+ }
49
+ catch (err) {
50
+ findings.push({
51
+ checkId: id,
52
+ severity: 'error',
53
+ message: `requirements YAML 解析失败:${err.message}`,
54
+ path: (0, utils_1.rel)(ctx.harnessRoot, file),
55
+ });
56
+ }
57
+ }
58
+ }
59
+ const mdDir = node_path_1.default.join(ctx.harnessRoot, 'requirements', 'md');
60
+ if (rawFiles.length > 0 && (await (0, utils_1.listDirEntries)(mdDir)).length === 0) {
61
+ findings.push({
62
+ checkId: id,
63
+ severity: 'warning',
64
+ message: 'requirements/raw 有文件但 requirements/md/ 为空,请确认 S30 已完成',
65
+ path: 'requirements/md',
66
+ });
67
+ }
68
+ return { id, title: '需求文档', findings };
69
+ }