cadet-agent 0.32.1 → 0.33.1
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 +1 -1
- package/src/cli.mjs +19 -2
- package/src/harness/index.mjs +1 -1
- package/src/harness/state.mjs +58 -2
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
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
|
|
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,55 @@ 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
|
+
// 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'],
|
|
730
|
+
]);
|
|
731
|
+
|
|
732
|
+
/** Is `from → to` one of the declared ungated forward edges? */
|
|
733
|
+
export function isUngatedForwardEdge(fromPhase, toPhase) {
|
|
734
|
+
return UNGATED_FORWARD_EDGES.some(([from, to]) => from === fromPhase && to === toPhase);
|
|
735
|
+
}
|
|
736
|
+
|
|
688
737
|
/**
|
|
689
738
|
* Check one gate for a transition. Shared by the primary `gates` set and the
|
|
690
739
|
* strict-closure `revalidate` set so the two can never drift apart.
|
|
@@ -770,8 +819,15 @@ export function evaluateTransition(state, toPhase, context = {}) {
|
|
|
770
819
|
|
|
771
820
|
const spec = requiredGates(toPhase);
|
|
772
821
|
if (!spec) {
|
|
773
|
-
// Ungated transitions
|
|
774
|
-
|
|
822
|
+
// Ungated transitions are legal ONLY along the declared forward edges
|
|
823
|
+
// (bootstrap + planning progression). A target that is neither gated nor a
|
|
824
|
+
// declared forward edge is rejected — most importantly, this makes `closed`
|
|
825
|
+
// terminal instead of an any-to-any escape hatch.
|
|
826
|
+
if (isUngatedForwardEdge(fromPhase, toPhase)) {
|
|
827
|
+
return { allowed: true, fromPhase, toPhase, missingGates, staleEvidence, errors, revalidated: [] };
|
|
828
|
+
}
|
|
829
|
+
errors.push(`illegal transition "${fromPhase}" → "${toPhase}" (not a gated transition, and not a declared forward edge)`);
|
|
830
|
+
return { allowed: false, fromPhase, toPhase, missingGates, staleEvidence, errors, revalidated: [] };
|
|
775
831
|
}
|
|
776
832
|
if (spec.from !== fromPhase) {
|
|
777
833
|
errors.push(`illegal transition "${fromPhase}" → "${toPhase}" (expected from "${spec.from}")`);
|