svharness 0.13.3 → 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.
- package/README.md +53 -11
- package/dist/commands/apply.js +268 -26
- package/dist/commands/doctor/check-bootstrap.js +32 -0
- package/dist/commands/doctor/check-chinese-heuristic.js +64 -0
- package/dist/commands/doctor/check-convert-pairing.js +54 -0
- package/dist/commands/doctor/check-empty-dirs.js +51 -0
- package/dist/commands/doctor/check-incoming.js +47 -0
- package/dist/commands/doctor/check-memory.js +23 -0
- package/dist/commands/doctor/check-policy-grep.js +44 -0
- package/dist/commands/doctor/check-references.js +78 -0
- package/dist/commands/doctor/check-requirements.js +69 -0
- package/dist/commands/doctor/check-skills-tasks.js +103 -0
- package/dist/commands/doctor/check-specs.js +83 -0
- package/dist/commands/doctor/check-state-phases.js +93 -0
- package/dist/commands/doctor/check-wiki.js +52 -0
- package/dist/commands/doctor/index.js +101 -0
- package/dist/commands/doctor/report.js +79 -0
- package/dist/commands/doctor/types.js +2 -0
- package/dist/commands/doctor/utils.js +130 -0
- package/dist/commands/doctor.js +51 -0
- package/dist/commands/init.js +2 -2
- package/dist/config/index.js +2 -1
- package/dist/config/merge-options.js +16 -0
- package/dist/core/agent-injector.js +50 -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/core/state.js +7 -1
- package/dist/index.js +57 -4
- package/dist/utils/skill-md-validate.js +85 -0
- package/package.json +3 -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-orchestrator-flow.md +8 -1
- package/templates/_shared/build-rules/harness-build-rule-pre-seal-review.md +39 -0
- package/templates/_shared/build-skills/harness-build-skill-agent-env-merge.md +2 -0
- package/templates/_shared/build-skills/harness-build-skill-orchestrator.md +7 -1
- package/templates/_shared/build-skills/harness-build-skill-pre-seal-review.md +51 -0
- package/templates/_shared/build-skills/harness-build-skills-main.md +1 -0
- package/templates/_shared/meta/AGENTS_APPLY.md.ejs +69 -32
- package/templates/_shared/meta/README.md.ejs +2 -1
- package/templates/svharness.config.example.yaml +12 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
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.checkSkillsTasks = checkSkillsTasks;
|
|
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 skill_md_validate_1 = require("../../utils/skill-md-validate");
|
|
11
|
+
async function countTaskCategories(templatesDir) {
|
|
12
|
+
if (!(await fs_extra_1.default.pathExists(templatesDir)))
|
|
13
|
+
return 0;
|
|
14
|
+
const entries = await fs_extra_1.default.readdir(templatesDir, { withFileTypes: true });
|
|
15
|
+
let matched = 0;
|
|
16
|
+
const seen = new Set();
|
|
17
|
+
for (const ent of entries) {
|
|
18
|
+
if (!ent.isDirectory())
|
|
19
|
+
continue;
|
|
20
|
+
const dirPath = node_path_1.default.join(templatesDir, ent.name);
|
|
21
|
+
const mdFiles = (await fs_extra_1.default.readdir(dirPath)).filter((n) => n.endsWith('.md'));
|
|
22
|
+
if (mdFiles.length === 0)
|
|
23
|
+
continue;
|
|
24
|
+
const haystack = `${ent.name} ${mdFiles.join(' ')}`.toLowerCase();
|
|
25
|
+
for (let i = 0; i < utils_1.TASK_CATEGORY_HINTS.length; i++) {
|
|
26
|
+
if (seen.has(i))
|
|
27
|
+
continue;
|
|
28
|
+
if (utils_1.TASK_CATEGORY_HINTS[i].some((kw) => haystack.includes(kw.toLowerCase()))) {
|
|
29
|
+
seen.add(i);
|
|
30
|
+
matched++;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (matched < 3) {
|
|
35
|
+
const flatMd = await fs_extra_1.default
|
|
36
|
+
.readdir(templatesDir)
|
|
37
|
+
.then((names) => names.filter((n) => n.endsWith('.md')));
|
|
38
|
+
if (flatMd.length >= 3)
|
|
39
|
+
matched = 3;
|
|
40
|
+
}
|
|
41
|
+
return matched;
|
|
42
|
+
}
|
|
43
|
+
async function checkSkillsTasks(ctx) {
|
|
44
|
+
const id = 'skills-tasks';
|
|
45
|
+
const findings = [];
|
|
46
|
+
const skillsDir = node_path_1.default.join(ctx.harnessRoot, 'agent-env', 'skills');
|
|
47
|
+
const templatesDir = node_path_1.default.join(ctx.harnessRoot, 'tasks', 'templates');
|
|
48
|
+
if (!(await fs_extra_1.default.pathExists(skillsDir))) {
|
|
49
|
+
findings.push({
|
|
50
|
+
checkId: id,
|
|
51
|
+
severity: 'error',
|
|
52
|
+
message: '缺少 agent-env/skills/',
|
|
53
|
+
path: 'agent-env/skills',
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
const skillDirs = (await fs_extra_1.default.readdir(skillsDir, { withFileTypes: true })).filter((d) => d.isDirectory() && !d.name.startsWith('.'));
|
|
58
|
+
const runtimeSkills = skillDirs.filter((d) => !d.name.startsWith('harness-build-'));
|
|
59
|
+
if (runtimeSkills.length === 0) {
|
|
60
|
+
findings.push({
|
|
61
|
+
checkId: id,
|
|
62
|
+
severity: 'error',
|
|
63
|
+
message: 'agent-env/skills/ 无运行期 skill 子目录',
|
|
64
|
+
path: 'agent-env/skills',
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
for (const dir of runtimeSkills) {
|
|
68
|
+
const skillMd = node_path_1.default.join(skillsDir, dir.name, 'SKILL.md');
|
|
69
|
+
if (!(await fs_extra_1.default.pathExists(skillMd))) {
|
|
70
|
+
findings.push({
|
|
71
|
+
checkId: id,
|
|
72
|
+
severity: 'error',
|
|
73
|
+
message: `skill 目录缺少 SKILL.md:${dir.name}`,
|
|
74
|
+
path: (0, utils_1.rel)(ctx.harnessRoot, node_path_1.default.join(skillsDir, dir.name)),
|
|
75
|
+
});
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
for (const msg of await (0, skill_md_validate_1.validateFrontmatterFields)({
|
|
79
|
+
targetRoot: ctx.harnessRoot,
|
|
80
|
+
file: skillMd,
|
|
81
|
+
requiredKeys: ['name', 'description'],
|
|
82
|
+
})) {
|
|
83
|
+
findings.push({ checkId: id, severity: 'error', message: msg, path: (0, utils_1.rel)(ctx.harnessRoot, skillMd) });
|
|
84
|
+
}
|
|
85
|
+
for (const msg of await (0, skill_md_validate_1.validateSkillNameMatchesDir)({
|
|
86
|
+
targetRoot: ctx.harnessRoot,
|
|
87
|
+
file: skillMd,
|
|
88
|
+
})) {
|
|
89
|
+
findings.push({ checkId: id, severity: 'warning', message: msg, path: (0, utils_1.rel)(ctx.harnessRoot, skillMd) });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const categoryCount = await countTaskCategories(templatesDir);
|
|
94
|
+
if (categoryCount < 3) {
|
|
95
|
+
findings.push({
|
|
96
|
+
checkId: id,
|
|
97
|
+
severity: 'error',
|
|
98
|
+
message: `tasks/templates/ 须覆盖三类典型场景(当前识别 ${categoryCount}/3)`,
|
|
99
|
+
path: 'tasks/templates',
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return { id, title: 'Skills 与 Tasks', findings };
|
|
103
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
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.checkSpecs = checkSpecs;
|
|
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 ajv_1 = __importDefault(require("ajv"));
|
|
11
|
+
const ajv_formats_1 = __importDefault(require("ajv-formats"));
|
|
12
|
+
const utils_1 = require("./utils");
|
|
13
|
+
const SPEC_DOMAINS = ['signals', 'ui', 'behavior', 'interfaces'];
|
|
14
|
+
async function checkSpecs(ctx) {
|
|
15
|
+
const id = 'specs';
|
|
16
|
+
const findings = [];
|
|
17
|
+
const ajv = new ajv_1.default({ allErrors: true, strict: false });
|
|
18
|
+
(0, ajv_formats_1.default)(ajv);
|
|
19
|
+
for (const domain of SPEC_DOMAINS) {
|
|
20
|
+
const domainDir = node_path_1.default.join(ctx.harnessRoot, 'specs', domain);
|
|
21
|
+
const schemaPath = node_path_1.default.join(domainDir, 'schema.json');
|
|
22
|
+
if (!(await fs_extra_1.default.pathExists(schemaPath))) {
|
|
23
|
+
findings.push({
|
|
24
|
+
checkId: id,
|
|
25
|
+
severity: 'error',
|
|
26
|
+
message: `缺少 schema:specs/${domain}/schema.json`,
|
|
27
|
+
path: `specs/${domain}/schema.json`,
|
|
28
|
+
});
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const specFiles = (await (0, utils_1.listRealFilesRecursive)(domainDir, { extensions: ['.yaml', '.yml'] })).filter((f) => !f.endsWith('schema.json') && !f.endsWith('schema.yaml'));
|
|
32
|
+
const moduleYaml = specFiles.filter((f) => node_path_1.default.basename(f) !== 'schema.json');
|
|
33
|
+
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
|
+
});
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
let validate;
|
|
43
|
+
try {
|
|
44
|
+
const schema = JSON.parse(await fs_extra_1.default.readFile(schemaPath, 'utf8'));
|
|
45
|
+
validate = ajv.compile(schema);
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
findings.push({
|
|
49
|
+
checkId: id,
|
|
50
|
+
severity: 'error',
|
|
51
|
+
message: `specs/${domain}/schema.json 无效:${err.message}`,
|
|
52
|
+
path: `specs/${domain}/schema.json`,
|
|
53
|
+
});
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
for (const file of moduleYaml) {
|
|
57
|
+
try {
|
|
58
|
+
const doc = js_yaml_1.default.load(await fs_extra_1.default.readFile(file, 'utf8'));
|
|
59
|
+
if (!validate(doc)) {
|
|
60
|
+
const detail = (validate.errors ?? [])
|
|
61
|
+
.slice(0, 3)
|
|
62
|
+
.map((e) => `${e.instancePath || '/'} ${e.message}`)
|
|
63
|
+
.join('; ');
|
|
64
|
+
findings.push({
|
|
65
|
+
checkId: id,
|
|
66
|
+
severity: 'error',
|
|
67
|
+
message: `schema 校验失败:${detail}`,
|
|
68
|
+
path: (0, utils_1.rel)(ctx.harnessRoot, file),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
findings.push({
|
|
74
|
+
checkId: id,
|
|
75
|
+
severity: 'error',
|
|
76
|
+
message: `YAML 解析失败:${err.message}`,
|
|
77
|
+
path: (0, utils_1.rel)(ctx.harnessRoot, file),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { id, title: 'Specs 四域与 schema', findings };
|
|
83
|
+
}
|