@sabaiway/agent-workflow-kit 5.2.0 → 5.3.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.
@@ -281,7 +281,33 @@ export const evaluateVetoOverride = ({ records, vetoReceipt, tree }) => {
281
281
  // and their records remain forgeable (each store's own header says so) — a backdated or
282
282
  // backwards-clock instant can move a red across this boundary; the cross-store arming-fence
283
283
  // hardening is queued (FLOW-ARMING-FENCE-CROSS-STORE), never pretended here.
284
- export const collectUnansweredRedRefusals = ({ flowRecords, coreRecords, currentBase, owner }) => {
284
+ //
285
+ // The lane split (FLOW-FINAL-RED-DEADLOCK) — the same reasoning the D10 arm already applies one arm
286
+ // away (see the `consumer` paragraph in computeFlowDecision's own header; a line reference there
287
+ // would rot on the next insertion). The strict rule demands a receipt the gate's OWN run has not
288
+ // written yet: run-gates appends the final receipt only AFTER every gate has run, so an in-matrix
289
+ // flow-check can never see the completed retry that would answer the newest red, and each --final
290
+ // mints red N+1 — no number of rerun-causes converges. On the 'gate' lane a current-base red is
291
+ // therefore ALSO answered by a provable IN-PROGRESS retry: an authoritative rerun-cause naming its
292
+ // attempt AND binding the CURRENT fingerprint, plus a final-start at that fingerprint ordered
293
+ // STRICTLY AFTER that red whose attempt carries no completed final — the shape run-gates creates
294
+ // before any gate runs (run-gates.mjs:636-658), so inside a real final run the conjunction holds by
295
+ // construction while a standalone check on a quiet tree still refuses. The relaxation carries the
296
+ // SAME base correlation the strict rule demands of the retry tree: a fingerprint is not unique to a
297
+ // base (a clean tree hashes identically under every HEAD), so without it a cause minted at another
298
+ // base would answer the gate and leave a green final the commit boundary is guaranteed to reject —
299
+ // the relaxation must only ever admit a state a completed retry could actually clear. It is opt-in
300
+ // by EXACT match: any other consumer — unknown, misspelled, absent — evaluates the strict rule, and
301
+ // a tree whose fingerprint is unresolvable or ambiguously correlated never relaxes. The
302
+ // 'commit-guard' lane is unchanged; the commit boundary still demands a real completed retry.
303
+ // Stated residual: an INTERRUPTED final run leaves exactly that record shape with no live run
304
+ // behind it, so a standalone check reads PASS in that window. It authorizes nothing — commit-guard
305
+ // refuses the dangling start independently (commit-guard.mjs:213-223) and consults the STRICT lane
306
+ // (commit-guard.mjs:253) — and the window closes on the next completed final run. The real fix is a
307
+ // runner-attested capability (a one-time unpublished nonce over stdin or an inherited FD, verified
308
+ // against a one-way commitment recorded in the final-start); it needs its own IPC contract and is
309
+ // QUEUED, never pretended here.
310
+ export const collectUnansweredRedRefusals = ({ flowRecords, coreRecords, currentBase, owner, consumer = 'commit-guard', currentFingerprint = null }) => {
285
311
  if (!hasOwnAdoption(flowRecords, owner)) return [];
286
312
  const adoptionInstants = flowRecords
287
313
  .filter((r) => r.kind === CHAIN_KIND && r.purpose === 'adoption' && r.owner === owner)
@@ -302,6 +328,13 @@ export const collectUnansweredRedRefusals = ({ flowRecords, coreRecords, current
302
328
  && basesAt(g.fingerprintBefore).length === 1 && basesAt(g.fingerprintBefore)[0] === currentBase
303
329
  && firstFinalByAttempt.get(g.attempt) === gi
304
330
  && c.fingerprint === g.fingerprintBefore));
331
+ const completedAttempts = new Set(finals.map(({ r }) => r.attempt));
332
+ const relaxes = consumer === 'gate' && typeof currentFingerprint === 'string' && currentFingerprint !== ''
333
+ && basesAt(currentFingerprint).length === 1 && basesAt(currentFingerprint)[0] === currentBase;
334
+ const inProgressRetryFor = (red, redAt) => rerunCauses.some((c) => c.attempt === red.attempt
335
+ && c.fingerprint === currentFingerprint
336
+ && coreRecords.some((s, si) => si > redAt && s.kind === 'final-start'
337
+ && s.fingerprint === currentFingerprint && !completedAttempts.has(s.attempt)));
305
338
  for (const { r, i } of finals) {
306
339
  if (r.status !== 'red') continue;
307
340
  if (armingInstant !== null && isCanonicalInstant(r.timestamp) && Date.parse(r.timestamp) < armingInstant) continue;
@@ -315,7 +348,7 @@ export const collectUnansweredRedRefusals = ({ flowRecords, coreRecords, current
315
348
  continue;
316
349
  }
317
350
  if (bases[0] !== currentBase) continue;
318
- if (!answeredBy(r, i)) {
351
+ if (!answeredBy(r, i) && !(relaxes && inProgressRetryFor(r, i))) {
319
352
  refusals.push(`a red final (attempt "${r.attempt}") on the CURRENT base (${bases[0] == null ? 'null' : short(bases[0])}) has no later completed retry cleared by a rerun-cause — an unanswered red never passes an armed flow (#65). recovery (edit the quoted cause, then paste; mint on the RETRY tree): ${writerCommand(`rerun-cause --attempt=${shellQuote(r.attempt)} --cause='<the stated cause>'`)}`);
320
353
  }
321
354
  }
@@ -568,13 +601,14 @@ export const computeAllPathWorktreeSurface = (cwd) => {
568
601
  return { ok: true, paths: [...new Set([...tracked, ...untracked])] };
569
602
  };
570
603
 
571
- // decideFlowCheck({ flowRead, coreRead, owner, motion?, evidence? }) → { refusals, advisories }.
572
- // Pure — consumes the FULL read-results of both stores; store health fails closed BEFORE any
573
- // content judgment. `motion` ({ currentBase, resolveBaseDelta, resolveChangedSurface }) arms the
574
- // Step-1.4 base-motion refusals; `evidence` ({ receipts, tree, backends }) arms the three Phase-1
575
- // rungs (#65/#25/#42 — each self-gates on an OWN adoption). Absent inputs keep the decision
576
- // byte-identical to the Plan-2 checker.
577
- export const decideFlowCheck = ({ flowRead, coreRead, owner, flowPath = 'the flow store', corePath = 'the core evidence store', motion = null, evidence = null }) => {
604
+ // decideFlowCheck({ flowRead, coreRead, owner, motion?, evidence?, consumer? }) → { refusals,
605
+ // advisories }. Pure — consumes the FULL read-results of both stores; store health fails closed
606
+ // BEFORE any content judgment. `motion` ({ currentBase, resolveBaseDelta, resolveChangedSurface })
607
+ // arms the Step-1.4 base-motion refusals; `evidence` ({ receipts, tree, backends }) arms the three
608
+ // Phase-1 rungs (#65/#25/#42 — each self-gates on an OWN adoption). Absent inputs keep the decision
609
+ // byte-identical to the Plan-2 checker. `consumer` rides through to the #65 lane split and defaults
610
+ // to the STRICT lane, so a caller that forgets to thread it inherits strictness.
611
+ export const decideFlowCheck = ({ flowRead, coreRead, owner, flowPath = 'the flow store', corePath = 'the core evidence store', motion = null, evidence = null, consumer = 'commit-guard' }) => {
578
612
  const refusals = [];
579
613
  const advisories = [];
580
614
  if (flowRead.readError) refusals.push(`the flow store is unreadable (${flowRead.readError}) — the checker consumes the FULL read-result; inspect ${flowPath} (fail closed)`);
@@ -594,7 +628,7 @@ export const decideFlowCheck = ({ flowRead, coreRead, owner, flowPath = 'the flo
594
628
  refusals.push(...deltaRefusals(records, owner));
595
629
  refusals.push(...degradeOrderingRefusals(coreRead.records));
596
630
  if (evidence != null) {
597
- refusals.push(...collectUnansweredRedRefusals({ flowRecords: records, coreRecords: coreRead.records, currentBase: evidence.tree.base, owner }));
631
+ refusals.push(...collectUnansweredRedRefusals({ flowRecords: records, coreRecords: coreRead.records, currentBase: evidence.tree.base, owner, consumer, currentFingerprint: evidence.tree.fingerprint }));
598
632
  refusals.push(...collectDegradeCoverageRefusals({ flowRecords: records, coreRecords: coreRead.records, tree: evidence.tree, owner, backends: evidence.degradeBackends }));
599
633
  refusals.push(...collectReceiptCoverageRefusals({ flowRecords: records, receipts: evidence.receipts, tree: evidence.tree, owner, backends: evidence.receiptBackends, declaredPaths: evidence.declaredPaths, refreshCap: evidence.refreshCap }));
600
634
  }
@@ -611,7 +645,9 @@ export const decideFlowCheck = ({ flowRead, coreRead, owner, flowPath = 'the flo
611
645
  // #42 never falls open). `consumer` (Plan 4 Decision 2): the D10 flow→final comparison runs ONLY
612
646
  // on the 'commit-guard' lane — the default 'gate' lane (the in-matrix flow-check --check gate)
613
647
  // stays inert on it, because during a final run the "latest final" is by construction the
614
- // PREVIOUS one and an in-matrix comparison would make a new green final unreachable.
648
+ // PREVIOUS one and an in-matrix comparison would make a new green final unreachable. The SAME
649
+ // distinction reaches the #65 unanswered-red rung (its own header states the split): the 'gate'
650
+ // lane also answers a red under a provable in-progress retry, for the same reason.
615
651
  export const computeFlowDecision = ({ cwd = process.cwd(), consumer = 'gate', probes = {} } = {}) => {
616
652
  const fingerprintProbe = probes.fingerprint ?? computeTreeFingerprint;
617
653
  const owner = deriveFlowOwner(cwd);
@@ -745,7 +781,7 @@ export const computeFlowDecision = ({ cwd = process.cwd(), consumer = 'gate', pr
745
781
  }
746
782
  }
747
783
  }
748
- const { refusals, advisories } = decideFlowCheck({ flowRead, coreRead, owner, flowPath, corePath, motion, evidence });
784
+ const { refusals, advisories } = decideFlowCheck({ flowRead, coreRead, owner, flowPath, corePath, motion, evidence, consumer });
749
785
  // Semantic refusals bind only an ARMED store; the D10 binding refusals ride the commit-guard
750
786
  // lane UNCONDITIONALLY — a deleted or truncated store must never un-arm the binding.
751
787
  const effectiveRefusals = healthBroken ? refusals : [...(armed ? [...refusals, ...evidenceRefusals] : []), ...bindingRefusals];