@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.
Files changed (158) hide show
  1. package/.agents/plugins/marketplace.json +1 -1
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/.codex-plugin/plugin.json +1 -1
  5. package/README.md +29 -32
  6. package/bin/agent-targets.json +11 -0
  7. package/bin/asset-fingerprint.cjs +38 -0
  8. package/bin/cleanup-backups.cjs +16 -0
  9. package/bin/i3-composer-agent.cjs +27 -12
  10. package/bin/install.cjs +74 -28
  11. package/bin/native-content.cjs +29 -0
  12. package/bin/wizard.cjs +2 -1
  13. package/docs/antigravity-setup.md +1 -1
  14. package/docs/codebuddy-setup.md +11 -3
  15. package/docs/cursor-setup.md +2 -2
  16. package/docs/getting-started.md +0 -2
  17. package/docs/opencode-setup.md +4 -4
  18. package/docs/workbuddy-setup.md +92 -0
  19. package/package.json +36 -20
  20. package/plugin.json +1 -1
  21. package/CHANGELOG.md +0 -51
  22. package/NOTICE.md +0 -15
  23. package/docs/advanced-per-agent-configuration.md +0 -70
  24. package/docs/comparison.md +0 -131
  25. package/docs/developer-onboarding.md +0 -116
  26. package/docs/skill-anatomy.md +0 -203
  27. package/evals/README.md +0 -101
  28. package/evals/cases/i3-api-and-interface-design.json +0 -45
  29. package/evals/cases/i3-browser-testing-with-devtools.json +0 -44
  30. package/evals/cases/i3-ci-cd-and-automation.json +0 -45
  31. package/evals/cases/i3-code-review-and-quality.json +0 -49
  32. package/evals/cases/i3-code-simplification.json +0 -45
  33. package/evals/cases/i3-constraint-driven-development.json +0 -79
  34. package/evals/cases/i3-context-engineering.json +0 -44
  35. package/evals/cases/i3-debugging-and-error-recovery.json +0 -58
  36. package/evals/cases/i3-deprecation-and-migration.json +0 -52
  37. package/evals/cases/i3-documentation-and-adrs.json +0 -44
  38. package/evals/cases/i3-doubt-driven-development.json +0 -43
  39. package/evals/cases/i3-frontend-ui-engineering.json +0 -52
  40. package/evals/cases/i3-git-workflow-and-versioning.json +0 -47
  41. package/evals/cases/i3-idea-refine.json +0 -43
  42. package/evals/cases/i3-incremental-implementation.json +0 -57
  43. package/evals/cases/i3-interview-me.json +0 -42
  44. package/evals/cases/i3-observability-and-instrumentation.json +0 -44
  45. package/evals/cases/i3-performance-optimization.json +0 -53
  46. package/evals/cases/i3-planning-and-task-breakdown.json +0 -45
  47. package/evals/cases/i3-security-and-hardening.json +0 -53
  48. package/evals/cases/i3-shipping-and-launch.json +0 -56
  49. package/evals/cases/i3-source-driven-development.json +0 -44
  50. package/evals/cases/i3-spec-driven-development.json +0 -72
  51. package/evals/cases/i3-test-driven-development.json +0 -73
  52. package/evals/cases/i3-using-agent-skills.json +0 -44
  53. package/evals/fixtures/i3-api-and-interface-design/service-brief.md +0 -19
  54. package/evals/fixtures/i3-browser-testing-with-devtools/README.md +0 -5
  55. package/evals/fixtures/i3-browser-testing-with-devtools/index.html +0 -24
  56. package/evals/fixtures/i3-browser-testing-with-devtools/server.js +0 -15
  57. package/evals/fixtures/i3-ci-cd-and-automation/package.json +0 -8
  58. package/evals/fixtures/i3-ci-cd-and-automation/src/slug.js +0 -3
  59. package/evals/fixtures/i3-ci-cd-and-automation/test/slug.test.js +0 -9
  60. package/evals/fixtures/i3-code-review-and-quality/user-search.diff +0 -16
  61. package/evals/fixtures/i3-code-simplification/config-parser.js +0 -46
  62. package/evals/fixtures/i3-code-simplification/config-parser.test.js +0 -15
  63. package/evals/fixtures/i3-context-engineering/context-audit.md +0 -15
  64. package/evals/fixtures/i3-debugging-and-error-recovery/pagination.js +0 -8
  65. package/evals/fixtures/i3-debugging-and-error-recovery/pagination.test.js +0 -9
  66. package/evals/fixtures/i3-debugging-and-error-recovery/time-pressure.md +0 -6
  67. package/evals/fixtures/i3-deprecation-and-migration/api-inventory.md +0 -9
  68. package/evals/fixtures/i3-documentation-and-adrs/decision-context.md +0 -16
  69. package/evals/fixtures/i3-doubt-driven-development/migration-plan.md +0 -19
  70. package/evals/fixtures/i3-frontend-ui-engineering/Button.tsx +0 -7
  71. package/evals/fixtures/i3-frontend-ui-engineering/design-system.md +0 -11
  72. package/evals/fixtures/i3-git-workflow-and-versioning/.eval/working-tree.patch +0 -21
  73. package/evals/fixtures/i3-git-workflow-and-versioning/app.js +0 -7
  74. package/evals/fixtures/i3-git-workflow-and-versioning/app.test.js +0 -9
  75. package/evals/fixtures/i3-incremental-implementation/reports.js +0 -7
  76. package/evals/fixtures/i3-incremental-implementation/reports.test.js +0 -12
  77. package/evals/fixtures/i3-incremental-implementation/tasks/plan.md +0 -8
  78. package/evals/fixtures/i3-observability-and-instrumentation/operations.md +0 -11
  79. package/evals/fixtures/i3-observability-and-instrumentation/payment-retry.js +0 -14
  80. package/evals/fixtures/i3-performance-optimization/benchmark.js +0 -15
  81. package/evals/fixtures/i3-performance-optimization/products.js +0 -14
  82. package/evals/fixtures/i3-planning-and-task-breakdown/notifications-spec.md +0 -18
  83. package/evals/fixtures/i3-security-and-hardening/webhook.js +0 -11
  84. package/evals/fixtures/i3-security-and-hardening/webhook.test.js +0 -13
  85. package/evals/fixtures/i3-shipping-and-launch/authority-pressure.md +0 -6
  86. package/evals/fixtures/i3-shipping-and-launch/launch-status.md +0 -11
  87. package/evals/fixtures/i3-source-driven-development/framework-task.md +0 -10
  88. package/evals/fixtures/i3-spec-driven-development/billing-brief.md +0 -16
  89. package/evals/fixtures/i3-test-driven-development/BUG.md +0 -10
  90. package/evals/fixtures/i3-test-driven-development/README.md +0 -28
  91. package/evals/fixtures/i3-test-driven-development/package.json +0 -9
  92. package/evals/fixtures/i3-test-driven-development/src/split.js +0 -8
  93. package/evals/fixtures/i3-test-driven-development/test/split.test.js +0 -13
  94. package/evals/fixtures/i3-using-agent-skills/incident.md +0 -6
  95. package/evals/fixtures/incremental-implementation-pressure/draft-export.js +0 -17
  96. package/evals/fixtures/incremental-implementation-pressure/scenario.md +0 -9
  97. package/evals/fixtures/spec-driven-development-decomposition/portal-brief.md +0 -22
  98. package/evals/fixtures/test-driven-development-ecosystem/README.md +0 -9
  99. package/evals/fixtures/test-driven-development-ecosystem/ledger.py +0 -15
  100. package/evals/fixtures/test-driven-development-ecosystem/test_ledger.py +0 -19
  101. package/evals/plugin/code-review-fires/graders/off-by-one.md +0 -7
  102. package/evals/plugin/code-review-fires/graders/severity-labels.md +0 -4
  103. package/evals/plugin/code-review-fires/graders/skill-fired.md +0 -5
  104. package/evals/plugin/code-review-fires/prompt.md +0 -31
  105. package/evals/plugin/code-review-stays-quiet/graders/not-fired.md +0 -8
  106. package/evals/plugin/code-review-stays-quiet/graders/tdd-fired.md +0 -6
  107. package/evals/plugin/code-review-stays-quiet/prompt.md +0 -8
  108. package/evals/plugin/code-review-stays-quiet-on-commit-message/graders/not-fired.md +0 -8
  109. package/evals/plugin/code-review-stays-quiet-on-commit-message/prompt.md +0 -25
  110. package/evals/skill-impact.md +0 -6
  111. package/rebrand/INSTALLATION-SPEC.md +0 -47
  112. package/rebrand/MAINTENANCE.md +0 -114
  113. package/rebrand/NOTICE.md +0 -15
  114. package/rebrand/PR-WORKFLOW.md +0 -34
  115. package/rebrand/PREFIX-SPEC.md +0 -16
  116. package/rebrand/UNIFICATION-SPEC.md +0 -33
  117. package/rebrand/VALIDATION.md +0 -32
  118. package/rebrand/VERSIONING.md +0 -35
  119. package/rebrand/apply.cjs +0 -217
  120. package/rebrand/brand.json +0 -13
  121. package/rebrand/catalog.cjs +0 -70
  122. package/rebrand/fork-patches.json +0 -27
  123. package/rebrand/identifiers.cjs +0 -41
  124. package/rebrand/overlay/.github/workflows/test-plugin-install.yml +0 -177
  125. package/rebrand/overlay/CHANGELOG.md +0 -51
  126. package/rebrand/overlay/bin/agent-targets.json +0 -726
  127. package/rebrand/overlay/bin/i3-composer-agent.cjs +0 -190
  128. package/rebrand/overlay/bin/install.cjs +0 -180
  129. package/rebrand/overlay/bin/skills-registry.LICENSE +0 -21
  130. package/rebrand/overlay/bin/wizard.cjs +0 -66
  131. package/rebrand/overlay/scripts/validate-brand.cjs +0 -44
  132. package/rebrand/overlay/tests/bundle.test.cjs +0 -85
  133. package/rebrand/overlay/tests/installer.test.cjs +0 -241
  134. package/rebrand/package.template.json +0 -82
  135. package/rebrand/rebrand.sh +0 -4
  136. package/rebrand/record-upstream.cjs +0 -37
  137. package/rebrand/templates/README.md +0 -130
  138. package/rebrand/templates/codebuddy-setup.md +0 -89
  139. package/rebrand/templates/codex-setup.md +0 -45
  140. package/rebrand/tests/pipeline.test.cjs +0 -253
  141. package/rebrand/upstream.json +0 -171
  142. package/scripts/floor-guard-reference-test.js +0 -291
  143. package/scripts/lib/skill-lint-test.js +0 -650
  144. package/scripts/lib/skill-lint.js +0 -586
  145. package/scripts/run-evals-test.js +0 -623
  146. package/scripts/run-evals.js +0 -638
  147. package/scripts/validate-artifact-paths-test.js +0 -179
  148. package/scripts/validate-artifact-paths.js +0 -130
  149. package/scripts/validate-brand.cjs +0 -44
  150. package/scripts/validate-commands-test.js +0 -216
  151. package/scripts/validate-commands.js +0 -223
  152. package/scripts/validate-reference-links-test.js +0 -495
  153. package/scripts/validate-reference-links.js +0 -187
  154. package/scripts/validate-skills.js +0 -69
  155. package/scripts/validate-versions-test.js +0 -32
  156. package/scripts/validate-versions.js +0 -35
  157. package/tests/bundle.test.cjs +0 -85
  158. package/tests/installer.test.cjs +0 -241
