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.
@@ -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