cadet-agent 0.33.0 → 0.34.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/README.md +4 -0
- package/package.json +37 -37
- package/src/cli.mjs +25 -3
- package/src/harness/state.mjs +9 -2
- package/src/harness/verify-acs.mjs +42 -3
package/README.md
CHANGED
|
@@ -168,6 +168,10 @@ Hard gates are enforced at every phase transition. The agent reads `.cadet/state
|
|
|
168
168
|
| review → validation | `codeReviewCompleted`, `securityReviewPassed`, `acceptanceCriteriaValidated` |
|
|
169
169
|
| validation → closed | `designArtifactSyncConfirmed` |
|
|
170
170
|
|
|
171
|
+
**`closed` is end-of-epic, not per-story.** `validation → closed` is taken only when no stories remain (`NEXT_STORY → no → CLOSED` above). When an epic still has stories, the next story re-enters from `validation → implementation` (`NEXT_STORY → yes → IMPL`). Do not close a story individually: `closed` is terminal, and there is no transition out of it.
|
|
172
|
+
|
|
173
|
+
The full set of legal transitions is the three gated rows above **plus** the ungated forward edges (classification, planning progression, `story-breakdown → implementation`, and the `validation → implementation` next-story loop). Any transition outside that set is rejected with a named reason.
|
|
174
|
+
|
|
171
175
|
### Harness
|
|
172
176
|
|
|
173
177
|
Gates are backed by **evidence**, not assertion. Each claimed gate must have a fresh, non-superseded evidence record bound to the current work item, input tree hash, and acceptance criteria. The harness also bounds context, tokens, tool calls, retries, wall-clock time, cost, and archive sizes — and those bounds are enforced, not advisory.
|
package/package.json
CHANGED
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "cadet-agent",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Cross-IDE agent framework for Unity/C# game-development — one-command install",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"cadet-agent": "bin/cli.mjs"
|
|
8
|
-
},
|
|
9
|
-
"scripts": {
|
|
10
|
-
"test": "node --test test/*.test.mjs",
|
|
11
|
-
"lint": "lychee --offline --include-fragments \"**/*.md\"",
|
|
12
|
-
"verify": "npm test && npm run lint"
|
|
13
|
-
},
|
|
14
|
-
"files": [
|
|
15
|
-
"bin/",
|
|
16
|
-
"src/"
|
|
17
|
-
],
|
|
18
|
-
"keywords": [
|
|
19
|
-
"cadet",
|
|
20
|
-
"cadet-agent",
|
|
21
|
-
"unity",
|
|
22
|
-
"game-development",
|
|
23
|
-
"ai-agent",
|
|
24
|
-
"copilot",
|
|
25
|
-
"cursor",
|
|
26
|
-
"claude-code"
|
|
27
|
-
],
|
|
28
|
-
"license": "CC-BY-4.0",
|
|
29
|
-
"repository": {
|
|
30
|
-
"type": "git",
|
|
31
|
-
"url": "git+https://github.com/naishtech/cadet-agent.git"
|
|
32
|
-
},
|
|
33
|
-
"homepage": "https://github.com/naishtech/cadet-agent#readme",
|
|
34
|
-
"engines": {
|
|
35
|
-
"node": ">=18.0.0"
|
|
36
|
-
}
|
|
37
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "cadet-agent",
|
|
3
|
+
"version": "0.34.0",
|
|
4
|
+
"description": "Cross-IDE agent framework for Unity/C# game-development — one-command install",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"cadet-agent": "bin/cli.mjs"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node --test test/*.test.mjs",
|
|
11
|
+
"lint": "lychee --offline --include-fragments \"**/*.md\"",
|
|
12
|
+
"verify": "npm test && npm run lint"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"bin/",
|
|
16
|
+
"src/"
|
|
17
|
+
],
|
|
18
|
+
"keywords": [
|
|
19
|
+
"cadet",
|
|
20
|
+
"cadet-agent",
|
|
21
|
+
"unity",
|
|
22
|
+
"game-development",
|
|
23
|
+
"ai-agent",
|
|
24
|
+
"copilot",
|
|
25
|
+
"cursor",
|
|
26
|
+
"claude-code"
|
|
27
|
+
],
|
|
28
|
+
"license": "CC-BY-4.0",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/naishtech/cadet-agent.git"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/naishtech/cadet-agent#readme",
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18.0.0"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/cli.mjs
CHANGED
|
@@ -94,6 +94,7 @@ function parseArgs(argv) {
|
|
|
94
94
|
case '--story': opts.story = argv[++i]; break;
|
|
95
95
|
case '--report': opts.report = argv[++i]; break;
|
|
96
96
|
case '--write-coverage': opts.writeCoverage = true; break;
|
|
97
|
+
case '--strict-orphans': opts.strictOrphans = true; break;
|
|
97
98
|
case '--dry-run': opts.dryRun = true; break;
|
|
98
99
|
case '--older-than-ms': opts.olderThanMs = Number(argv[++i]); break;
|
|
99
100
|
case '--agents-md': opts.agentsMd = argv[++i]; break;
|
|
@@ -618,39 +619,60 @@ async function cmdHarness(opts) {
|
|
|
618
619
|
const inventory = parseTestInventory(reportText);
|
|
619
620
|
const coverage = compareCoverage(criteria, inventory);
|
|
620
621
|
const gaps = describeCoverageGaps(coverage);
|
|
622
|
+
// The inverse direction: tests that ran but are declared on no AC. Always
|
|
623
|
+
// reported; only fatal when explicitly requested, because a consumer may
|
|
624
|
+
// legitimately carry helper tests that belong to no single criterion.
|
|
625
|
+
const orphanGaps = describeCoverageGaps(coverage, { includeOrphans: true }).slice(gaps.length);
|
|
626
|
+
const orphans = coverage.orphaned || [];
|
|
621
627
|
|
|
622
628
|
// Under strict closure an unknown/empty inventory can never prove coverage,
|
|
623
629
|
// even if every AC declared no tests in a way that looked consistent.
|
|
624
630
|
const unknownInventory = inventory.format === 'unknown' || inventory.names.length === 0;
|
|
625
|
-
const
|
|
631
|
+
const orphanBlocking = opts.strictOrphans === true && orphans.length > 0;
|
|
632
|
+
const effectiveOk = coverage.ok && !unknownInventory && !orphanBlocking;
|
|
626
633
|
|
|
627
634
|
if (!strict) {
|
|
628
635
|
// v2/v3 parity: report, write nothing, exit 0.
|
|
629
636
|
if (opts.format === 'json') {
|
|
630
|
-
emit(opts, '', { ok: effectiveOk, story: opts.story, ac: coverage.ac, inventorySize: coverage.inventorySize, format: inventory.format, gateSet: false, reportPath });
|
|
637
|
+
emit(opts, '', { ok: effectiveOk, story: opts.story, ac: coverage.ac, orphaned: orphans, inventorySize: coverage.inventorySize, format: inventory.format, gateSet: false, reportPath });
|
|
631
638
|
} else if (effectiveOk) {
|
|
632
639
|
console.log(`✅ AC coverage verified for ${opts.story} (${coverage.ac.length} criteria, ${coverage.inventorySize} tests in inventory).`);
|
|
633
640
|
console.log(' strictClosure is off — reported only, state.json unchanged.');
|
|
641
|
+
if (orphans.length > 0) {
|
|
642
|
+
// A warning goes to stderr even on the success path, so it is not lost
|
|
643
|
+
// in stdout piping and matches how every other warning is emitted.
|
|
644
|
+
console.error(` ⚠️ ${orphans.length} test(s) declared on no acceptance criterion (reported only):`);
|
|
645
|
+
for (const g of orphanGaps) console.error(g);
|
|
646
|
+
}
|
|
634
647
|
} else {
|
|
635
648
|
console.error(`⚠️ AC coverage gaps in ${opts.story} (strictClosure off — reported only):`);
|
|
636
649
|
if (unknownInventory) console.error(` no test inventory could be derived from ${reportPath || 'the report'} (format: ${inventory.format}).`);
|
|
637
650
|
for (const g of gaps) console.error(g);
|
|
651
|
+
for (const g of orphanGaps) console.error(g);
|
|
638
652
|
}
|
|
639
653
|
if (!effectiveOk) process.exit(1);
|
|
640
654
|
return;
|
|
641
655
|
}
|
|
642
656
|
|
|
643
657
|
if (!effectiveOk) {
|
|
644
|
-
const detail = { ok: false, story: opts.story, ac: coverage.ac, inventorySize: coverage.inventorySize, format: inventory.format, gateSet: false, code: unknownInventory ? 'inventory-unknown' : 'coverage-gap' };
|
|
658
|
+
const detail = { ok: false, story: opts.story, ac: coverage.ac, orphaned: orphans, inventorySize: coverage.inventorySize, format: inventory.format, gateSet: false, code: unknownInventory ? 'inventory-unknown' : (orphanBlocking ? 'orphaned-tests' : 'coverage-gap') };
|
|
645
659
|
if (opts.format === 'json') emit(opts, '', detail);
|
|
646
660
|
else {
|
|
647
661
|
console.error(`❌ Cannot set acceptanceCriteriaValidated for ${opts.story}:`);
|
|
648
662
|
if (unknownInventory) console.error(` no test inventory could be derived from ${reportPath || 'the report'} (format: ${inventory.format}). An unparseable report proves nothing.`);
|
|
649
663
|
for (const g of gaps) console.error(g);
|
|
664
|
+
if (orphanBlocking) for (const g of orphanGaps) console.error(g);
|
|
650
665
|
}
|
|
651
666
|
process.exit(1);
|
|
652
667
|
}
|
|
653
668
|
|
|
669
|
+
if (orphans.length > 0) {
|
|
670
|
+
// Passing, but the inverse-direction drift is visible rather than silent.
|
|
671
|
+
const notice = `⚠️ ${orphans.length} test(s) ran but are declared on no acceptance criterion (not fatal; pass --strict-orphans to enforce).`;
|
|
672
|
+
if (opts.format === 'json') console.error(notice);
|
|
673
|
+
else for (const g of [notice, ...orphanGaps]) console.error(g);
|
|
674
|
+
}
|
|
675
|
+
|
|
654
676
|
const at = new Date();
|
|
655
677
|
const criteriaStrings = coverage.ac.flatMap((a) => [a.id, ...a.declared]);
|
|
656
678
|
const nowIso = at.toISOString();
|
package/src/harness/state.mjs
CHANGED
|
@@ -718,8 +718,15 @@ const UNGATED_FORWARD_EDGES = Object.freeze([
|
|
|
718
718
|
['architectureComplete', 'story-breakdown'],
|
|
719
719
|
['spikes', 'story-breakdown'],
|
|
720
720
|
['story-breakdown', 'implementation'],
|
|
721
|
-
//
|
|
722
|
-
|
|
721
|
+
// Next-story loop. The workflow is
|
|
722
|
+
// VALIDATE -->|"gate: designArtifactSyncConfirmed"| NEXT_STORY
|
|
723
|
+
// NEXT_STORY -->|"yes"| IMPL
|
|
724
|
+
// NEXT_STORY -->|"no"| CLOSED
|
|
725
|
+
// so the next story in an epic re-enters implementation from `validation`.
|
|
726
|
+
// `closed` stays terminal — it means the epic/plan is finished
|
|
727
|
+
// (Resume: "All work is complete for the current epic(s)") — and is
|
|
728
|
+
// deliberately NOT an escape hatch for starting the next story.
|
|
729
|
+
['validation', 'implementation'],
|
|
723
730
|
]);
|
|
724
731
|
|
|
725
732
|
/** Is `from → to` one of the declared ungated forward edges? */
|
|
@@ -240,10 +240,21 @@ function splitTestList(text) {
|
|
|
240
240
|
/**
|
|
241
241
|
* Compare a story's declared tests against a run's inventory.
|
|
242
242
|
*
|
|
243
|
-
* Returns `{ ok, ac: [{ id, declared, found, status }], inventorySize, format }`.
|
|
243
|
+
* Returns `{ ok, ac: [{ id, declared, found, status }], orphaned, inventorySize, format }`.
|
|
244
244
|
* `status` is `covered` (all declared found), `missing` (some declared absent),
|
|
245
245
|
* or `undeclared` (the AC declares no test at all).
|
|
246
246
|
*
|
|
247
|
+
* `orphaned` is the INVERSE direction: tests that ran but are declared on no AC.
|
|
248
|
+
* This module previously checked only declared→delivered, so a delivered test
|
|
249
|
+
* attached to no criterion was invisible — the drift that recurred three times
|
|
250
|
+
* before this was added. Note the status name `undeclared` does NOT cover this
|
|
251
|
+
* case: it means "this AC declares no tests", not "this test is on no AC".
|
|
252
|
+
*
|
|
253
|
+
* `orphaned` deliberately does NOT affect `ok`. Consumers legitimately have
|
|
254
|
+
* helper tests and parameterised fixtures that belong to no single criterion, so
|
|
255
|
+
* making orphans fatal would break every existing story. Callers that want
|
|
256
|
+
* enforcement pass `--strict-orphans` (see describeCoverageGaps and cli.mjs).
|
|
257
|
+
*
|
|
247
258
|
* Every gap is reported together; the caller renders all of them, never just the
|
|
248
259
|
* first.
|
|
249
260
|
*/
|
|
@@ -252,8 +263,13 @@ export function compareCoverage(criteria, inventory) {
|
|
|
252
263
|
const ac = [];
|
|
253
264
|
let ok = true;
|
|
254
265
|
|
|
266
|
+
// Union of everything declared anywhere, so a test declared on any AC is not
|
|
267
|
+
// an orphan just because it is not on the AC being examined.
|
|
268
|
+
const declaredAnywhere = new Set();
|
|
269
|
+
|
|
255
270
|
for (const c of criteria) {
|
|
256
271
|
const declared = (c.tests || []).map((t) => String(t));
|
|
272
|
+
for (const t of declared) declaredAnywhere.add(normalizeTestName(t));
|
|
257
273
|
if (declared.length === 0) {
|
|
258
274
|
ok = false;
|
|
259
275
|
ac.push({ id: c.id, declared: [], found: [], status: 'undeclared' });
|
|
@@ -265,16 +281,34 @@ export function compareCoverage(criteria, inventory) {
|
|
|
265
281
|
ac.push({ id: c.id, declared, found: present, status });
|
|
266
282
|
}
|
|
267
283
|
|
|
284
|
+
// Preserve report order and the report's own spelling, deduped by normalized
|
|
285
|
+
// name so an inventory that repeats a test does not repeat the warning.
|
|
286
|
+
const orphaned = [];
|
|
287
|
+
const seenOrphan = new Set();
|
|
288
|
+
for (const raw of inventory?.names || []) {
|
|
289
|
+
const key = normalizeTestName(raw);
|
|
290
|
+
if (!key || declaredAnywhere.has(key) || seenOrphan.has(key)) continue;
|
|
291
|
+
seenOrphan.add(key);
|
|
292
|
+
orphaned.push(key);
|
|
293
|
+
}
|
|
294
|
+
|
|
268
295
|
return {
|
|
269
296
|
ok,
|
|
270
297
|
ac,
|
|
298
|
+
orphaned,
|
|
271
299
|
inventorySize: (inventory?.names || []).length,
|
|
272
300
|
format: inventory?.format || 'unknown',
|
|
273
301
|
};
|
|
274
302
|
}
|
|
275
303
|
|
|
276
|
-
/**
|
|
277
|
-
|
|
304
|
+
/**
|
|
305
|
+
* Format the gaps as concrete, actionable lines (spec §5.1 step 4).
|
|
306
|
+
*
|
|
307
|
+
* `includeOrphans` appends the inverse-direction gaps. It is opt-in so the
|
|
308
|
+
* default call site keeps its previous output shape, and so a caller can report
|
|
309
|
+
* orphans without treating them as failures.
|
|
310
|
+
*/
|
|
311
|
+
export function describeCoverageGaps(coverage, { includeOrphans = false } = {}) {
|
|
278
312
|
const lines = [];
|
|
279
313
|
for (const entry of coverage.ac) {
|
|
280
314
|
if (entry.status === 'undeclared') {
|
|
@@ -287,5 +321,10 @@ export function describeCoverageGaps(coverage) {
|
|
|
287
321
|
}
|
|
288
322
|
}
|
|
289
323
|
}
|
|
324
|
+
if (includeOrphans) {
|
|
325
|
+
for (const t of coverage.orphaned || []) {
|
|
326
|
+
lines.push(` "${t}" ran but is declared on no acceptance criterion — attach it to the criterion it proves, or remove it.`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
290
329
|
return lines;
|
|
291
330
|
}
|