cadet-agent 0.32.0 → 0.33.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 +24 -3
- package/src/harness/index.mjs +1 -1
- package/src/harness/state.mjs +51 -2
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -39,6 +39,7 @@ function showHelp() {
|
|
|
39
39
|
cadet-agent state validate Validate .cadet/state.json against the v2 schema
|
|
40
40
|
cadet-agent state migrate Atomically migrate v1 state to v2 (backup on write)
|
|
41
41
|
cadet-agent state transition --to <phase> Enforce the transition matrix + evidence
|
|
42
|
+
cadet-agent state transition --to <phase> --dry-run Check only; writes nothing
|
|
42
43
|
|
|
43
44
|
cadet-agent harness record Append a sanitized span/evidence/decision event
|
|
44
45
|
cadet-agent harness confirm Record manual-confirmation evidence (writes ledger + state)
|
|
@@ -93,6 +94,7 @@ function parseArgs(argv) {
|
|
|
93
94
|
case '--story': opts.story = argv[++i]; break;
|
|
94
95
|
case '--report': opts.report = argv[++i]; break;
|
|
95
96
|
case '--write-coverage': opts.writeCoverage = true; break;
|
|
97
|
+
case '--dry-run': opts.dryRun = true; break;
|
|
96
98
|
case '--older-than-ms': opts.olderThanMs = Number(argv[++i]); break;
|
|
97
99
|
case '--agents-md': opts.agentsMd = argv[++i]; break;
|
|
98
100
|
case '--yes': case '-y': opts.yes = true; break;
|
|
@@ -204,16 +206,23 @@ async function cmdState(opts) {
|
|
|
204
206
|
if (!exists) fail(opts, 'No .cadet/state.json found. Initialise state before transitioning.', () => 2);
|
|
205
207
|
// Freshness is enforced against the current working tree: evaluateTransition
|
|
206
208
|
// recomputes each gate's input-tree hash from the evidence's relevant files.
|
|
209
|
+
//
|
|
210
|
+
// `--dry-run` reports the SAME verdict and stops. It is the only safe way to
|
|
211
|
+
// ask "would this transition be allowed?" — running the command without the
|
|
212
|
+
// flag applies the transition. A check that is documented as a dry run must
|
|
213
|
+
// not have side effects, so the write below is gated on `!opts.dryRun`.
|
|
207
214
|
const evaluation = evaluateTransition(state, opts.to, { rootDir: opts.targetDir });
|
|
208
215
|
if (!evaluation.allowed) {
|
|
209
216
|
const detail = {
|
|
210
217
|
ok: false,
|
|
211
218
|
allowed: false,
|
|
219
|
+
dryRun: opts.dryRun === true,
|
|
220
|
+
applied: false,
|
|
212
221
|
missingGates: evaluation.missingGates,
|
|
213
222
|
staleEvidence: evaluation.staleEvidence,
|
|
214
223
|
errors: evaluation.errors,
|
|
215
224
|
};
|
|
216
|
-
const lines = [
|
|
225
|
+
const lines = [`❌ Transition rejected${opts.dryRun ? ' (dry run — nothing was written)' : ''}:`];
|
|
217
226
|
for (const e of evaluation.errors) lines.push(` ${e}`);
|
|
218
227
|
if (evaluation.missingGates.length) lines.push(` missing gates/evidence: ${evaluation.missingGates.join(', ')}`);
|
|
219
228
|
for (const s of evaluation.staleEvidence) lines.push(` stale: ${s.gate} — ${s.reason || (s.reasons || []).join('; ')}`);
|
|
@@ -221,9 +230,17 @@ async function cmdState(opts) {
|
|
|
221
230
|
else console.error(lines.join('\n'));
|
|
222
231
|
process.exit(1);
|
|
223
232
|
}
|
|
233
|
+
if (opts.dryRun) {
|
|
234
|
+
emit(
|
|
235
|
+
opts,
|
|
236
|
+
`✅ Transition ${state.session?.currentPhase} → ${opts.to} would be allowed (dry run — nothing was written).`,
|
|
237
|
+
{ ok: true, allowed: true, dryRun: true, applied: false, to: opts.to, from: state.session?.currentPhase },
|
|
238
|
+
);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
224
241
|
const next = applyTransition(state, opts.to, { rootDir: opts.targetDir });
|
|
225
242
|
writeState(opts.targetDir, next);
|
|
226
|
-
emit(opts, `✅ Transitioned to ${opts.to}.`, { ok: true, allowed: true, to: opts.to });
|
|
243
|
+
emit(opts, `✅ Transitioned to ${opts.to}.`, { ok: true, allowed: true, dryRun: false, applied: true, to: opts.to });
|
|
227
244
|
return;
|
|
228
245
|
}
|
|
229
246
|
|
|
@@ -652,7 +669,11 @@ async function cmdHarness(opts) {
|
|
|
652
669
|
relevantFiles: [opts.story, ...(reportPath ? [reportPath] : [])].map((f) => f.replace(/\\/g, '/')),
|
|
653
670
|
createdAt: at,
|
|
654
671
|
expiresAt: null,
|
|
655
|
-
|
|
672
|
+
// Schema + validator require an object carrying a `scope`, not a bare
|
|
673
|
+
// string: state.schema.json#/$defs/evidence references
|
|
674
|
+
// harness.schema.json#/$defs/freshnessPolicy, which has required:["scope"]
|
|
675
|
+
// with scope ∈ story|phase|run|manual.
|
|
676
|
+
freshnessPolicy: { scope: 'story' },
|
|
656
677
|
source: 'automated',
|
|
657
678
|
});
|
|
658
679
|
|
package/src/harness/index.mjs
CHANGED
|
@@ -25,7 +25,7 @@ export {
|
|
|
25
25
|
export {
|
|
26
26
|
STATE_VERSION, READABLE_STATE_VERSIONS, validateState, migrateStateV1toV2, migrateStateFile,
|
|
27
27
|
createEvidence, computeInputTreeHash, workItemIdOf, evidenceFreshness,
|
|
28
|
-
latestEvidenceForGate, activeExceptions, requiredGates, evaluateTransition, resolveStrict,
|
|
28
|
+
latestEvidenceForGate, activeExceptions, requiredGates, isUngatedForwardEdge, evaluateTransition, resolveStrict,
|
|
29
29
|
applyTransition, resetGatesForNewWorkItem, statePathFor, readState, writeState, writeJsonAtomic, StateError,
|
|
30
30
|
} from './state.mjs';
|
|
31
31
|
|
package/src/harness/state.mjs
CHANGED
|
@@ -685,6 +685,48 @@ export function requiredGates(toPhase) {
|
|
|
685
685
|
return null;
|
|
686
686
|
}
|
|
687
687
|
|
|
688
|
+
/**
|
|
689
|
+
* Ungated forward edges — transitions that carry no gate requirement but are
|
|
690
|
+
* still legal. These are the bootstrap and planning-progression edges from the
|
|
691
|
+
* workflow (README mermaid + Workflow.md): classification can drop straight to
|
|
692
|
+
* implementation, and the planning phases advance without gates.
|
|
693
|
+
*
|
|
694
|
+
* This list exists because `requiredGates` returns null for any phase that is
|
|
695
|
+
* never a *target* of a gated transition (implementation, requirements,
|
|
696
|
+
* architecture, …). Treating null as "ungated, therefore legal" allowed a
|
|
697
|
+
* transition into those phases from ANYWHERE — including out of the terminal
|
|
698
|
+
* `closed` phase. The set below is the closed enumeration of the intended
|
|
699
|
+
* forward edges; anything not in it (or in TRANSITIONS) is rejected.
|
|
700
|
+
*/
|
|
701
|
+
const UNGATED_FORWARD_EDGES = Object.freeze([
|
|
702
|
+
// Classification (context-resolution) routes to planning or straight to work.
|
|
703
|
+
['context-resolution', 'requirements'],
|
|
704
|
+
['context-resolution', 'architecture'],
|
|
705
|
+
['context-resolution', 'implementation'],
|
|
706
|
+
// Planning progression.
|
|
707
|
+
['requirements', 'architecture'],
|
|
708
|
+
['requirements', 'requirementsComplete'],
|
|
709
|
+
['requirementsComplete', 'architecture'],
|
|
710
|
+
['requirementsComplete', 'architectureComplete'],
|
|
711
|
+
['architecture', 'architectureComplete'],
|
|
712
|
+
['architecture', 'spikes'],
|
|
713
|
+
['requirements', 'spikes'],
|
|
714
|
+
['requirementsComplete', 'spikes'],
|
|
715
|
+
['architectureComplete', 'spikes'],
|
|
716
|
+
['spikes', 'architecture'],
|
|
717
|
+
['spikes', 'architectureComplete'],
|
|
718
|
+
['architectureComplete', 'story-breakdown'],
|
|
719
|
+
['spikes', 'story-breakdown'],
|
|
720
|
+
['story-breakdown', 'implementation'],
|
|
721
|
+
// Re-entering work for a new story/epic from a review/vallidation outcome.
|
|
722
|
+
['story-breakdown', 'story-breakdown'],
|
|
723
|
+
]);
|
|
724
|
+
|
|
725
|
+
/** Is `from → to` one of the declared ungated forward edges? */
|
|
726
|
+
export function isUngatedForwardEdge(fromPhase, toPhase) {
|
|
727
|
+
return UNGATED_FORWARD_EDGES.some(([from, to]) => from === fromPhase && to === toPhase);
|
|
728
|
+
}
|
|
729
|
+
|
|
688
730
|
/**
|
|
689
731
|
* Check one gate for a transition. Shared by the primary `gates` set and the
|
|
690
732
|
* strict-closure `revalidate` set so the two can never drift apart.
|
|
@@ -770,8 +812,15 @@ export function evaluateTransition(state, toPhase, context = {}) {
|
|
|
770
812
|
|
|
771
813
|
const spec = requiredGates(toPhase);
|
|
772
814
|
if (!spec) {
|
|
773
|
-
// Ungated transitions
|
|
774
|
-
|
|
815
|
+
// Ungated transitions are legal ONLY along the declared forward edges
|
|
816
|
+
// (bootstrap + planning progression). A target that is neither gated nor a
|
|
817
|
+
// declared forward edge is rejected — most importantly, this makes `closed`
|
|
818
|
+
// terminal instead of an any-to-any escape hatch.
|
|
819
|
+
if (isUngatedForwardEdge(fromPhase, toPhase)) {
|
|
820
|
+
return { allowed: true, fromPhase, toPhase, missingGates, staleEvidence, errors, revalidated: [] };
|
|
821
|
+
}
|
|
822
|
+
errors.push(`illegal transition "${fromPhase}" → "${toPhase}" (not a gated transition, and not a declared forward edge)`);
|
|
823
|
+
return { allowed: false, fromPhase, toPhase, missingGates, staleEvidence, errors, revalidated: [] };
|
|
775
824
|
}
|
|
776
825
|
if (spec.from !== fromPhase) {
|
|
777
826
|
errors.push(`illegal transition "${fromPhase}" → "${toPhase}" (expected from "${spec.from}")`);
|