svharness 0.14.1 → 0.14.2

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 (32) hide show
  1. package/README.md +14 -5
  2. package/dist/commands/doctor/check-bootstrap.js +4 -2
  3. package/dist/commands/doctor/check-empty-dirs.js +15 -7
  4. package/dist/commands/doctor/check-memory.js +2 -1
  5. package/dist/commands/doctor/check-requirements-coverage.js +100 -0
  6. package/dist/commands/doctor/check-requirements.js +9 -6
  7. package/dist/commands/doctor/check-review-report.js +113 -0
  8. package/dist/commands/doctor/check-skills-tasks.js +2 -1
  9. package/dist/commands/doctor/check-specs.js +10 -6
  10. package/dist/commands/doctor/check-state-phases.js +23 -0
  11. package/dist/commands/doctor/index.js +4 -0
  12. package/dist/commands/doctor/utils.js +54 -4
  13. package/dist/commands/init.js +6 -3
  14. package/dist/core/build-project-entry.js +2 -0
  15. package/dist/core/render-meta.js +1 -0
  16. package/dist/core/state.js +22 -4
  17. package/dist/utils/yaml-safe-path.js +33 -0
  18. package/package.json +1 -2
  19. package/templates/_shared/build-rules/harness-build-rule-convert-check.md +6 -0
  20. package/templates/_shared/build-rules/harness-build-rule-orchestrator-flow.md +18 -7
  21. package/templates/_shared/build-rules/harness-build-rule-pre-seal-review.md +32 -12
  22. package/templates/_shared/build-rules/harness-build-rule-requirements-extraction.md +43 -0
  23. package/templates/_shared/build-rules/harness-build-rule-specs-schema.md +14 -0
  24. package/templates/_shared/build-rules/harness-build-rule-user-interaction.md +8 -0
  25. package/templates/_shared/build-skills/harness-build-harness-data-review.md +16 -0
  26. package/templates/_shared/build-skills/harness-build-skill-orchestrator.md +16 -6
  27. package/templates/_shared/build-skills/harness-build-skill-pre-seal-review.md +193 -31
  28. package/templates/_shared/build-skills/harness-build-skill-spec-builder.md +47 -11
  29. package/templates/_shared/build-skills/harness-build-skills-main.md +3 -2
  30. package/templates/_shared/meta/task_list.md.ejs +44 -0
  31. package/templates/_shared/skeleton/agent-env/review-profiles/_default.yaml +64 -0
  32. package/templates/_shared/skeleton/agent-env/review-profiles/android-compose.yaml +35 -0
package/README.md CHANGED
@@ -168,7 +168,7 @@ harness-build-{skill|rule}-<semantic-name>
168
168
  | 资源类型 | 目录 | 命名模式 | 现有成员 |
169
169
  |----------|------|----------|----------|
170
170
  | **Build skill** | `templates/_shared/build-skills/` | `harness-build-skill-<name>.md` | `skills-main` / `orchestrator` / `spec-builder` / `references-intake` / `agent-env-merge` / `knowledge-builder` / `wiki-writer` |
171
- | **Build rule** | `templates/_shared/build-rules/` | `harness-build-rule-<name>.md` | `agent-agnostic` / `chinese-only` / `memory-write` / `orchestrator-flow` / `skills-tasks-output` / `specs-schema` / `user-interaction` |
171
+ | **Build rule** | `templates/_shared/build-rules/` | `harness-build-rule-<name>.md` | `agent-agnostic` / `chinese-only` / `memory-write` / `orchestrator-flow` / `requirements-extraction` / `skills-tasks-output` / `specs-schema` / `user-interaction` |
172
172
 
173
173
  约束:
174
174
 
@@ -183,10 +183,10 @@ harness-build-{skill|rule}-<semantic-name>
183
183
  | 阶段 | 动作 | 产物 |
184
184
  |------|------|------|
185
185
  | **S10_wiki** | 构建 baseline wiki(仅当有 baseline 时出现,项目经验底座) | `baseline/wiki/` |
186
- | **S20_collect_inputs** | 添加相关文件:需求/参考文档 + 额外运行期资源征询(`--extra-skills` 单入口,可混放 skills/rules) | `requirements/raw/` + `references/raw/` + `agent-env/_incoming/skills/` |
186
+ | **S20_collect_inputs** | 添加相关文件:需求/参考文档 + 额外运行期资源征询(`--extra-skills` 单入口,可混放 skills/rules) | `requirements/raw/` + `references/raw/` + `agent-env/_incoming/skills/` + `task_list.md` |
187
187
  | **S30_convert_docs** | 非 Markdown 原始文档转 Markdown | `requirements/md/` + `references/md/` |