@@ -1,623 +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 test = require('node:test');
11
- const { materializeWorkspace, parseGrading, clearGradingSlot, persistGradingOutcome, extractExecutorModel } = require('./run-evals');
12
-
13
- const RUNNER = path.join(__dirname, 'run-evals.js');
14
-
15
- function writeJson(file, value) {
16
- fs.mkdirSync(path.dirname(file), { recursive: true });
17
- fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
18
- }
19
-
20
- function writeSkill(root, name, description) {
21
- const dir = path.join(root, 'skills', name);
22
- fs.mkdirSync(dir, { recursive: true });
23
- fs.writeFileSync(
24
- path.join(dir, 'SKILL.md'),
25
- `---\nname: ${name}\ndescription: ${description}\n---\n\n# ${name}\n`,
26
- );
27
- }
28
-
29
- function behavioralEval(files = ['project/context.txt']) {
30
- return {
31
- id: 1,
32
- prompt: 'Inspect the attached project and complete the requested work.',
33
- expected_output: 'A verified result grounded in the attached project',
34
- files,
35
- expectations: ['The attached project is inspected before reporting a result'],
36
- };
37
- }
38
-
39
- function completeCase(skillName, positivePrompt, topK = 1, files) {
40
- return {
41
- skill_name: skillName,
42
- trigger: {
43
- positive: [1, 2, 3].map(() => ({ prompt: positivePrompt, top_k: topK })),
44
- negative: [
45
- { prompt: 'unrelated banana request' },
46
- { prompt: 'unrelated orange request' },
47
- ],
48
- },
49
- evals: [behavioralEval(files)],
50
- };
51
- }
52
-
53
- function makeSandbox() {
54
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-skills-run-evals-test-'));
55
- fs.mkdirSync(path.join(root, 'scripts'), { recursive: true });
56
- fs.mkdirSync(path.join(root, 'evals', 'cases'), { recursive: true });
57
- fs.mkdirSync(path.join(root, 'evals', 'fixtures', 'project'), { recursive: true });
58
- fs.copyFileSync(RUNNER, path.join(root, 'scripts', 'run-evals.js'));
59
- fs.writeFileSync(path.join(root, 'evals', 'fixtures', 'project', 'context.txt'), 'fixture\n');
60
- return root;
61
- }
62
-
63
- function run(root, args = []) {
64
- return spawnSync(process.execPath, [path.join(root, 'scripts', 'run-evals.js'), ...args], {
65
- cwd: root,
66
- encoding: 'utf8',
67
- });
68
- }
69
-
70
- test('accepts a complete and consistent grader result', () => {
71
- const raw = JSON.stringify({
72
- expectations: [
73
- { id: 1, text: 'first expectation', passed: true, evidence: 'observed in the trace' },
74
- { id: 2, text: 'second expectation', passed: false, evidence: 'not observed in the trace' },
75
- ],
76
- summary: { passed: 1, failed: 1, total: 2, pass_rate: 0.5 },
77
- });
78
-
79
- assert.deepEqual(parseGrading(raw, ['first expectation', 'second expectation']), JSON.parse(raw));
80
- });
81
-
82
- test('rejects grader results that omit expectations', () => {
83
- const raw = JSON.stringify({
84
- expectations: [
85
- { id: 1, text: 'first expectation', passed: true, evidence: 'observed in the trace' },
86
- ],
87
- summary: { passed: 1, failed: 0, total: 1, pass_rate: 1 },
88
- });
89
-
90
- assert.equal(parseGrading(raw, ['first expectation', 'second expectation']), null);
91
- });
92
-
93
- test('rejects null expectation entries without throwing', () => {
94
- const cases = [
95
- { results: [null], declared: ['first expectation'] },
96
- { results: [{ id: 1, text: 'first expectation', passed: true, evidence: 'observed' }, null], declared: ['first expectation', 'second expectation'] },
97
- ];
98
- for (const { results, declared } of cases) {
99
- const raw = JSON.stringify({
100
- expectations: results,
101
- summary: {
102
- passed: results.length - 1,
103
- failed: 1,
104
- total: results.length,
105
- pass_rate: (results.length - 1) / results.length,
106
- },
107
- });
108
-
109
- assert.equal(parseGrading(raw, declared), null);
110
- }
111
- });
112
-
113
- test('rejects incomplete or inconsistent grader summaries', () => {
114
- const declared = ['expected behavior'];
115
- const expectation = { id: 1, text: 'expected behavior', passed: false, evidence: 'not observed' };
116
- const cases = [
117
- {
118
- expectations: [],
119
- summary: { passed: 0, failed: 0, total: 0, pass_rate: 0 },
120
- declared: [],
121
- },
122
- {
123
- expectations: [{ id: 1, text: 'expected behavior', passed: false }],
124
- summary: { passed: 0, failed: 1, total: 1, pass_rate: 0 },
125
- declared,
126
- },
127
- {
128
- expectations: [expectation],
129
- summary: { passed: 1, failed: 0, total: 1, pass_rate: 1 },
130
- declared,
131
- },
132
- {
133
- expectations: [expectation],
134
- summary: { passed: 0, total: 1, pass_rate: 0 },
135
- declared,
136
- },
137
- {
138
- expectations: [expectation],
139
- summary: { passed: 0, failed: 1, total: 1 },
140
- declared,
141
- },
142
- ];
143
-
144
- for (const { declared: d, ...grading } of cases) {
145
- assert.equal(parseGrading(JSON.stringify(grading), d), null);
146
- }
147
- });
148
-
149
- test('fails when a skill has no eval case file', () => {
150
- const root = makeSandbox();
151
- writeSkill(root, 'alpha-skill', 'Handles alpha widgets. Use when changing alpha widgets.');
152
-
153
- const result = run(root);
154
-
155
- assert.equal(result.status, 1, result.stdout + result.stderr);
156
- assert.match(result.stdout, /no eval case file/);
157
- });
158
-
159
- test('fails when an eval case is below the required minimums', () => {
160
- const root = makeSandbox();
161
- writeSkill(root, 'alpha-skill', 'Handles alpha widgets. Use when changing alpha widgets.');
162
- writeJson(path.join(root, 'evals', 'cases', 'alpha-skill.json'), {
163
- skill_name: 'alpha-skill',
164
- trigger: {
165
- positive: [{ prompt: 'change alpha widget', top_k: 1 }],
166
- negative: [],
167
- },
168
- evals: [behavioralEval()],
169
- });
170
-
171
- const result = run(root);
172
-
173
- assert.equal(result.status, 1, result.stdout + result.stderr);
174
- assert.match(result.stdout, /below required minimums/);
175
- });
176
-
177
- test('fails when a behavioral eval references a missing fixture', () => {
178
- const root = makeSandbox();
179
- writeSkill(root, 'alpha-skill', 'Handles alpha widgets. Use when changing alpha widgets.');
180
- writeJson(
181
- path.join(root, 'evals', 'cases', 'alpha-skill.json'),
182
- completeCase('alpha-skill', 'change alpha widget', 1, ['missing/project.txt']),
183
- );
184
-
185
- const result = run(root);
186
-
187
- assert.equal(result.status, 1, result.stdout + result.stderr);
188
- assert.match(result.stdout, /fixture not found/);
189
- });
190
-
191
- test('requires fixtures for execution evals', () => {
192
- const root = makeSandbox();
193
- writeSkill(root, 'alpha-skill', 'Handles alpha widgets. Use when changing alpha widgets.');
194
- writeJson(
195
- path.join(root, 'evals', 'cases', 'alpha-skill.json'),
196
- completeCase('alpha-skill', 'change alpha widget', 1, []),
197
- );
198
-
199
- const result = run(root);
200
-
201
- assert.equal(result.status, 1, result.stdout + result.stderr);
202
- assert.match(result.stdout, /needs a non-empty files\[\] fixture list/);
203
- });
204
-
205
- test('allows dialogue evals without fixtures', () => {
206
- const root = makeSandbox();
207
- writeSkill(root, 'alpha-skill', 'Handles alpha widgets. Use when changing alpha widgets.');
208
- const evalCase = completeCase('alpha-skill', 'change alpha widget');
209
- evalCase.evals = [{ ...behavioralEval([]), kind: 'dialogue' }];
210
- writeJson(path.join(root, 'evals', 'cases', 'alpha-skill.json'), evalCase);
211
-
212
- const result = run(root);
213
-
214
- assert.equal(result.status, 0, result.stdout + result.stderr);
215
- });
216
-
217
- test('rejects provisional execution evals', () => {
218
- const root = makeSandbox();
219
- writeSkill(root, 'alpha-skill', 'Handles alpha widgets. Use when changing alpha widgets.');
220
- const evalCase = completeCase('alpha-skill', 'change alpha widget');
221
- evalCase.evals[0].trust_level = 'provisional';
222
- writeJson(path.join(root, 'evals', 'cases', 'alpha-skill.json'), evalCase);
223
-
224
- const result = run(root);
225
-
226
- assert.equal(result.status, 1, result.stdout + result.stderr);
227
- assert.match(result.stdout, /is still provisional/);
228
- });
229
-
230
- test('allows dialogue evals with a legacy provisional marker', () => {
231
- const root = makeSandbox();
232
- writeSkill(root, 'alpha-skill', 'Handles alpha widgets. Use when changing alpha widgets.');
233
- const evalCase = completeCase('alpha-skill', 'change alpha widget');
234
- evalCase.evals = [{ ...behavioralEval([]), kind: 'dialogue', trust_level: 'provisional' }];
235
- writeJson(path.join(root, 'evals', 'cases', 'alpha-skill.json'), evalCase);
236
-
237
- const result = run(root);
238
-
239
- assert.equal(result.status, 0, result.stdout + result.stderr);
240
- });
241
-
242
- test('rejects unknown behavioral eval kinds', () => {
243
- const root = makeSandbox();
244
- writeSkill(root, 'alpha-skill', 'Handles alpha widgets. Use when changing alpha widgets.');
245
- const evalCase = completeCase('alpha-skill', 'change alpha widget');
246
- evalCase.evals[0].kind = 'conversation';
247
- writeJson(path.join(root, 'evals', 'cases', 'alpha-skill.json'), evalCase);
248
-
249
- const result = run(root);
250
-
251
- assert.equal(result.status, 1, result.stdout + result.stderr);
252
- assert.match(result.stdout, /unknown kind "conversation"/);
253
- });
254
-
255
- test('dry-runs a fixtureless dialogue eval', () => {
256
- const root = makeSandbox();
257
- writeSkill(root, 'alpha-skill', 'Handles alpha widgets. Use when changing alpha widgets.');
258
- const evalCase = completeCase('alpha-skill', 'change alpha widget');
259
- evalCase.evals = [{ ...behavioralEval([]), kind: 'dialogue' }];
260
- writeJson(path.join(root, 'evals', 'cases', 'alpha-skill.json'), evalCase);
261
-
262
- const result = run(root, ['--behavioral', 'alpha-skill', '--dry-run']);
263
-
264
- assert.equal(result.status, 0, result.stdout + result.stderr);
265
- assert.match(result.stdout, /dialogue transcript/);
266
- });
267
-
268
- test('enforces the configured rank-1 floor', () => {
269
- const root = makeSandbox();
270
- writeSkill(root, 'alpha-skill', 'Handles widget work. Use when implementing widget changes.');
271
- writeSkill(
272
- root,
273
- 'beta-skill',
274
- 'Diagnoses urgent widget failures in production. Use when repairing urgent widget failures.',
275
- );
276
- writeJson(
277
- path.join(root, 'evals', 'cases', 'alpha-skill.json'),
278
- completeCase('alpha-skill', 'urgent widget failure production', 2),
279
- );
280
- writeJson(
281
- path.join(root, 'evals', 'cases', 'beta-skill.json'),
282
- completeCase('beta-skill', 'repair urgent widget failure', 1),
283
- );
284
-
285
- const passing = run(root, ['--min-rank1', '50']);
286
- const failing = run(root, ['--min-rank1', '60']);
287
-
288
- assert.equal(passing.status, 0, passing.stdout + passing.stderr);
289
- assert.equal(failing.status, 1, failing.stdout + failing.stderr);
290
- assert.match(failing.stdout, /below required 60%/);
291
- });
292
-
293
- test('rejects an invalid rank-1 floor', () => {
294
- const root = makeSandbox();
295
-
296
- const result = run(root, ['--min-rank1', '101']);
297
-
298
- assert.equal(result.status, 1, result.stdout + result.stderr);
299
- assert.match(result.stderr, /--min-rank1 must be a number from 0 to 100/);
300
- });
301
-
302
- // ---------- parseGrading expectation-binding tests ----------
303
-
304
- test('accepts reordered-but-complete grader results', () => {
305
- const expectations = ['first expectation', 'second expectation'];
306
- const raw = JSON.stringify({
307
- expectations: [
308
- { id: 2, text: 'second expectation', passed: false, evidence: 'not observed' },
309
- { id: 1, text: 'first expectation', passed: true, evidence: 'observed in the trace' },
310
- ],
311
- summary: { passed: 1, failed: 1, total: 2, pass_rate: 0.5 },
312
- });
313
- const result = parseGrading(raw, expectations);
314
- assert.notEqual(result, null);
315
- assert.equal(result.summary.passed, 1);
316
- assert.equal(result.summary.failed, 1);
317
- });
318
-
319
- test('rejects duplicate grader results for the same expectation', () => {
320
- const expectations = ['first expectation', 'second expectation'];
321
- // Valid baseline: each id appears exactly once
322
- const validRaw = JSON.stringify({
323
- expectations: [
324
- { id: 1, text: 'first expectation', passed: true, evidence: 'observed' },
325
- { id: 2, text: 'second expectation', passed: false, evidence: 'not observed' },
326
- ],
327
- summary: { passed: 1, failed: 1, total: 2, pass_rate: 0.5 },
328
- });
329
- assert.notEqual(parseGrading(validRaw, expectations), null);
330
-
331
- // Duplicate: id 1 appears twice, id 2 is missing
332
- const dupRaw = JSON.stringify({
333
- expectations: [
334
- { id: 1, text: 'first expectation', passed: true, evidence: 'observed' },
335
- { id: 1, text: 'first expectation', passed: true, evidence: 'observed again' },
336
- ],
337
- summary: { passed: 2, failed: 0, total: 2, pass_rate: 1 },
338
- });
339
- assert.equal(parseGrading(dupRaw, expectations), null);
340
- });
341
-
342
- test('rejects grader results whose ids are not in the declared set', () => {
343
- const expectations = ['first expectation', 'second expectation'];
344
- // Valid baseline
345
- const validRaw = JSON.stringify({
346
- expectations: [
347
- { id: 1, text: 'first expectation', passed: true, evidence: 'observed' },
348
- { id: 2, text: 'second expectation', passed: false, evidence: 'not observed' },
349
- ],
350
- summary: { passed: 1, failed: 1, total: 2, pass_rate: 0.5 },
351
- });
352
- assert.notEqual(parseGrading(validRaw, expectations), null);
353
-
354
- // id 3 is out of range 1..2
355
- const badIdRaw = JSON.stringify({
356
- expectations: [
357
- { id: 1, text: 'first expectation', passed: true, evidence: 'observed' },
358
- { id: 3, text: 'unknown expectation', passed: false, evidence: 'not found' },
359
- ],
360
- summary: { passed: 1, failed: 1, total: 2, pass_rate: 0.5 },
361
- });
362
- assert.equal(parseGrading(badIdRaw, expectations), null);
363
- });
364
-
365
- test('rejects a result set that omits a declared expectation', () => {
366
- const expectations = ['first expectation', 'second expectation', 'third expectation'];
367
- // Valid baseline
368
- const validRaw = JSON.stringify({
369
- expectations: [
370
- { id: 1, text: 'first expectation', passed: true, evidence: 'observed' },
371
- { id: 2, text: 'second expectation', passed: true, evidence: 'observed' },
372
- { id: 3, text: 'third expectation', passed: false, evidence: 'not observed' },
373
- ],
374
- summary: { passed: 2, failed: 1, total: 3, pass_rate: 2 / 3 },
375
- });
376
- assert.notEqual(parseGrading(validRaw, expectations), null);
377
-
378
- // Only 2 results for 3 expectations (id 3 omitted)
379
- const partialRaw = JSON.stringify({
380
- expectations: [
381
- { id: 1, text: 'first expectation', passed: true, evidence: 'observed' },
382
- { id: 2, text: 'second expectation', passed: true, evidence: 'observed' },
383
- ],
384
- summary: { passed: 2, failed: 0, total: 2, pass_rate: 1 },
385
- });
386
- assert.equal(parseGrading(partialRaw, expectations), null);
387
- });
388
-
389
- test('derives pass_rate from counters rather than trusting the grader value', () => {
390
- const expectations = ['first expectation', 'second expectation'];
391
- // Correct pass_rate should be accepted
392
- const validRaw = JSON.stringify({
393
- expectations: [
394
- { id: 1, text: 'first expectation', passed: true, evidence: 'observed' },
395
- { id: 2, text: 'second expectation', passed: false, evidence: 'not observed' },
396
- ],
397
- summary: { passed: 1, failed: 1, total: 2, pass_rate: 0.5 },
398
- });
399
- const valid = parseGrading(validRaw, expectations);
400
- assert.notEqual(valid, null);
401
- assert.equal(valid.summary.pass_rate, 0.5);
402
-
403
- // Wrong pass_rate with correct counters: accepted, but recomputed
404
- const wrongRaw = JSON.stringify({
405
- expectations: [
406
- { id: 1, text: 'first expectation', passed: true, evidence: 'observed' },
407
- { id: 2, text: 'second expectation', passed: false, evidence: 'not observed' },
408
- ],
409
- summary: { passed: 1, failed: 1, total: 2, pass_rate: 0.999 },
410
- });
411
- const corrected = parseGrading(wrongRaw, expectations);
412
- assert.notEqual(corrected, null);
413
- assert.equal(corrected.summary.pass_rate, 0.5);
414
- // The integer counters stay exact checks
415
- assert.equal(corrected.summary.passed, 1);
416
- assert.equal(corrected.summary.failed, 1);
417
- });
418
-
419
- test('replaces paraphrased grader text with the declared expectation', () => {
420
- const expectations = ['first expectation', 'second expectation'];
421
- const raw = JSON.stringify({
422
- expectations: [
423
- { id: 2, text: 'the agent did the second thing', passed: false, evidence: 'not observed' },
424
- { id: 1, text: 'roughly the first one', passed: true, evidence: 'observed' },
425
- ],
426
- summary: { passed: 1, failed: 1, total: 2, pass_rate: 0.5 },
427
- });
428
-
429
- const result = parseGrading(raw, expectations);
430
- assert.notEqual(result, null);
431
- assert.equal(result.expectations.find((r) => r.id === 1).text, 'first expectation');
432
- assert.equal(result.expectations.find((r) => r.id === 2).text, 'second expectation');
433
- });
434
-
435
- // ---------- persistGradingOutcome stale-cleanup tests ----------
436
-
437
- test('rejected grading writes raw output', () => {
438
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'grading-cleanup-'));
439
- try {
440
- const base = path.join(dir, 'my-skill.eval-1');
441
-
442
- const result = persistGradingOutcome(base, null, 'unparseable grader output');
443
-
444
- assert.equal(result, false);
445
- assert.equal(fs.existsSync(`${base}.grading.raw.txt`), true, 'raw output must be written');
446
- assert.equal(fs.readFileSync(`${base}.grading.raw.txt`, 'utf8'), 'unparseable grader output');
447
- } finally {
448
- fs.rmSync(dir, { recursive: true, force: true });
449
- }
450
- });
451
-
452
- test('rejected grading succeeds even when no prior grading.json exists', () => {
453
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'grading-cleanup-'));
454
- try {
455
- const base = path.join(dir, 'my-skill.eval-1');
456
-
457
- const result = persistGradingOutcome(base, null, 'bad output');
458
-
459
- assert.equal(result, false);
460
- assert.equal(fs.existsSync(`${base}.grading.raw.txt`), true);
461
- } finally {
462
- fs.rmSync(dir, { recursive: true, force: true });
463
- }
464
- });
465
-
466
- test('accepted grading writes grading.json', () => {
467
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'grading-cleanup-'));
468
- try {
469
- const base = path.join(dir, 'my-skill.eval-1');
470
- const grading = {
471
- expectations: [{ id: 1, text: 'x', passed: true, evidence: 'y' }],
472
- summary: { passed: 1, failed: 0, total: 1, pass_rate: 1 },
473
- };
474
-
475
- const result = persistGradingOutcome(base, grading, 'unused');
476
-
477
- assert.equal(result, true);
478
- const written = JSON.parse(fs.readFileSync(`${base}.grading.json`, 'utf8'));
479
- assert.deepEqual(written, grading);
480
- } finally {
481
- fs.rmSync(dir, { recursive: true, force: true });
482
- }
483
- });
484
-
485
- // ---------- upfront slot-clearing tests ----------
486
-
487
- test('a grader that throws leaves no result file behind', () => {
488
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'grading-crash-'));
489
- try {
490
- const base = path.join(dir, 'my-skill.eval-1');
491
- // Stale files from a prior run
492
- fs.writeFileSync(`${base}.grading.json`, '{"previous":"run"}\n');
493
- fs.writeFileSync(`${base}.grading.raw.txt`, 'previous raw output');
494
-
495
- clearGradingSlot(base);
496
-
497
- // Executor or grader crashes — persistGradingOutcome is never called
498
-
499
- assert.equal(fs.existsSync(`${base}.grading.json`), false, 'stale grading.json must not survive a crash');
500
- assert.equal(fs.existsSync(`${base}.grading.raw.txt`), false, 'stale grading.raw.txt must not survive a crash');
501
- } finally {
502
- fs.rmSync(dir, { recursive: true, force: true });
503
- }
504
- });
505
-
506
- test('successful grading leaves no stale raw file behind', () => {
507
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'grading-success-'));
508
- try {
509
- const base = path.join(dir, 'my-skill.eval-1');
510
- // Stale raw from a prior rejected run
511
- fs.writeFileSync(`${base}.grading.raw.txt`, 'previous raw output');
512
-
513
- clearGradingSlot(base);
514
-
515
- // Successful grading
516
- const grading = {
517
- expectations: [{ id: 1, text: 'x', passed: true, evidence: 'y' }],
518
- summary: { passed: 1, failed: 0, total: 1, pass_rate: 1 },
519
- };
520
- persistGradingOutcome(base, grading, 'unused');
521
-
522
- assert.equal(fs.existsSync(`${base}.grading.json`), true, 'grading.json must be written');
523
- assert.equal(fs.existsSync(`${base}.grading.raw.txt`), false, 'stale grading.raw.txt must not survive');
524
- } finally {
525
- fs.rmSync(dir, { recursive: true, force: true });
526
- }
527
- });
528
-
529
- // ---------- extractExecutorModel tests ----------
530
-
531
- test('extracts model from the stream-json init event', () => {
532
- const trace = [
533
- '{"type":"system","subtype":"init","model":"claude-sonnet-4-6-20250514","session_id":"abc"}',
534
- '{"type":"assistant","message":{"id":"msg_1","content":[{"type":"text","text":"Hi"}]}}',
535
- '{"type":"result","subtype":"success","result":"Hi"}',
536
- ].join('\n');
537
-
538
- assert.equal(extractExecutorModel(trace), 'claude-sonnet-4-6-20250514');
539
- });
540
-
541
- test('returns null when the trace has no init event', () => {
542
- const trace = [
543
- '{"type":"assistant","message":{"id":"msg_1","content":[]}}',
544
- '{"type":"result","subtype":"success","result":"done"}',
545
- ].join('\n');
546
-
547
- assert.equal(extractExecutorModel(trace), null);
548
- });
549
-
550
- test('returns null when the init event has no model field', () => {
551
- const trace = '{"type":"system","subtype":"init","session_id":"abc"}\n';
552
-
553
- assert.equal(extractExecutorModel(trace), null);
554
- });
555
-
556
- test('tolerates non-JSON lines in the trace', () => {
557
- const trace = [
558
- 'not json',
559
- '{"type":"system","subtype":"init","model":"claude-opus-4-6","session_id":"abc"}',
560
- ].join('\n');
561
-
562
- assert.equal(extractExecutorModel(trace), 'claude-opus-4-6');
563
- });
564
-
565
- // ---------- persistGradingOutcome run identity tests ----------
566
-
567
- test('accepted grading includes run metadata when provided', () => {
568
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'grading-run-meta-'));
569
- try {
570
- const base = path.join(dir, 'my-skill.eval-1');
571
- const grading = {
572
- expectations: [{ id: 1, text: 'x', passed: true, evidence: 'y' }],
573
- summary: { passed: 1, failed: 0, total: 1, pass_rate: 1 },
574
- };
575
- const runMeta = {
576
- executor_model: 'claude-sonnet-4-6-20250514',
577
- grader_model: 'unknown',
578
- timestamp: '2026-09-21T00:00:00.000Z',
579
- };
580
-
581
- persistGradingOutcome(base, grading, 'unused', runMeta);
582
-
583
- const written = JSON.parse(fs.readFileSync(`${base}.grading.json`, 'utf8'));
584
- assert.deepEqual(written.run, runMeta);
585
- assert.deepEqual(written.expectations, grading.expectations);
586
- assert.deepEqual(written.summary, grading.summary);
587
- } finally {
588
- fs.rmSync(dir, { recursive: true, force: true });
589
- }
590
- });
591
-
592
- test('accepted grading omits run key when no metadata is provided', () => {
593
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'grading-no-meta-'));
594
- try {
595
- const base = path.join(dir, 'my-skill.eval-1');
596
- const grading = {
597
- expectations: [{ id: 1, text: 'x', passed: true, evidence: 'y' }],
598
- summary: { passed: 1, failed: 0, total: 1, pass_rate: 1 },
599
- };
600
-
601
- persistGradingOutcome(base, grading, 'unused');
602
-
603
- const written = JSON.parse(fs.readFileSync(`${base}.grading.json`, 'utf8'));
604
- assert.equal('run' in written, false);
605
- } finally {
606
- fs.rmSync(dir, { recursive: true, force: true });
607
- }
608
- });
609
-
610
- test('materializes a git baseline and applies a working-tree patch', () => {
611
- const workspace = materializeWorkspace({ files: ['i3-git-workflow-and-versioning'] });
612
- try {
613
- const status = spawnSync('git', ['status', '--short'], { cwd: workspace, encoding: 'utf8' });
614
- const commits = spawnSync('git', ['rev-list', '--count', 'HEAD'], { cwd: workspace, encoding: 'utf8' });
615
-
616
- assert.equal(status.status, 0, status.stdout + status.stderr);
617
- assert.match(status.stdout, / M i3-git-workflow-and-versioning\/app\.js/);
618
- assert.equal(commits.stdout.trim(), '1');
619
- assert.equal(fs.existsSync(path.join(workspace, '.eval')), false);
620
- } finally {
621
- fs.rmSync(workspace, { recursive: true, force: true });
622
- }
623
- });