@givolt/i3-composer-agent 2.0.0 → 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-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +29 -32
- 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 +27 -12
- package/bin/install.cjs +74 -28
- package/bin/native-content.cjs +29 -0
- package/bin/wizard.cjs +2 -1
- package/docs/antigravity-setup.md +1 -1
- package/docs/codebuddy-setup.md +11 -3
- package/docs/cursor-setup.md +2 -2
- package/docs/getting-started.md +0 -2
- package/docs/opencode-setup.md +4 -4
- package/docs/workbuddy-setup.md +92 -0
- package/package.json +36 -20
- package/plugin.json +1 -1
- package/CHANGELOG.md +0 -51
- package/NOTICE.md +0 -15
- 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/i3-api-and-interface-design.json +0 -45
- package/evals/cases/i3-browser-testing-with-devtools.json +0 -44
- package/evals/cases/i3-ci-cd-and-automation.json +0 -45
- package/evals/cases/i3-code-review-and-quality.json +0 -49
- package/evals/cases/i3-code-simplification.json +0 -45
- package/evals/cases/i3-constraint-driven-development.json +0 -79
- package/evals/cases/i3-context-engineering.json +0 -44
- package/evals/cases/i3-debugging-and-error-recovery.json +0 -58
- package/evals/cases/i3-deprecation-and-migration.json +0 -52
- package/evals/cases/i3-documentation-and-adrs.json +0 -44
- package/evals/cases/i3-doubt-driven-development.json +0 -43
- package/evals/cases/i3-frontend-ui-engineering.json +0 -52
- package/evals/cases/i3-git-workflow-and-versioning.json +0 -47
- package/evals/cases/i3-idea-refine.json +0 -43
- package/evals/cases/i3-incremental-implementation.json +0 -57
- package/evals/cases/i3-interview-me.json +0 -42
- package/evals/cases/i3-observability-and-instrumentation.json +0 -44
- package/evals/cases/i3-performance-optimization.json +0 -53
- package/evals/cases/i3-planning-and-task-breakdown.json +0 -45
- package/evals/cases/i3-security-and-hardening.json +0 -53
- package/evals/cases/i3-shipping-and-launch.json +0 -56
- package/evals/cases/i3-source-driven-development.json +0 -44
- package/evals/cases/i3-spec-driven-development.json +0 -72
- package/evals/cases/i3-test-driven-development.json +0 -73
- package/evals/cases/i3-using-agent-skills.json +0 -44
- package/evals/fixtures/i3-api-and-interface-design/service-brief.md +0 -19
- package/evals/fixtures/i3-browser-testing-with-devtools/README.md +0 -5
- package/evals/fixtures/i3-browser-testing-with-devtools/index.html +0 -24
- package/evals/fixtures/i3-browser-testing-with-devtools/server.js +0 -15
- package/evals/fixtures/i3-ci-cd-and-automation/package.json +0 -8
- package/evals/fixtures/i3-ci-cd-and-automation/src/slug.js +0 -3
- package/evals/fixtures/i3-ci-cd-and-automation/test/slug.test.js +0 -9
- package/evals/fixtures/i3-code-review-and-quality/user-search.diff +0 -16
- package/evals/fixtures/i3-code-simplification/config-parser.js +0 -46
- package/evals/fixtures/i3-code-simplification/config-parser.test.js +0 -15
- package/evals/fixtures/i3-context-engineering/context-audit.md +0 -15
- package/evals/fixtures/i3-debugging-and-error-recovery/pagination.js +0 -8
- package/evals/fixtures/i3-debugging-and-error-recovery/pagination.test.js +0 -9
- package/evals/fixtures/i3-debugging-and-error-recovery/time-pressure.md +0 -6
- package/evals/fixtures/i3-deprecation-and-migration/api-inventory.md +0 -9
- package/evals/fixtures/i3-documentation-and-adrs/decision-context.md +0 -16
- package/evals/fixtures/i3-doubt-driven-development/migration-plan.md +0 -19
- package/evals/fixtures/i3-frontend-ui-engineering/Button.tsx +0 -7
- package/evals/fixtures/i3-frontend-ui-engineering/design-system.md +0 -11
- package/evals/fixtures/i3-git-workflow-and-versioning/.eval/working-tree.patch +0 -21
- package/evals/fixtures/i3-git-workflow-and-versioning/app.js +0 -7
- package/evals/fixtures/i3-git-workflow-and-versioning/app.test.js +0 -9
- package/evals/fixtures/i3-incremental-implementation/reports.js +0 -7
- package/evals/fixtures/i3-incremental-implementation/reports.test.js +0 -12
- package/evals/fixtures/i3-incremental-implementation/tasks/plan.md +0 -8
- package/evals/fixtures/i3-observability-and-instrumentation/operations.md +0 -11
- package/evals/fixtures/i3-observability-and-instrumentation/payment-retry.js +0 -14
- package/evals/fixtures/i3-performance-optimization/benchmark.js +0 -15
- package/evals/fixtures/i3-performance-optimization/products.js +0 -14
- package/evals/fixtures/i3-planning-and-task-breakdown/notifications-spec.md +0 -18
- package/evals/fixtures/i3-security-and-hardening/webhook.js +0 -11
- package/evals/fixtures/i3-security-and-hardening/webhook.test.js +0 -13
- package/evals/fixtures/i3-shipping-and-launch/authority-pressure.md +0 -6
- package/evals/fixtures/i3-shipping-and-launch/launch-status.md +0 -11
- package/evals/fixtures/i3-source-driven-development/framework-task.md +0 -10
- package/evals/fixtures/i3-spec-driven-development/billing-brief.md +0 -16
- package/evals/fixtures/i3-test-driven-development/BUG.md +0 -10
- package/evals/fixtures/i3-test-driven-development/README.md +0 -28
- package/evals/fixtures/i3-test-driven-development/package.json +0 -9
- package/evals/fixtures/i3-test-driven-development/src/split.js +0 -8
- package/evals/fixtures/i3-test-driven-development/test/split.test.js +0 -13
- package/evals/fixtures/i3-using-agent-skills/incident.md +0 -6
- package/evals/fixtures/incremental-implementation-pressure/draft-export.js +0 -17
- package/evals/fixtures/incremental-implementation-pressure/scenario.md +0 -9
- package/evals/fixtures/spec-driven-development-decomposition/portal-brief.md +0 -22
- 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/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 -47
- package/rebrand/MAINTENANCE.md +0 -114
- package/rebrand/NOTICE.md +0 -15
- package/rebrand/PR-WORKFLOW.md +0 -34
- package/rebrand/PREFIX-SPEC.md +0 -16
- package/rebrand/UNIFICATION-SPEC.md +0 -33
- package/rebrand/VALIDATION.md +0 -32
- package/rebrand/VERSIONING.md +0 -35
- package/rebrand/apply.cjs +0 -217
- package/rebrand/brand.json +0 -13
- package/rebrand/catalog.cjs +0 -70
- package/rebrand/fork-patches.json +0 -27
- package/rebrand/identifiers.cjs +0 -41
- package/rebrand/overlay/.github/workflows/test-plugin-install.yml +0 -177
- package/rebrand/overlay/CHANGELOG.md +0 -51
- 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 -180
- 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 -241
- 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 -130
- package/rebrand/templates/codebuddy-setup.md +0 -89
- package/rebrand/templates/codex-setup.md +0 -45
- package/rebrand/tests/pipeline.test.cjs +0 -253
- 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 -586
- 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 -130
- package/scripts/validate-brand.cjs +0 -44
- package/scripts/validate-commands-test.js +0 -216
- package/scripts/validate-commands.js +0 -223
- 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 -241
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
'use strict';
|
|
4
|
-
|
|
5
|
-
const assert = require('node:assert/strict');
|
|
6
|
-
const fs = require('node:fs');
|
|
7
|
-
const os = require('node:os');
|
|
8
|
-
const path = require('node:path');
|
|
9
|
-
const { spawnSync } = require('node:child_process');
|
|
10
|
-
const { afterEach, test } = require('node:test');
|
|
11
|
-
|
|
12
|
-
const VALIDATOR = path.join(__dirname, 'validate-artifact-paths.js');
|
|
13
|
-
const sandboxes = [];
|
|
14
|
-
|
|
15
|
-
function makeSandbox() {
|
|
16
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-skills-validate-artifact-paths-test-'));
|
|
17
|
-
const scriptsDir = path.join(root, 'scripts');
|
|
18
|
-
fs.mkdirSync(scriptsDir, { recursive: true });
|
|
19
|
-
fs.copyFileSync(VALIDATOR, path.join(scriptsDir, 'validate-artifact-paths.js'));
|
|
20
|
-
sandboxes.push(root);
|
|
21
|
-
return root;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function writeFile(root, relativePath, content) {
|
|
25
|
-
const file = path.join(root, relativePath);
|
|
26
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
27
|
-
fs.writeFileSync(file, content);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function run(root) {
|
|
31
|
-
return spawnSync(process.execPath, [path.join(root, 'scripts', 'validate-artifact-paths.js')], {
|
|
32
|
-
cwd: root,
|
|
33
|
-
encoding: 'utf8',
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
afterEach(() => {
|
|
38
|
-
for (const root of sandboxes.splice(0)) {
|
|
39
|
-
fs.rmSync(root, { recursive: true, force: true });
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test('passes when producers and consumers use the canonical artifact paths', () => {
|
|
44
|
-
const root = makeSandbox();
|
|
45
|
-
writeFile(root, '.claude/commands/spec.md', 'Save the spec as `SPEC.md` in the project root.\n');
|
|
46
|
-
writeFile(root, '.claude/commands/plan.md', 'Save the plan to `tasks/plan.md` and task list to `tasks/todo.md`.\n');
|
|
47
|
-
writeFile(root, '.claude/commands/build.md', 'Look for a spec at `SPEC.md`, `docs/SPEC.md`, or under `spec/`. Require `tasks/plan.md`.\n');
|
|
48
|
-
writeFile(root, 'skills/i3-spec-driven-development/SKILL.md', 'Save the plan to `tasks/plan.md` and the task list to `tasks/todo.md`.\n');
|
|
49
|
-
writeFile(root, 'skills/i3-planning-and-task-breakdown/SKILL.md', 'Save to `tasks/plan.md` and `tasks/todo.md`.\n');
|
|
50
|
-
|
|
51
|
-
const result = run(root);
|
|
52
|
-
|
|
53
|
-
assert.equal(result.status, 0, result.stdout + result.stderr);
|
|
54
|
-
assert.match(result.stdout, /5 files checked — 0 error\(s\) — PASSED/);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test('fails when a producer drifts to an unapproved artifact path (the #93 regression)', () => {
|
|
58
|
-
const root = makeSandbox();
|
|
59
|
-
// Only the producers are changed to the per-feature layout; consumers stay on the old paths.
|
|
60
|
-
writeFile(root, '.claude/commands/spec.md', 'Save the spec to `docs/features/[feature-name]/spec.md`.\n');
|
|
61
|
-
writeFile(root, '.claude/commands/plan.md', 'Save the plan to `docs/features/[feature-name]/plan.md`.\n');
|
|
62
|
-
writeFile(root, '.claude/commands/build.md', 'Require a spec at `SPEC.md` and a plan at `tasks/plan.md`.\n');
|
|
63
|
-
|
|
64
|
-
const result = run(root);
|
|
65
|
-
|
|
66
|
-
assert.equal(result.status, 1, result.stdout + result.stderr);
|
|
67
|
-
assert.match(result.stdout, /docs\/features\/\[feature-name\]\/spec\.md/);
|
|
68
|
-
assert.match(result.stdout, /docs\/features\/\[feature-name\]\/plan\.md/);
|
|
69
|
-
assert.match(result.stdout, /error\(s\) — FAILED/);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test('reports the offending file and line number', () => {
|
|
73
|
-
const root = makeSandbox();
|
|
74
|
-
writeFile(root, '.claude/commands/plan.md', 'Intro line.\nSave the plan to `plan.md` in the root.\n');
|
|
75
|
-
|
|
76
|
-
const result = run(root);
|
|
77
|
-
|
|
78
|
-
assert.equal(result.status, 1, result.stdout + result.stderr);
|
|
79
|
-
assert.match(result.stdout, /\.claude\/commands\/plan\.md/);
|
|
80
|
-
assert.match(result.stdout, /L2:/);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
test('accepts the docs/SPEC.md alternate spec location', () => {
|
|
84
|
-
const root = makeSandbox();
|
|
85
|
-
writeFile(root, '.claude/commands/build.md', 'Look for the spec at `docs/SPEC.md`.\n');
|
|
86
|
-
|
|
87
|
-
const result = run(root);
|
|
88
|
-
|
|
89
|
-
assert.equal(result.status, 0, result.stdout + result.stderr);
|
|
90
|
-
assert.match(result.stdout, /1 files checked — 0 error\(s\) — PASSED/);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test('ignores non-artifact markdown references (no false positives)', () => {
|
|
94
|
-
const root = makeSandbox();
|
|
95
|
-
writeFile(
|
|
96
|
-
root,
|
|
97
|
-
'skills/i3-spec-driven-development/SKILL.md',
|
|
98
|
-
'See `SKILL.md` and `references/testing-patterns.md`. Save the plan to `tasks/plan.md`.\n',
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
const result = run(root);
|
|
102
|
-
|
|
103
|
-
assert.equal(result.status, 0, result.stdout + result.stderr);
|
|
104
|
-
assert.match(result.stdout, /1 files checked — 0 error\(s\) — PASSED/);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test('skips guarded files that do not exist', () => {
|
|
108
|
-
const root = makeSandbox();
|
|
109
|
-
writeFile(root, '.claude/commands/spec.md', 'Save the spec as `SPEC.md`.\n');
|
|
110
|
-
// No other guarded files present.
|
|
111
|
-
|
|
112
|
-
const result = run(root);
|
|
113
|
-
|
|
114
|
-
assert.equal(result.status, 0, result.stdout + result.stderr);
|
|
115
|
-
assert.match(result.stdout, /1 files checked — 0 error\(s\) — PASSED/);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
// ─── Every command surface is guarded, not only the Claude Code copy ────────
|
|
119
|
-
//
|
|
120
|
-
// The Gemini and root TOML command sets carry their own prompt bodies, which
|
|
121
|
-
// validate-commands.js deliberately does not compare. A producer can therefore
|
|
122
|
-
// drift on one surface while `.claude/commands/` stays correct, and before
|
|
123
|
-
// these files were guarded that drift merged silently.
|
|
124
|
-
|
|
125
|
-
test('fails when a Gemini command drifts while the Claude Code copy stays canonical', () => {
|
|
126
|
-
const root = makeSandbox();
|
|
127
|
-
writeFile(root, '.claude/commands/spec.md', 'Save the spec as `SPEC.md` in the project root.\n');
|
|
128
|
-
writeFile(
|
|
129
|
-
root,
|
|
130
|
-
'.gemini/commands/spec.toml',
|
|
131
|
-
'description = "Start spec-driven development"\n\nprompt = """\nSave the spec to `docs/features/[feature-name]/spec.md`.\n"""\n',
|
|
132
|
-
);
|
|
133
|
-
|
|
134
|
-
const result = run(root);
|
|
135
|
-
|
|
136
|
-
assert.equal(result.status, 1, result.stdout + result.stderr);
|
|
137
|
-
assert.match(result.stdout, /\.gemini\/commands\/spec\.toml/);
|
|
138
|
-
assert.match(result.stdout, /L4: docs\/features\/\[feature-name\]\/spec\.md/);
|
|
139
|
-
assert.match(result.stdout, /1 error\(s\) — FAILED/);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
test('fails when a root TOML consumer drifts away from the producers', () => {
|
|
143
|
-
const root = makeSandbox();
|
|
144
|
-
writeFile(root, 'commands/planning.toml', 'prompt = """\nSave the plan to `tasks/plan.md` and the task list to `tasks/todo.md`.\n"""\n');
|
|
145
|
-
writeFile(root, 'commands/build.toml', 'prompt = """\nRead the plan from `planning/plan.md` before starting.\n"""\n');
|
|
146
|
-
|
|
147
|
-
const result = run(root);
|
|
148
|
-
|
|
149
|
-
assert.equal(result.status, 1, result.stdout + result.stderr);
|
|
150
|
-
assert.match(result.stdout, /commands\/build\.toml/);
|
|
151
|
-
assert.match(result.stdout, /planning\/plan\.md/);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
test('passes when all three command surfaces agree on the canonical paths', () => {
|
|
155
|
-
const root = makeSandbox();
|
|
156
|
-
for (const dir of ['.claude/commands', '.gemini/commands', 'commands']) {
|
|
157
|
-
const ext = dir === '.claude/commands' ? 'md' : 'toml';
|
|
158
|
-
const planStem = dir === '.claude/commands' ? 'plan' : 'planning';
|
|
159
|
-
writeFile(root, `${dir}/spec.${ext}`, 'Save the spec as `SPEC.md`.\n');
|
|
160
|
-
writeFile(root, `${dir}/${planStem}.${ext}`, 'Save to `tasks/plan.md` and `tasks/todo.md`.\n');
|
|
161
|
-
writeFile(root, `${dir}/build.${ext}`, 'Require `SPEC.md` or `docs/SPEC.md`, and `tasks/plan.md`.\n');
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const result = run(root);
|
|
165
|
-
|
|
166
|
-
assert.equal(result.status, 0, result.stdout + result.stderr);
|
|
167
|
-
assert.match(result.stdout, /9 files checked — 0 error\(s\) — PASSED/);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
test('guards the Copilot setup doc, whose prompt-file aliases name the artifacts', () => {
|
|
171
|
-
const root = makeSandbox();
|
|
172
|
-
writeFile(root, 'docs/copilot-setup.md', 'Read `tasks/plan.md`, then write the spec to `specs/[name]/spec.md`.\n');
|
|
173
|
-
|
|
174
|
-
const result = run(root);
|
|
175
|
-
|
|
176
|
-
assert.equal(result.status, 1, result.stdout + result.stderr);
|
|
177
|
-
assert.match(result.stdout, /docs\/copilot-setup\.md/);
|
|
178
|
-
assert.match(result.stdout, /specs\/\[name\]\/spec\.md/);
|
|
179
|
-
});
|
|
@@ -1,130 +0,0 @@
|
|
|
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();
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
const assert = require('node:assert/strict');
|
|
5
|
-
const fs = require('node:fs');
|
|
6
|
-
const path = require('node:path');
|
|
7
|
-
const root = path.resolve(__dirname, '..');
|
|
8
|
-
const brand = require('../rebrand/brand.json');
|
|
9
|
-
const pkg = require('../package.json');
|
|
10
|
-
const upstream = require('../rebrand/upstream.json');
|
|
11
|
-
const { validateCatalog } = require('../rebrand/catalog.cjs');
|
|
12
|
-
validateCatalog(root, upstream, { branded: true });
|
|
13
|
-
function json(relative) { return JSON.parse(fs.readFileSync(path.join(root, relative), 'utf8')); }
|
|
14
|
-
assert.equal(pkg.name, brand.npmName);
|
|
15
|
-
assert.equal(pkg.version, brand.releaseVersion);
|
|
16
|
-
assert.equal(brand.upstreamCommit, upstream.upstreamCommit);
|
|
17
|
-
assert.equal(pkg.i3ComposerAgent.upstreamVersion, upstream.upstreamVersion);
|
|
18
|
-
assert.equal(pkg.i3ComposerAgent.upstreamCommit, upstream.upstreamCommit);
|
|
19
|
-
assert.equal(pkg.i3ComposerAgent.upstreamRepository, brand.upstream);
|
|
20
|
-
assert.equal(Object.keys(pkg.bin).length, 1, 'npx must resolve one executable');
|
|
21
|
-
assert.ok(fs.existsSync(path.join(root, pkg.bin[brand.name])));
|
|
22
|
-
for (const relative of ['plugin.json', '.claude-plugin/plugin.json', '.codex-plugin/plugin.json']) {
|
|
23
|
-
assert.equal(json(relative).name, brand.name, `${relative}: inconsistent plugin identity`);
|
|
24
|
-
}
|
|
25
|
-
for (const relative of ['.claude-plugin/marketplace.json', '.agents/plugins/marketplace.json']) {
|
|
26
|
-
const marketplace = json(relative);
|
|
27
|
-
assert.equal(marketplace.name, brand.name);
|
|
28
|
-
assert.equal(marketplace.plugins[0].name, brand.name);
|
|
29
|
-
assert.ok(marketplace.plugins[0].source === './' || marketplace.plugins[0].source.path === './', 'marketplace must load this distribution');
|
|
30
|
-
}
|
|
31
|
-
assert.ok(json('.codex-plugin/plugin.json').interface.shortDescription.length <= 30);
|
|
32
|
-
const skills = fs.readdirSync(path.join(root, 'skills')).filter(name => fs.existsSync(path.join(root, 'skills', name, 'SKILL.md')));
|
|
33
|
-
assert.equal(skills.length, upstream.skills.length, 'brand conversion must preserve the reviewed upstream skills');
|
|
34
|
-
assert.ok(skills.includes('i3-using-agent-skills'), 'meta-skill ID must use the branded prefix');
|
|
35
|
-
const commands = fs.readdirSync(path.join(root, '.claude/commands')).filter(name => name.endsWith('.md'));
|
|
36
|
-
assert.equal(commands.length, upstream.commands.length, 'brand conversion must preserve the reviewed upstream commands');
|
|
37
|
-
for (const command of commands) {
|
|
38
|
-
const body = fs.readFileSync(path.join(root, '.claude/commands', command), 'utf8');
|
|
39
|
-
assert.ok(!body.includes('agent-skills:'), `${command}: stale plugin namespace`);
|
|
40
|
-
for (const match of body.matchAll(/(?<!\/)i3-composer-agent:([a-z0-9-]+)/g)) assert.ok(skills.includes(match[1]), `${command}: unknown skill ${match[1]}`);
|
|
41
|
-
}
|
|
42
|
-
for (const relative of pkg.i3ComposerAgent.pluginFiles) assert.ok(fs.existsSync(path.join(root, relative)), `bundle is missing ${relative}`);
|
|
43
|
-
assert.match(fs.readFileSync(path.join(root, 'LICENSE'), 'utf8'), /Copyright \(c\) 2025 Addy Osmani/);
|
|
44
|
-
console.log('Brand identity, package entrypoint, reviewed upstream catalog, and attribution are consistent.');
|
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
'use strict';
|
|
4
|
-
|
|
5
|
-
const assert = require('node:assert/strict');
|
|
6
|
-
const fs = require('node:fs');
|
|
7
|
-
const os = require('node:os');
|
|
8
|
-
const path = require('node:path');
|
|
9
|
-
const { spawnSync } = require('node:child_process');
|
|
10
|
-
const { afterEach, test } = require('node:test');
|
|
11
|
-
|
|
12
|
-
const VALIDATOR = path.join(__dirname, 'validate-commands.js');
|
|
13
|
-
// The validator shares the frontmatter-validity rules with validate-skills, so
|
|
14
|
-
// the sandbox needs the lib alongside it, not just the script.
|
|
15
|
-
const SKILL_LINT = path.join(__dirname, 'lib', 'skill-lint.js');
|
|
16
|
-
const sandboxes = [];
|
|
17
|
-
|
|
18
|
-
function makeSandbox() {
|
|
19
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-skills-validate-commands-test-'));
|
|
20
|
-
const scriptsDir = path.join(root, 'scripts');
|
|
21
|
-
fs.mkdirSync(scriptsDir, { recursive: true });
|
|
22
|
-
fs.copyFileSync(VALIDATOR, path.join(scriptsDir, 'validate-commands.js'));
|
|
23
|
-
fs.mkdirSync(path.join(scriptsDir, 'lib'), { recursive: true });
|
|
24
|
-
fs.copyFileSync(SKILL_LINT, path.join(scriptsDir, 'lib', 'skill-lint.js'));
|
|
25
|
-
sandboxes.push(root);
|
|
26
|
-
return root;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function writeFile(root, relativePath, content) {
|
|
30
|
-
const file = path.join(root, relativePath);
|
|
31
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
32
|
-
fs.writeFileSync(file, content);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function writeClaudeCommand(root, stem, descriptionLine) {
|
|
36
|
-
writeFile(
|
|
37
|
-
root,
|
|
38
|
-
path.join('.claude', 'commands', `${stem}.md`),
|
|
39
|
-
`---\n${descriptionLine}\n---\n\n# Command\n`,
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function writeTomlCommand(root, directory, stem, descriptionLine) {
|
|
44
|
-
writeFile(root, path.join(directory, `${stem}.toml`), `${descriptionLine}\nprompt = "Run command"\n`);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function writeMatchingCommands(root, stem, description) {
|
|
48
|
-
writeClaudeCommand(root, stem, `description: ${description}`);
|
|
49
|
-
writeTomlCommand(root, path.join('.gemini', 'commands'), stem, `description = "${description}"`);
|
|
50
|
-
writeTomlCommand(root, 'commands', stem, `description = "${description}"`);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function run(root) {
|
|
54
|
-
return spawnSync(process.execPath, [path.join(root, 'scripts', 'validate-commands.js')], {
|
|
55
|
-
cwd: root,
|
|
56
|
-
encoding: 'utf8',
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
afterEach(() => {
|
|
61
|
-
for (const root of sandboxes.splice(0)) {
|
|
62
|
-
fs.rmSync(root, { recursive: true, force: true });
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test('passes matching command twins and maps plan to planning', () => {
|
|
67
|
-
const root = makeSandbox();
|
|
68
|
-
const description = 'Break work into ordered tasks';
|
|
69
|
-
writeClaudeCommand(root, 'plan', `description: ${description}`);
|
|
70
|
-
writeTomlCommand(root, path.join('.gemini', 'commands'), 'planning', `description = '${description}'`);
|
|
71
|
-
writeTomlCommand(root, 'commands', 'planning', `description = '${description}'`);
|
|
72
|
-
|
|
73
|
-
const result = run(root);
|
|
74
|
-
|
|
75
|
-
assert.equal(result.status, 0, result.stdout + result.stderr);
|
|
76
|
-
assert.match(result.stdout, /✓ plan \(planning in toml dirs\)/);
|
|
77
|
-
assert.match(result.stdout, /1 commands checked — 0 error\(s\) — PASSED/);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test('fails when a Claude command is missing a TOML twin', () => {
|
|
81
|
-
const root = makeSandbox();
|
|
82
|
-
const description = 'Review a change';
|
|
83
|
-
writeClaudeCommand(root, 'review', `description: ${description}`);
|
|
84
|
-
writeTomlCommand(root, path.join('.gemini', 'commands'), 'review', `description = "${description}"`);
|
|
85
|
-
|
|
86
|
-
const result = run(root);
|
|
87
|
-
|
|
88
|
-
assert.equal(result.status, 1, result.stdout + result.stderr);
|
|
89
|
-
assert.match(result.stdout, /review — missing in: commands/);
|
|
90
|
-
assert.match(result.stdout, /1 commands checked — 1 error\(s\) — FAILED/);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test('fails when a TOML command has no Claude twin', () => {
|
|
94
|
-
const root = makeSandbox();
|
|
95
|
-
writeTomlCommand(root, path.join('.gemini', 'commands'), 'deploy', 'description = "Deploy a change"');
|
|
96
|
-
writeTomlCommand(root, 'commands', 'deploy', 'description = "Deploy a change"');
|
|
97
|
-
|
|
98
|
-
const result = run(root);
|
|
99
|
-
|
|
100
|
-
assert.equal(result.status, 1, result.stdout + result.stderr);
|
|
101
|
-
assert.match(result.stdout, /deploy — present in toml dirs but missing in \.claude\/commands/);
|
|
102
|
-
assert.match(result.stdout, /1 commands checked — 1 error\(s\) — FAILED/);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
test('reports all descriptions when command twins drift', () => {
|
|
106
|
-
const root = makeSandbox();
|
|
107
|
-
writeClaudeCommand(root, 'review', 'description: Review a change');
|
|
108
|
-
writeTomlCommand(root, path.join('.gemini', 'commands'), 'review', 'description = "Inspect a change"');
|
|
109
|
-
writeTomlCommand(root, 'commands', 'review', 'description = "Audit a change"');
|
|
110
|
-
|
|
111
|
-
const result = run(root);
|
|
112
|
-
|
|
113
|
-
assert.equal(result.status, 1, result.stdout + result.stderr);
|
|
114
|
-
assert.match(result.stdout, /\.claude:\s+Review a change/);
|
|
115
|
-
assert.match(result.stdout, /\.gemini:\s+Inspect a change/);
|
|
116
|
-
assert.match(result.stdout, /commands\/:\s+Audit a change/);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
test('fails with an actionable error for a malformed description', () => {
|
|
120
|
-
const root = makeSandbox();
|
|
121
|
-
writeMatchingCommands(root, 'review', 'Review a change');
|
|
122
|
-
writeFile(
|
|
123
|
-
root,
|
|
124
|
-
path.join('.gemini', 'commands', 'review.toml'),
|
|
125
|
-
'prompt = "Missing description"\n',
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
const result = run(root);
|
|
129
|
-
|
|
130
|
-
assert.equal(result.status, 1, result.stdout + result.stderr);
|
|
131
|
-
assert.match(result.stdout, /\.gemini\/commands\/review — missing or malformed description/);
|
|
132
|
-
assert.match(result.stdout, /1 commands checked — 1 error\(s\) — FAILED/);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
test('parses escaped quotes in double-quoted TOML descriptions', () => {
|
|
136
|
-
const root = makeSandbox();
|
|
137
|
-
writeClaudeCommand(root, 'review', 'description: Review "important" changes');
|
|
138
|
-
writeTomlCommand(
|
|
139
|
-
root,
|
|
140
|
-
path.join('.gemini', 'commands'),
|
|
141
|
-
'review',
|
|
142
|
-
'description = "Review \\"important\\" changes"',
|
|
143
|
-
);
|
|
144
|
-
writeTomlCommand(
|
|
145
|
-
root,
|
|
146
|
-
'commands',
|
|
147
|
-
'review',
|
|
148
|
-
'description = "Review \\"important\\" changes"',
|
|
149
|
-
);
|
|
150
|
-
|
|
151
|
-
const result = run(root);
|
|
152
|
-
|
|
153
|
-
assert.equal(result.status, 0, result.stdout + result.stderr);
|
|
154
|
-
assert.match(result.stdout, /1 commands checked — 0 error\(s\) — PASSED/);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
// Claude parses a command's frontmatter as YAML when the command is loaded, and
|
|
158
|
-
// `descriptionFromMd` splits each line on its first colon — so a command whose
|
|
159
|
-
// frontmatter is not valid YAML passed every check here. Same class as the
|
|
160
|
-
// SKILL.md gap, on the other set of files the #494 thread checked by hand.
|
|
161
|
-
|
|
162
|
-
test('a command whose frontmatter is valid YAML passes', () => {
|
|
163
|
-
const root = makeSandbox();
|
|
164
|
-
writeMatchingCommands(root, 'build', 'Build the thing');
|
|
165
|
-
const result = run(root);
|
|
166
|
-
assert.equal(result.status, 0, result.stdout);
|
|
167
|
-
assert.match(result.stdout, /Checking Claude command frontmatter/);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
test('an unquoted colon in a command description is rejected', () => {
|
|
171
|
-
const root = makeSandbox();
|
|
172
|
-
// Valid to the splitter, rejected by a real YAML parser: it reads a nested
|
|
173
|
-
// mapping. The TOML siblings keep the same text so only the .md is at fault.
|
|
174
|
-
writeClaudeCommand(root, 'build', 'description: Build the thing: quickly');
|
|
175
|
-
writeTomlCommand(root, path.join('.gemini', 'commands'), 'build', 'description = "Build the thing: quickly"');
|
|
176
|
-
writeTomlCommand(root, 'commands', 'build', 'description = "Build the thing: quickly"');
|
|
177
|
-
|
|
178
|
-
const result = run(root);
|
|
179
|
-
assert.equal(result.status, 1, result.stdout);
|
|
180
|
-
assert.match(result.stdout, /unquoted value containing/);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
test('quoting the same description makes it pass', () => {
|
|
184
|
-
const root = makeSandbox();
|
|
185
|
-
writeClaudeCommand(root, 'build', 'description: "Build the thing: quickly"');
|
|
186
|
-
writeTomlCommand(root, path.join('.gemini', 'commands'), 'build', 'description = "Build the thing: quickly"');
|
|
187
|
-
writeTomlCommand(root, 'commands', 'build', 'description = "Build the thing: quickly"');
|
|
188
|
-
|
|
189
|
-
const result = run(root);
|
|
190
|
-
assert.equal(result.status, 0, result.stdout);
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
test('a tab-indented command frontmatter is rejected', () => {
|
|
194
|
-
const root = makeSandbox();
|
|
195
|
-
writeFile(
|
|
196
|
-
root,
|
|
197
|
-
path.join('.claude', 'commands', 'build.md'),
|
|
198
|
-
'---\ndescription: Build the thing\nmeta:\n\tlevel: core\n---\n\n# Command\n',
|
|
199
|
-
);
|
|
200
|
-
writeTomlCommand(root, path.join('.gemini', 'commands'), 'build', 'description = "Build the thing"');
|
|
201
|
-
writeTomlCommand(root, 'commands', 'build', 'description = "Build the thing"');
|
|
202
|
-
|
|
203
|
-
const result = run(root);
|
|
204
|
-
assert.equal(result.status, 1, result.stdout);
|
|
205
|
-
assert.match(result.stdout, /indents with a tab/);
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
test('the TOML directories are not put through the YAML rules', () => {
|
|
209
|
-
const root = makeSandbox();
|
|
210
|
-
// A TOML description legitimately carries a colon. Running the YAML rules over
|
|
211
|
-
// these files would fail every command that has one.
|
|
212
|
-
writeMatchingCommands(root, 'build', 'Build the thing quickly');
|
|
213
|
-
const result = run(root);
|
|
214
|
-
assert.equal(result.status, 0, result.stdout);
|
|
215
|
-
assert.doesNotMatch(result.stdout, /\.gemini.*unquoted value/);
|
|
216
|
-
});
|