@givolt/i3-composer-agent 1.0.1 → 2.1.0
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/commands/{build.md → i3-build.md} +10 -10
- package/.claude/commands/{code-simplify.md → i3-code-simplify.md} +2 -2
- package/.claude/commands/{constraints.md → i3-constraints.md} +4 -4
- package/.claude/commands/{plan.md → i3-plan.md} +1 -1
- package/.claude/commands/{review.md → i3-review.md} +3 -3
- package/.claude/commands/{ship.md → i3-ship.md} +4 -4
- package/.claude/commands/{spec.md → i3-spec.md} +1 -1
- package/.claude/commands/{test.md → i3-test.md} +2 -2
- package/.claude/commands/{webperf.md → i3-webperf.md} +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.gemini/commands/{build.toml → i3-build.toml} +10 -10
- package/.gemini/commands/{code-simplify.toml → i3-code-simplify.toml} +2 -2
- package/.gemini/commands/{constraints.toml → i3-constraints.toml} +4 -4
- package/.gemini/commands/{planning.toml → i3-planning.toml} +1 -1
- package/{commands/review.toml → .gemini/commands/i3-review.toml} +3 -3
- package/.gemini/commands/{ship.toml → i3-ship.toml} +4 -4
- package/.gemini/commands/{spec.toml → i3-spec.toml} +1 -1
- package/{commands/test.toml → .gemini/commands/i3-test.toml} +2 -2
- package/.gemini/commands/{webperf.toml → i3-webperf.toml} +1 -1
- package/README.md +72 -69
- package/agents/code-reviewer.md +2 -2
- package/agents/security-auditor.md +1 -1
- package/agents/test-engineer.md +1 -1
- package/agents/web-performance-auditor.md +3 -3
- package/bin/agent-targets.json +11 -0
- package/bin/asset-fingerprint.cjs +38 -0
- package/bin/cleanup-backups.cjs +16 -0
- package/bin/i3-composer-agent.cjs +29 -14
- package/bin/install.cjs +81 -19
- package/bin/native-content.cjs +29 -0
- package/bin/wizard.cjs +2 -1
- package/commands/{build.toml → i3-build.toml} +10 -10
- package/commands/{code-simplify.toml → i3-code-simplify.toml} +2 -2
- package/commands/{constraints.toml → i3-constraints.toml} +4 -4
- package/commands/{planning.toml → i3-planning.toml} +1 -1
- package/{.gemini/commands/review.toml → commands/i3-review.toml} +3 -3
- package/commands/{ship.toml → i3-ship.toml} +4 -4
- package/commands/{spec.toml → i3-spec.toml} +1 -1
- package/{.gemini/commands/test.toml → commands/i3-test.toml} +2 -2
- package/commands/{webperf.toml → i3-webperf.toml} +1 -1
- package/docs/adoption-guide.md +35 -35
- package/docs/agents.md +12 -12
- package/docs/antigravity-setup.md +14 -14
- package/docs/codebuddy-setup.md +27 -18
- package/docs/codex-setup.md +6 -6
- package/docs/commandcode-setup.md +5 -5
- package/docs/copilot-cli-setup.md +5 -5
- package/docs/copilot-setup.md +22 -22
- package/docs/cursor-setup.md +22 -22
- package/docs/gemini-cli-setup.md +26 -26
- package/docs/getting-started.md +36 -38
- package/docs/opencode-setup.md +31 -31
- package/docs/windsurf-setup.md +7 -7
- package/docs/workbuddy-setup.md +92 -0
- package/hooks/SDD-CACHE.md +3 -3
- package/hooks/SIMPLIFY-IGNORE.md +2 -2
- package/hooks/session-start-test.sh +1 -1
- package/hooks/session-start.sh +3 -3
- package/package.json +36 -20
- package/plugin.json +1 -1
- package/references/accessibility-checklist.md +1 -1
- package/references/definition-of-done.md +7 -7
- package/references/observability-checklist.md +3 -3
- package/references/orchestration-patterns.md +17 -17
- package/references/performance-checklist.md +2 -2
- package/references/security-checklist.md +1 -1
- package/references/testing-patterns.md +1 -1
- package/skills/{api-and-interface-design → i3-api-and-interface-design}/SKILL.md +2 -2
- package/skills/{browser-testing-with-devtools → i3-browser-testing-with-devtools}/SKILL.md +1 -1
- package/skills/{ci-cd-and-automation → i3-ci-cd-and-automation}/SKILL.md +2 -2
- package/skills/{code-review-and-quality → i3-code-review-and-quality}/SKILL.md +5 -5
- package/skills/{code-simplification → i3-code-simplification}/SKILL.md +1 -1
- package/skills/{constraint-driven-development → i3-constraint-driven-development}/SKILL.md +15 -15
- package/skills/{context-engineering → i3-context-engineering}/SKILL.md +1 -1
- package/skills/{debugging-and-error-recovery → i3-debugging-and-error-recovery}/SKILL.md +2 -2
- package/skills/{deprecation-and-migration → i3-deprecation-and-migration}/SKILL.md +2 -2
- package/skills/{documentation-and-adrs → i3-documentation-and-adrs}/SKILL.md +1 -1
- package/skills/{doubt-driven-development → i3-doubt-driven-development}/SKILL.md +8 -8
- package/skills/{frontend-ui-engineering → i3-frontend-ui-engineering}/SKILL.md +1 -1
- package/skills/{git-workflow-and-versioning → i3-git-workflow-and-versioning}/SKILL.md +4 -4
- package/skills/{idea-refine → i3-idea-refine}/SKILL.md +2 -2
- package/skills/{idea-refine → i3-idea-refine}/scripts/idea-refine.sh +1 -1
- package/skills/{incremental-implementation → i3-incremental-implementation}/SKILL.md +3 -3
- package/skills/{interview-me → i3-interview-me}/SKILL.md +12 -12
- package/skills/{observability-and-instrumentation → i3-observability-and-instrumentation}/SKILL.md +5 -5
- package/skills/{performance-optimization → i3-performance-optimization}/SKILL.md +1 -1
- package/skills/{performance-optimization → i3-performance-optimization}/references/optimization-patterns.md +1 -1
- package/skills/{planning-and-task-breakdown → i3-planning-and-task-breakdown}/SKILL.md +2 -2
- package/skills/{security-and-hardening → i3-security-and-hardening}/SKILL.md +3 -3
- package/skills/{security-and-hardening → i3-security-and-hardening}/references/hardening-patterns.md +1 -1
- package/skills/{shipping-and-launch → i3-shipping-and-launch}/SKILL.md +2 -2
- package/skills/{source-driven-development → i3-source-driven-development}/SKILL.md +2 -2
- package/skills/{spec-driven-development → i3-spec-driven-development}/SKILL.md +7 -7
- package/skills/{test-driven-development → i3-test-driven-development}/SKILL.md +2 -2
- package/skills/{using-agent-skills → i3-using-agent-skills}/SKILL.md +67 -67
- package/CHANGELOG.md +0 -34
- package/NOTICE.md +0 -14
- package/docs/advanced-per-agent-configuration.md +0 -70
- package/docs/comparison.md +0 -131
- package/docs/developer-onboarding.md +0 -116
- package/docs/skill-anatomy.md +0 -203
- package/evals/README.md +0 -101
- package/evals/cases/api-and-interface-design.json +0 -45
- package/evals/cases/browser-testing-with-devtools.json +0 -44
- package/evals/cases/ci-cd-and-automation.json +0 -45
- package/evals/cases/code-review-and-quality.json +0 -49
- package/evals/cases/code-simplification.json +0 -45
- package/evals/cases/constraint-driven-development.json +0 -79
- package/evals/cases/context-engineering.json +0 -44
- package/evals/cases/debugging-and-error-recovery.json +0 -58
- package/evals/cases/deprecation-and-migration.json +0 -52
- package/evals/cases/documentation-and-adrs.json +0 -44
- package/evals/cases/doubt-driven-development.json +0 -43
- package/evals/cases/frontend-ui-engineering.json +0 -52
- package/evals/cases/git-workflow-and-versioning.json +0 -47
- package/evals/cases/idea-refine.json +0 -43
- package/evals/cases/incremental-implementation.json +0 -57
- package/evals/cases/interview-me.json +0 -42
- package/evals/cases/observability-and-instrumentation.json +0 -44
- package/evals/cases/performance-optimization.json +0 -53
- package/evals/cases/planning-and-task-breakdown.json +0 -45
- package/evals/cases/security-and-hardening.json +0 -53
- package/evals/cases/shipping-and-launch.json +0 -56
- package/evals/cases/source-driven-development.json +0 -44
- package/evals/cases/spec-driven-development.json +0 -72
- package/evals/cases/test-driven-development.json +0 -73
- package/evals/cases/using-agent-skills.json +0 -44
- package/evals/fixtures/api-and-interface-design/service-brief.md +0 -19
- package/evals/fixtures/browser-testing-with-devtools/README.md +0 -5
- package/evals/fixtures/browser-testing-with-devtools/index.html +0 -24
- package/evals/fixtures/browser-testing-with-devtools/server.js +0 -15
- package/evals/fixtures/ci-cd-and-automation/package.json +0 -8
- package/evals/fixtures/ci-cd-and-automation/src/slug.js +0 -3
- package/evals/fixtures/ci-cd-and-automation/test/slug.test.js +0 -9
- package/evals/fixtures/code-review-and-quality/user-search.diff +0 -16
- package/evals/fixtures/code-simplification/config-parser.js +0 -46
- package/evals/fixtures/code-simplification/config-parser.test.js +0 -15
- package/evals/fixtures/context-engineering/context-audit.md +0 -15
- package/evals/fixtures/debugging-and-error-recovery/pagination.js +0 -8
- package/evals/fixtures/debugging-and-error-recovery/pagination.test.js +0 -9
- package/evals/fixtures/debugging-and-error-recovery/time-pressure.md +0 -6
- package/evals/fixtures/deprecation-and-migration/api-inventory.md +0 -9
- package/evals/fixtures/documentation-and-adrs/decision-context.md +0 -16
- package/evals/fixtures/doubt-driven-development/migration-plan.md +0 -19
- package/evals/fixtures/frontend-ui-engineering/Button.tsx +0 -7
- package/evals/fixtures/frontend-ui-engineering/design-system.md +0 -11
- package/evals/fixtures/git-workflow-and-versioning/.eval/working-tree.patch +0 -21
- package/evals/fixtures/git-workflow-and-versioning/app.js +0 -7
- package/evals/fixtures/git-workflow-and-versioning/app.test.js +0 -9
- package/evals/fixtures/incremental-implementation/reports.js +0 -7
- package/evals/fixtures/incremental-implementation/reports.test.js +0 -12
- package/evals/fixtures/incremental-implementation/tasks/plan.md +0 -8
- package/evals/fixtures/incremental-implementation-pressure/draft-export.js +0 -17
- package/evals/fixtures/incremental-implementation-pressure/scenario.md +0 -9
- package/evals/fixtures/observability-and-instrumentation/operations.md +0 -11
- package/evals/fixtures/observability-and-instrumentation/payment-retry.js +0 -14
- package/evals/fixtures/performance-optimization/benchmark.js +0 -15
- package/evals/fixtures/performance-optimization/products.js +0 -14
- package/evals/fixtures/planning-and-task-breakdown/notifications-spec.md +0 -18
- package/evals/fixtures/security-and-hardening/webhook.js +0 -11
- package/evals/fixtures/security-and-hardening/webhook.test.js +0 -13
- package/evals/fixtures/shipping-and-launch/authority-pressure.md +0 -6
- package/evals/fixtures/shipping-and-launch/launch-status.md +0 -11
- package/evals/fixtures/source-driven-development/framework-task.md +0 -10
- package/evals/fixtures/spec-driven-development/billing-brief.md +0 -16
- package/evals/fixtures/spec-driven-development-decomposition/portal-brief.md +0 -22
- package/evals/fixtures/test-driven-development/BUG.md +0 -10
- package/evals/fixtures/test-driven-development/README.md +0 -28
- package/evals/fixtures/test-driven-development/package.json +0 -9
- package/evals/fixtures/test-driven-development/src/split.js +0 -8
- package/evals/fixtures/test-driven-development/test/split.test.js +0 -13
- package/evals/fixtures/test-driven-development-ecosystem/README.md +0 -9
- package/evals/fixtures/test-driven-development-ecosystem/ledger.py +0 -15
- package/evals/fixtures/test-driven-development-ecosystem/test_ledger.py +0 -19
- package/evals/fixtures/using-agent-skills/incident.md +0 -6
- package/evals/plugin/code-review-fires/graders/off-by-one.md +0 -7
- package/evals/plugin/code-review-fires/graders/severity-labels.md +0 -4
- package/evals/plugin/code-review-fires/graders/skill-fired.md +0 -5
- package/evals/plugin/code-review-fires/prompt.md +0 -31
- package/evals/plugin/code-review-stays-quiet/graders/not-fired.md +0 -8
- package/evals/plugin/code-review-stays-quiet/graders/tdd-fired.md +0 -6
- package/evals/plugin/code-review-stays-quiet/prompt.md +0 -8
- package/evals/plugin/code-review-stays-quiet-on-commit-message/graders/not-fired.md +0 -8
- package/evals/plugin/code-review-stays-quiet-on-commit-message/prompt.md +0 -25
- package/evals/skill-impact.md +0 -6
- package/rebrand/INSTALLATION-SPEC.md +0 -46
- package/rebrand/MAINTENANCE.md +0 -113
- package/rebrand/NOTICE.md +0 -14
- package/rebrand/PR-WORKFLOW.md +0 -34
- package/rebrand/UNIFICATION-SPEC.md +0 -33
- package/rebrand/VALIDATION.md +0 -32
- package/rebrand/VERSIONING.md +0 -33
- package/rebrand/apply.cjs +0 -194
- package/rebrand/brand.json +0 -13
- package/rebrand/catalog.cjs +0 -56
- package/rebrand/fork-patches.json +0 -12
- package/rebrand/overlay/.github/workflows/test-plugin-install.yml +0 -177
- package/rebrand/overlay/CHANGELOG.md +0 -34
- package/rebrand/overlay/bin/agent-targets.json +0 -726
- package/rebrand/overlay/bin/i3-composer-agent.cjs +0 -190
- package/rebrand/overlay/bin/install.cjs +0 -164
- package/rebrand/overlay/bin/skills-registry.LICENSE +0 -21
- package/rebrand/overlay/bin/wizard.cjs +0 -66
- package/rebrand/overlay/scripts/validate-brand.cjs +0 -44
- package/rebrand/overlay/tests/bundle.test.cjs +0 -85
- package/rebrand/overlay/tests/installer.test.cjs +0 -193
- package/rebrand/package.template.json +0 -82
- package/rebrand/rebrand.sh +0 -4
- package/rebrand/record-upstream.cjs +0 -37
- package/rebrand/templates/README.md +0 -124
- package/rebrand/templates/codebuddy-setup.md +0 -88
- package/rebrand/templates/codex-setup.md +0 -45
- package/rebrand/tests/pipeline.test.cjs +0 -185
- package/rebrand/upstream.json +0 -171
- package/scripts/floor-guard-reference-test.js +0 -291
- package/scripts/lib/skill-lint-test.js +0 -650
- package/scripts/lib/skill-lint.js +0 -584
- package/scripts/run-evals-test.js +0 -623
- package/scripts/run-evals.js +0 -638
- package/scripts/validate-artifact-paths-test.js +0 -179
- package/scripts/validate-artifact-paths.js +0 -128
- package/scripts/validate-brand.cjs +0 -44
- package/scripts/validate-commands-test.js +0 -216
- package/scripts/validate-commands.js +0 -222
- package/scripts/validate-reference-links-test.js +0 -495
- package/scripts/validate-reference-links.js +0 -187
- package/scripts/validate-skills.js +0 -69
- package/scripts/validate-versions-test.js +0 -32
- package/scripts/validate-versions.js +0 -35
- package/tests/bundle.test.cjs +0 -85
- package/tests/installer.test.cjs +0 -193
- /package/skills/{constraint-driven-development → i3-constraint-driven-development}/references/floor-guard.md +0 -0
- /package/skills/{idea-refine → i3-idea-refine}/examples.md +0 -0
- /package/skills/{idea-refine → i3-idea-refine}/frameworks.md +0 -0
- /package/skills/{idea-refine → i3-idea-refine}/refinement-criteria.md +0 -0
|
@@ -1,650 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
'use strict';
|
|
4
|
-
|
|
5
|
-
const assert = require('node:assert/strict');
|
|
6
|
-
const { test } = require('node:test');
|
|
7
|
-
|
|
8
|
-
const fs = require('node:fs');
|
|
9
|
-
const os = require('node:os');
|
|
10
|
-
const path = require('node:path');
|
|
11
|
-
|
|
12
|
-
const { lintSkillContent, lintSkillLayout, topLevelFrontmatterKeys } = require('./skill-lint.js');
|
|
13
|
-
|
|
14
|
-
const KNOWN = new Set(['alpha', 'beta']);
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Build a throwaway skill directory. `dirs` are created empty; `files` maps a
|
|
18
|
-
* path within the skill to its contents, creating parents as needed.
|
|
19
|
-
*/
|
|
20
|
-
function makeSkillDir({ dirs = [], files = {} } = {}) {
|
|
21
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'skill-layout-'));
|
|
22
|
-
for (const d of dirs) fs.mkdirSync(path.join(root, d), { recursive: true });
|
|
23
|
-
for (const [rel, body] of Object.entries(files)) {
|
|
24
|
-
const abs = path.join(root, rel);
|
|
25
|
-
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
26
|
-
fs.writeFileSync(abs, body);
|
|
27
|
-
}
|
|
28
|
-
return root;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** A SKILL.md body carrying every required section, so tests can isolate frontmatter. */
|
|
32
|
-
function withAllSections(frontmatter) {
|
|
33
|
-
return [
|
|
34
|
-
frontmatter,
|
|
35
|
-
'',
|
|
36
|
-
'## Overview',
|
|
37
|
-
'x',
|
|
38
|
-
'## When to Use',
|
|
39
|
-
'x',
|
|
40
|
-
'## Common Rationalizations',
|
|
41
|
-
'x',
|
|
42
|
-
'## Red Flags',
|
|
43
|
-
'x',
|
|
44
|
-
'## Verification',
|
|
45
|
-
'x',
|
|
46
|
-
'',
|
|
47
|
-
].join('\n');
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const VALID_FRONTMATTER = [
|
|
51
|
-
'---',
|
|
52
|
-
'name: alpha',
|
|
53
|
-
'description: Designs alphas. Use when building one.',
|
|
54
|
-
'---',
|
|
55
|
-
].join('\n');
|
|
56
|
-
|
|
57
|
-
// ─── Section exemptions ──────────────────────────────────────────────────────
|
|
58
|
-
|
|
59
|
-
test('a directory named after an Object.prototype key is not exempt from section checks', () => {
|
|
60
|
-
// `constructor` satisfies KEBAB_CASE, and `dirName in SECTION_EXEMPT_SKILLS`
|
|
61
|
-
// finds it on the prototype chain — silently skipping every section check.
|
|
62
|
-
const content = [
|
|
63
|
-
'---',
|
|
64
|
-
'name: constructor',
|
|
65
|
-
'description: Does a thing. Use when you need it.',
|
|
66
|
-
'---',
|
|
67
|
-
'',
|
|
68
|
-
'no sections here',
|
|
69
|
-
'',
|
|
70
|
-
].join('\n');
|
|
71
|
-
|
|
72
|
-
const { errors, exempt } = lintSkillContent('constructor', content, KNOWN);
|
|
73
|
-
|
|
74
|
-
assert.equal(exempt, false, 'exemptions must come from the allowlist, not the prototype chain');
|
|
75
|
-
assert.equal(errors.filter(e => /Missing required section/.test(e)).length, 5);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test('a genuinely allowlisted skill is still exempt', () => {
|
|
79
|
-
const content = [
|
|
80
|
-
'---',
|
|
81
|
-
'name: using-agent-skills',
|
|
82
|
-
'description: Routes to other skills. Use when choosing one.',
|
|
83
|
-
'---',
|
|
84
|
-
'',
|
|
85
|
-
'no sections here',
|
|
86
|
-
'',
|
|
87
|
-
].join('\n');
|
|
88
|
-
|
|
89
|
-
const { errors, exempt } = lintSkillContent('using-agent-skills', content, KNOWN);
|
|
90
|
-
|
|
91
|
-
assert.equal(exempt, true);
|
|
92
|
-
assert.deepEqual(errors.filter(e => /Missing required section/.test(e)), []);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
test('a skill claiming its own exemption without being allowlisted fails loud', () => {
|
|
96
|
-
const content = withAllSections(
|
|
97
|
-
['---', 'name: alpha', 'description: Designs alphas. Use when building one.', 'exempt: sections', '---'].join('\n')
|
|
98
|
-
);
|
|
99
|
-
const { errors } = lintSkillContent('alpha', content, KNOWN);
|
|
100
|
-
// Two errors: 'exempt' is not a spec key, and the exemption itself is refused.
|
|
101
|
-
assert.equal(errors.length, 2);
|
|
102
|
-
assert.equal(errors.filter(e => /not in the validator's SECTION_EXEMPT_SKILLS allowlist/.test(e)).length, 1);
|
|
103
|
-
assert.equal(errors.filter(e => /Frontmatter key 'exempt' is not an Agent Skills spec field/.test(e)).length, 1);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
// ─── Guardrails on the rules this change sits beside ─────────────────────────
|
|
107
|
-
// Deliberately narrow: #428 rewrites frontmatter parsing and the cross-reference
|
|
108
|
-
// patterns, so asserting their behaviour here would collide with that work.
|
|
109
|
-
|
|
110
|
-
test('a fully valid skill produces no errors', () => {
|
|
111
|
-
const { errors } = lintSkillContent('alpha', withAllSections(VALID_FRONTMATTER), KNOWN);
|
|
112
|
-
assert.deepEqual(errors, []);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test('reports a description with no trigger clause', () => {
|
|
116
|
-
const content = withAllSections(
|
|
117
|
-
['---', 'name: alpha', 'description: Designs alpha things and nothing more.', '---'].join('\n')
|
|
118
|
-
);
|
|
119
|
-
const { errors } = lintSkillContent('alpha', content, KNOWN);
|
|
120
|
-
assert.equal(errors.length, 1);
|
|
121
|
-
assert.match(errors[0], /no 'when to use' trigger/);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test('a description whose only triggers are negated is rejected regardless of how many there are', () => {
|
|
125
|
-
const content = withAllSections(
|
|
126
|
-
[
|
|
127
|
-
'---',
|
|
128
|
-
'name: alpha',
|
|
129
|
-
'description: Designs alphas. Do not use when building betas. Never use when the input is JSON.',
|
|
130
|
-
'---',
|
|
131
|
-
].join('\n')
|
|
132
|
-
);
|
|
133
|
-
const { errors } = lintSkillContent('alpha', content, KNOWN);
|
|
134
|
-
assert.equal(errors.length, 1);
|
|
135
|
-
assert.match(errors[0], /no 'when to use' trigger/);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
test('reports frontmatter name that disagrees with the directory', () => {
|
|
139
|
-
const content = withAllSections(
|
|
140
|
-
['---', 'name: beta', 'description: Designs alphas. Use when building one.', '---'].join('\n')
|
|
141
|
-
);
|
|
142
|
-
const { errors } = lintSkillContent('alpha', content, KNOWN);
|
|
143
|
-
assert.equal(errors.length, 1);
|
|
144
|
-
assert.match(errors[0], /does not match directory name/);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
test('reports a workflow step declared without a matching process section', () => {
|
|
148
|
-
const content = withAllSections(VALID_FRONTMATTER).replace(
|
|
149
|
-
'## Common Rationalizations',
|
|
150
|
-
[
|
|
151
|
-
'## The Optimization Workflow',
|
|
152
|
-
'',
|
|
153
|
-
'```',
|
|
154
|
-
'1. MEASURE → Establish a baseline',
|
|
155
|
-
'2. GUARD → Prevent regression',
|
|
156
|
-
'```',
|
|
157
|
-
'',
|
|
158
|
-
'### Step 1: Measure',
|
|
159
|
-
'',
|
|
160
|
-
'Measure first.',
|
|
161
|
-
'',
|
|
162
|
-
'## Common Rationalizations',
|
|
163
|
-
].join('\n'),
|
|
164
|
-
);
|
|
165
|
-
|
|
166
|
-
const { errors } = lintSkillContent('alpha', content, KNOWN);
|
|
167
|
-
|
|
168
|
-
assert.equal(errors.length, 1);
|
|
169
|
-
assert.match(errors[0], /Workflow declares Step 2 but has no matching process section/);
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
test('reports a missing frontmatter block', () => {
|
|
173
|
-
const { errors } = lintSkillContent('alpha', '## Overview\nx\n', KNOWN);
|
|
174
|
-
assert.equal(errors.length, 1);
|
|
175
|
-
assert.match(errors[0], /Missing or malformed YAML frontmatter/);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
// ─── Fenced-block stripping (#437) ───────────────────────────────────────────
|
|
179
|
-
//
|
|
180
|
-
// Observed through the required-section rule: a `## Overview` heading that
|
|
181
|
-
// lives inside a fenced block must NOT satisfy the check, so the presence of
|
|
182
|
-
// "Missing required section: ## Overview" proves the block was stripped, and
|
|
183
|
-
// its absence proves prose outside the block survived.
|
|
184
|
-
|
|
185
|
-
const FENCE_KNOWN = new Set(['fenced']);
|
|
186
|
-
|
|
187
|
-
/** A SKILL.md with every required section except Overview, which the caller supplies. */
|
|
188
|
-
function skillWithOverview(overviewBlock) {
|
|
189
|
-
return [
|
|
190
|
-
'---',
|
|
191
|
-
'name: fenced',
|
|
192
|
-
'description: Exercises fence parsing. Use when testing the linter.',
|
|
193
|
-
'---',
|
|
194
|
-
'',
|
|
195
|
-
overviewBlock,
|
|
196
|
-
'',
|
|
197
|
-
'## When to Use',
|
|
198
|
-
'x',
|
|
199
|
-
'## Common Rationalizations',
|
|
200
|
-
'x',
|
|
201
|
-
'## Red Flags',
|
|
202
|
-
'x',
|
|
203
|
-
'## Verification',
|
|
204
|
-
'x',
|
|
205
|
-
'',
|
|
206
|
-
].join('\n');
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const OVERVIEW = '## Overview\nx';
|
|
210
|
-
const overviewMissing = ({ errors }) => errors.includes('Missing required section: ## Overview');
|
|
211
|
-
|
|
212
|
-
test('a real Overview heading satisfies the required-section check', () => {
|
|
213
|
-
const result = lintSkillContent('fenced', skillWithOverview(OVERVIEW), FENCE_KNOWN);
|
|
214
|
-
assert.deepEqual(result.errors, []);
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
for (const [form, block] of [
|
|
218
|
-
['a backtick fence', '```markdown\n## Overview\n```'],
|
|
219
|
-
['an unlabeled fence', '```\n## Overview\n```'],
|
|
220
|
-
['a tilde fence', '~~~markdown\n## Overview\n~~~'],
|
|
221
|
-
['a fence indented one space', ' ```\n## Overview\n ```'],
|
|
222
|
-
['a fence indented three spaces', ' ```\n## Overview\n ```'],
|
|
223
|
-
['a fence with a longer closer', '```\n## Overview\n`````'],
|
|
224
|
-
['a four-backtick fence', '````\n## Overview\n````'],
|
|
225
|
-
]) {
|
|
226
|
-
test(`a heading inside ${form} does not satisfy the check`, () => {
|
|
227
|
-
const result = lintSkillContent('fenced', skillWithOverview(block), FENCE_KNOWN);
|
|
228
|
-
assert.equal(overviewMissing(result), true, `heading inside ${form} leaked into prose`);
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// The closer must be recognised, or every line after it is swallowed.
|
|
233
|
-
for (const [form, block] of [
|
|
234
|
-
['a same-length closer', '```\nexample\n```\n\n' + OVERVIEW],
|
|
235
|
-
['a longer closer', '```\nexample\n`````\n\n' + OVERVIEW],
|
|
236
|
-
['an indented closer', '```\nexample\n ```\n\n' + OVERVIEW],
|
|
237
|
-
['a tilde fence closer', '~~~\nexample\n~~~\n\n' + OVERVIEW],
|
|
238
|
-
['a closer with trailing spaces', '```\nexample\n``` \n\n' + OVERVIEW],
|
|
239
|
-
]) {
|
|
240
|
-
test(`prose after ${form} is still linted`, () => {
|
|
241
|
-
const result = lintSkillContent('fenced', skillWithOverview(block), FENCE_KNOWN);
|
|
242
|
-
assert.equal(overviewMissing(result), false, `prose after ${form} was swallowed`);
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
test('a shorter run of the same marker does not close a longer fence', () => {
|
|
247
|
-
const block = '````\n```\n## Overview\n```\n````';
|
|
248
|
-
const result = lintSkillContent('fenced', skillWithOverview(block), FENCE_KNOWN);
|
|
249
|
-
assert.equal(overviewMissing(result), true);
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
test('a backtick run does not close a tilde fence, and vice versa', () => {
|
|
253
|
-
for (const block of ['~~~\n```\n## Overview\n~~~', '```\n~~~\n## Overview\n```']) {
|
|
254
|
-
const result = lintSkillContent('fenced', skillWithOverview(block), FENCE_KNOWN);
|
|
255
|
-
assert.equal(overviewMissing(result), true);
|
|
256
|
-
}
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
test('a fence indented four spaces is an indented code block, not a fence', () => {
|
|
260
|
-
// Four spaces makes the line indented code in CommonMark; the heading that
|
|
261
|
-
// follows is regular prose and must still satisfy the check.
|
|
262
|
-
const block = ' ```\n' + OVERVIEW;
|
|
263
|
-
const result = lintSkillContent('fenced', skillWithOverview(block), FENCE_KNOWN);
|
|
264
|
-
assert.equal(overviewMissing(result), false);
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
test('a backtick run followed by inline backticks is prose, not an opener', () => {
|
|
268
|
-
// CommonMark forbids backticks in the info string of a backtick fence, so
|
|
269
|
-
// this line is ordinary prose and the heading below it must still count.
|
|
270
|
-
const block = '```js``` is how you write inline code for a fence\n' + OVERVIEW;
|
|
271
|
-
const result = lintSkillContent('fenced', skillWithOverview(block), FENCE_KNOWN);
|
|
272
|
-
assert.equal(overviewMissing(result), false);
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
test('an unterminated fence swallows everything after it and fails loud', () => {
|
|
276
|
-
const block = '```\n' + OVERVIEW;
|
|
277
|
-
const result = lintSkillContent('fenced', skillWithOverview(block), FENCE_KNOWN);
|
|
278
|
-
assert.equal(overviewMissing(result), true);
|
|
279
|
-
assert.equal(result.errors.includes('Missing required section: ## Verification'), true);
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
test('CRLF line endings are handled', () => {
|
|
283
|
-
const content = skillWithOverview('```\n## Overview\n```').replace(/\n/g, '\r\n');
|
|
284
|
-
const result = lintSkillContent('fenced', content, FENCE_KNOWN);
|
|
285
|
-
assert.equal(overviewMissing(result), true);
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
// ── Frontmatter must be valid YAML, not merely splittable ────────────────────
|
|
289
|
-
// `parseFrontmatter` splits each line on its first colon, which is forgiving by
|
|
290
|
-
// design. The hosts that read these skills are not: Cursor parses the
|
|
291
|
-
// frontmatter as YAML when a skill is attached to a message, and a parse
|
|
292
|
-
// failure fails the whole request and takes the chat's context with it (#494).
|
|
293
|
-
// Each shape below was confirmed rejected by a strict parser (ruby psych) while
|
|
294
|
-
// passing every other check in this linter.
|
|
295
|
-
|
|
296
|
-
/** Frontmatter that is otherwise complete, so only YAML validity varies. */
|
|
297
|
-
function fmLines(...lines) {
|
|
298
|
-
return withAllSections(['---', 'name: alpha', ...lines, '---'].join('\n'));
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
const yamlErrors = result => result.errors.filter(e => e.startsWith('Frontmatter line '));
|
|
302
|
-
|
|
303
|
-
test('a valid frontmatter reports no YAML error', () => {
|
|
304
|
-
const result = lintSkillContent('alpha', fmLines('description: Use when you need alpha'), KNOWN);
|
|
305
|
-
assert.deepEqual(yamlErrors(result), []);
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
test('an unquoted value containing a colon is rejected', () => {
|
|
309
|
-
// YAML reads `Use when: X` as a nested mapping and errors; the split-on-first
|
|
310
|
-
// -colon parser reads it as a plain string and never notices.
|
|
311
|
-
const result = lintSkillContent(
|
|
312
|
-
'alpha',
|
|
313
|
-
fmLines('description: Use when you need alpha: auth, secrets and review'),
|
|
314
|
-
KNOWN,
|
|
315
|
-
);
|
|
316
|
-
assert.equal(yamlErrors(result).length, 1);
|
|
317
|
-
assert.match(yamlErrors(result)[0], /unquoted value containing/);
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
test('quoting the same value makes it valid again', () => {
|
|
321
|
-
const result = lintSkillContent(
|
|
322
|
-
'alpha',
|
|
323
|
-
fmLines('description: "Use when you need alpha: auth, secrets and review"'),
|
|
324
|
-
KNOWN,
|
|
325
|
-
);
|
|
326
|
-
assert.deepEqual(yamlErrors(result), []);
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
test('a colon with no trailing space is left alone', () => {
|
|
330
|
-
// `https://example.com` is a perfectly good YAML scalar. The rule keys on
|
|
331
|
-
// colon-space, not on colons, so ordinary URLs do not trip it.
|
|
332
|
-
const result = lintSkillContent(
|
|
333
|
-
'alpha',
|
|
334
|
-
fmLines('description: Use when you need alpha', 'docs: https://example.com/a:b'),
|
|
335
|
-
KNOWN,
|
|
336
|
-
);
|
|
337
|
-
assert.deepEqual(yamlErrors(result), []);
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
test('a tab used for indentation is rejected', () => {
|
|
341
|
-
const result = lintSkillContent(
|
|
342
|
-
'alpha',
|
|
343
|
-
fmLines('description: Use when you need alpha', 'meta:', '\tlevel: core'),
|
|
344
|
-
KNOWN,
|
|
345
|
-
);
|
|
346
|
-
assert.equal(yamlErrors(result).length, 1);
|
|
347
|
-
assert.match(yamlErrors(result)[0], /indents with a tab/);
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
test('an unterminated quote is rejected', () => {
|
|
351
|
-
const result = lintSkillContent('alpha', fmLines('description: "Use when you need alpha'), KNOWN);
|
|
352
|
-
assert.equal(yamlErrors(result).length, 1);
|
|
353
|
-
assert.match(yamlErrors(result)[0], /never closes/);
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
// A plain (unquoted) YAML scalar may not BEGIN with certain indicator characters.
|
|
357
|
-
// The set below was not read off the spec — it was measured against js-yaml, both
|
|
358
|
-
// with and without a following space, and only characters invalid in *both* forms
|
|
359
|
-
// with no legitimate single-line use are rejected here. Anything ambiguous is left
|
|
360
|
-
// alone on purpose, and the second test pins that so the rule cannot be widened
|
|
361
|
-
// into false positives later.
|
|
362
|
-
//
|
|
363
|
-
// The backtick is the one that actually bites this repo: descriptions routinely
|
|
364
|
-
// name other skills, and `\`alpha\` designs things` is a natural way to start one.
|
|
365
|
-
for (const [label, value] of [
|
|
366
|
-
['a backtick', '`alpha` designs things. Use when alpha.'],
|
|
367
|
-
['an at sign', '@team owns this. Use when alpha.'],
|
|
368
|
-
['a percent sign', '%complete coverage. Use when alpha.'],
|
|
369
|
-
]) {
|
|
370
|
-
test(`an unquoted value starting with ${label} is rejected`, () => {
|
|
371
|
-
const result = lintSkillContent(
|
|
372
|
-
'alpha',
|
|
373
|
-
fmLines(`description: ${value}`),
|
|
374
|
-
KNOWN,
|
|
375
|
-
);
|
|
376
|
-
assert.equal(yamlErrors(result).length, 1);
|
|
377
|
-
assert.match(yamlErrors(result)[0], /reserved|cannot begin|indicator/i);
|
|
378
|
-
});
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
for (const [label, value] of [
|
|
382
|
-
['a dash', '- Designs things. Use when alpha.'],
|
|
383
|
-
['a question mark', '? Designs things. Use when alpha.'],
|
|
384
|
-
['an ampersand', '& Designs things. Use when alpha.'],
|
|
385
|
-
]) {
|
|
386
|
-
test(`an unquoted value starting with ${label} and a space is rejected`, () => {
|
|
387
|
-
const result = lintSkillContent(
|
|
388
|
-
'alpha',
|
|
389
|
-
fmLines(`description: ${value}`),
|
|
390
|
-
KNOWN,
|
|
391
|
-
);
|
|
392
|
-
assert.equal(yamlErrors(result).length, 1);
|
|
393
|
-
});
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
test('quoting the value makes every reserved start valid again', () => {
|
|
397
|
-
for (const value of ['`alpha` x', '@team x', '%x', '- x', '? x', '& x']) {
|
|
398
|
-
const result = lintSkillContent(
|
|
399
|
-
'alpha',
|
|
400
|
-
fmLines(`description: "${value}"`),
|
|
401
|
-
KNOWN,
|
|
402
|
-
);
|
|
403
|
-
assert.deepEqual(yamlErrors(result), [], `quoted ${value} must be accepted`);
|
|
404
|
-
}
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
test('starts that YAML accepts are deliberately NOT rejected', () => {
|
|
408
|
-
// Each parses cleanly under PyYAML and psych, so flagging them would be a
|
|
409
|
-
// false positive on valid frontmatter. Pinned so the rule stays narrow.
|
|
410
|
-
//
|
|
411
|
-
// `,leading comma is fine` used to be in this list. It is not fine — both
|
|
412
|
-
// parsers reject it, and this test was pinning a claim I had asserted without
|
|
413
|
-
// measuring. It now appears in the rejected set below instead.
|
|
414
|
-
for (const value of [
|
|
415
|
-
':platform is fine', // a colon not followed by a space
|
|
416
|
-
'-hyphenated is fine', // a dash not followed by a space
|
|
417
|
-
'Designs `alpha` things', // a backtick anywhere but the first character
|
|
418
|
-
'Designs @team things', // an at sign anywhere but the first character
|
|
419
|
-
'#not-a-comment-here',
|
|
420
|
-
'&anchor-like but valid', // `&foo` parses; only `& ` is an indicator
|
|
421
|
-
'[a, b]', // a flow sequence is valid, so `[` stays allowed
|
|
422
|
-
// `{a: b}` is valid YAML too, and `{` is likewise not flagged here — but it
|
|
423
|
-
// trips the pre-existing colon-space rule, so it is not asserted as accepted.
|
|
424
|
-
// That false positive predates this change and is left alone.
|
|
425
|
-
]) {
|
|
426
|
-
const result = lintSkillContent('alpha', fmLines(`description: ${value}`), KNOWN);
|
|
427
|
-
assert.deepEqual(yamlErrors(result), [], `${value} must be accepted`);
|
|
428
|
-
}
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
test('indicator characters both parsers reject are flagged', () => {
|
|
432
|
-
// Measured, not read off the spec: each of these is rejected by PyYAML and by
|
|
433
|
-
// psych, and none has a legitimate use at the start of a plain scalar.
|
|
434
|
-
for (const value of [
|
|
435
|
-
',leading comma',
|
|
436
|
-
'*alias-like',
|
|
437
|
-
'`skill` does X',
|
|
438
|
-
'@team owns this',
|
|
439
|
-
'%directive-like',
|
|
440
|
-
]) {
|
|
441
|
-
const result = lintSkillContent('alpha', fmLines(`description: ${value}`), KNOWN);
|
|
442
|
-
assert.equal(yamlErrors(result).length, 1, `${value} must be rejected`);
|
|
443
|
-
}
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
test('a one-line block scalar header is flagged but a real block scalar is not', () => {
|
|
447
|
-
// `|` with content on the same line is not a block scalar — it is a plain
|
|
448
|
-
// scalar opening with an indicator, and both parsers reject it. `|` alone,
|
|
449
|
-
// with indented lines under it, is valid and must stay allowed.
|
|
450
|
-
for (const value of ['|folded text', '>folded text']) {
|
|
451
|
-
const result = lintSkillContent('alpha', fmLines(`description: ${value}`), KNOWN);
|
|
452
|
-
assert.equal(yamlErrors(result).length, 1, `${value} must be rejected`);
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
const genuine = lintSkillContent('alpha', fmLines('description: |'), KNOWN);
|
|
456
|
-
assert.deepEqual(yamlErrors(genuine), [], 'a bare block-scalar header must be accepted');
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
test('a duplicate key is not reported, because YAML accepts it', () => {
|
|
460
|
-
// Deliberate boundary: `safe_load` accepts duplicate keys, so flagging them
|
|
461
|
-
// here would fail files no host rejects. The rule tracks the parser, not taste.
|
|
462
|
-
const result = lintSkillContent(
|
|
463
|
-
'alpha',
|
|
464
|
-
fmLines('description: Use when you need alpha', 'description: Use when you need alpha'),
|
|
465
|
-
KNOWN,
|
|
466
|
-
);
|
|
467
|
-
assert.deepEqual(yamlErrors(result), []);
|
|
468
|
-
});
|
|
469
|
-
|
|
470
|
-
test('the error names the line so the fix is obvious', () => {
|
|
471
|
-
const result = lintSkillContent(
|
|
472
|
-
'alpha',
|
|
473
|
-
fmLines('description: Use when you need alpha', 'owner: team: platform'),
|
|
474
|
-
KNOWN,
|
|
475
|
-
);
|
|
476
|
-
assert.match(yamlErrors(result)[0], /^Frontmatter line 4 /);
|
|
477
|
-
});
|
|
478
|
-
|
|
479
|
-
// ─── Context budget ──────────────────────────────────────────────────────────
|
|
480
|
-
|
|
481
|
-
test('warns when SKILL.md exceeds the 500-line context budget', () => {
|
|
482
|
-
const padded = withAllSections(VALID_FRONTMATTER) + '\n'.repeat(600);
|
|
483
|
-
|
|
484
|
-
const { errors, warnings } = lintSkillContent('alpha', padded, KNOWN);
|
|
485
|
-
|
|
486
|
-
assert.equal(errors.length, 0, 'an over-budget skill must not block CI');
|
|
487
|
-
assert.equal(warnings.length, 1);
|
|
488
|
-
assert.match(warnings[0], /over the 500-line context budget/);
|
|
489
|
-
});
|
|
490
|
-
|
|
491
|
-
test('a SKILL.md at exactly the budget is not flagged', () => {
|
|
492
|
-
const base = withAllSections(VALID_FRONTMATTER);
|
|
493
|
-
const baseLines = (base.match(/\n/g) || []).length;
|
|
494
|
-
const atBudget = base + '\n'.repeat(500 - baseLines);
|
|
495
|
-
|
|
496
|
-
assert.equal((atBudget.match(/\n/g) || []).length, 500, 'fixture must sit exactly on the boundary');
|
|
497
|
-
const { warnings } = lintSkillContent('alpha', atBudget, KNOWN);
|
|
498
|
-
|
|
499
|
-
assert.equal(warnings.length, 0);
|
|
500
|
-
});
|
|
501
|
-
|
|
502
|
-
// ─── Layout ──────────────────────────────────────────────────────────────────
|
|
503
|
-
|
|
504
|
-
test('reports an empty scripts/ directory', () => {
|
|
505
|
-
const dir = makeSkillDir({ dirs: ['scripts'] });
|
|
506
|
-
|
|
507
|
-
const errors = lintSkillLayout(dir);
|
|
508
|
-
|
|
509
|
-
assert.equal(errors.length, 1);
|
|
510
|
-
assert.match(errors[0], /Empty directory `scripts\/`/);
|
|
511
|
-
});
|
|
512
|
-
|
|
513
|
-
test('reports a directory that only nests more empty directories', () => {
|
|
514
|
-
const dir = makeSkillDir({ dirs: ['references', 'references/deep'] });
|
|
515
|
-
|
|
516
|
-
const errors = lintSkillLayout(dir);
|
|
517
|
-
|
|
518
|
-
assert.equal(errors.length, 1, 'the outermost empty directory is named once, not every level');
|
|
519
|
-
assert.match(errors[0], /Empty directory `references\/`/);
|
|
520
|
-
});
|
|
521
|
-
|
|
522
|
-
test('a directory holding a file is not empty', () => {
|
|
523
|
-
const dir = makeSkillDir({ files: { 'scripts/helper.sh': '#!/bin/bash\nset -e\n' } });
|
|
524
|
-
|
|
525
|
-
assert.deepEqual(lintSkillLayout(dir), []);
|
|
526
|
-
});
|
|
527
|
-
|
|
528
|
-
test('reports a supporting .md file that is not lowercase-hyphen-separated', () => {
|
|
529
|
-
const dir = makeSkillDir({ files: { 'Refinement_Criteria.md': 'x\n' } });
|
|
530
|
-
|
|
531
|
-
const errors = lintSkillLayout(dir);
|
|
532
|
-
|
|
533
|
-
assert.equal(errors.length, 1);
|
|
534
|
-
assert.match(errors[0], /Supporting file `Refinement_Criteria\.md` is not lowercase-hyphen-separated/);
|
|
535
|
-
});
|
|
536
|
-
|
|
537
|
-
test('names a badly named supporting file by its path within the skill', () => {
|
|
538
|
-
const dir = makeSkillDir({ files: { 'references/Floor_Guard.md': 'x\n' } });
|
|
539
|
-
|
|
540
|
-
const errors = lintSkillLayout(dir);
|
|
541
|
-
|
|
542
|
-
assert.equal(errors.length, 1);
|
|
543
|
-
assert.match(errors[0], /`references\/Floor_Guard\.md`/);
|
|
544
|
-
});
|
|
545
|
-
|
|
546
|
-
test('SKILL.md is exempt from the supporting-file naming rule', () => {
|
|
547
|
-
const dir = makeSkillDir({ files: { 'SKILL.md': 'x\n', 'examples.md': 'x\n' } });
|
|
548
|
-
|
|
549
|
-
assert.deepEqual(lintSkillLayout(dir), []);
|
|
550
|
-
});
|
|
551
|
-
|
|
552
|
-
test('non-markdown files are left to the Script Requirements conventions', () => {
|
|
553
|
-
const dir = makeSkillDir({ files: { 'scripts/Idea_Refine.sh': '#!/bin/bash\nset -e\n' } });
|
|
554
|
-
|
|
555
|
-
assert.deepEqual(lintSkillLayout(dir), []);
|
|
556
|
-
});
|
|
557
|
-
|
|
558
|
-
// ─── Spec-only top-level frontmatter keys ────────────────────────────────────
|
|
559
|
-
//
|
|
560
|
-
// docs/advanced-per-agent-configuration.md: the specification reserves the top
|
|
561
|
-
// level for name, description, license, compatibility, metadata and
|
|
562
|
-
// allowed-tools. Vendor and runtime fields go under `metadata` or in a
|
|
563
|
-
// per-agent adapter file, never at the top level of a published SKILL.md.
|
|
564
|
-
|
|
565
|
-
function skillWithFrontmatter(lines) {
|
|
566
|
-
return withAllSections(['---', ...lines, '---'].join('\n'));
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
const SPEC_KEY_RE = /is not an Agent Skills spec field/;
|
|
570
|
-
|
|
571
|
-
test('a vendor field at the top level is rejected and the message names the key', () => {
|
|
572
|
-
const { errors } = lintSkillContent('alpha', skillWithFrontmatter([
|
|
573
|
-
'name: alpha',
|
|
574
|
-
'description: Designs alphas. Use when building one.',
|
|
575
|
-
'model: claude-opus-5',
|
|
576
|
-
]), KNOWN);
|
|
577
|
-
assert.equal(errors.length, 1);
|
|
578
|
-
assert.match(errors[0], /Frontmatter key 'model'/);
|
|
579
|
-
assert.match(errors[0], SPEC_KEY_RE);
|
|
580
|
-
assert.match(errors[0], /advanced-per-agent-configuration\.md/);
|
|
581
|
-
});
|
|
582
|
-
|
|
583
|
-
test('every unknown top-level key is reported, not just the first', () => {
|
|
584
|
-
const { errors } = lintSkillContent('alpha', skillWithFrontmatter([
|
|
585
|
-
'name: alpha',
|
|
586
|
-
'description: Designs alphas. Use when building one.',
|
|
587
|
-
'max_turns: 10',
|
|
588
|
-
'tools: [read_file]',
|
|
589
|
-
'context: fork',
|
|
590
|
-
]), KNOWN);
|
|
591
|
-
const keyErrors = errors.filter(e => SPEC_KEY_RE.test(e));
|
|
592
|
-
assert.deepEqual(keyErrors.map(e => e.match(/key '([^']+)'/)[1]), ['max_turns', 'tools', 'context']);
|
|
593
|
-
});
|
|
594
|
-
|
|
595
|
-
test('all six specification keys are accepted at the top level', () => {
|
|
596
|
-
const { errors } = lintSkillContent('alpha', skillWithFrontmatter([
|
|
597
|
-
'name: alpha',
|
|
598
|
-
'description: Designs alphas. Use when building one.',
|
|
599
|
-
'license: MIT',
|
|
600
|
-
'compatibility: Requires git and a test runner',
|
|
601
|
-
'allowed-tools: Read Grep',
|
|
602
|
-
'metadata:',
|
|
603
|
-
' author: someone',
|
|
604
|
-
]), KNOWN);
|
|
605
|
-
assert.deepEqual(errors.filter(e => SPEC_KEY_RE.test(e)), []);
|
|
606
|
-
});
|
|
607
|
-
|
|
608
|
-
test('vendor fields nested under metadata are not top-level keys', () => {
|
|
609
|
-
const { errors } = lintSkillContent('alpha', skillWithFrontmatter([
|
|
610
|
-
'name: alpha',
|
|
611
|
-
'description: Designs alphas. Use when building one.',
|
|
612
|
-
'metadata:',
|
|
613
|
-
' model: gemini-3-pro',
|
|
614
|
-
' max_turns: 10',
|
|
615
|
-
' tools:',
|
|
616
|
-
' - read_file',
|
|
617
|
-
]), KNOWN);
|
|
618
|
-
assert.deepEqual(errors.filter(e => SPEC_KEY_RE.test(e)), []);
|
|
619
|
-
});
|
|
620
|
-
|
|
621
|
-
test('topLevelFrontmatterKeys sees only column-zero keys, in order', () => {
|
|
622
|
-
const content = [
|
|
623
|
-
'---',
|
|
624
|
-
'name: alpha',
|
|
625
|
-
'# a comment: with a colon',
|
|
626
|
-
'metadata:',
|
|
627
|
-
' model: x',
|
|
628
|
-
' tools:',
|
|
629
|
-
' - a',
|
|
630
|
-
'allowed-tools: Read',
|
|
631
|
-
'---',
|
|
632
|
-
'',
|
|
633
|
-
'body: not frontmatter',
|
|
634
|
-
].join('\n');
|
|
635
|
-
assert.deepEqual(topLevelFrontmatterKeys(content), ['name', 'metadata', 'allowed-tools']);
|
|
636
|
-
});
|
|
637
|
-
|
|
638
|
-
test('topLevelFrontmatterKeys returns [] without a frontmatter block', () => {
|
|
639
|
-
assert.deepEqual(topLevelFrontmatterKeys('## Overview\nx\n'), []);
|
|
640
|
-
});
|
|
641
|
-
|
|
642
|
-
test('the spec-key check tolerates CRLF frontmatter', () => {
|
|
643
|
-
const content = skillWithFrontmatter([
|
|
644
|
-
'name: alpha',
|
|
645
|
-
'description: Designs alphas. Use when building one.',
|
|
646
|
-
'temperature: 0.2',
|
|
647
|
-
]).replace(/\n/g, '\r\n');
|
|
648
|
-
const { errors } = lintSkillContent('alpha', content, KNOWN);
|
|
649
|
-
assert.equal(errors.filter(e => /key 'temperature'/.test(e)).length, 1);
|
|
650
|
-
});
|