188
- | **S40_extract_requirements** | 条目化需求 | `requirements/yaml/*.yaml` |
189
- | **S50_generate_specs** | 生成规格 | `specs/<layer>/<module>.<layer>.yaml` + schema 校验 |
188
+ | **S40_extract_requirements** | 条目化需求 + 覆盖率门禁 | `requirements/yaml/*.yaml` + `requirements/coverage-report.yaml` |
189
+ | **S50_generate_specs** | 生成规格 + REQ→spec 覆盖 | `specs/<layer>/<module>.<layer>.yaml` + schema 校验 + 覆盖率结果 |
190
190
  | **S60_process_references** | references 处理(`svharness convert` + 结构化索引 + 用户确认落地) | `references/md/` |
191
191
  | **S61_confirm_baseline_extraction** | 确认是否自动从 baseline 提取 skills/rules(表单确认) | `.harness-build-state.yaml`(`phases.S61_confirm_baseline_extraction.baseline_auto_extract`) |
192
192
  | **S65_customize_agent_env** | agent-env 定制(extra-skills/extra-rules 冲突建议与重命名建议 → 用户确认 → 写入) | `agent-env/rules/` + `agent-env/skills/` |
@@ -201,6 +201,7 @@ harness-build-{skill|rule}-<semantic-name>
201
201
  > **S60_process_references 专注 references:必须执行 `svharness convert`,并产出结构化索引(规制约束 / skills 候选 / signals / manuals),经表单确认后再进入后续阶段。**
202
202
  > **S61_confirm_baseline_extraction 必须显式确认是否启用 baseline 自动提取 skills/rules;未经确认不得在 S65 自动提取。**
203
203
  > **S65_customize_agent_env 专注 extra-skills/extra-rules:来源可来自 `--extra-skills` 导入后的 `_incoming/manifest.yaml`,先冲突识别与重命名建议,再表单确认写入;推荐命名:`harness-apply-skills-<topic>` 与 `harness-apply-rules-<topic>.md/.mdc`。**
204
+ > **S40/S50/S85 的 DONE 都带覆盖率门禁:未闭环 gap(或未备案 waiver)不得推进。**
204
205
 
205
206
  ---
206
207
 
@@ -416,7 +417,15 @@ svharness doctor --harness ./my-app-harness --mode pre-seal --format json --repo
416
417
  | `--report` | JSON 报告路径(默认 `<harness>/doctor-report.json`) |
417
418
  | `--strict` | 将 warning 视为 error |
418
419
 
419
- 退出码:`0` 通过,`1` 存在 error。通过 doctor 后,由 `harness-build-skill-pre-seal-review` 完成语义层全面审查并经用户确认,方可将 S85 标为 DONE 并进入 S90
420
+ 退出码:`0` 通过,`1` 存在 error。
421
+
422
+ **S85 Agent 审查(合并 skill)**:doctor 通过后,由 `harness-build-skill-pre-seal-review`(v2,已吸收 `harness-build-harness-data-review`)产出 **`HARNESS-REVIEW-REPORT.md`**:
423
+
424
+ - **Part A**:从 `harness.yaml` 发现 assets,完整性清单(arch-agnostic)
425
+ - **Part B**:按 `specs.layers` 与 `agent-env/review-profiles/<arch>.yaml` 做深度评分
426
+ - **门禁**:`overall_score >= 5.0`(C 级)、`critical_count == 0`、报告 frontmatter `gate_pass: true`、用户表单确认
427
+
428
+ `doctor` 另含 `check-review-report`(warning):S85 已 DONE 时校验报告 frontmatter 与 state 中 `review_gate_pass` 一致。数量覆盖率阻断仍由 S40/S50 表单门禁负责。
420
429
 
421
430
  ### `convert` —— 文档 → Markdown 预处理(对接 S20_collect_inputs)
422
431
 
