@givolt/i3-composer-agent 2.2.1 → 2.2.3
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 +46 -18
- package/bin/asset-fingerprint.cjs +3 -5
- package/bin/i3-composer-agent.cjs +10 -5
- package/bin/install.cjs +54 -66
- package/bin/legacy-native-files.json +329 -0
- package/bin/native-command-validation.cjs +22 -0
- package/bin/native-content.cjs +0 -1
- package/bin/native-layout.cjs +169 -0
- package/bin/native-migration.cjs +99 -0
- package/bin/native-runtime.cjs +62 -0
- package/bin/wizard.cjs +3 -1
- package/docs/advanced-per-agent-configuration.md +78 -0
- package/docs/codebuddy-setup.md +4 -3
- package/docs/codex-setup.md +5 -3
- package/docs/workbuddy-setup.md +8 -6
- package/evals/README.md +40 -21
- package/evals/skill-impact.md +6 -0
- package/package.json +39 -18
- package/plugin.json +1 -1
- package/scripts/floor-guard-reference-test.js +291 -0
- package/scripts/lib/skill-lint-test.js +650 -0
- package/scripts/run-evals-test.js +623 -0
- package/scripts/run-regressions.js +19 -0
- package/scripts/validate-artifact-paths-test.js +179 -0
- package/scripts/validate-commands-test.js +216 -0
- package/scripts/validate-reference-links-test.js +495 -0
- package/scripts/validate-versions-test.js +32 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
// Contract test for skills/i3-constraint-driven-development/references/floor-guard.md.
|
|
6
|
+
// The fenced `js` block is extracted unchanged and run, with real git, against small
|
|
7
|
+
// fixtures: each is a fresh repository with one source file, one test file and a
|
|
8
|
+
// CONSTRAINTS.md carrying a floor bullet, a coverage minimum, a bundle maximum and an
|
|
9
|
+
// Exceptions table. Loosening moves must exit 1, tightening and no-op moves must exit 0.
|
|
10
|
+
|
|
11
|
+
const assert = require('node:assert/strict');
|
|
12
|
+
const fs = require('node:fs');
|
|
13
|
+
const os = require('node:os');
|
|
14
|
+
const path = require('node:path');
|
|
15
|
+
const { spawnSync } = require('node:child_process');
|
|
16
|
+
const { after, before, test } = require('node:test');
|
|
17
|
+
|
|
18
|
+
const REFERENCE = path.join(
|
|
19
|
+
__dirname, '..', 'skills', 'i3-constraint-driven-development', 'references', 'floor-guard.md',
|
|
20
|
+
);
|
|
21
|
+
const CONSTRAINTS = [
|
|
22
|
+
'# Constraints',
|
|
23
|
+
'',
|
|
24
|
+
'Last reviewed: 2026-08-08 by @owner',
|
|
25
|
+
'',
|
|
26
|
+
'## Floor',
|
|
27
|
+
'- No skipped tests.',
|
|
28
|
+
'- Latency: p95 under 200 ms and p99 under 900 ms.',
|
|
29
|
+
'',
|
|
30
|
+
'| Dimension | Rule |',
|
|
31
|
+
'|---|---|',
|
|
32
|
+
'| Coverage | >= 80% |',
|
|
33
|
+
'| Bundle | <= 200 KB |',
|
|
34
|
+
'',
|
|
35
|
+
'## Exceptions',
|
|
36
|
+
'',
|
|
37
|
+
'| ID | Rule | Owner | Expires |',
|
|
38
|
+
'|---|---|---|---|',
|
|
39
|
+
'| W1 | no-explicit-any | @owner | 2026-11-01 |',
|
|
40
|
+
'',
|
|
41
|
+
].join('\n');
|
|
42
|
+
|
|
43
|
+
let scratch;
|
|
44
|
+
let guard;
|
|
45
|
+
const sandboxes = [];
|
|
46
|
+
|
|
47
|
+
before(() => {
|
|
48
|
+
scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-skills-floor-guard-test-'));
|
|
49
|
+
const source = fs.readFileSync(REFERENCE, 'utf8');
|
|
50
|
+
const match = source.match(/```js\n([\s\S]*?)\n```/);
|
|
51
|
+
assert.ok(match, 'floor-guard.md must contain one fenced js block');
|
|
52
|
+
guard = path.join(scratch, 'floor-guard.mjs');
|
|
53
|
+
fs.writeFileSync(guard, match[1] + '\n');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
after(() => {
|
|
57
|
+
for (const dir of sandboxes) fs.rmSync(dir, { recursive: true, force: true });
|
|
58
|
+
if (scratch) fs.rmSync(scratch, { recursive: true, force: true });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
function git(cwd, ...args) {
|
|
62
|
+
const result = spawnSync('git', args, {
|
|
63
|
+
cwd,
|
|
64
|
+
encoding: 'utf8',
|
|
65
|
+
env: {
|
|
66
|
+
...process.env,
|
|
67
|
+
GIT_AUTHOR_NAME: 'Floor Guard Test',
|
|
68
|
+
GIT_AUTHOR_EMAIL: 'floor-guard@example.invalid',
|
|
69
|
+
GIT_COMMITTER_NAME: 'Floor Guard Test',
|
|
70
|
+
GIT_COMMITTER_EMAIL: 'floor-guard@example.invalid',
|
|
71
|
+
GIT_CONFIG_GLOBAL: '/dev/null',
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
assert.equal(result.status, 0, `git ${args.join(' ')} failed: ${result.stderr}`);
|
|
75
|
+
return result.stdout;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function makeRepo() {
|
|
79
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-skills-floor-guard-fixture-'));
|
|
80
|
+
sandboxes.push(root);
|
|
81
|
+
git(root, 'init', '-q');
|
|
82
|
+
fs.writeFileSync(path.join(root, 'app.js'), 'module.exports = 1;\n');
|
|
83
|
+
fs.writeFileSync(
|
|
84
|
+
path.join(root, 'app.test.js'),
|
|
85
|
+
"const assert = require('assert');\nassert.equal(require('./app'), 1);\n",
|
|
86
|
+
);
|
|
87
|
+
fs.writeFileSync(path.join(root, 'CONSTRAINTS.md'), CONSTRAINTS);
|
|
88
|
+
git(root, 'add', '.');
|
|
89
|
+
git(root, 'commit', '-qm', 'baseline');
|
|
90
|
+
return root;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function editConstraints(root, from, to) {
|
|
94
|
+
const file = path.join(root, 'CONSTRAINTS.md');
|
|
95
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
96
|
+
assert.ok(text.includes(from), `fixture CONSTRAINTS.md must contain ${JSON.stringify(from)}`);
|
|
97
|
+
fs.writeFileSync(file, text.replace(from, to));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function commitFile(root, name, text) {
|
|
101
|
+
fs.writeFileSync(path.join(root, name), text);
|
|
102
|
+
git(root, 'add', name);
|
|
103
|
+
git(root, 'commit', '-qm', `add ${name}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function runGuard(root, cwd = root) {
|
|
107
|
+
return spawnSync(process.execPath, [guard, '--base', 'HEAD'], { cwd, encoding: 'utf8' });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const cases = [
|
|
111
|
+
['unchanged repository is clean', 0, () => {}],
|
|
112
|
+
['a suppression added to a tracked file is a violation', 1, (root) => {
|
|
113
|
+
fs.writeFileSync(path.join(root, 'app.js'), '// @ts-ignore\nmodule.exports = 1;\n');
|
|
114
|
+
}],
|
|
115
|
+
['a suppression in a new untracked file is a violation', 1, (root) => {
|
|
116
|
+
fs.writeFileSync(path.join(root, 'new.js'), '// @ts-ignore\nmodule.exports = 1;\n');
|
|
117
|
+
}],
|
|
118
|
+
['a skipped test in a new untracked file is a violation', 1, (root) => {
|
|
119
|
+
fs.writeFileSync(path.join(root, 'new.test.js'), "test.skip('regression', () => {});\n");
|
|
120
|
+
}],
|
|
121
|
+
['a suppression in a staged new file is a violation', 1, (root) => {
|
|
122
|
+
fs.writeFileSync(path.join(root, 'new.js'), '// @ts-ignore\nmodule.exports = 1;\n');
|
|
123
|
+
git(root, 'add', '-A');
|
|
124
|
+
}],
|
|
125
|
+
['a deleted test file is a violation', 1, (root) => {
|
|
126
|
+
fs.unlinkSync(path.join(root, 'app.test.js'));
|
|
127
|
+
}],
|
|
128
|
+
['a staged deleted test file is a violation', 1, (root) => {
|
|
129
|
+
fs.unlinkSync(path.join(root, 'app.test.js'));
|
|
130
|
+
git(root, 'add', '-A');
|
|
131
|
+
}],
|
|
132
|
+
['an assertion removed from a surviving test file is a violation', 1, (root) => {
|
|
133
|
+
fs.writeFileSync(path.join(root, 'app.test.js'), "const assert = require('assert');\n");
|
|
134
|
+
}],
|
|
135
|
+
['lowering a minimum is a violation', 1, (root) => {
|
|
136
|
+
editConstraints(root, '| Coverage | >= 80% |', '| Coverage | >= 60% |');
|
|
137
|
+
}],
|
|
138
|
+
['raising a minimum is silent', 0, (root) => {
|
|
139
|
+
editConstraints(root, '| Coverage | >= 80% |', '| Coverage | >= 90% |');
|
|
140
|
+
}],
|
|
141
|
+
['raising a maximum is a violation', 1, (root) => {
|
|
142
|
+
editConstraints(root, '| Bundle | <= 200 KB |', '| Bundle | <= 300 KB |');
|
|
143
|
+
}],
|
|
144
|
+
['lowering a maximum is silent', 0, (root) => {
|
|
145
|
+
editConstraints(root, '| Bundle | <= 200 KB |', '| Bundle | <= 150 KB |');
|
|
146
|
+
}],
|
|
147
|
+
['deleting one threshold from a rule is a violation', 1, (root) => {
|
|
148
|
+
editConstraints(root, 'p95 under 200 ms and p99 under 900 ms.', 'p95 under 200 ms.');
|
|
149
|
+
}],
|
|
150
|
+
['a number inserted before a threshold is silent', 0, (root) => {
|
|
151
|
+
editConstraints(root, '| Coverage | >= 80% |', '| Coverage | per ADR 7, >= 80% |');
|
|
152
|
+
}],
|
|
153
|
+
['adding a threshold to a rule is silent', 0, (root) => {
|
|
154
|
+
editConstraints(root, '| Bundle | <= 200 KB |', '| Bundle | <= 200 KB, <= 60 KB gzipped |');
|
|
155
|
+
}],
|
|
156
|
+
['a deleted floor rule is a violation', 1, (root) => {
|
|
157
|
+
editConstraints(root, '- No skipped tests.\n', '');
|
|
158
|
+
}],
|
|
159
|
+
['a new exception row is a violation', 1, (root) => {
|
|
160
|
+
const file = path.join(root, 'CONSTRAINTS.md');
|
|
161
|
+
fs.appendFileSync(file, '| W2 | temporary waiver | @owner | 2026-10-01 |\n');
|
|
162
|
+
}],
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
for (const [name, expectedExit, mutate] of cases) {
|
|
166
|
+
test(`floor guard: ${name}`, () => {
|
|
167
|
+
const root = makeRepo();
|
|
168
|
+
mutate(root);
|
|
169
|
+
const result = runGuard(root);
|
|
170
|
+
assert.equal(
|
|
171
|
+
result.status, expectedExit,
|
|
172
|
+
`expected exit ${expectedExit}, got ${result.status}\nstdout: ${result.stdout}\nstderr: ${result.stderr}`,
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
test('floor guard: a removed exception row is silent', () => {
|
|
178
|
+
const root = makeRepo();
|
|
179
|
+
editConstraints(root, '| W1 | no-explicit-any | @owner | 2026-11-01 |\n', '');
|
|
180
|
+
const result = runGuard(root);
|
|
181
|
+
assert.equal(result.status, 0, result.stderr);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('floor guard: a missing merge base exits 2, never 0', () => {
|
|
185
|
+
const root = makeRepo();
|
|
186
|
+
const result = spawnSync(process.execPath, [guard, '--base', 'no-such-ref'], {
|
|
187
|
+
cwd: root, encoding: 'utf8',
|
|
188
|
+
});
|
|
189
|
+
assert.equal(result.status, 2, result.stderr);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('floor guard: a threshold with no readable direction is reported when it changes', () => {
|
|
193
|
+
const root = makeRepo();
|
|
194
|
+
editConstraints(root, '| W1 | no-explicit-any | @owner | 2026-11-01 |', '| W1 | no-explicit-any | @owner | 2027-11-01 |');
|
|
195
|
+
const result = runGuard(root);
|
|
196
|
+
assert.equal(result.status, 1, result.stderr);
|
|
197
|
+
assert.match(result.stderr, /threshold-changed/);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('floor guard: a renamed rule is reported as removed, with a note that its label may have changed', () => {
|
|
201
|
+
const root = makeRepo();
|
|
202
|
+
editConstraints(root, '| Coverage | >= 80% |', '| Code coverage | >= 80% |');
|
|
203
|
+
const result = runGuard(root);
|
|
204
|
+
assert.equal(result.status, 1, result.stderr);
|
|
205
|
+
assert.match(result.stderr, /\[rule-removed\]/);
|
|
206
|
+
assert.match(result.stderr, /label changed/);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('floor guard: a threshold that loses its direction words is reported as removed, with a note saying so', () => {
|
|
210
|
+
const root = makeRepo();
|
|
211
|
+
editConstraints(root, '| Coverage | >= 80% |', '| Coverage | 80% |');
|
|
212
|
+
const result = runGuard(root);
|
|
213
|
+
assert.equal(result.status, 1, result.stderr);
|
|
214
|
+
assert.match(result.stderr, /\[threshold-removed\]/);
|
|
215
|
+
assert.match(result.stderr, /direction words/);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Inside a hunk every line is content. An added line that starts with `++` shows in the diff
|
|
219
|
+
// as `+++...`, and a removed line that starts with `--` as `---...`: neither is a file header.
|
|
220
|
+
test('floor guard: an added line that starts with ++ is still checked', () => {
|
|
221
|
+
const root = makeRepo();
|
|
222
|
+
fs.writeFileSync(
|
|
223
|
+
path.join(root, 'app.js'),
|
|
224
|
+
'let n = 0;\n++n; // eslint-disable-line no-plusplus\nmodule.exports = n;\n',
|
|
225
|
+
);
|
|
226
|
+
const result = runGuard(root);
|
|
227
|
+
assert.equal(result.status, 1, result.stderr);
|
|
228
|
+
assert.match(result.stderr, /\[silenced-checker\] app\.js:/);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test('floor guard: an added line that starts with "++ " does not rename the file for later hunks', () => {
|
|
232
|
+
const root = makeRepo();
|
|
233
|
+
commitFile(root, 'counter.test.js', [
|
|
234
|
+
"const assert = require('assert');",
|
|
235
|
+
'let n = 0;',
|
|
236
|
+
'n += 1;',
|
|
237
|
+
'const m = 2;',
|
|
238
|
+
'assert.equal(n, 1);',
|
|
239
|
+
'assert.equal(m, 2);',
|
|
240
|
+
'',
|
|
241
|
+
].join('\n'));
|
|
242
|
+
const file = path.join(root, 'counter.test.js');
|
|
243
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
244
|
+
fs.writeFileSync(file, text.replace('n += 1;', '++ n;').replace('assert.equal(m, 2);\n', ''));
|
|
245
|
+
const result = runGuard(root);
|
|
246
|
+
assert.equal(result.status, 1, result.stderr);
|
|
247
|
+
assert.match(result.stderr, /\[assertion-removed\] counter\.test\.js:/);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('floor guard: a removed line that starts with -- is still checked', () => {
|
|
251
|
+
const root = makeRepo();
|
|
252
|
+
commitFile(root, 'countdown.test.js', [
|
|
253
|
+
"const assert = require('assert');",
|
|
254
|
+
'let n = 1;',
|
|
255
|
+
'--n; assert.equal(n, 0);',
|
|
256
|
+
'',
|
|
257
|
+
].join('\n'));
|
|
258
|
+
const file = path.join(root, 'countdown.test.js');
|
|
259
|
+
fs.writeFileSync(file, fs.readFileSync(file, 'utf8').replace('--n; assert.equal(n, 0);\n', ''));
|
|
260
|
+
const result = runGuard(root);
|
|
261
|
+
assert.equal(result.status, 1, result.stderr);
|
|
262
|
+
assert.match(result.stderr, /\[assertion-removed\] countdown\.test\.js:/);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// `git ls-files` lists only the current directory's subtree, relative to it, while `git diff`
|
|
266
|
+
// covers the whole tree with paths from the top. Run from a subfolder, the guard has to see the
|
|
267
|
+
// same files, under the same names, as it does from the top.
|
|
268
|
+
test('floor guard: run from a subfolder, an untracked file outside it is still checked', () => {
|
|
269
|
+
const root = makeRepo();
|
|
270
|
+
fs.mkdirSync(path.join(root, 'sub'));
|
|
271
|
+
fs.writeFileSync(path.join(root, 'new.js'), '// @ts-ignore\nmodule.exports = 1;\n');
|
|
272
|
+
const result = runGuard(root, path.join(root, 'sub'));
|
|
273
|
+
assert.equal(result.status, 1, result.stderr);
|
|
274
|
+
assert.match(result.stderr, /\[silenced-checker\] new\.js:/);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test('floor guard: run from a subfolder, an untracked file inside it is named from the top', () => {
|
|
278
|
+
const root = makeRepo();
|
|
279
|
+
fs.mkdirSync(path.join(root, 'sub'));
|
|
280
|
+
fs.writeFileSync(path.join(root, 'sub', 'new.js'), '// @ts-ignore\nmodule.exports = 1;\n');
|
|
281
|
+
const result = runGuard(root, path.join(root, 'sub'));
|
|
282
|
+
assert.equal(result.status, 1, result.stderr);
|
|
283
|
+
assert.match(result.stderr, /\[silenced-checker\] sub\/new\.js:/);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test('floor guard: outside a git work tree exits 2, never 0', () => {
|
|
287
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-skills-floor-guard-no-repo-'));
|
|
288
|
+
sandboxes.push(dir);
|
|
289
|
+
const result = runGuard(dir);
|
|
290
|
+
assert.equal(result.status, 2, result.stderr);
|
|
291
|
+
});
|