@givolt/i3-composer-agent 2.2.0 → 2.2.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/.agents/plugins/marketplace.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +52 -13
- package/bin/asset-fingerprint.cjs +1 -0
- package/bin/i3-composer-agent.cjs +10 -6
- package/bin/install.cjs +24 -9
- package/bin/native-content.cjs +1 -0
- package/bin/tool-selector.cjs +164 -0
- package/bin/wizard.cjs +7 -44
- package/docs/codebuddy-setup.md +11 -3
- package/docs/codex-setup.md +6 -3
- package/docs/workbuddy-setup.md +8 -1
- package/evals/README.md +86 -0
- package/evals/cases/i3-api-and-interface-design.json +45 -0
- package/evals/cases/i3-browser-testing-with-devtools.json +44 -0
- package/evals/cases/i3-ci-cd-and-automation.json +45 -0
- package/evals/cases/i3-code-review-and-quality.json +49 -0
- package/evals/cases/i3-code-simplification.json +45 -0
- package/evals/cases/i3-constraint-driven-development.json +79 -0
- package/evals/cases/i3-context-engineering.json +44 -0
- package/evals/cases/i3-debugging-and-error-recovery.json +58 -0
- package/evals/cases/i3-deprecation-and-migration.json +52 -0
- package/evals/cases/i3-documentation-and-adrs.json +44 -0
- package/evals/cases/i3-doubt-driven-development.json +43 -0
- package/evals/cases/i3-frontend-ui-engineering.json +52 -0
- package/evals/cases/i3-git-workflow-and-versioning.json +47 -0
- package/evals/cases/i3-idea-refine.json +43 -0
- package/evals/cases/i3-incremental-implementation.json +57 -0
- package/evals/cases/i3-interview-me.json +42 -0
- package/evals/cases/i3-observability-and-instrumentation.json +44 -0
- package/evals/cases/i3-performance-optimization.json +53 -0
- package/evals/cases/i3-planning-and-task-breakdown.json +45 -0
- package/evals/cases/i3-security-and-hardening.json +53 -0
- package/evals/cases/i3-shipping-and-launch.json +56 -0
- package/evals/cases/i3-source-driven-development.json +44 -0
- package/evals/cases/i3-spec-driven-development.json +72 -0
- package/evals/cases/i3-test-driven-development.json +73 -0
- package/evals/cases/i3-using-agent-skills.json +44 -0
- package/evals/fixtures/i3-api-and-interface-design/service-brief.md +19 -0
- package/evals/fixtures/i3-browser-testing-with-devtools/README.md +5 -0
- package/evals/fixtures/i3-browser-testing-with-devtools/index.html +24 -0
- package/evals/fixtures/i3-browser-testing-with-devtools/server.js +15 -0
- package/evals/fixtures/i3-ci-cd-and-automation/package.json +8 -0
- package/evals/fixtures/i3-ci-cd-and-automation/src/slug.js +3 -0
- package/evals/fixtures/i3-ci-cd-and-automation/test/slug.test.js +9 -0
- package/evals/fixtures/i3-code-review-and-quality/user-search.diff +16 -0
- package/evals/fixtures/i3-code-simplification/config-parser.js +46 -0
- package/evals/fixtures/i3-code-simplification/config-parser.test.js +15 -0
- package/evals/fixtures/i3-context-engineering/context-audit.md +15 -0
- package/evals/fixtures/i3-debugging-and-error-recovery/pagination.js +8 -0
- package/evals/fixtures/i3-debugging-and-error-recovery/pagination.test.js +9 -0
- package/evals/fixtures/i3-debugging-and-error-recovery/time-pressure.md +6 -0
- package/evals/fixtures/i3-deprecation-and-migration/api-inventory.md +9 -0
- package/evals/fixtures/i3-documentation-and-adrs/decision-context.md +16 -0
- package/evals/fixtures/i3-doubt-driven-development/migration-plan.md +19 -0
- package/evals/fixtures/i3-frontend-ui-engineering/Button.tsx +7 -0
- package/evals/fixtures/i3-frontend-ui-engineering/design-system.md +11 -0
- package/evals/fixtures/i3-git-workflow-and-versioning/.eval/working-tree.patch +21 -0
- package/evals/fixtures/i3-git-workflow-and-versioning/app.js +7 -0
- package/evals/fixtures/i3-git-workflow-and-versioning/app.test.js +9 -0
- package/evals/fixtures/i3-incremental-implementation/reports.js +7 -0
- package/evals/fixtures/i3-incremental-implementation/reports.test.js +12 -0
- package/evals/fixtures/i3-incremental-implementation/tasks/plan.md +8 -0
- package/evals/fixtures/i3-observability-and-instrumentation/operations.md +11 -0
- package/evals/fixtures/i3-observability-and-instrumentation/payment-retry.js +14 -0
- package/evals/fixtures/i3-performance-optimization/benchmark.js +15 -0
- package/evals/fixtures/i3-performance-optimization/products.js +14 -0
- package/evals/fixtures/i3-planning-and-task-breakdown/notifications-spec.md +18 -0
- package/evals/fixtures/i3-security-and-hardening/webhook.js +11 -0
- package/evals/fixtures/i3-security-and-hardening/webhook.test.js +13 -0
- package/evals/fixtures/i3-shipping-and-launch/authority-pressure.md +6 -0
- package/evals/fixtures/i3-shipping-and-launch/launch-status.md +11 -0
- package/evals/fixtures/i3-source-driven-development/framework-task.md +10 -0
- package/evals/fixtures/i3-spec-driven-development/billing-brief.md +16 -0
- package/evals/fixtures/i3-test-driven-development/BUG.md +10 -0
- package/evals/fixtures/i3-test-driven-development/README.md +28 -0
- package/evals/fixtures/i3-test-driven-development/package.json +9 -0
- package/evals/fixtures/i3-test-driven-development/src/split.js +8 -0
- package/evals/fixtures/i3-test-driven-development/test/split.test.js +13 -0
- package/evals/fixtures/i3-using-agent-skills/incident.md +6 -0
- package/evals/fixtures/incremental-implementation-pressure/draft-export.js +17 -0
- package/evals/fixtures/incremental-implementation-pressure/scenario.md +9 -0
- package/evals/fixtures/spec-driven-development-decomposition/portal-brief.md +22 -0
- package/evals/fixtures/test-driven-development-ecosystem/README.md +9 -0
- package/evals/fixtures/test-driven-development-ecosystem/ledger.py +15 -0
- package/evals/fixtures/test-driven-development-ecosystem/test_ledger.py +19 -0
- package/evals/plugin/code-review-fires/graders/off-by-one.md +7 -0
- package/evals/plugin/code-review-fires/graders/severity-labels.md +4 -0
- package/evals/plugin/code-review-fires/graders/skill-fired.md +5 -0
- package/evals/plugin/code-review-fires/prompt.md +31 -0
- package/evals/plugin/code-review-stays-quiet/graders/not-fired.md +8 -0
- package/evals/plugin/code-review-stays-quiet/graders/tdd-fired.md +6 -0
- package/evals/plugin/code-review-stays-quiet/prompt.md +8 -0
- package/evals/plugin/code-review-stays-quiet-on-commit-message/graders/not-fired.md +8 -0
- package/evals/plugin/code-review-stays-quiet-on-commit-message/prompt.md +25 -0
- package/evals/zh-CN/cases/i3-api-and-interface-design.json +46 -0
- package/evals/zh-CN/cases/i3-browser-testing-with-devtools.json +45 -0
- package/evals/zh-CN/cases/i3-ci-cd-and-automation.json +46 -0
- package/evals/zh-CN/cases/i3-code-review-and-quality.json +46 -0
- package/evals/zh-CN/cases/i3-code-simplification.json +46 -0
- package/evals/zh-CN/cases/i3-constraint-driven-development.json +70 -0
- package/evals/zh-CN/cases/i3-context-engineering.json +45 -0
- package/evals/zh-CN/cases/i3-debugging-and-error-recovery.json +60 -0
- package/evals/zh-CN/cases/i3-deprecation-and-migration.json +45 -0
- package/evals/zh-CN/cases/i3-documentation-and-adrs.json +45 -0
- package/evals/zh-CN/cases/i3-doubt-driven-development.json +45 -0
- package/evals/zh-CN/cases/i3-frontend-ui-engineering.json +45 -0
- package/evals/zh-CN/cases/i3-git-workflow-and-versioning.json +45 -0
- package/evals/zh-CN/cases/i3-idea-refine.json +44 -0
- package/evals/zh-CN/cases/i3-incremental-implementation.json +59 -0
- package/evals/zh-CN/cases/i3-interview-me.json +43 -0
- package/evals/zh-CN/cases/i3-observability-and-instrumentation.json +46 -0
- package/evals/zh-CN/cases/i3-performance-optimization.json +46 -0
- package/evals/zh-CN/cases/i3-planning-and-task-breakdown.json +46 -0
- package/evals/zh-CN/cases/i3-security-and-hardening.json +46 -0
- package/evals/zh-CN/cases/i3-shipping-and-launch.json +59 -0
- package/evals/zh-CN/cases/i3-source-driven-development.json +45 -0
- package/evals/zh-CN/cases/i3-spec-driven-development.json +62 -0
- package/evals/zh-CN/cases/i3-test-driven-development.json +76 -0
- package/evals/zh-CN/cases/i3-using-agent-skills.json +45 -0
- package/package.json +234 -5
- package/plugin.json +1 -1
- package/scripts/lib/skill-lint.js +586 -0
- package/scripts/run-evals.js +659 -0
- package/scripts/validate-artifact-paths.js +130 -0
- package/scripts/validate-commands.js +224 -0
- package/scripts/validate-reference-links.js +187 -0
- package/scripts/validate-skills.js +70 -0
- package/scripts/validate-versions.js +35 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* validate-artifact-paths.js
|
|
4
|
+
*
|
|
5
|
+
* Guards the spec -> plan -> build pipeline against silent artifact-path drift.
|
|
6
|
+
*
|
|
7
|
+
* The `/spec` and `/plan` commands (producers) write their artifacts to a set
|
|
8
|
+
* of paths that the `/build` command and the spec/plan skills (consumers) read
|
|
9
|
+
* back. When a producer moves an artifact without updating the consumers — as
|
|
10
|
+
* in PR #93, which pointed `/spec` and `/plan` at docs/features/[name]/ while
|
|
11
|
+
* `/build` still required SPEC.md and tasks/plan.md — the pipeline breaks, and
|
|
12
|
+
* nothing else in CI catches it (command parity only compares descriptions).
|
|
13
|
+
*
|
|
14
|
+
* This validator enforces one canonical set of spec/plan/todo artifact paths
|
|
15
|
+
* across every file in the pipeline. Changing the convention means updating
|
|
16
|
+
* ARTIFACT_ALLOWLIST *and* every guarded file in the same change; CI fails
|
|
17
|
+
* until they agree.
|
|
18
|
+
*
|
|
19
|
+
* Scope is deliberately narrow: only spec/plan/todo artifacts, only the files
|
|
20
|
+
* that define the pipeline. It is not a general markdown path linter.
|
|
21
|
+
*
|
|
22
|
+
* Exit codes: 0 = all clear, 1 = one or more drifted paths.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
'use strict';
|
|
26
|
+
|
|
27
|
+
const fs = require('fs');
|
|
28
|
+
const path = require('path');
|
|
29
|
+
|
|
30
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
31
|
+
|
|
32
|
+
// The canonical spec/plan/todo artifact paths. These are the only artifact
|
|
33
|
+
// file paths the pipeline files may reference. To change the convention, edit
|
|
34
|
+
// this list and update every guarded file to match — CI enforces the pairing.
|
|
35
|
+
const ARTIFACT_ALLOWLIST = new Set([
|
|
36
|
+
'SPEC.md', // spec, project root (produced by /spec, read by /build)
|
|
37
|
+
'docs/SPEC.md', // spec, alternate location accepted by /build
|
|
38
|
+
'tasks/plan.md', // plan (produced by /plan, read by /build)
|
|
39
|
+
'tasks/todo.md', // task list (produced by /plan)
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
// The files that make up the spec -> plan -> build pipeline. Absent files are
|
|
43
|
+
// skipped, not failed: this validator checks path consistency, not presence.
|
|
44
|
+
//
|
|
45
|
+
// The command bodies exist once per host surface (validate-commands.js checks
|
|
46
|
+
// that the three sets stay in step on descriptions, but their prompt bodies
|
|
47
|
+
// are allowed to differ), so a producer or consumer can drift on one surface
|
|
48
|
+
// while the Claude Code copy stays correct. Every surface is guarded here.
|
|
49
|
+
const GUARDED_FILES = [
|
|
50
|
+
// Claude Code commands
|
|
51
|
+
'.claude/commands/spec.md',
|
|
52
|
+
'.claude/commands/plan.md',
|
|
53
|
+
'.claude/commands/build.md',
|
|
54
|
+
// Gemini CLI commands
|
|
55
|
+
'.gemini/commands/spec.toml',
|
|
56
|
+
'.gemini/commands/planning.toml',
|
|
57
|
+
'.gemini/commands/build.toml',
|
|
58
|
+
// Root command set (Antigravity, Codex, and other TOML-based hosts)
|
|
59
|
+
'commands/spec.toml',
|
|
60
|
+
'commands/planning.toml',
|
|
61
|
+
'commands/build.toml',
|
|
62
|
+
// Skills the commands invoke
|
|
63
|
+
'skills/i3-spec-driven-development/SKILL.md',
|
|
64
|
+
'skills/i3-planning-and-task-breakdown/SKILL.md',
|
|
65
|
+
// Docs that tell users where the artifacts live
|
|
66
|
+
'docs/getting-started.md',
|
|
67
|
+
'docs/adoption-guide.md',
|
|
68
|
+
'docs/copilot-setup.md', // its prompt-file aliases name the artifacts directly
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
// Matches a path-like token ending in a spec/plan/todo artifact filename,
|
|
72
|
+
// including an optional directory prefix with bracket placeholders like
|
|
73
|
+
// docs/features/[feature-name]/spec.md. Case-insensitive so SPEC.md and a
|
|
74
|
+
// drifted spec.md are both caught, then compared against the allowlist.
|
|
75
|
+
const ARTIFACT_RE = /(?:[A-Za-z0-9._[\]-]+\/)*(?:spec|plan|todo)\.md/gi;
|
|
76
|
+
|
|
77
|
+
GUARDED_FILES.push(...GUARDED_FILES.filter(relative => /(?:^|\/)commands\//.test(relative)).map(relative => relative.replace(/(commands\/)([^/]+)(\.md|\.toml)$/, '$1i3-$2$3')));
|
|
78
|
+
|
|
79
|
+
function findViolations(relPath) {
|
|
80
|
+
const abs = path.join(ROOT, relPath);
|
|
81
|
+
if (!fs.existsSync(abs)) return null; // skipped
|
|
82
|
+
|
|
83
|
+
const violations = [];
|
|
84
|
+
const lines = fs.readFileSync(abs, 'utf8').split(/\r?\n/);
|
|
85
|
+
lines.forEach((line, i) => {
|
|
86
|
+
const matches = line.match(ARTIFACT_RE);
|
|
87
|
+
if (!matches) return;
|
|
88
|
+
for (const match of matches) {
|
|
89
|
+
if (!ARTIFACT_ALLOWLIST.has(match)) {
|
|
90
|
+
violations.push({ line: i + 1, match });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
return violations;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function main() {
|
|
98
|
+
console.log('Checking spec/plan/todo artifact paths...\n');
|
|
99
|
+
|
|
100
|
+
let checked = 0;
|
|
101
|
+
let errors = 0;
|
|
102
|
+
|
|
103
|
+
for (const relPath of GUARDED_FILES) {
|
|
104
|
+
const violations = findViolations(relPath);
|
|
105
|
+
if (violations === null) continue; // file not present, skip
|
|
106
|
+
checked++;
|
|
107
|
+
|
|
108
|
+
if (violations.length === 0) {
|
|
109
|
+
console.log(` ✓ ${relPath}`);
|
|
110
|
+
} else {
|
|
111
|
+
console.log(` ✗ ${relPath}`);
|
|
112
|
+
for (const { line, match } of violations) {
|
|
113
|
+
console.log(` L${line}: ${match} — not an approved spec/plan/todo artifact path`);
|
|
114
|
+
errors++;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const status = errors > 0 ? 'FAILED' : 'PASSED';
|
|
120
|
+
console.log(`\n${checked} files checked — ${errors} error(s) — ${status}`);
|
|
121
|
+
|
|
122
|
+
if (errors > 0) {
|
|
123
|
+
console.log('\nThe spec -> plan -> build pipeline expects one set of artifact paths.');
|
|
124
|
+
console.log('Either use a path from ARTIFACT_ALLOWLIST, or change the convention');
|
|
125
|
+
console.log('across every guarded file and update the allowlist in the same change.');
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
main();
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* validate-commands.js
|
|
4
|
+
*
|
|
5
|
+
* Guards against silent drift across the three slash-command directories:
|
|
6
|
+
* .claude/commands/ (.md — Claude Code)
|
|
7
|
+
* .gemini/commands/ (.toml — Gemini CLI)
|
|
8
|
+
* commands/ (.toml — Antigravity CLI)
|
|
9
|
+
*
|
|
10
|
+
* Checks (errors block CI):
|
|
11
|
+
* - Every command present in one directory exists in all three
|
|
12
|
+
* - The 'description' field is identical across all three equivalents
|
|
13
|
+
* - Claude command frontmatter is valid YAML, not merely splittable
|
|
14
|
+
*
|
|
15
|
+
* What this does NOT check:
|
|
16
|
+
* Prompt body differences are intentional — each tool has its own
|
|
17
|
+
* syntax ($ARGUMENTS, i3-composer-agent: prefixes, GEMINI.md vs CLAUDE.md).
|
|
18
|
+
*
|
|
19
|
+
* Exit codes: 0 = all clear, 1 = one or more errors
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
|
|
27
|
+
// The same frontmatter-validity rules validate-skills applies to SKILL.md.
|
|
28
|
+
// `descriptionFromMd` below splits each line on its first colon, exactly as the
|
|
29
|
+
// skill reader used to, so a command whose frontmatter is not valid YAML passes
|
|
30
|
+
// every check here — and Claude Code parses that frontmatter when the command is
|
|
31
|
+
// loaded. The #494 thread verified all 25 SKILL.md files AND the command files by
|
|
32
|
+
// hand; this makes the second half a check too.
|
|
33
|
+
const { frontmatterYamlErrors } = require(path.join(__dirname, 'lib', 'skill-lint.js'));
|
|
34
|
+
|
|
35
|
+
// ─── Config ───────────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
38
|
+
|
|
39
|
+
const DIRS = {
|
|
40
|
+
claude: { dir: path.join(ROOT, '.claude', 'commands'), ext: '.md' },
|
|
41
|
+
gemini: { dir: path.join(ROOT, '.gemini', 'commands'), ext: '.toml' },
|
|
42
|
+
antigravity:{ dir: path.join(ROOT, 'commands'), ext: '.toml' },
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Commands where the file stem differs between Claude and the TOML dirs.
|
|
46
|
+
// Key = Claude stem, value = TOML stem.
|
|
47
|
+
const NAME_MAP = {
|
|
48
|
+
plan: 'planning',
|
|
49
|
+
'i3-plan': 'i3-planning',
|
|
50
|
+
};
|
|
51
|
+
const NAME_MAP_REVERSE = Object.fromEntries(
|
|
52
|
+
Object.entries(NAME_MAP).map(([k, v]) => [v, k])
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// ─── Parsers ──────────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
function descriptionFromMd(filePath) {
|
|
58
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
59
|
+
const match = content.match(/^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*\r?\n/);
|
|
60
|
+
if (!match) return null;
|
|
61
|
+
for (const line of match[1].split(/\r?\n/)) {
|
|
62
|
+
const colonIdx = line.indexOf(':');
|
|
63
|
+
if (colonIdx === -1) continue;
|
|
64
|
+
if (line.slice(0, colonIdx).trim() === 'description') {
|
|
65
|
+
return line.slice(colonIdx + 1).trim().replace(/^['"]|['"]$/g, '');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function descriptionFromToml(filePath) {
|
|
72
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
73
|
+
const doubleMatch = content.match(/^description\s*=\s*"((?:[^"\\]|\\.)*)"/m);
|
|
74
|
+
if (doubleMatch) return doubleMatch[1].replace(/\\"/g, '"');
|
|
75
|
+
const singleMatch = content.match(/^description\s*=\s*'([^']*)'/m);
|
|
76
|
+
return singleMatch ? singleMatch[1] : null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─── Loader ───────────────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
function loadCommands({ dir, ext }) {
|
|
82
|
+
if (!fs.existsSync(dir)) return {};
|
|
83
|
+
return Object.fromEntries(
|
|
84
|
+
fs.readdirSync(dir)
|
|
85
|
+
.filter(f => f.endsWith(ext))
|
|
86
|
+
.map(f => {
|
|
87
|
+
const stem = path.basename(f, ext);
|
|
88
|
+
const full = path.join(dir, f);
|
|
89
|
+
try {
|
|
90
|
+
const desc = ext === '.md' ? descriptionFromMd(full) : descriptionFromToml(full);
|
|
91
|
+
return [stem, desc];
|
|
92
|
+
} catch (e) {
|
|
93
|
+
console.log(` ✗ ${stem} — cannot read file: ${e.message}`);
|
|
94
|
+
return [stem, null];
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
function main() {
|
|
103
|
+
console.log("校验各宿主命令目录、元数据和描述一致性。");
|
|
104
|
+
const byTool = {
|
|
105
|
+
claude: loadCommands(DIRS.claude),
|
|
106
|
+
gemini: loadCommands(DIRS.gemini),
|
|
107
|
+
antigravity: loadCommands(DIRS.antigravity),
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Canonical command list: use Claude stems as the reference.
|
|
111
|
+
// Map each Claude stem to its TOML equivalent for lookup.
|
|
112
|
+
const claudeStems = Object.keys(byTool.claude).sort();
|
|
113
|
+
const allTomlStems = new Set([
|
|
114
|
+
...Object.keys(byTool.gemini),
|
|
115
|
+
...Object.keys(byTool.antigravity),
|
|
116
|
+
]);
|
|
117
|
+
const allCanonicalStems = new Set([
|
|
118
|
+
...claudeStems,
|
|
119
|
+
...[...allTomlStems].map(s => NAME_MAP_REVERSE[s] ?? s),
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
let errors = 0;
|
|
123
|
+
|
|
124
|
+
// ── Parity check ────────────────────────────────────────────────────────────
|
|
125
|
+
console.log('Checking command parity...');
|
|
126
|
+
|
|
127
|
+
// Commands in Claude not found in TOML dirs
|
|
128
|
+
for (const stem of claudeStems) {
|
|
129
|
+
const tomlStem = NAME_MAP[stem] ?? stem;
|
|
130
|
+
const missing = [];
|
|
131
|
+
if (!(tomlStem in byTool.gemini)) missing.push('.gemini/commands');
|
|
132
|
+
if (!(tomlStem in byTool.antigravity)) missing.push('commands');
|
|
133
|
+
if (missing.length) {
|
|
134
|
+
console.log(` ✗ ${stem} — missing in: ${missing.join(', ')}`);
|
|
135
|
+
errors++;
|
|
136
|
+
} else {
|
|
137
|
+
console.log(` ✓ ${stem}${stem !== tomlStem ? ` (${tomlStem} in toml dirs)` : ''}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Commands in TOML dirs not found in Claude
|
|
142
|
+
for (const stem of [...allTomlStems].sort()) {
|
|
143
|
+
const claudeStem = NAME_MAP_REVERSE[stem] ?? stem;
|
|
144
|
+
if (!(claudeStem in byTool.claude)) {
|
|
145
|
+
console.log(` ✗ ${stem} — present in toml dirs but missing in .claude/commands`);
|
|
146
|
+
errors++;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── Claude frontmatter validity ─────────────────────────────────────────────
|
|
151
|
+
// Only the .md directory: the TOML dirs are parsed by a real TOML parser
|
|
152
|
+
// already, so a malformed one surfaces as a missing description above.
|
|
153
|
+
console.log('\nChecking Claude command frontmatter...');
|
|
154
|
+
|
|
155
|
+
for (const stem of claudeStems) {
|
|
156
|
+
const full = path.join(DIRS.claude.dir, `${stem}${DIRS.claude.ext}`);
|
|
157
|
+
let yamlErrors = [];
|
|
158
|
+
try {
|
|
159
|
+
yamlErrors = frontmatterYamlErrors(fs.readFileSync(full, 'utf8'));
|
|
160
|
+
} catch (e) {
|
|
161
|
+
console.log(` ✗ ${stem} — cannot read file: ${e.message}`);
|
|
162
|
+
errors++;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (yamlErrors.length === 0) {
|
|
166
|
+
console.log(` ✓ ${stem}`);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
console.log(` ✗ ${stem}`);
|
|
170
|
+
for (const message of yamlErrors) {
|
|
171
|
+
console.log(` ${message}`);
|
|
172
|
+
errors++;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── Description sync check ──────────────────────────────────────────────────
|
|
177
|
+
console.log('\nChecking description sync...');
|
|
178
|
+
|
|
179
|
+
for (const claudeStem of claudeStems) {
|
|
180
|
+
const tomlStem = NAME_MAP[claudeStem] ?? claudeStem;
|
|
181
|
+
const descClaude = byTool.claude[claudeStem];
|
|
182
|
+
const descGemini = byTool.gemini[tomlStem];
|
|
183
|
+
const descAgy = byTool.antigravity[tomlStem];
|
|
184
|
+
|
|
185
|
+
const malformed = [
|
|
186
|
+
['.claude/commands', byTool.claude, claudeStem],
|
|
187
|
+
['.gemini/commands', byTool.gemini, tomlStem],
|
|
188
|
+
['commands/', byTool.antigravity, tomlStem],
|
|
189
|
+
].filter(([, commands, stem]) => Object.prototype.hasOwnProperty.call(commands, stem) && commands[stem] == null);
|
|
190
|
+
|
|
191
|
+
if (malformed.length) {
|
|
192
|
+
console.log(` ✗ ${claudeStem}`);
|
|
193
|
+
for (const [toolDir, , stem] of malformed) {
|
|
194
|
+
console.log(` ${toolDir}/${stem} — missing or malformed description`);
|
|
195
|
+
}
|
|
196
|
+
errors++;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (descClaude == null || descGemini == null || descAgy == null) {
|
|
201
|
+
// Missing file already flagged by parity check
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const allMatch = descClaude === descGemini && descGemini === descAgy;
|
|
206
|
+
|
|
207
|
+
if (allMatch) {
|
|
208
|
+
console.log(` ✓ ${claudeStem}`);
|
|
209
|
+
} else {
|
|
210
|
+
console.log(` ✗ ${claudeStem}`);
|
|
211
|
+
console.log(` .claude: ${descClaude}`);
|
|
212
|
+
console.log(` .gemini: ${descGemini}`);
|
|
213
|
+
console.log(` commands/: ${descAgy}`);
|
|
214
|
+
errors++;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const status = errors > 0 ? 'FAILED' : 'PASSED';
|
|
219
|
+
console.log(`\n${allCanonicalStems.size} commands checked — ${errors} error(s) — ${status}`);
|
|
220
|
+
|
|
221
|
+
if (errors > 0) process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
main();
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* validate-reference-links.js
|
|
4
|
+
*
|
|
5
|
+
* Guards links from skills to the shared `references/` checklists.
|
|
6
|
+
*
|
|
7
|
+
* Those checklists live in the repo-root `references/` directory, but every
|
|
8
|
+
* SKILL.md used to link them as `references/<file>.md` — a path relative to
|
|
9
|
+
* the skill's own directory, which is two levels below the root. All 18 links
|
|
10
|
+
* across 11 skills resolved to files that do not exist, in the repo and in
|
|
11
|
+
* every plugin-install layout (~/.claude/plugins/cache/..., ~/.codex/...).
|
|
12
|
+
* Agents that followed the guidance — for example i3-using-agent-skills pointing
|
|
13
|
+
* at the Definition of Done — hit a file-not-found and stalled.
|
|
14
|
+
*
|
|
15
|
+
* Nothing else in CI catches this: validate-artifact-paths.js is scoped to
|
|
16
|
+
* spec/plan/todo artifacts and is explicitly not a general markdown linter.
|
|
17
|
+
*
|
|
18
|
+
* The rule enforced here: every `references/*.md` link in a SKILL.md must
|
|
19
|
+
* resolve to an existing file relative to that skill's own directory. This
|
|
20
|
+
* accepts both conventions in CLAUDE.md — shared checklists reached via
|
|
21
|
+
* `../../references/`, and a skill's own colocated `references/` directory.
|
|
22
|
+
*
|
|
23
|
+
* The markdown files inside a skill's own `references/` directory get the
|
|
24
|
+
* same check, with each link resolved from the file that contains it. They
|
|
25
|
+
* sit one directory deeper than SKILL.md, so from there the shared checklists
|
|
26
|
+
* are `../../../references/`: the same off-by-a-level mistake, one level down.
|
|
27
|
+
*
|
|
28
|
+
* A `#fragment` after such a link must match a heading in the target file,
|
|
29
|
+
* slugged the way GitHub renders anchors. The file existing is not enough:
|
|
30
|
+
* renaming a heading breaks every link to it while the path still resolves.
|
|
31
|
+
*
|
|
32
|
+
* Scope is deliberately narrow: only `references/*.md` links, only SKILL.md
|
|
33
|
+
* and `skills/<name>/references/*.md` files. It is not a general markdown
|
|
34
|
+
* path linter — skills legitimately mention paths that do not exist yet
|
|
35
|
+
* (`tasks/todo.md`, `PERF.md`, `docs/ideas/[idea-name].md`), and those must
|
|
36
|
+
* not fail the build.
|
|
37
|
+
*
|
|
38
|
+
* Fenced code blocks are exempt for the same reason. Text inside a fence is
|
|
39
|
+
* an example, not a link an agent will follow — and this rule in particular
|
|
40
|
+
* has to be documentable: the failure message below tells authors to write
|
|
41
|
+
* `../../references/<file>.md` rather than `references/<file>.md`, which no
|
|
42
|
+
* skill could show as an example without failing the very check explaining it.
|
|
43
|
+
*
|
|
44
|
+
* Exit codes: 0 = all clear, 1 = one or more unresolvable links.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
'use strict';
|
|
48
|
+
|
|
49
|
+
const fs = require('fs');
|
|
50
|
+
const path = require('path');
|
|
51
|
+
const { stripFencedCodeBlocks } = require('./lib/skill-lint');
|
|
52
|
+
|
|
53
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
54
|
+
const SKILLS_DIR = path.join(ROOT, 'skills');
|
|
55
|
+
|
|
56
|
+
// Matches a link to a references/ markdown file, with any number of leading
|
|
57
|
+
// `../` segments: `references/x.md`, `../../references/x.md`. Anchored on a
|
|
58
|
+
// non-path character so `myreferences/x.md` does not match. An optional
|
|
59
|
+
// `#fragment` is captured separately.
|
|
60
|
+
const REFERENCE_LINK_RE = /(?<![A-Za-z0-9._/-])((?:\.\.\/)*references\/[A-Za-z0-9._-]+\.md)(?:#([\p{L}\p{N}_-]+))?/gu;
|
|
61
|
+
|
|
62
|
+
// GitHub's heading anchor: the rendered text, lowercased, with everything but
|
|
63
|
+
// letters, numbers, `_`, `-` and spaces dropped, then spaces turned to hyphens.
|
|
64
|
+
function slugify(heading) {
|
|
65
|
+
const text = heading
|
|
66
|
+
.replace(/!?\[([^\]]*)\]\([^)]*\)/g, '$1') // links and images keep their text
|
|
67
|
+
.replace(/<[^>]+>/g, ''); // inline HTML renders no text
|
|
68
|
+
return text.toLowerCase().replace(/[^\p{L}\p{M}\p{N}_\- ]/gu, '').replace(/ /g, '-');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// The anchors a file's ATX headings produce, outside fenced blocks. A repeated
|
|
72
|
+
// slug gets `-1`, `-2`, ... as on GitHub.
|
|
73
|
+
function headingAnchors(file) {
|
|
74
|
+
const anchors = new Set();
|
|
75
|
+
const seen = new Map();
|
|
76
|
+
for (const line of stripFencedCodeBlocks(fs.readFileSync(file, 'utf8')).split('\n')) {
|
|
77
|
+
const match = line.match(/^ {0,3}#{1,6}[ \t]+(.*?)(?:[ \t]+#+)?[ \t]*$/);
|
|
78
|
+
if (!match) continue;
|
|
79
|
+
const slug = slugify(match[1]);
|
|
80
|
+
const count = seen.get(slug) || 0;
|
|
81
|
+
anchors.add(count === 0 ? slug : `${slug}-${count}`);
|
|
82
|
+
seen.set(slug, count + 1);
|
|
83
|
+
}
|
|
84
|
+
return anchors;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// A link is resolved from the directory of the file that contains it.
|
|
88
|
+
function findViolations(file) {
|
|
89
|
+
const violations = [];
|
|
90
|
+
const baseDir = path.dirname(file);
|
|
91
|
+
// Share the linter's fence rules; blanked lines preserve diagnostic positions.
|
|
92
|
+
const lines = stripFencedCodeBlocks(fs.readFileSync(file, 'utf8')).split('\n');
|
|
93
|
+
|
|
94
|
+
lines.forEach((line, i) => {
|
|
95
|
+
for (const match of line.matchAll(REFERENCE_LINK_RE)) {
|
|
96
|
+
const [, link, anchor] = match;
|
|
97
|
+
const target = path.resolve(baseDir, link);
|
|
98
|
+
if (!fs.existsSync(target)) {
|
|
99
|
+
violations.push({ line: i + 1, link, target });
|
|
100
|
+
} else if (anchor && !headingAnchors(target).has(anchor)) {
|
|
101
|
+
violations.push({ line: i + 1, link, target, anchor });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return violations;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// The markdown files directly inside a skill's own references/ directory.
|
|
110
|
+
function skillReferenceFiles(skillDir) {
|
|
111
|
+
const dir = path.join(skillDir, 'references');
|
|
112
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) return [];
|
|
113
|
+
return fs.readdirSync(dir)
|
|
114
|
+
.filter((name) => name.endsWith('.md'))
|
|
115
|
+
.sort()
|
|
116
|
+
.map((name) => path.join(dir, name))
|
|
117
|
+
.filter((file) => fs.statSync(file).isFile());
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function toPosix(file) {
|
|
121
|
+
return path.relative(ROOT, file).split(path.sep).join('/');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function main() {
|
|
125
|
+
console.log('Checking references/ links in skills...\n');
|
|
126
|
+
|
|
127
|
+
if (!fs.existsSync(SKILLS_DIR)) {
|
|
128
|
+
console.log('No skills/ directory — nothing to check.');
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let checked = 0;
|
|
133
|
+
let errors = 0;
|
|
134
|
+
let referenceFileErrors = 0;
|
|
135
|
+
let anchorErrors = 0;
|
|
136
|
+
|
|
137
|
+
const skillNames = fs.readdirSync(SKILLS_DIR).sort();
|
|
138
|
+
for (const name of skillNames) {
|
|
139
|
+
const skillDir = path.join(SKILLS_DIR, name);
|
|
140
|
+
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
141
|
+
if (!fs.statSync(skillDir).isDirectory() || !fs.existsSync(skillFile)) continue;
|
|
142
|
+
|
|
143
|
+
checked++;
|
|
144
|
+
for (const file of [skillFile, ...skillReferenceFiles(skillDir)]) {
|
|
145
|
+
const violations = findViolations(file);
|
|
146
|
+
|
|
147
|
+
if (violations.length === 0) {
|
|
148
|
+
console.log(` ✓ ${toPosix(file)}`);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
console.log(` ✗ ${toPosix(file)}`);
|
|
153
|
+
for (const { line, link, target, anchor } of violations) {
|
|
154
|
+
errors++;
|
|
155
|
+
if (anchor) {
|
|
156
|
+
console.log(` L${line}: ${link}#${anchor} — no heading in ${toPosix(target)} produces #${anchor}`);
|
|
157
|
+
anchorErrors++;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
console.log(` L${line}: ${link} — resolves to ${toPosix(target)}, which does not exist`);
|
|
161
|
+
if (file !== skillFile) referenceFileErrors++;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const status = errors > 0 ? 'FAILED' : 'PASSED';
|
|
167
|
+
console.log(`\n${checked} skills checked — ${errors} error(s) — ${status}`);
|
|
168
|
+
|
|
169
|
+
if (errors > 0) {
|
|
170
|
+
if (errors > anchorErrors) {
|
|
171
|
+
console.log('\nLinks to references/ are resolved from the directory of the file that contains them.');
|
|
172
|
+
console.log('Shared checklists live in the repo-root references/, two levels up from a SKILL.md:');
|
|
173
|
+
console.log('use `../../references/<file>.md`, not `references/<file>.md`.');
|
|
174
|
+
if (referenceFileErrors > 0) {
|
|
175
|
+
console.log('From a file inside skills/<name>/references/ they are three levels up:');
|
|
176
|
+
console.log('use `../../../references/<file>.md`.');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (anchorErrors > 0) {
|
|
180
|
+
console.log("\nAnchors are matched against GitHub's heading slugs: the heading text lowercased,");
|
|
181
|
+
console.log('punctuation dropped, spaces turned to hyphens, and -1, -2, ... for repeated headings.');
|
|
182
|
+
}
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
main();
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* validate-skills.js
|
|
4
|
+
*
|
|
5
|
+
* CLI that validates every skill in skills/ against the rules in
|
|
6
|
+
* docs/skill-anatomy.md. The rules themselves live in scripts/lib/skill-lint.js
|
|
7
|
+
* (a single source of truth, importable and unit-testable); this file is a thin
|
|
8
|
+
* wrapper that walks the skills directory, runs the linter, prints the report,
|
|
9
|
+
* and sets the exit code.
|
|
10
|
+
*
|
|
11
|
+
* Exit codes: 0 = all clear, 1 = one or more errors
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
const { lintSkill } = require('./lib/skill-lint');
|
|
20
|
+
|
|
21
|
+
const SKILLS_DIR = path.resolve(__dirname, '..', 'skills');
|
|
22
|
+
|
|
23
|
+
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
function main() {
|
|
26
|
+
console.log("校验技能元数据、触发条件、完整章节与交叉引用(错误会阻止发行)。");
|
|
27
|
+
if (!fs.existsSync(SKILLS_DIR)) {
|
|
28
|
+
console.error(`ERROR: skills directory not found at ${SKILLS_DIR}`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const skillDirs = fs.readdirSync(SKILLS_DIR)
|
|
33
|
+
.filter(d => fs.statSync(path.join(SKILLS_DIR, d)).isDirectory())
|
|
34
|
+
.sort();
|
|
35
|
+
|
|
36
|
+
const knownSkills = new Set(skillDirs);
|
|
37
|
+
|
|
38
|
+
let totalErrors = 0;
|
|
39
|
+
let totalWarnings = 0;
|
|
40
|
+
|
|
41
|
+
for (const dirName of skillDirs) {
|
|
42
|
+
const { errors, warnings, exempt } = lintSkill(dirName, SKILLS_DIR, knownSkills);
|
|
43
|
+
totalErrors += errors.length;
|
|
44
|
+
totalWarnings += warnings.length;
|
|
45
|
+
|
|
46
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
47
|
+
const tag = exempt ? ' (section checks exempt)' : '';
|
|
48
|
+
console.log(` ✓ ${dirName}${tag}`);
|
|
49
|
+
} else {
|
|
50
|
+
const icon = errors.length > 0 ? ' ✗ ' : ' ⚠ ';
|
|
51
|
+
console.log(`${icon} ${dirName}`);
|
|
52
|
+
for (const msg of errors) console.log(` ERROR: ${msg}`);
|
|
53
|
+
for (const msg of warnings) console.log(` WARN: ${msg}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const status = totalErrors > 0 ? 'FAILED' : totalWarnings > 0 ? 'PASSED WITH WARNINGS' : 'PASSED';
|
|
58
|
+
console.log(`\n${skillDirs.length} skills checked — ${totalErrors} error(s), ${totalWarnings} warning(s) — ${status}`);
|
|
59
|
+
|
|
60
|
+
if (totalErrors > 0) process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Surface unexpected failures (fs errors, bad symlinks, …) as a structured
|
|
64
|
+
// one-line CI error instead of an uncaught stack trace.
|
|
65
|
+
try {
|
|
66
|
+
main();
|
|
67
|
+
} catch (err) {
|
|
68
|
+
console.error(`\nERROR: validate-skills failed unexpectedly: ${err.message}`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
const { readFileSync } = require("node:fs");
|
|
6
|
+
|
|
7
|
+
const manifestPaths = [
|
|
8
|
+
"package.json",
|
|
9
|
+
"plugin.json",
|
|
10
|
+
".codex-plugin/plugin.json",
|
|
11
|
+
".claude-plugin/plugin.json",
|
|
12
|
+
".claude-plugin/marketplace.json",
|
|
13
|
+
".agents/plugins/marketplace.json",
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
function readManifestVersion(manifestPath) {
|
|
17
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
18
|
+
return manifest.version ?? manifest.plugins?.[0]?.version;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const expectedVersion = readManifestVersion("plugin.json");
|
|
22
|
+
if (!expectedVersion) {
|
|
23
|
+
throw new Error("plugin.json is missing a version field");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
for (const manifestPath of manifestPaths) {
|
|
27
|
+
const version = readManifestVersion(manifestPath);
|
|
28
|
+
if (version !== expectedVersion) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`${manifestPath} has version ${version ?? "<missing>"}; expected ${expectedVersion}`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
console.log(`All plugin manifests use version ${expectedVersion}.`);
|