@@ -8,12 +8,14 @@ async function checkBootstrap(ctx) {
8
8
  if (ctx.mode !== 'pre-seal') {
9
9
  return { id, title: 'Bootstrap 模式', findings };
10
10
  }
11
- const harness = await (0, utils_1.readHarnessYaml)(ctx.harnessRoot);
11
+ const { doc: harness, error } = await (0, utils_1.readHarnessYaml)(ctx.harnessRoot);
12
12
  if (!harness) {
13
13
  findings.push({
14
14
  checkId: id,
15
15
  severity: 'error',
16
- message: '缺少或无法解析 harness.yaml',
16
+ message: error
17
+ ? `无法解析 harness.yaml:${error}`
18
+ : '缺少 harness.yaml',
17
19
  path: 'harness.yaml',
18
20
  });
19
21
  return { id, title: 'Bootstrap 模式', findings };
@@ -7,28 +7,36 @@ exports.checkEmptyDirs = checkEmptyDirs;
7
7
  const fs_extra_1 = __importDefault(require("fs-extra"));
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
9
  const utils_1 = require("./utils");
10
- async function collectEmptyDirs(harnessRoot, dir, findings, checkId) {
10
+ async function collectEmptyDirs(ctx, dir, findings, checkId) {
11
11
  if (!(await fs_extra_1.default.pathExists(dir)))
12
12
  return;
13
+ const state = await (0, utils_1.readBuildState)(ctx.harnessRoot);
13
14
  const entries = await fs_extra_1.default.readdir(dir, { withFileTypes: true });
14
15
  for (const ent of entries) {
15
16
  if (ent.name.startsWith('.') && ent.name !== '.gitkeep')
16
17
  continue;
17
18
  const full = node_path_1.default.join(dir, ent.name);
18
19
  if (ent.isDirectory()) {
19
- const relPath = (0, utils_1.rel)(harnessRoot, full);
20
+ const relPath = (0, utils_1.rel)(ctx.harnessRoot, full);
21
+ if ((0, utils_1.isBaselineCodeMirror)(relPath))
22
+ continue;
23
+ if ((0, utils_1.isSkillReferencePlaceholder)(relPath))
24
+ continue;
20
25
  if (utils_1.EMPTY_DIR_WHITELIST.has(relPath)) {
21
- await collectEmptyDirs(harnessRoot, full, findings, checkId);
26
+ await collectEmptyDirs(ctx, full, findings, checkId);
22
27
  continue;
23
28
  }
24
29
  if (relPath === 'agent-env/_incoming/skills') {
25
- const state = await (0, utils_1.readBuildState)(harnessRoot);
26
30
  const s65 = state?.phases.S65_customize_agent_env;
27
31
  if (s65?.status === 'DONE') {
28
- await collectEmptyDirs(harnessRoot, full, findings, checkId);
32
+ await collectEmptyDirs(ctx, full, findings, checkId);
29
33
  continue;
30
34
  }
31
35
  }
36
+ if ((0, utils_1.isDeferredEmptyDir)(relPath, ctx.mode, state)) {
37
+ await collectEmptyDirs(ctx, full, findings, checkId);
38
+ continue;
39
+ }
32
40
  if (await (0, utils_1.isEmptyOrGitkeepOnly)(full)) {
33
41
  findings.push({
34
42
  checkId,
@@ -38,7 +46,7 @@ async function collectEmptyDirs(harnessRoot, dir, findings, checkId) {
38
46
  });
39
47
  }
40
48
  else {
41
- await collectEmptyDirs(harnessRoot, full, findings, checkId);
49
+ await collectEmptyDirs(ctx, full, findings, checkId);
42
50
  }
43
51
  }
44
52
  }
@@ -46,6 +54,6 @@ async function collectEmptyDirs(harnessRoot, dir, findings, checkId) {
46
54
  async function checkEmptyDirs(ctx) {
47
55
  const id = 'empty-dirs';
48
56
  const findings = [];
49
- await collectEmptyDirs(ctx.harnessRoot, ctx.harnessRoot, findings, id);
57
+ await collectEmptyDirs(ctx, ctx.harnessRoot, findings, id);
50
58
  return { id, title: '空目录检查', findings };
51
59
  }
@@ -10,8 +10,9 @@ async function checkMemory(ctx) {
10
10
  const id = 'memory';
11
11
  const findings = [];
12
12
  const categoriesDir = node_path_1.default.join(ctx.harnessRoot, 'agent-env', 'memory', 'categories');
13
+ const state = await (0, utils_1.readBuildState)(ctx.harnessRoot);
13
14
  const mdFiles = await (0, utils_1.listRealFilesRecursive)(categoriesDir, { extensions: ['.md'] });
14
- if (mdFiles.length === 0) {
15
+ if (mdFiles.length === 0 && !(0, utils_1.isPhaseNotReached)(ctx.mode, state, 'S80_seed_memory')) {
15
16
  findings.push({
16
17
  checkId: id,
17
18
  severity: 'error',
@@ -0,0 +1,100 @@
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.checkRequirementsCoverage = checkRequirementsCoverage;
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
+ const SOURCE_ANCHOR_RE = /SyRD-[\w-]+/g;
12
+ const WILDCARD_RE = /\*/;
13
+ function collectStringValues(node, out) {
14
+ if (typeof node === 'string') {
15
+ out.push(node);
16
+ return;
17
+ }
18
+ if (Array.isArray(node)) {
19
+ for (const item of node)
20
+ collectStringValues(item, out);
21
+ return;
22
+ }
23
+ if (node && typeof node === 'object') {
24
+ for (const value of Object.values(node)) {
25
+ collectStringValues(value, out);
26
+ }
27
+ }
28
+ }
29
+ function collectAnchors(node, out) {
30
+ const values = [];
31
+ collectStringValues(node, values);
32
+ for (const value of values) {
33
+ const matches = value.match(SOURCE_ANCHOR_RE);
34
+ if (!matches)
35
+ continue;
36
+ for (const m of matches)
37
+ out.add(m);
38
+ }
39
+ }
40
+ async function checkRequirementsCoverage(ctx) {
41
+ const id = 'requirements-coverage';
42
+ const findings = [];
43
+ const state = await (0, utils_1.readBuildState)(ctx.harnessRoot);
44
+ const coveragePath = node_path_1.default.join(ctx.harnessRoot, 'requirements', 'coverage-report.yaml');
45
+ const hasCoverageReport = await fs_extra_1.default.pathExists(coveragePath);
46
+ const s40Done = state?.phases?.S40_extract_requirements?.status === 'DONE';
47
+ if (s40Done && !hasCoverageReport) {
48
+ findings.push({
49
+ checkId: id,
50
+ severity: 'warning',
51
+ message: 'S40_extract_requirements 已是 DONE,但缺少 requirements/coverage-report.yaml',
52
+ path: 'requirements/coverage-report.yaml',
53
+ });
54
+ }
55
+ const mdDir = node_path_1.default.join(ctx.harnessRoot, 'requirements', 'md');
56
+ const yamlDir = node_path_1.default.join(ctx.harnessRoot, 'requirements', 'yaml');
57
+ const mdFiles = await (0, utils_1.listRealFilesRecursive)(mdDir, { extensions: ['.md'] });
58
+ const reqYamlFiles = (await (0, utils_1.listRealFilesRecursive)(yamlDir, { extensions: ['.yaml', '.yml'] })).filter((f) => !f.endsWith('schema.json'));
59
+ const mdAnchors = new Set();
60
+ for (const mdFile of mdFiles) {
61
+ const raw = await fs_extra_1.default.readFile(mdFile, 'utf8');
62
+ const matches = raw.match(SOURCE_ANCHOR_RE);
63
+ if (!matches)
64
+ continue;
65
+ for (const m of matches)
66
+ mdAnchors.add(m);
67
+ }
68
+ const yamlAnchors = new Set();
69
+ for (const yamlFile of reqYamlFiles) {
70
+ try {
71
+ const parsed = js_yaml_1.default.load(await fs_extra_1.default.readFile(yamlFile, 'utf8'));
72
+ collectAnchors(parsed, yamlAnchors);
73
+ const values = [];
74
+ collectStringValues(parsed, values);
75
+ if (values.some((v) => WILDCARD_RE.test(v) && /SyRD-|REQ-/i.test(v))) {
76
+ findings.push({
77
+ checkId: id,
78
+ severity: 'warning',
79
+ message: '检测到可能的通配符聚合锚点,请确认已备案 aggregates/waiver',
80
+ path: (0, utils_1.rel)(ctx.harnessRoot, yamlFile),
81
+ });
82
+ }
83
+ }
84
+ catch {
85
+ // parse errors are handled by check-requirements
86
+ }
87
+ }
88
+ if (mdAnchors.size > 0) {
89
+ const ratio = yamlAnchors.size / mdAnchors.size;
90
+ if (ratio < 0.5) {
91
+ findings.push({
92
+ checkId: id,
93
+ severity: 'warning',
94
+ message: `requirements 锚点覆盖率偏低(${yamlAnchors.size}/${mdAnchors.size}=${ratio.toFixed(3)})`,
95
+ path: 'requirements',
96
+ });
97
+ }
98
+ }
99
+ return { id, title: '需求覆盖率', findings };
100
+ }
@@ -13,6 +13,7 @@ async function checkRequirements(ctx) {
13
13
  const findings = [];
14
14
  const rawDir = node_path_1.default.join(ctx.harnessRoot, 'requirements', 'raw');
15
15
  const yamlDir = node_path_1.default.join(ctx.harnessRoot, 'requirements', 'yaml');
16
+ const state = await (0, utils_1.readBuildState)(ctx.harnessRoot);
16
17
  const rawFiles = await (0, utils_1.listRealFilesRecursive)(rawDir);
17
18
  if (rawFiles.length === 0) {
18
19
  findings.push({
@@ -24,12 +25,14 @@ async function checkRequirements(ctx) {
24
25
  }
25
26
  const yamlFiles = (await (0, utils_1.listRealFilesRecursive)(yamlDir, { extensions: ['.yaml', '.yml'] })).filter((f) => !f.endsWith('schema.json'));
26
27
  if (yamlFiles.length === 0) {
27
- findings.push({
28
- checkId: id,
29
- severity: 'error',
30
- message: 'requirements/yaml/ 无条目化 YAML 产出',
31
- path: 'requirements/yaml',
32
- });
28
+ if (!(0, utils_1.isPhaseNotReached)(ctx.mode, state, 'S40_extract_requirements')) {
29
+ findings.push({
30
+ checkId: id,
31
+ severity: 'error',
32
+ message: 'requirements/yaml/ 无条目化 YAML 产出',
33
+ path: 'requirements/yaml',
34
+ });
35
+ }
33
36
  }
34
37
  else {
35
38
  for (const file of yamlFiles) {
@@ -0,0 +1,113 @@
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.checkReviewReport = checkReviewReport;
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ const fs_extra_1 = __importDefault(require("fs-extra"));
9
+ const js_yaml_1 = __importDefault(require("js-yaml"));
10
+ const utils_1 = require("./utils");
11
+ function parseFrontmatter(content) {
12
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
13
+ if (!match)
14
+ return null;
15
+ try {
16
+ return js_yaml_1.default.load(match[1]);
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
22
+ async function checkReviewReport(ctx) {
23
+ const id = 'review-report';
24
+ const findings = [];
25
+ const state = await (0, utils_1.readBuildState)(ctx.harnessRoot);
26
+ const s85 = state?.phases?.S85_pre_seal_validation;
27
+ const reportRel = (s85 && typeof s85 === 'object' && 'review_report_path' in s85 && s85.review_report_path) ||
28
+ 'HARNESS-REVIEW-REPORT.md';
29
+ const reportPath = node_path_1.default.join(ctx.harnessRoot, String(reportRel));
30
+ if (ctx.mode !== 'pre-seal' && ctx.mode !== 'health') {
31
+ return { id, title: '深度审查报告', findings };
32
+ }
33
+ if (!s85) {
34
+ if (ctx.mode === 'pre-seal') {
35
+ findings.push({
36
+ checkId: id,
37
+ severity: 'warning',
38
+ message: '状态文件缺少 S85_pre_seal_validation 阶段定义',
39
+ });
40
+ }
41
+ return { id, title: '深度审查报告', findings };
42
+ }
43
+ const s85Done = s85.status === 'DONE';
44
+ if (!(await fs_extra_1.default.pathExists(reportPath))) {
45
+ if (s85Done) {
46
+ findings.push({
47
+ checkId: id,
48
+ severity: 'warning',
49
+ message: `S85 已为 DONE,但未找到审查报告 ${reportRel}`,
50
+ path: (0, utils_1.rel)(ctx.harnessRoot, reportPath),
51
+ });
52
+ }
53
+ return { id, title: '深度审查报告', findings };
54
+ }
55
+ const content = await fs_extra_1.default.readFile(reportPath, 'utf8');
56
+ const fm = parseFrontmatter(content);
57
+ if (!fm) {
58
+ findings.push({
59
+ checkId: id,
60
+ severity: 'warning',
61
+ message: `${reportRel} 缺少可解析的 YAML frontmatter(需 gate_pass、overall_score 等)`,
62
+ path: (0, utils_1.rel)(ctx.harnessRoot, reportPath),
63
+ });
64
+ return { id, title: '深度审查报告', findings };
65
+ }
66
+ const minScore = typeof s85 === 'object' && 'min_depth_score' in s85 && s85.min_depth_score != null
67
+ ? Number(s85.min_depth_score)
68
+ : 5.0;
69
+ if (fm.gate_pass !== true) {
70
+ findings.push({
71
+ checkId: id,
72
+ severity: s85Done ? 'warning' : 'info',
73
+ message: `审查报告 gate_pass 不为 true(当前:${String(fm.gate_pass)})`,
74
+ path: (0, utils_1.rel)(ctx.harnessRoot, reportPath),
75
+ });
76
+ }
77
+ if (typeof fm.overall_score === 'number' && fm.overall_score < minScore) {
78
+ findings.push({
79
+ checkId: id,
80
+ severity: 'warning',
81
+ message: `overall_score ${fm.overall_score} 低于门禁 ${minScore}`,
82
+ path: (0, utils_1.rel)(ctx.harnessRoot, reportPath),
83
+ });
84
+ }
85
+ if (typeof fm.critical_count === 'number' && fm.critical_count > 0) {
86
+ findings.push({
87
+ checkId: id,
88
+ severity: 'warning',
89
+ message: `critical_count=${fm.critical_count},深度审查存在 Critical 差距`,
90
+ path: (0, utils_1.rel)(ctx.harnessRoot, reportPath),
91
+ });
92
+ }
93
+ if (s85Done) {
94
+ if (typeof s85 === 'object' && s85.review_gate_pass !== true) {
95
+ findings.push({
96
+ checkId: id,
97
+ severity: 'warning',
98
+ message: 'S85 为 DONE 但 review_gate_pass 不为 true,与报告/frontmatter 不一致',
99
+ });
100
+ }
101
+ if (typeof s85 === 'object' &&
102
+ 'critical_gaps' in s85 &&
103
+ typeof s85.critical_gaps === 'number' &&
104
+ s85.critical_gaps > 0) {
105
+ findings.push({
106
+ checkId: id,
107
+ severity: 'warning',
108
+ message: `S85 为 DONE 但 critical_gaps=${s85.critical_gaps}`,
109
+ });
110
+ }
111
+ }
112
+ return { id, title: '深度审查报告', findings };
113
+ }
@@ -90,8 +90,9 @@ async function checkSkillsTasks(ctx) {
90
90
  }
91
91
  }
92
92
  }
93
+ const state = await (0, utils_1.readBuildState)(ctx.harnessRoot);
93
94
  const categoryCount = await countTaskCategories(templatesDir);
94
- if (categoryCount < 3) {
95
+ if (categoryCount < 3 && !(0, utils_1.isPhaseNotReached)(ctx.mode, state, 'S70_runtime_assets')) {
95
96
  findings.push({
96
97
  checkId: id,
97
98
  severity: 'error',
@@ -16,6 +16,8 @@ async function checkSpecs(ctx) {
16
16
  const findings = [];
17
17
  const ajv = new ajv_1.default({ allErrors: true, strict: false });
18
18
  (0, ajv_formats_1.default)(ajv);
19
+ const state = await (0, utils_1.readBuildState)(ctx.harnessRoot);
20
+ const deferSpecs = (0, utils_1.isPhaseNotReached)(ctx.mode, state, 'S50_generate_specs');
19
21
  for (const domain of SPEC_DOMAINS) {
20
22
  const domainDir = node_path_1.default.join(ctx.harnessRoot, 'specs', domain);
21
23
  const schemaPath = node_path_1.default.join(domainDir, 'schema.json');
@@ -31,12 +33,14 @@ async function checkSpecs(ctx) {
31
33
  const specFiles = (await (0, utils_1.listRealFilesRecursive)(domainDir, { extensions: ['.yaml', '.yml'] })).filter((f) => !f.endsWith('schema.json') && !f.endsWith('schema.yaml'));
32
34
  const moduleYaml = specFiles.filter((f) => node_path_1.default.basename(f) !== 'schema.json');
33
35
  if (moduleYaml.length === 0) {
34
- findings.push({
35
- checkId: id,
36
- severity: 'error',
37
- message: `specs/${domain}/ 无模块 YAML(除 schema.json 外)`,
38
- path: `specs/${domain}`,
39
- });
36
+ if (!deferSpecs) {
37
+ findings.push({
38
+ checkId: id,
39
+ severity: 'error',
40
+ message: `specs/${domain}/ 无模块 YAML(除 schema.json 外)`,
41
+ path: `specs/${domain}`,
42
+ });
43
+ }
40
44
  continue;
41
45
  }
42
46
  let validate;
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.checkStatePhases = checkStatePhases;
7
7
  const node_path_1 = __importDefault(require("node:path"));
8
+ const fs_extra_1 = __importDefault(require("fs-extra"));
8
9
  const utils_1 = require("./utils");
9
10
  const PRE_SEAL_PHASES = [
10
11
  'S00_bootstrap',
@@ -89,5 +90,27 @@ async function checkStatePhases(ctx) {
89
90
  message: '状态文件缺少 S85_pre_seal_validation,请由 orchestrator 补丁插入后再封存',
90
91
  });
91
92
  }
93
+ if (state.phases.S40_extract_requirements?.status === 'DONE') {
94
+ const coveragePath = node_path_1.default.join(ctx.harnessRoot, 'requirements', 'coverage-report.yaml');
95
+ if (!(await fs_extra_1.default.pathExists(coveragePath))) {
96
+ findings.push({
97
+ checkId: id,
98
+ severity: 'warning',
99
+ message: 'S40_extract_requirements 已 DONE,但未找到 requirements/coverage-report.yaml',
100
+ path: (0, utils_1.rel)(ctx.harnessRoot, coveragePath),
101
+ });
102
+ }
103
+ }
104
+ if (state.phases.S50_generate_specs?.status === 'DONE') {
105
+ const specCoveragePath = node_path_1.default.join(ctx.harnessRoot, 'specs', 'coverage-report.yaml');
106
+ if (!(await fs_extra_1.default.pathExists(specCoveragePath))) {
107
+ findings.push({
108
+ checkId: id,
109
+ severity: 'warning',
110
+ message: 'S50_generate_specs 已 DONE,但未找到 specs/coverage-report.yaml',
111
+ path: (0, utils_1.rel)(ctx.harnessRoot, specCoveragePath),
112
+ });
113
+ }
114
+ }
92
115
  return { id, title: '构建阶段状态', findings };
93
116
  }
@@ -44,6 +44,7 @@ const check_state_phases_1 = require("./check-state-phases");
44
44
  const check_empty_dirs_1 = require("./check-empty-dirs");
45
45
  const check_convert_pairing_1 = require("./check-convert-pairing");
46
46
  const check_requirements_1 = require("./check-requirements");
47
+ const check_requirements_coverage_1 = require("./check-requirements-coverage");
47
48
  const check_specs_1 = require("./check-specs");
48
49
  const check_references_1 = require("./check-references");
49
50
  const check_skills_tasks_1 = require("./check-skills-tasks");
@@ -53,6 +54,7 @@ const check_wiki_1 = require("./check-wiki");
53
54
  const check_policy_grep_1 = require("./check-policy-grep");
54
55
  const check_chinese_heuristic_1 = require("./check-chinese-heuristic");
55
56
  const check_bootstrap_1 = require("./check-bootstrap");
57
+ const check_review_report_1 = require("./check-review-report");
56
58
  const report_1 = require("./report");
57
59
  var report_2 = require("./report");
58
60
  Object.defineProperty(exports, "formatDoctorReportText", { enumerable: true, get: function () { return report_2.formatDoctorReportText; } });
@@ -71,6 +73,7 @@ async function runDoctorChecks(input) {
71
73
  check_empty_dirs_1.checkEmptyDirs,
72
74
  check_convert_pairing_1.checkConvertPairing,
73
75
  check_requirements_1.checkRequirements,
76
+ check_requirements_coverage_1.checkRequirementsCoverage,
74
77
  check_specs_1.checkSpecs,
75
78
  check_references_1.checkReferences,
76
79
  check_skills_tasks_1.checkSkillsTasks,
@@ -79,6 +82,7 @@ async function runDoctorChecks(input) {
79
82
  check_wiki_1.checkWiki,
80
83
  check_policy_grep_1.checkPolicyGrep,
81
84
  check_chinese_heuristic_1.checkChineseHeuristic,
85
+ check_review_report_1.checkReviewReport,
82
86
  ];
83
87
  const checks = [];
84
88
  for (const run of runners) {
@@ -13,9 +13,14 @@ exports.isEmptyOrGitkeepOnly = isEmptyOrGitkeepOnly;
13
13
  exports.basenameWithoutExt = basenameWithoutExt;
14
14
  exports.readBuildState = readBuildState;
15
15
  exports.readHarnessYaml = readHarnessYaml;
16
+ exports.isPhaseNotReached = isPhaseNotReached;
17
+ exports.isDeferredEmptyDir = isDeferredEmptyDir;
18
+ exports.isBaselineCodeMirror = isBaselineCodeMirror;
19
+ exports.isSkillReferencePlaceholder = isSkillReferencePlaceholder;
16
20
  const fs_extra_1 = __importDefault(require("fs-extra"));
17
21
  const node_path_1 = __importDefault(require("node:path"));
18
22
  const js_yaml_1 = __importDefault(require("js-yaml"));
23
+ const yaml_safe_path_1 = require("../../utils/yaml-safe-path");
19
24
  const SKIP_FILE_NAMES = new Set(['.gitkeep', '.DS_Store', 'Thumbs.db']);
20
25
  function rel(harnessRoot, abs) {
21
26
  return node_path_1.default.relative(harnessRoot, abs).replace(/\\/g, '/');
@@ -95,14 +100,59 @@ async function readBuildState(harnessRoot) {
95
100
  async function readHarnessYaml(harnessRoot) {
96
101
  const p = node_path_1.default.join(harnessRoot, 'harness.yaml');
97
102
  if (!(await fs_extra_1.default.pathExists(p)))
98
- return undefined;
103
+ return {};
99
104
  try {
100
- return js_yaml_1.default.load(await fs_extra_1.default.readFile(p, 'utf8'));
105
+ const raw = await fs_extra_1.default.readFile(p, 'utf8');
106
+ const doc = js_yaml_1.default.load((0, yaml_safe_path_1.preprocessHarnessYamlText)(raw));
107
+ return { doc };
101
108
  }
102
- catch {
103
- return undefined;
109
+ catch (err) {
110
+ return { error: err.message };
104
111
  }
105
112
  }
113
+ /** In health mode, skip checks for artifacts not required until `phaseId` is DONE. */
114
+ function isPhaseNotReached(mode, state, phaseId) {
115
+ return mode === 'health' && state?.phases[phaseId]?.status !== 'DONE';
116
+ }
117
+ function pathPrefixMatches(relPath, prefix) {
118
+ const p = prefix.replace(/\/+$/, '');
119
+ return relPath === p || relPath.startsWith(`${p}/`);
120
+ }
121
+ /** Paths that are allowed empty before a build phase completes (health mode only). */
122
+ function isDeferredEmptyDir(relPath, mode, state) {
123
+ if (mode !== 'health' || !state)
124
+ return false;
125
+ const rules = [
126
+ ['requirements/yaml', 'S40_extract_requirements'],
127
+ ['references', 'S60_process_references'],
128
+ ['specs', 'S50_generate_specs'],
129
+ ['tasks/templates', 'S70_runtime_assets'],
130
+ ['agent-env/memory/categories', 'S80_seed_memory'],
131
+ ['agent-env/memory/inbox', 'S80_seed_memory'],
132
+ ['baseline/wiki/zh', 'S10_wiki'],
133
+ ];
134
+ for (const [prefix, phaseId] of rules) {
135
+ if (!pathPrefixMatches(relPath, prefix))
136
+ continue;
137
+ if (state.phases[phaseId]?.status !== 'DONE')
138
+ return true;
139
+ }
140
+ const incoming = state.phases.S65_customize_agent_env;
141
+ if (relPath === 'agent-env/_incoming/skills' &&
142
+ (incoming?.incoming_total ?? 0) === 0 &&
143
+ incoming?.status !== 'DONE') {
144
+ return true;
145
+ }
146
+ return false;
147
+ }
148
+ /** Do not scan copied baseline snapshot for harness scaffold placeholders. */
149
+ function isBaselineCodeMirror(relPath) {
150
+ return relPath === 'baseline/code' || relPath.startsWith('baseline/code/');
151
+ }
152
+ /** Arch seed skills may ship empty reference leaf dirs. */
153
+ function isSkillReferencePlaceholder(relPath) {
154
+ return /^agent-env\/skills\/[^/]+\/references(\/|$)/.test(relPath);
155
+ }
106
156
  exports.EMPTY_DIR_WHITELIST = new Set([
107
157
  'commands/install',
108
158
  'commands/update',
@@ -19,6 +19,7 @@ const next_steps_1 = require("../core/next-steps");
19
19
  const build_project_entry_1 = require("../core/build-project-entry");
20
20
  const project_ignore_1 = require("../core/project-ignore");
21
21
  const harness_name_1 = require("../utils/harness-name");
22
+ const yaml_safe_path_1 = require("../utils/yaml-safe-path");
22
23
  const repomix_pack_1 = require("../core/repomix-pack");
23
24
  const extra_assets_intake_1 = require("../core/extra-assets-intake");
24
25
  const baseline_copy_1 = require("../utils/baseline-copy");
@@ -229,9 +230,11 @@ async function runInit(opts) {
229
230
  name: validated.name,
230
231
  arch: validated.arch,
231
232
  agent: validated.agent,
232
- sourcePath: validated.baseline ?? '',
233
- requirementsPath: opts.requirements ? node_path_1.default.resolve(cwd, opts.requirements) : '',
234
- referencesPath: opts.references ? node_path_1.default.resolve(cwd, opts.references) : '',
233
+ sourcePath: validated.baseline ? (0, yaml_safe_path_1.toYamlSafePath)(validated.baseline) : '',
234
+ requirementsPath: opts.requirements
235
+ ? (0, yaml_safe_path_1.toYamlSafePath)(node_path_1.default.resolve(cwd, opts.requirements))
236
+ : '',
237
+ referencesPath: opts.references ? (0, yaml_safe_path_1.toYamlSafePath)(node_path_1.default.resolve(cwd, opts.references)) : '',
235
238
  createdAt,
236
239
  version: cliVersion,
237
240
  };
@@ -55,6 +55,8 @@ function renderBuildProjectEntryMarkdown(input) {
55
55
  '| `harness-build-skill-agent-env-merge` | S61 baseline 自动提取确认 + S65 合并写入 |',
56
56
  '| `harness-build-skill-knowledge-builder` | S10 baseline 样本 + S70 skills/tasks 索引 |',
57
57
  '| `harness-build-skill-wiki-writer` | 按 `TASKS.md` 撰写 baseline wiki |',
58
+ '| `harness-build-skill-pre-seal-review` | S85 合并审查(完整性 + 深度质量,`HARNESS-REVIEW-REPORT.md`) |',
59
+ '| `harness-build-harness-data-review` | 已合并 → `harness-build-skill-pre-seal-review` |',
58
60
  '',
59
61
  '## 3. 构建规则目录(与 harness 分离,位于项目根)',
60
62
  '',
@@ -14,6 +14,7 @@ const META_FILES = [
14
14
  { tplName: 'AGENTS_BUILD.md.ejs', outName: 'AGENTS_BUILD.md' },
15
15
  { tplName: 'AGENTS_APPLY.md.ejs', outName: 'AGENTS_APPLY.md' },
16
16
  { tplName: 'README.md.ejs', outName: 'README.md' },
17
+ { tplName: 'task_list.md.ejs', outName: 'task_list.md' },
17
18
  { tplName: 'VERSION.ejs', outName: 'VERSION' },
18
19
  { tplName: 'CHANGELOG.md.ejs', outName: 'CHANGELOG.md' },
19
20
  ];