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 +1 -1
- package/src/cli.mjs +5 -0
- package/src/harness/policy.mjs +6 -0
- package/src/harness/state.mjs +21 -3
package/package.json
CHANGED
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
|
package/src/harness/policy.mjs
CHANGED
|
@@ -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. */
|
package/src/harness/state.mjs
CHANGED
|
@@ -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
|
|
235
|
-
//
|
|
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
|
-
|
|
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 `
|