cadet-agent 0.56.0 → 0.60.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/README.md +48 -14
- package/package.json +1 -1
- package/src/cli.mjs +268 -21
- package/src/harness/commands.mjs +75 -0
- package/src/harness/context-protocol.mjs +3 -3
- package/src/harness/gates.mjs +28 -1
- package/src/harness/index.mjs +13 -1
- package/src/harness/play-form.mjs +193 -0
- package/src/harness/play.mjs +271 -0
- package/src/harness/policy.mjs +61 -1
- package/src/harness/state.mjs +53 -2
package/src/harness/state.mjs
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
REACHABILITY_GATE, REACHABILITY_TRANSITION_FROM,
|
|
16
16
|
DESIGN_REVIEW_GATE, DESIGN_REVIEW_TRANSITION_FROM,
|
|
17
17
|
HUMAN_ACCEPTANCE_GATE, HUMAN_ACCEPTANCE_TRANSITION_FROM,
|
|
18
|
+
USER_PLAY_GATE, USER_PLAY_TRANSITION_FROM,
|
|
18
19
|
ARCHITECTURE_GATE, ARCHITECTURE_TRANSITION_FROM, ARCHITECTURE_TRANSITION_TO,
|
|
19
20
|
architectureFitnessActive,
|
|
20
21
|
} from './policy.mjs';
|
|
@@ -435,11 +436,50 @@ export function validateState(state, context = {}) {
|
|
|
435
436
|
const scope = Array.isArray(entry.scope) ? entry.scope : (entry.scope ? [entry.scope] : []);
|
|
436
437
|
for (const s of scope) excepted.add(String(s));
|
|
437
438
|
}
|
|
439
|
+
// The same ambiguity, seen from the other side: a story the session moved on
|
|
440
|
+
// FROM, with evidence behind it, that still reads `planned` — which makes a
|
|
441
|
+
// finished story indistinguishable from one that was never started.
|
|
442
|
+
//
|
|
443
|
+
// The data already tells the two apart. `storyCompletions` records the
|
|
444
|
+
// boundary and how much the item left behind, and its comment says why the
|
|
445
|
+
// count is there: so "a completion with nothing behind it is visible rather
|
|
446
|
+
// than implied". A row with nothing behind it is a hollow completion — an
|
|
447
|
+
// item begun and then abandoned — and is left alone here. A row with records
|
|
448
|
+
// in it is a story that was carried to a verdict, and `planned` is then a
|
|
449
|
+
// false statement about it.
|
|
450
|
+
//
|
|
451
|
+
// It is an ERROR, not a warning, for the reason the rule below gives, and its
|
|
452
|
+
// remedy has the same shape: say what happened. A story that was superseded
|
|
453
|
+
// rather than delivered is not `planned` either.
|
|
454
|
+
//
|
|
455
|
+
// `in-progress` is deliberately NOT checked: `state begin` on an item that
|
|
456
|
+
// still carries its old completion row leaves it active, so active-plus-row
|
|
457
|
+
// is a legitimate combination and refusing it would be a false accusation.
|
|
458
|
+
const completedWith = new Map();
|
|
459
|
+
if (Array.isArray(state.storyCompletions)) {
|
|
460
|
+
for (const row of state.storyCompletions) {
|
|
461
|
+
if (!isPlainObject(row)) continue;
|
|
462
|
+
if (typeof row.workItemId !== 'string' || row.workItemId.length === 0) continue;
|
|
463
|
+
const records = Number(row.evidenceRecords);
|
|
464
|
+
if (!(records > 0)) continue;
|
|
465
|
+
completedWith.set(row.workItemId, records);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
438
468
|
for (const [epicId, epic] of Object.entries(state.epics)) {
|
|
439
469
|
if (!isPlainObject(epic) || !isPlainObject(epic.stories)) continue;
|
|
440
470
|
for (const [storyId, status] of Object.entries(epic.stories)) {
|
|
441
|
-
if (status !== 'done') continue;
|
|
442
471
|
const workItemId = `${epicId}::${storyId}`;
|
|
472
|
+
if (status === 'planned' && completedWith.has(workItemId)) {
|
|
473
|
+
errors.push({
|
|
474
|
+
path: `epics.${epicId}.stories.${storyId}`,
|
|
475
|
+
message: `story "${storyId}" reads "planned" but the completion row for its work item `
|
|
476
|
+
+ `"${workItemId}" records ${completedWith.get(workItemId)} evidence record(s). A story the `
|
|
477
|
+
+ 'session moved on from, with evidence behind it, is not planned: it is done, or it is '
|
|
478
|
+
+ 'superseded. Leaving it planned makes a finished story indistinguishable from one that '
|
|
479
|
+
+ 'was never started.',
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
if (status !== 'done') continue;
|
|
443
483
|
if (evidenced.has(workItemId)) continue;
|
|
444
484
|
if (excepted.has(workItemId)) continue;
|
|
445
485
|
errors.push({
|
|
@@ -1336,7 +1376,7 @@ export function conditionalEdgeGates(fromPhase, toPhase, policy = null) {
|
|
|
1336
1376
|
return policy?.designReview?.enabled === true ? [DESIGN_REVIEW_GATE] : [];
|
|
1337
1377
|
}
|
|
1338
1378
|
|
|
1339
|
-
export function requiredGates(toPhase, { reachability = false, humanAcceptance = false, architectureFitness = false } = {}) {
|
|
1379
|
+
export function requiredGates(toPhase, { reachability = false, humanAcceptance = false, architectureFitness = false, userPlay = false } = {}) {
|
|
1340
1380
|
for (const [from, spec] of Object.entries(TRANSITIONS)) {
|
|
1341
1381
|
if (spec.to === toPhase) {
|
|
1342
1382
|
const gates = [...spec.gates];
|
|
@@ -1359,6 +1399,16 @@ export function requiredGates(toPhase, { reachability = false, humanAcceptance =
|
|
|
1359
1399
|
// revalidated gate is judged without its phase stamp — which for this one would
|
|
1360
1400
|
// mean accepting a record written in any phase at all.
|
|
1361
1401
|
if (humanAcceptance && from === HUMAN_ACCEPTANCE_TRANSITION_FROM) gates.push(HUMAN_ACCEPTANCE_GATE);
|
|
1402
|
+
// User play joins the STORY boundary (`review -> validation`) and nothing else, and
|
|
1403
|
+
// only when the repository has asked for it. Placement: a playthrough is a claim
|
|
1404
|
+
// about a delivered, reviewed story, and this is the edge a story crosses before the
|
|
1405
|
+
// next one starts — `validation -> implementation` is the next-story loop and stays
|
|
1406
|
+
// unblocked, which is why the requirement cannot live there. It is deliberately NOT
|
|
1407
|
+
// in `revalidate`: at `validation -> closed` the epic's own
|
|
1408
|
+
// `humanAcceptanceConfirmed` covers the whole epic, and demanding every story's play
|
|
1409
|
+
// again at closure would multiply the person's work at the exact moment they are
|
|
1410
|
+
// being asked to accept the finished thing.
|
|
1411
|
+
if (userPlay && from === USER_PLAY_TRANSITION_FROM) gates.push(USER_PLAY_GATE);
|
|
1362
1412
|
// Architecture fitness joins `implementation -> review`, and only when the project
|
|
1363
1413
|
// has declared checks. It is appended at evaluation time rather than written into
|
|
1364
1414
|
// the transition table, so a project that declares nothing keeps the frozen lists
|
|
@@ -1579,6 +1629,7 @@ export function evaluateTransition(state, toPhase, context = {}) {
|
|
|
1579
1629
|
reachability: context.policy?.reachability?.enabled === true,
|
|
1580
1630
|
humanAcceptance: context.policy?.humanAcceptance?.enabled === true,
|
|
1581
1631
|
architectureFitness: architectureFitnessActive(context.policy),
|
|
1632
|
+
userPlay: context.policy?.userPlay?.enabled === true,
|
|
1582
1633
|
});
|
|
1583
1634
|
// An edge with no `TRANSITIONS` entry can still carry a gate. It is resolved HERE,
|
|
1584
1635
|
// before the ungated-edge shortcut below, because that shortcut is what would
|