cadet-agent 0.36.0 → 0.37.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cadet-agent",
3
- "version": "0.36.0",
3
+ "version": "0.37.0",
4
4
  "description": "Cross-IDE agent framework for Unity/C# game-development — one-command install",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.mjs CHANGED
@@ -58,10 +58,15 @@ function showHelp() {
58
58
  --gate Gate name (harness verify|confirm)
59
59
  --command Command override (harness verify)
60
60
  --files Comma-separated relevant files to bind evidence to (harness verify|confirm)
61
+ --commit Revision the gate attests, as a hex SHA (harness verify|confirm)
61
62
  --reason Why automation was unavailable (harness confirm)
62
63
  --expires-at ISO-8601 expiry bounding the confirmation (harness confirm)
63
64
  --environment key=value,... describing what was verified (harness confirm)
64
65
  --scope Comma-separated scope of the confirmation (harness confirm)
66
+ --story Story markdown declaring the acceptance criteria (harness verify-acs)
67
+ --report Test report to derive the inventory from (harness verify-acs|matrix-check)
68
+ --matrix TDD matrix markdown to check (harness matrix-check)
69
+ --inventory Newline-separated test names, when no report is available (harness matrix-check)
65
70
  --agents-md keep|overwrite|merge for an existing AGENTS.md (init/sync)
66
71
  --yes, -y Never prompt; keep existing files (non-interactive installs)
67
72
  --help, -h Show this help
@@ -162,6 +162,7 @@ export const EXCEPTION_CATEGORIES = Object.freeze([
162
162
  'unscoped-freshness',
163
163
  'documentation-only',
164
164
  'tooling-gap',
165
+ 'pre-harness-story',
165
166
  ]);
166
167
 
167
168
  /** Default expiry (in days) per category. `null` means "no default bound". */
@@ -172,6 +173,11 @@ export const EXCEPTION_EXPIRY_DAYS = Object.freeze({
172
173
  'unscoped-freshness': 1,
173
174
  'documentation-only': null, // scoped to the work item
174
175
  'tooling-gap': 14,
176
+ // No default bound: this records a PERMANENT historical fact (a story closed
177
+ // before the harness existed, whose gates were never recorded as evidence).
178
+ // The gap will never close on its own, so a time-bounded exception would only
179
+ // re-raise the same finding every N days without anything having changed.
180
+ 'pre-harness-story': null,
175
181
  });
176
182
 
177
183
  /** Categories whose exception must carry a closure review note. */
@@ -231,19 +231,37 @@ export function validateState(state, context = {}) {
231
231
  // It is an ERROR, not a warning, because a `done` story with no evidence is
232
232
  // indistinguishable from a story that was never verified — which is the
233
233
  // condition the framework exists to prevent. Projects that closed stories
234
- // before the harness existed can resolve it with a scoped gate exception or
235
- // by re-recording; silently tolerating it is what let the gap grow.
234
+ // before the harness existed resolve it with a scoped `pre-harness-story`
235
+ // exception naming the story's work item; silently tolerating it is what let
236
+ // the gap grow.
236
237
  if (isPlainObject(state.epics) && Array.isArray(state.gateEvidence)) {
237
238
  const evidenced = new Set(
238
239
  state.gateEvidence
239
240
  .map((e) => (isPlainObject(e) ? e.workItemId : null))
240
241
  .filter((id) => typeof id === 'string' && id.length > 0),
241
242
  );
243
+ // A scoped exception is the FIRST-CLASS escape for a permanent historical
244
+ // gap. `activeExceptions` is keyed on the ACTIVE work item, which is the
245
+ // current story — the wrong scope here, where we walk every completed story
246
+ // — so the scope match is done explicitly against each story's work-item id.
247
+ // Only a valid, categorised exception counts: an unknown category is not a
248
+ // loophole, it is a typo, and `validateGateException` rejects it separately.
249
+ const excepted = new Set();
250
+ if (Array.isArray(state.changeHistory)) {
251
+ for (const entry of state.changeHistory) {
252
+ if (!isPlainObject(entry) || entry.type !== 'gate-exception') continue;
253
+ if (!EXCEPTION_CATEGORIES.includes(entry.category)) continue;
254
+ const scope = Array.isArray(entry.scope) ? entry.scope : (entry.scope ? [entry.scope] : []);
255
+ for (const s of scope) excepted.add(String(s));
256
+ }
257
+ }
242
258
  for (const [epicId, epic] of Object.entries(state.epics)) {
243
259
  if (!isPlainObject(epic) || !isPlainObject(epic.stories)) continue;
244
260
  for (const [storyId, status] of Object.entries(epic.stories)) {
245
261
  if (status !== 'done') continue;
246
- if (evidenced.has(`${epicId}::${storyId}`)) continue;
262
+ const workItemId = `${epicId}::${storyId}`;
263
+ if (evidenced.has(workItemId)) continue;
264
+ if (excepted.has(workItemId)) continue;
247
265
  errors.push({
248
266
  path: `epics.${epicId}.stories.${storyId}`,
249
267
  message: `story "${storyId}" is marked done but has no evidence record for its work item `