cadet-agent 0.30.0 → 0.31.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 +171 -6
- package/src/harness/index.mjs +4 -3
- package/src/harness/policy.mjs +560 -359
- package/src/harness/state.mjs +919 -661
- package/src/harness/verification.mjs +523 -490
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
validateState, migrateStateFile, readState, writeState, evaluateTransition, applyTransition,
|
|
7
7
|
workItemIdOf, loadPolicy, RunLedger, loadRun, listRuns, cleanupRuns, buildReport, formatReport,
|
|
8
8
|
runVerificationLoop, commandForGate, detectCapabilities, runsDir, gitChangedFiles, PolicyError, StateError,
|
|
9
|
-
detectRepoRole, describeRepoRole,
|
|
9
|
+
detectRepoRole, describeRepoRole, GATES, manualConfirmation,
|
|
10
10
|
} from './harness/index.mjs';
|
|
11
11
|
|
|
12
12
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -39,6 +39,7 @@ function showHelp() {
|
|
|
39
39
|
cadet-agent state transition --to <phase> Enforce the transition matrix + evidence
|
|
40
40
|
|
|
41
41
|
cadet-agent harness record Append a sanitized span/evidence/decision event
|
|
42
|
+
cadet-agent harness confirm Record manual-confirmation evidence (writes ledger + state)
|
|
42
43
|
cadet-agent harness verify Run a bounded, classified verification loop
|
|
43
44
|
cadet-agent harness report Summarize budget consumption and failures
|
|
44
45
|
cadet-agent harness cleanup Apply the retention policy to .cadet/runs/
|
|
@@ -49,9 +50,13 @@ function showHelp() {
|
|
|
49
50
|
--source Release API URL override (for forked deployments)
|
|
50
51
|
--format human|json (default: human)
|
|
51
52
|
--to Target phase (state transition)
|
|
52
|
-
--gate Gate name (harness verify)
|
|
53
|
+
--gate Gate name (harness verify|confirm)
|
|
53
54
|
--command Command override (harness verify)
|
|
54
|
-
--files Comma-separated relevant files to bind evidence to (harness verify)
|
|
55
|
+
--files Comma-separated relevant files to bind evidence to (harness verify|confirm)
|
|
56
|
+
--reason Why automation was unavailable (harness confirm)
|
|
57
|
+
--expires-at ISO-8601 expiry bounding the confirmation (harness confirm)
|
|
58
|
+
--environment key=value,... describing what was verified (harness confirm)
|
|
59
|
+
--scope Comma-separated scope of the confirmation (harness confirm)
|
|
55
60
|
--agents-md keep|overwrite|merge for an existing AGENTS.md (init/sync)
|
|
56
61
|
--yes, -y Never prompt; keep existing files (non-interactive installs)
|
|
57
62
|
--help, -h Show this help
|
|
@@ -77,6 +82,9 @@ function parseArgs(argv) {
|
|
|
77
82
|
case '--run': opts.runId = argv[++i]; break;
|
|
78
83
|
case '--type': opts.type = argv[++i]; break;
|
|
79
84
|
case '--reason': opts.reason = argv[++i]; break;
|
|
85
|
+
case '--expires-at': opts.expiresAt = argv[++i]; break;
|
|
86
|
+
case '--environment': opts.environment = argv[++i]; break;
|
|
87
|
+
case '--scope': opts.scope = (argv[++i] || '').split(',').map((s) => s.trim()).filter(Boolean); break;
|
|
80
88
|
case '--evidence-status': opts.evidenceStatus = argv[++i]; break;
|
|
81
89
|
case '--files': opts.files = (argv[++i] || '').split(',').map((s) => s.trim()).filter(Boolean); break;
|
|
82
90
|
case '--older-than-ms': opts.olderThanMs = Number(argv[++i]); break;
|
|
@@ -96,6 +104,28 @@ function emit(opts, human, json) {
|
|
|
96
104
|
}
|
|
97
105
|
}
|
|
98
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Parse `--environment "projectPath=...,editorVersion=...,tool=...,host=..."`
|
|
109
|
+
* into an object. Unknown keys are preserved: an unusual environment is still
|
|
110
|
+
* evidence, and silently dropping a field would misrepresent what was verified.
|
|
111
|
+
*/
|
|
112
|
+
function parseEnvironment(raw) {
|
|
113
|
+
const env = {};
|
|
114
|
+
if (!raw) return env;
|
|
115
|
+
for (const part of String(raw).split(',')) {
|
|
116
|
+
const eq = part.indexOf('=');
|
|
117
|
+
if (eq === -1) {
|
|
118
|
+
const key = part.trim();
|
|
119
|
+
if (key) env[key] = true;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const key = part.slice(0, eq).trim();
|
|
123
|
+
const value = part.slice(eq + 1).trim();
|
|
124
|
+
if (key) env[key] = value;
|
|
125
|
+
}
|
|
126
|
+
return env;
|
|
127
|
+
}
|
|
128
|
+
|
|
99
129
|
function fail(opts, message, code = json => json.exitCode || 1, json = {}) {
|
|
100
130
|
const exitCode = code(json);
|
|
101
131
|
if (opts.format === 'json') {
|
|
@@ -127,8 +157,12 @@ async function cmdState(opts) {
|
|
|
127
157
|
);
|
|
128
158
|
return;
|
|
129
159
|
}
|
|
130
|
-
// Pass rootDir so stale/foreign evidence is caught at validation time
|
|
131
|
-
|
|
160
|
+
// Pass rootDir so stale/foreign evidence is caught at validation time, and
|
|
161
|
+
// the resolved policy so strict-closure rules are actually enforced. Without
|
|
162
|
+
// the policy, `strictClosure` was invisible here and every strict rule was
|
|
163
|
+
// silently skipped — the feature would "install cleanly and do nothing".
|
|
164
|
+
const policy = loadPolicy(opts.targetDir);
|
|
165
|
+
const result = validateState(state, { rootDir: opts.targetDir, strictClosure: policy.strictClosure });
|
|
132
166
|
const role = detectRepoRole(opts.targetDir);
|
|
133
167
|
const repoRoleDetail = describeRepoRole(role);
|
|
134
168
|
if (opts.format === 'json') {
|
|
@@ -212,6 +246,137 @@ async function cmdHarness(opts) {
|
|
|
212
246
|
return;
|
|
213
247
|
}
|
|
214
248
|
|
|
249
|
+
if (sub === 'confirm') {
|
|
250
|
+
const gate = opts.gate;
|
|
251
|
+
if (!gate) fail(opts, 'harness confirm requires --gate <gate>');
|
|
252
|
+
if (!GATES.includes(gate)) fail(opts, `unknown gate "${gate}". Valid gates: ${GATES.join(', ')}`);
|
|
253
|
+
|
|
254
|
+
const { exists, state } = readState(opts.targetDir);
|
|
255
|
+
if (!exists) fail(opts, 'No .cadet/state.json found. Initialise state before recording confirmation.', () => 2);
|
|
256
|
+
|
|
257
|
+
const strict = policy.strictClosure?.enabled === true ? policy.strictClosure : null;
|
|
258
|
+
const mc = strict?.manualConfirmation || null;
|
|
259
|
+
// One reference instant for the whole command, captured before any work.
|
|
260
|
+
// Reading Date.now() at the check instead made the validity boundary
|
|
261
|
+
// non-deterministic: process latency absorbed a small overage, so the same
|
|
262
|
+
// input could pass or fail run to run.
|
|
263
|
+
const requestedAt = new Date();
|
|
264
|
+
|
|
265
|
+
// Collect EVERY missing field so the caller fixes the record in one pass,
|
|
266
|
+
// rather than discovering one omission per invocation.
|
|
267
|
+
const missing = [];
|
|
268
|
+
if (mc?.requireReason !== false && strict && (!opts.reason || String(opts.reason).trim() === '')) missing.push('--reason');
|
|
269
|
+
if (mc?.requireExpiresAt !== false && strict) {
|
|
270
|
+
if (!opts.expiresAt) missing.push('--expires-at');
|
|
271
|
+
else if (Number.isNaN(Date.parse(opts.expiresAt))) missing.push('--expires-at (not an ISO-8601 date-time)');
|
|
272
|
+
}
|
|
273
|
+
if (mc?.requireEnvironment !== false && strict && (!opts.environment || String(opts.environment).trim() === '')) missing.push('--environment');
|
|
274
|
+
if (mc?.requireScope !== false && strict && (!opts.scope || opts.scope.length === 0)) missing.push('--scope');
|
|
275
|
+
if (missing.length) {
|
|
276
|
+
fail(opts, `strictClosure requires manual-confirmation metadata. Missing: ${missing.join(', ')}.`, () => 1, { ok: false, gate, code: 'strict-metadata-missing', missing });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// A gate listed in disallowManualFor may never be satisfied by a human
|
|
280
|
+
// assertion; point at the automated path instead of accepting the record.
|
|
281
|
+
if (strict && Array.isArray(strict.disallowManualFor) && strict.disallowManualFor.includes(gate)) {
|
|
282
|
+
fail(opts, `manual-confirmation is not permitted for gate "${gate}" under strictClosure.disallowManualFor; run "cadet-agent harness verify --gate ${gate}" instead.`, () => 1, { ok: false, gate, code: 'manual-disallowed' });
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Bound the validity window: an expiry far in the future is how a manual
|
|
286
|
+
// assertion silently becomes permanent. Measured against `requestedAt`, the
|
|
287
|
+
// single instant captured at command start, so the boundary is deterministic
|
|
288
|
+
// and agrees with `validateState` (which anchors to the present too).
|
|
289
|
+
if (mc?.maxValidityMs !== null && mc?.maxValidityMs !== undefined && opts.expiresAt) {
|
|
290
|
+
const window = Date.parse(opts.expiresAt) - requestedAt.getTime();
|
|
291
|
+
if (Number.isFinite(window) && window > mc.maxValidityMs) {
|
|
292
|
+
fail(opts, `requested validity ${window}ms exceeds strictClosure.manualConfirmation.maxValidityMs (${mc.maxValidityMs}ms).`, () => 1, { ok: false, gate, code: 'validity-exceeded', requestedMs: window, maxValidityMs: mc.maxValidityMs });
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Freshness binding mirrors `harness verify`: never record a gate against an
|
|
297
|
+
// unknown input tree unless the repository explicitly opted out.
|
|
298
|
+
const allowEmpty = policy?.allowEmptyFreshness === true;
|
|
299
|
+
let relevantFiles;
|
|
300
|
+
if (opts.files && opts.files.length) {
|
|
301
|
+
relevantFiles = opts.files.map((f) => f.replace(/\\/g, '/'));
|
|
302
|
+
} else {
|
|
303
|
+
const changed = gitChangedFiles(opts.targetDir);
|
|
304
|
+
if (!changed.available) {
|
|
305
|
+
if (!allowEmpty) {
|
|
306
|
+
fail(opts, `cannot establish freshness coverage: ${changed.reason}. Pass --files <paths>, or enable allowEmptyFreshness in .cadet/harness.json.`, () => 1, { ok: false, gate, code: 'freshness-unavailable' });
|
|
307
|
+
}
|
|
308
|
+
relevantFiles = [];
|
|
309
|
+
} else {
|
|
310
|
+
relevantFiles = changed.files;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const workItemId = state ? workItemIdOf(state) : 'unscoped';
|
|
315
|
+
const phase = state?.session?.currentPhase || 'implementation';
|
|
316
|
+
// Reuse the command-start instant so the recorded createdAt and the validity
|
|
317
|
+
// check describe the same moment.
|
|
318
|
+
const at = requestedAt;
|
|
319
|
+
const environment = parseEnvironment(opts.environment);
|
|
320
|
+
|
|
321
|
+
const { evidence } = manualConfirmation({
|
|
322
|
+
gate,
|
|
323
|
+
workItemId,
|
|
324
|
+
phase,
|
|
325
|
+
projectPath: environment.projectPath || null,
|
|
326
|
+
editorVersion: environment.editorVersion || null,
|
|
327
|
+
scope: opts.scope || [],
|
|
328
|
+
reason: opts.reason || null,
|
|
329
|
+
expiresAt: opts.expiresAt || null,
|
|
330
|
+
environment,
|
|
331
|
+
relevantFiles,
|
|
332
|
+
rootDir: opts.targetDir,
|
|
333
|
+
approvedBy: opts.approvedBy || 'user',
|
|
334
|
+
at,
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// Ledger first, then state. The ledger is append-only and merely references
|
|
338
|
+
// the evidence id; state.json carries the gate claim. Writing state first
|
|
339
|
+
// would let an interruption leave a gate claimed true with no ledger entry.
|
|
340
|
+
// This order fails toward "less proven", never "claimed but unbacked".
|
|
341
|
+
const ledger = new RunLedger({
|
|
342
|
+
targetDir: opts.targetDir,
|
|
343
|
+
policy,
|
|
344
|
+
runId: state?.activeRunId || null,
|
|
345
|
+
workItemId,
|
|
346
|
+
phase,
|
|
347
|
+
});
|
|
348
|
+
ledger.addEvidence(evidence);
|
|
349
|
+
ledger.addDecision({
|
|
350
|
+
kind: 'manual-confirmation',
|
|
351
|
+
reason: opts.reason || 'manual confirmation recorded',
|
|
352
|
+
gate,
|
|
353
|
+
evidenceId: evidence.evidenceId,
|
|
354
|
+
approvedBy: evidence.approvedBy || 'user',
|
|
355
|
+
});
|
|
356
|
+
ledger.finalize({ status: 'ok' });
|
|
357
|
+
const ledgerPath = ledger.persist();
|
|
358
|
+
|
|
359
|
+
const next = { ...state };
|
|
360
|
+
const prior = Array.isArray(state.gateEvidence) ? state.gateEvidence : [];
|
|
361
|
+
next.gateEvidence = [
|
|
362
|
+
// Immutability: supersede prior passing evidence, never delete it.
|
|
363
|
+
...prior.map((e) => (e.gate === gate && (e.status === 'passed' || e.status === 'manual-confirmation')
|
|
364
|
+
? { ...e, status: 'superseded', supersededBy: evidence.evidenceId }
|
|
365
|
+
: e)),
|
|
366
|
+
evidence,
|
|
367
|
+
];
|
|
368
|
+
next.gates = { ...(state.gates || {}), [gate]: true };
|
|
369
|
+
writeState(opts.targetDir, next);
|
|
370
|
+
|
|
371
|
+
const superseded = prior.filter((e) => e.gate === gate && (e.status === 'passed' || e.status === 'manual-confirmation')).length;
|
|
372
|
+
emit(
|
|
373
|
+
opts,
|
|
374
|
+
`✅ Recorded manual confirmation for gate "${gate}". Evidence: ${evidence.evidenceId}\n Ledger: ${ledgerPath}`,
|
|
375
|
+
{ ok: true, gate, evidenceId: evidence.evidenceId, runId: ledger.runId, path: ledgerPath, stateUpdated: true, superseded },
|
|
376
|
+
);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
215
380
|
if (sub === 'record') {
|
|
216
381
|
const { state } = readState(opts.targetDir);
|
|
217
382
|
const ledger = new RunLedger({
|
|
@@ -391,7 +556,7 @@ async function cmdHarness(opts) {
|
|
|
391
556
|
return;
|
|
392
557
|
}
|
|
393
558
|
|
|
394
|
-
fail(opts, `Unknown harness subcommand: ${sub || '(none)'}. Use record|verify|report|cleanup|capabilities.`);
|
|
559
|
+
fail(opts, `Unknown harness subcommand: ${sub || '(none)'}. Use record|confirm|verify|report|cleanup|capabilities.`);
|
|
395
560
|
}
|
|
396
561
|
|
|
397
562
|
export async function run(argv) {
|
package/src/harness/index.mjs
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
export {
|
|
9
9
|
PHASES, GATES, TRANSITIONS, EVIDENCE_STATUSES, RETRY_CLASSES, CONTEXT_TIERS,
|
|
10
10
|
DEFAULT_BUDGETS, HARD_CEILINGS, DEFAULT_ARCHIVE_LIMITS, DEFAULT_OUTPUT_POLICY,
|
|
11
|
-
DEFAULT_RETENTION, DEFAULT_ESTIMATION, DEFAULT_HOOK_POLICY,
|
|
11
|
+
DEFAULT_RETENTION, DEFAULT_ESTIMATION, DEFAULT_HOOK_POLICY, DEFAULT_STRICT_CLOSURE,
|
|
12
|
+
EXCEPTION_CATEGORIES, EXCEPTION_EXPIRY_DAYS, EXCEPTION_REQUIRES_REVIEW_NOTE, AGENT_OWNED_GATES,
|
|
12
13
|
validatePolicy, defaultPolicy, loadPolicy, budgetForScope, policyPath, PolicyError,
|
|
13
14
|
} from './policy.mjs';
|
|
14
15
|
|
|
@@ -22,9 +23,9 @@ export {
|
|
|
22
23
|
} from './util.mjs';
|
|
23
24
|
|
|
24
25
|
export {
|
|
25
|
-
STATE_VERSION, validateState, migrateStateV1toV2, migrateStateFile,
|
|
26
|
+
STATE_VERSION, READABLE_STATE_VERSIONS, validateState, migrateStateV1toV2, migrateStateFile,
|
|
26
27
|
createEvidence, computeInputTreeHash, workItemIdOf, evidenceFreshness,
|
|
27
|
-
latestEvidenceForGate, activeExceptions, requiredGates, evaluateTransition,
|
|
28
|
+
latestEvidenceForGate, activeExceptions, requiredGates, evaluateTransition, resolveStrict,
|
|
28
29
|
applyTransition, resetGatesForNewWorkItem, statePathFor, readState, writeState, writeJsonAtomic, StateError,
|
|
29
30
|
} from './state.mjs';
|
|
30
31
|
|