svharness 0.13.5 → 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 (49) hide show
  1. package/README.md +43 -7
  2. package/dist/commands/apply.js +7 -77
  3. package/dist/commands/doctor/check-bootstrap.js +34 -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 +59 -0
  7. package/dist/commands/doctor/check-incoming.js +47 -0
  8. package/dist/commands/doctor/check-memory.js +24 -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-coverage.js +100 -0
  12. package/dist/commands/doctor/check-requirements.js +72 -0
  13. package/dist/commands/doctor/check-review-report.js +113 -0
  14. package/dist/commands/doctor/check-skills-tasks.js +104 -0
  15. package/dist/commands/doctor/check-specs.js +87 -0
  16. package/dist/commands/doctor/check-state-phases.js +116 -0
  17. package/dist/commands/doctor/check-wiki.js +52 -0
  18. package/dist/commands/doctor/index.js +105 -0
  19. package/dist/commands/doctor/report.js +79 -0
  20. package/dist/commands/doctor/types.js +2 -0
  21. package/dist/commands/doctor/utils.js +180 -0
  22. package/dist/commands/doctor.js +51 -0
  23. package/dist/commands/init.js +6 -3
  24. package/dist/config/index.js +2 -1
  25. package/dist/config/merge-options.js +13 -0
  26. package/dist/core/agent-injector.js +1 -0
  27. package/dist/core/build-project-entry.js +2 -0
  28. package/dist/core/render-meta.js +1 -0
  29. package/dist/core/state.js +29 -5
  30. package/dist/index.js +41 -0
  31. package/dist/utils/skill-md-validate.js +85 -0
  32. package/dist/utils/yaml-safe-path.js +33 -0
  33. package/package.json +3 -2
  34. package/templates/_shared/build-rules/harness-build-rule-convert-check.md +6 -0
  35. package/templates/_shared/build-rules/harness-build-rule-orchestrator-flow.md +21 -3
  36. package/templates/_shared/build-rules/harness-build-rule-pre-seal-review.md +59 -0
  37. package/templates/_shared/build-rules/harness-build-rule-requirements-extraction.md +43 -0
  38. package/templates/_shared/build-rules/harness-build-rule-specs-schema.md +14 -0
  39. package/templates/_shared/build-rules/harness-build-rule-user-interaction.md +8 -0
  40. package/templates/_shared/build-skills/harness-build-harness-data-review.md +16 -0
  41. package/templates/_shared/build-skills/harness-build-skill-orchestrator.md +19 -3
  42. package/templates/_shared/build-skills/harness-build-skill-pre-seal-review.md +213 -0
  43. package/templates/_shared/build-skills/harness-build-skill-spec-builder.md +47 -11
  44. package/templates/_shared/build-skills/harness-build-skills-main.md +3 -1
  45. package/templates/_shared/meta/README.md.ejs +2 -1
  46. package/templates/_shared/meta/task_list.md.ejs +44 -0
  47. package/templates/_shared/skeleton/agent-env/review-profiles/_default.yaml +64 -0
  48. package/templates/_shared/skeleton/agent-env/review-profiles/android-compose.yaml +35 -0
  49. package/templates/svharness.config.example.yaml +8 -0
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.writeDoctorReportFile = exports.formatDoctorReportJson = exports.formatDoctorReportText = void 0;
40
+ exports.runDoctorChecks = runDoctorChecks;
41
+ exports.renderDoctorReport = renderDoctorReport;
42
+ const node_path_1 = __importDefault(require("node:path"));
43
+ const check_state_phases_1 = require("./check-state-phases");
44
+ const check_empty_dirs_1 = require("./check-empty-dirs");
45
+ const check_convert_pairing_1 = require("./check-convert-pairing");
46
+ const check_requirements_1 = require("./check-requirements");
47
+ const check_requirements_coverage_1 = require("./check-requirements-coverage");
48
+ const check_specs_1 = require("./check-specs");
49
+ const check_references_1 = require("./check-references");
50
+ const check_skills_tasks_1 = require("./check-skills-tasks");
51
+ const check_memory_1 = require("./check-memory");
52
+ const check_incoming_1 = require("./check-incoming");
53
+ const check_wiki_1 = require("./check-wiki");
54
+ const check_policy_grep_1 = require("./check-policy-grep");
55
+ const check_chinese_heuristic_1 = require("./check-chinese-heuristic");
56
+ const check_bootstrap_1 = require("./check-bootstrap");
57
+ const check_review_report_1 = require("./check-review-report");
58
+ const report_1 = require("./report");
59
+ var report_2 = require("./report");
60
+ Object.defineProperty(exports, "formatDoctorReportText", { enumerable: true, get: function () { return report_2.formatDoctorReportText; } });
61
+ Object.defineProperty(exports, "formatDoctorReportJson", { enumerable: true, get: function () { return report_2.formatDoctorReportJson; } });
62
+ Object.defineProperty(exports, "writeDoctorReportFile", { enumerable: true, get: function () { return report_2.writeDoctorReportFile; } });
63
+ async function runDoctorChecks(input) {
64
+ const harnessRoot = node_path_1.default.resolve(input.harnessRoot);
65
+ const ctx = {
66
+ harnessRoot,
67
+ mode: input.mode,
68
+ strict: input.strict,
69
+ };
70
+ const runners = [
71
+ check_state_phases_1.checkStatePhases,
72
+ check_bootstrap_1.checkBootstrap,
73
+ check_empty_dirs_1.checkEmptyDirs,
74
+ check_convert_pairing_1.checkConvertPairing,
75
+ check_requirements_1.checkRequirements,
76
+ check_requirements_coverage_1.checkRequirementsCoverage,
77
+ check_specs_1.checkSpecs,
78
+ check_references_1.checkReferences,
79
+ check_skills_tasks_1.checkSkillsTasks,
80
+ check_memory_1.checkMemory,
81
+ check_incoming_1.checkIncoming,
82
+ check_wiki_1.checkWiki,
83
+ check_policy_grep_1.checkPolicyGrep,
84
+ check_chinese_heuristic_1.checkChineseHeuristic,
85
+ check_review_report_1.checkReviewReport,
86
+ ];
87
+ const checks = [];
88
+ for (const run of runners) {
89
+ checks.push(await run(ctx));
90
+ }
91
+ return (0, report_1.buildDoctorReport)({
92
+ harnessRoot,
93
+ mode: input.mode,
94
+ checks,
95
+ strict: input.strict,
96
+ });
97
+ }
98
+ async function renderDoctorReport(report, format) {
99
+ if (format === 'json') {
100
+ const { formatDoctorReportJson } = await Promise.resolve().then(() => __importStar(require('./report')));
101
+ return formatDoctorReportJson(report);
102
+ }
103
+ const { formatDoctorReportText } = await Promise.resolve().then(() => __importStar(require('./report')));
104
+ return formatDoctorReportText(report);
105
+ }
@@ -0,0 +1,79 @@
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.buildDoctorReport = buildDoctorReport;
7
+ exports.formatDoctorReportText = formatDoctorReportText;
8
+ exports.formatDoctorReportJson = formatDoctorReportJson;
9
+ exports.writeDoctorReportFile = writeDoctorReportFile;
10
+ const fs_extra_1 = __importDefault(require("fs-extra"));
11
+ const node_path_1 = __importDefault(require("node:path"));
12
+ const picocolors_1 = __importDefault(require("picocolors"));
13
+ function countBySeverity(checks, severity) {
14
+ let n = 0;
15
+ for (const c of checks) {
16
+ for (const f of c.findings) {
17
+ if (f.severity === severity)
18
+ n++;
19
+ }
20
+ }
21
+ return n;
22
+ }
23
+ function buildDoctorReport(input) {
24
+ let errorCount = countBySeverity(input.checks, 'error');
25
+ const warningCount = countBySeverity(input.checks, 'warning');
26
+ if (input.strict) {
27
+ errorCount += warningCount;
28
+ }
29
+ return {
30
+ harnessRoot: input.harnessRoot,
31
+ mode: input.mode,
32
+ passed: errorCount === 0,
33
+ errorCount,
34
+ warningCount: input.strict ? 0 : warningCount,
35
+ checks: input.checks,
36
+ generatedAt: new Date().toISOString(),
37
+ };
38
+ }
39
+ function formatDoctorReportText(report) {
40
+ const lines = [];
41
+ lines.push(`svharness doctor — ${report.mode}`);
42
+ lines.push(`harness: ${report.harnessRoot}`);
43
+ lines.push(`时间: ${report.generatedAt}`);
44
+ lines.push(report.passed
45
+ ? picocolors_1.default.green('结果: 通过')
46
+ : picocolors_1.default.red(`结果: 未通过(error: ${report.errorCount}, warning: ${report.warningCount})`));
47
+ lines.push('');
48
+ for (const check of report.checks) {
49
+ const errors = check.findings.filter((f) => f.severity === 'error');
50
+ const warnings = check.findings.filter((f) => f.severity === 'warning');
51
+ const infos = check.findings.filter((f) => f.severity === 'info');
52
+ if (check.findings.length === 0) {
53
+ lines.push(picocolors_1.default.green(`[OK] ${check.title}`));
54
+ continue;
55
+ }
56
+ lines.push(`[${check.id}] ${check.title}`);
57
+ for (const f of errors) {
58
+ lines.push(picocolors_1.default.red(` ERROR: ${f.message}${f.path ? ` (${f.path})` : ''}`));
59
+ }
60
+ for (const f of warnings) {
61
+ lines.push(picocolors_1.default.yellow(` WARN: ${f.message}${f.path ? ` (${f.path})` : ''}`));
62
+ }
63
+ for (const f of infos) {
64
+ lines.push(` INFO: ${f.message}${f.path ? ` (${f.path})` : ''}`);
65
+ }
66
+ lines.push('');
67
+ }
68
+ return lines.join('\n');
69
+ }
70
+ function formatDoctorReportJson(report) {
71
+ return JSON.stringify(report, null, 2);
72
+ }
73
+ async function writeDoctorReportFile(reportPath, report, format) {
74
+ await fs_extra_1.default.ensureDir(node_path_1.default.dirname(reportPath));
75
+ const body = format === 'json' || reportPath.endsWith('.json')
76
+ ? formatDoctorReportJson(report)
77
+ : formatDoctorReportText(report);
78
+ await fs_extra_1.default.writeFile(reportPath, body, 'utf8');
79
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,180 @@
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.TASK_CATEGORY_HINTS = exports.AGENT_BLACKLIST = exports.EMPTY_DIR_WHITELIST = void 0;
7
+ exports.rel = rel;
8
+ exports.isRealFileName = isRealFileName;
9
+ exports.listDirEntries = listDirEntries;
10
+ exports.listRealFilesRecursive = listRealFilesRecursive;
11
+ exports.dirHasOnlyGitkeep = dirHasOnlyGitkeep;
12
+ exports.isEmptyOrGitkeepOnly = isEmptyOrGitkeepOnly;
13
+ exports.basenameWithoutExt = basenameWithoutExt;
14
+ exports.readBuildState = readBuildState;
15
+ exports.readHarnessYaml = readHarnessYaml;
16
+ exports.isPhaseNotReached = isPhaseNotReached;
17
+ exports.isDeferredEmptyDir = isDeferredEmptyDir;
18
+ exports.isBaselineCodeMirror = isBaselineCodeMirror;
19
+ exports.isSkillReferencePlaceholder = isSkillReferencePlaceholder;
20
+ const fs_extra_1 = __importDefault(require("fs-extra"));
21
+ const node_path_1 = __importDefault(require("node:path"));
22
+ const js_yaml_1 = __importDefault(require("js-yaml"));
23
+ const yaml_safe_path_1 = require("../../utils/yaml-safe-path");
24
+ const SKIP_FILE_NAMES = new Set(['.gitkeep', '.DS_Store', 'Thumbs.db']);
25
+ function rel(harnessRoot, abs) {
26
+ return node_path_1.default.relative(harnessRoot, abs).replace(/\\/g, '/');
27
+ }
28
+ function isRealFileName(name) {
29
+ if (SKIP_FILE_NAMES.has(name))
30
+ return false;
31
+ if (name.startsWith('.'))
32
+ return false;
33
+ return true;
34
+ }
35
+ async function listDirEntries(dir) {
36
+ if (!(await fs_extra_1.default.pathExists(dir)))
37
+ return [];
38
+ const names = await fs_extra_1.default.readdir(dir);
39
+ return names.filter(isRealFileName);
40
+ }
41
+ async function listRealFilesRecursive(root, opts) {
42
+ const out = [];
43
+ if (!(await fs_extra_1.default.pathExists(root)))
44
+ return out;
45
+ async function walk(dir) {
46
+ const entries = await fs_extra_1.default.readdir(dir, { withFileTypes: true });
47
+ for (const ent of entries) {
48
+ if (!isRealFileName(ent.name))
49
+ continue;
50
+ const full = node_path_1.default.join(dir, ent.name);
51
+ if (ent.isDirectory()) {
52
+ await walk(full);
53
+ }
54
+ else if (ent.isFile()) {
55
+ if (opts?.extensions) {
56
+ const ext = node_path_1.default.extname(ent.name).toLowerCase();
57
+ if (!opts.extensions.includes(ext))
58
+ continue;
59
+ }
60
+ out.push(full);
61
+ }
62
+ }
63
+ }
64
+ await walk(root);
65
+ return out;
66
+ }
67
+ async function dirHasOnlyGitkeep(dir) {
68
+ if (!(await fs_extra_1.default.pathExists(dir)))
69
+ return false;
70
+ const entries = await fs_extra_1.default.readdir(dir);
71
+ const meaningful = entries.filter((n) => n !== '.gitkeep' && !n.startsWith('.'));
72
+ if (meaningful.length === 0) {
73
+ return entries.includes('.gitkeep') || entries.length === 0;
74
+ }
75
+ return false;
76
+ }
77
+ async function isEmptyOrGitkeepOnly(dir) {
78
+ if (!(await fs_extra_1.default.pathExists(dir)))
79
+ return true;
80
+ const entries = await fs_extra_1.default.readdir(dir);
81
+ const meaningful = entries.filter((n) => n !== '.gitkeep' && !n.startsWith('.'));
82
+ return meaningful.length === 0;
83
+ }
84
+ function basenameWithoutExt(filePath) {
85
+ return node_path_1.default.basename(filePath, node_path_1.default.extname(filePath));
86
+ }
87
+ async function readBuildState(harnessRoot) {
88
+ const statePath = node_path_1.default.join(harnessRoot, '.harness-build-state.yaml');
89
+ if (!(await fs_extra_1.default.pathExists(statePath)))
90
+ return undefined;
91
+ try {
92
+ const raw = await fs_extra_1.default.readFile(statePath, 'utf8');
93
+ const body = raw.replace(/^#.*\n/gm, '');
94
+ return js_yaml_1.default.load(body);
95
+ }
96
+ catch {
97
+ return undefined;
98
+ }
99
+ }
100
+ async function readHarnessYaml(harnessRoot) {
101
+ const p = node_path_1.default.join(harnessRoot, 'harness.yaml');
102
+ if (!(await fs_extra_1.default.pathExists(p)))
103
+ return {};
104
+ try {
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 };
108
+ }
109
+ catch (err) {
110
+ return { error: err.message };
111
+ }
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
+ }
156
+ exports.EMPTY_DIR_WHITELIST = new Set([
157
+ 'commands/install',
158
+ 'commands/update',
159
+ 'agent-env/tools',
160
+ ]);
161
+ exports.AGENT_BLACKLIST = [
162
+ 'qoder',
163
+ 'codechat',
164
+ 'cursor',
165
+ 'claude-code',
166
+ 'opencode',
167
+ 'claude',
168
+ 'chatgpt',
169
+ 'gpt-4',
170
+ '.qoder/',
171
+ '.codechat/',
172
+ '.cursor/',
173
+ '.claude/',
174
+ '.opencode/',
175
+ ];
176
+ exports.TASK_CATEGORY_HINTS = [
177
+ ['新增', '信号', '接口', '数据', 'increment', 'add'],
178
+ ['重构', '适配', 'refactor', 'adapt', 'migrate'],
179
+ ['测试', '校验', 'test', 'verify', 'validation', 'check'],
180
+ ];
@@ -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.runDoctor = runDoctor;
7
+ const fs_extra_1 = __importDefault(require("fs-extra"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const index_1 = require("./doctor/index");
10
+ const logger_1 = require("../utils/logger");
11
+ async function runDoctor(opts) {
12
+ if (opts.verbose)
13
+ (0, logger_1.setVerbose)(true);
14
+ const cwd = opts.cwd ?? process.cwd();
15
+ const harnessInput = opts.harness?.trim();
16
+ if (!harnessInput) {
17
+ throw new Error('--harness <path> 是必填参数(可在配置文件的 doctor.harness 中提供)');
18
+ }
19
+ const harnessRoot = node_path_1.default.resolve(cwd, harnessInput);
20
+ if (!(await fs_extra_1.default.pathExists(harnessRoot))) {
21
+ throw new Error(`harness 目录不存在:${harnessRoot}`);
22
+ }
23
+ const stateFile = node_path_1.default.join(harnessRoot, '.harness-build-state.yaml');
24
+ if (!(await fs_extra_1.default.pathExists(stateFile))) {
25
+ throw new Error(`未找到 .harness-build-state.yaml,请先在该目录运行 svharness build:${harnessRoot}`);
26
+ }
27
+ const mode = opts.mode ?? 'pre-seal';
28
+ if (mode !== 'pre-seal' && mode !== 'health') {
29
+ throw new Error(`--mode 仅支持 pre-seal | health,收到:${mode}`);
30
+ }
31
+ const format = opts.format ?? 'text';
32
+ if (format !== 'text' && format !== 'json') {
33
+ throw new Error(`--format 仅支持 text | json,收到:${format}`);
34
+ }
35
+ const strict = !!opts.strict;
36
+ logger_1.logger.info(`doctor 检查:${harnessRoot}(mode=${mode}${strict ? ', strict' : ''})`);
37
+ const report = await (0, index_1.runDoctorChecks)({ harnessRoot, mode, strict });
38
+ const body = await (0, index_1.renderDoctorReport)(report, format);
39
+ logger_1.logger.plain(body);
40
+ const outPath = opts.report?.trim()
41
+ ? node_path_1.default.resolve(cwd, opts.report.trim())
42
+ : node_path_1.default.join(harnessRoot, 'doctor-report.json');
43
+ await (0, index_1.writeDoctorReportFile)(outPath, report, 'json');
44
+ logger_1.logger.info(`JSON 报告已写入:${outPath}`);
45
+ if (report.passed) {
46
+ logger_1.logger.success('doctor 检查通过');
47
+ return 0;
48
+ }
49
+ logger_1.logger.error(`doctor 检查未通过(error: ${report.errorCount})`);
50
+ return 1;
51
+ }
@@ -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
  };
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.saveConfigSection = exports.mergeConvertOptions = exports.mergeApplyOptions = exports.mergeBuildOptions = exports.resolveConfigHarnessName = exports.normalizeConfig = exports.resolveConfigPath = exports.loadConfig = exports.CONFIG_SCHEMA_VERSION = exports.DEFAULT_CONFIG_FILENAME = void 0;
3
+ exports.saveConfigSection = exports.mergeDoctorOptions = exports.mergeConvertOptions = exports.mergeApplyOptions = exports.mergeBuildOptions = exports.resolveConfigHarnessName = exports.normalizeConfig = exports.resolveConfigPath = exports.loadConfig = exports.CONFIG_SCHEMA_VERSION = exports.DEFAULT_CONFIG_FILENAME = void 0;
4
4
  var constants_1 = require("./constants");
5
5
  Object.defineProperty(exports, "DEFAULT_CONFIG_FILENAME", { enumerable: true, get: function () { return constants_1.DEFAULT_CONFIG_FILENAME; } });
6
6
  Object.defineProperty(exports, "CONFIG_SCHEMA_VERSION", { enumerable: true, get: function () { return constants_1.CONFIG_SCHEMA_VERSION; } });
@@ -14,5 +14,6 @@ var merge_options_1 = require("./merge-options");
14
14
  Object.defineProperty(exports, "mergeBuildOptions", { enumerable: true, get: function () { return merge_options_1.mergeBuildOptions; } });
15
15
  Object.defineProperty(exports, "mergeApplyOptions", { enumerable: true, get: function () { return merge_options_1.mergeApplyOptions; } });
16
16
  Object.defineProperty(exports, "mergeConvertOptions", { enumerable: true, get: function () { return merge_options_1.mergeConvertOptions; } });
17
+ Object.defineProperty(exports, "mergeDoctorOptions", { enumerable: true, get: function () { return merge_options_1.mergeDoctorOptions; } });
17
18
  var save_config_1 = require("./save-config");
18
19
  Object.defineProperty(exports, "saveConfigSection", { enumerable: true, get: function () { return save_config_1.saveConfigSection; } });
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.mergeBuildOptions = mergeBuildOptions;
4
4
  exports.mergeApplyOptions = mergeApplyOptions;
5
5
  exports.mergeConvertOptions = mergeConvertOptions;
6
+ exports.mergeDoctorOptions = mergeDoctorOptions;
6
7
  const normalize_1 = require("./normalize");
7
8
  function getSource(cmd, key) {
8
9
  if (!cmd)
@@ -116,3 +117,15 @@ function mergeConvertOptions(cli, configSection, defaults, cmd) {
116
117
  verbose: pickBool('verbose', cli.verbose, cfg.verbose, defaults?.verbose, cmd),
117
118
  };
118
119
  }
120
+ function mergeDoctorOptions(cli, configSection, defaults, cmd) {
121
+ const cfg = mergeSection(configSection, defaults);
122
+ return {
123
+ harness: pickString('harness', cli.harness, cfg.harness, cmd),
124
+ mode: pickString('mode', cli.mode, cfg.mode, cmd),
125
+ format: pickString('format', cli.format, cfg.format, cmd),
126
+ report: pickString('report', cli.report, cfg.report, cmd),
127
+ strict: pickBool('strict', cli.strict, cfg.strict, undefined, cmd),
128
+ yes: pickBool('yes', cli.yes, cfg.yes, defaults?.yes, cmd),
129
+ verbose: pickBool('verbose', cli.verbose, cfg.verbose, defaults?.verbose, cmd),
130
+ };
131
+ }
@@ -19,6 +19,7 @@ const BUILD_SKILLS = [
19
19
  'harness-build-skill-agent-env-merge.md',
20
20
  'harness-build-skill-knowledge-builder.md',
21
21
  'harness-build-skill-wiki-writer.md',
22
+ 'harness-build-skill-pre-seal-review.md',
22
23
  ];
23
24
  async function writeInjectedFile(input) {
24
25
  const exists = await fs_extra_1.default.pathExists(input.dstPath);
@@ -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
  ];
@@ -16,7 +16,8 @@ const logger_1 = require("../utils/logger");
16
16
  *
17
17
  * Phase order: S00_bootstrap → [S10_wiki] → S20_collect_inputs → S30_convert_docs → S40_extract_requirements
18
18
  * → S50_generate_specs → S60_process_references → S61_confirm_baseline_extraction
19
- * → S65_customize_agent_env → S70_runtime_assets → S80_seed_memory → S90_finalize
19
+ * → S65_customize_agent_env → S70_runtime_assets → S80_seed_memory
20
+ * → S85_pre_seal_validation → S90_finalize
20
21
  *
21
22
  * Design: S10_wiki comes before S20_collect_inputs because baseline wiki is the project
22
23
  * experience foundation — it provides architectural knowledge that helps the
@@ -42,11 +43,25 @@ function buildInitialState(input) {
42
43
  };
43
44
  }
44
45
  // S20_collect_inputs: collect related inputs (requirements/references/optional extra skills & rules)
45
- phases.S20_collect_inputs = { status: 'PENDING', manual_required: true };
46
+ phases.S20_collect_inputs = {
47
+ status: 'PENDING',
48
+ manual_required: true,
49
+ task_list_file: 'task_list.md',
50
+ };
46
51
  // S30_convert_docs: convert non-md raw docs to markdown for downstream entry-ization
47
52
  phases.S30_convert_docs = { status: 'PENDING', requires_agent: true };
48
- phases.S40_extract_requirements = { status: 'PENDING', requires_agent: true };
49
- phases.S50_generate_specs = { status: 'PENDING', requires_agent: true };
53
+ phases.S40_extract_requirements = {
54
+ status: 'PENDING',
55
+ requires_agent: true,
56
+ coverage_report: 'requirements/coverage-report.yaml',
57
+ coverage_ratio: null,
58
+ };
59
+ phases.S50_generate_specs = {
60
+ status: 'PENDING',
61
+ requires_agent: true,
62
+ spec_coverage_report: 'specs/coverage-report.yaml',
63
+ spec_coverage_ratio: null,
64
+ };
50
65
  // S60: references convert + structured indexing
51
66
  phases.S60_process_references = { status: 'PENDING', requires_agent: true };
52
67
  // S61: explicitly confirm whether baseline auto-extraction (skills/rules) is enabled
@@ -67,12 +82,21 @@ function buildInitialState(input) {
67
82
  };
68
83
  phases.S70_runtime_assets = { status: 'PENDING', requires_agent: true };
69
84
  phases.S80_seed_memory = { status: 'PENDING', requires_agent: true };
85
+ phases.S85_pre_seal_validation = {
86
+ status: 'PENDING',
87
+ requires_agent: true,
88
+ doctor_required: true,
89
+ review_report_path: 'HARNESS-REVIEW-REPORT.md',
90
+ depth_review_required: true,
91
+ min_depth_score: 5.0,
92
+ min_depth_grade: 'C',
93
+ };
70
94
  phases.S90_finalize = { status: 'PENDING' };
71
95
  // current_phase starts at S10_wiki (if present) or S20_collect_inputs (if no baseline)
72
96
  const startPhase = wp !== 'none' ? 'S10_wiki' : 'S20_collect_inputs';
73
97
  const startAction = wp !== 'none'
74
98
  ? '请先构建 baseline wiki(项目经验底座),完成后进入 S20 输入文件收集。'
75
- : '请先完成 S20 输入文件收集:requirements/raw、references/raw,并确认是否有额外 skills/rules。';
99
+ : '请先完成 S20 输入文件收集:requirements/raw、references/raw,并按 task_list.md 执行阶段门禁。';
76
100
  const state = {
77
101
  project: input.name,
78
102
  arch: input.arch,
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ const init_1 = require("./commands/init");
9
9
  const apply_1 = require("./commands/apply");
10
10
  const convert_1 = require("./commands/convert");
11
11
  const wizard_1 = require("./commands/wizard");
12
+ const doctor_1 = require("./commands/doctor");
12
13
  const config_1 = require("./config");
13
14
  /**
14
15
  * Default values surfaced to users (also reflected in README "全部参数" tables).
@@ -314,6 +315,46 @@ function main() {
314
315
  process.exitCode = 1;
315
316
  }
316
317
  });
318
+ program
319
+ .command('doctor')
320
+ .description('检查 harness 健康度与封存前完整性(阶段状态、空目录、文档配对、specs schema 等)。')
321
+ .option('--config <path>', `从配置文件读取 doctor 节(默认 ${config_1.DEFAULT_CONFIG_FILENAME})`)
322
+ .option('--harness <path>', 'harness 根目录(可在配置文件的 doctor.harness 中提供)')
323
+ .option('--mode <mode>', '检查模式:pre-seal(封存前,默认)| health(日常巡检)', 'pre-seal')
324
+ .option('--format <format>', '输出格式:text | json', 'text')
325
+ .option('--report <path>', 'JSON 报告输出路径(默认 <harness>/doctor-report.json)')
326
+ .option('--strict', '将 warning 视为 error')
327
+ .option('-y, --yes', '跳过交互确认(预留)')
328
+ .option('--verbose', '显示详细日志')
329
+ .action(async (opts, cmd) => {
330
+ try {
331
+ const loaded = (0, config_1.loadConfig)({ configPath: opts.config });
332
+ const merged = (0, config_1.mergeDoctorOptions)(opts, loaded?.config.doctor, loaded?.config.defaults, cmd);
333
+ const mode = merged.mode ?? 'pre-seal';
334
+ if (mode !== 'pre-seal' && mode !== 'health') {
335
+ throw new Error(`--mode 仅支持 pre-seal | health,收到:${mode}`);
336
+ }
337
+ const format = merged.format ?? 'text';
338
+ if (format !== 'text' && format !== 'json') {
339
+ throw new Error(`--format 仅支持 text | json,收到:${format}`);
340
+ }
341
+ const exitCode = await (0, doctor_1.runDoctor)({
342
+ harness: merged.harness ?? '',
343
+ mode,
344
+ format,
345
+ report: merged.report,
346
+ strict: !!merged.strict,
347
+ verbose: !!merged.verbose,
348
+ });
349
+ if (exitCode !== 0) {
350
+ process.exitCode = exitCode;
351
+ }
352
+ }
353
+ catch (err) {
354
+ logger_1.logger.error(err.message);
355
+ process.exitCode = 1;
356
+ }
357
+ });
317
358
  program.parseAsync(process.argv).catch((err) => {
318
359
  logger_1.logger.error(err.message);
319
360
  process.exitCode = 1;