@sabaiway/agent-workflow-kit 10.5.0 → 10.6.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.
@@ -9,6 +9,11 @@
9
9
  // `--check` turns the report into a gate exit code (declare it in docs/ai/gates.json).
10
10
  //
11
11
  // Normative `--check` exit contract (the single home of this list — SKILL.md points here):
12
+ // Before the review-recipe exits, configured plan-execution.execute=delegated with a plan and a
13
+ // dirty, fingerprintable tree audits the delegation ledger. An absent ledger is inert; an
14
+ // unavailable ledger, a HEAD error or a SUBSTITUTED held session refuses unless a codex-exec degrade at the
15
+ // substituted return's tree, standing after every later fold, accepts it, or the fold of that thread's
16
+ // retry lifts it (codex never grants it).
12
17
  // exit 0 when the CONFIGURED plan-execution.review recipe is solo (or the computed default is —
13
18
  // absent config with no reviewer backend ready); when no plan is in flight (docs/plans/
14
19
  // holds no top-level .md that is not queue.md and not scratch by the naming convention:
@@ -105,6 +110,10 @@ import { selectReliedOnReceipt, evaluateVetoOverride, evaluateInternalAttestatio
105
110
  // graph; re-exported here so every historical consumer keeps its import site.
106
111
  import { PLANS_REL, isScratchPlanName, plansInFlight } from './plan-files.mjs';
107
112
  import { shellQuoteArg } from './repo-lex.mjs';
113
+ import { readDelegationLedger } from './dispatch-store-read.mjs';
114
+ import {
115
+ decideHeldSession, HELD_EXECUTE_WRAPPER, HELD_RECEIPT_BACKEND, judgeLedger,
116
+ } from './held-session.mjs';
108
117
 
109
118
  export { PLANS_REL, isScratchPlanName, plansInFlight, shellQuoteArg };
110
119
  // The canonical review-domain primitives live in the core-evidence DAG bottom (ONE home for the
@@ -138,6 +147,7 @@ export {
138
147
 
139
148
  const ACTIVITY = 'plan-execution';
140
149
  const SLOT = 'review';
150
+ const CORE_EVIDENCE_TOOL = shellQuoteArg(join(dirname(fileURLToPath(import.meta.url)), 'core-evidence.mjs'));
141
151
  const GIT_MAX_BUFFER = 256 * 1024 * 1024; // a full-tree diff can be large; never truncate silently
142
152
  // --await (BUGFREE-3 / AD-049, item (d)) bounds + poll cadence. The default timeout is generous —
143
153
  // a real grounded bridge review can take minutes — and every value is overridable (--timeout / the
@@ -296,10 +306,35 @@ export const degradeRecordSet = ({ cwd, env = process.env, fingerprint }) => {
296
306
  const storePath = resolveEvidencePath(cwd, env);
297
307
  const read = storePath ? readEvidence(storePath) : { records: [], malformed: 0, malformedReasons: [] };
298
308
  const unavailable = (read.malformed ?? 0) > 0 || read.readError != null;
309
+ const records = unavailable ? [] : authoritativeOfKind(read.records, 'degrade');
299
310
  const set = unavailable || fingerprint == null
300
311
  ? new Set()
301
- : new Set(authoritativeOfKind(read.records, 'degrade').filter((r) => r.fingerprint === fingerprint).map((r) => r.backend));
302
- return { set, storePath, malformed: read.malformed ?? 0, readError: read.readError ?? null, unavailable };
312
+ : new Set(records.filter((r) => r.fingerprint === fingerprint).map((r) => r.backend));
313
+ return { set, records, storePath, malformed: read.malformed ?? 0, readError: read.readError ?? null, unavailable };
314
+ };
315
+
316
+ export const selectHeldSessionDegrades = (records) =>
317
+ records.filter((record) => record.kind === 'degrade' && record.backend === HELD_EXECUTE_WRAPPER);
318
+
319
+ export const shouldReadHeldSession = ({ configuredExecute, plans, fingerprint, clean }) =>
320
+ configuredExecute === 'delegated' && plans.length > 0 && fingerprint !== null && clean === false;
321
+
322
+ export const buildHeldSessionState = ({
323
+ cwd,
324
+ env,
325
+ configuredExecute,
326
+ plans,
327
+ fingerprint,
328
+ clean,
329
+ degrades,
330
+ resolveStore,
331
+ readStore,
332
+ audit,
333
+ readHead,
334
+ }) => {
335
+ if (!shouldReadHeldSession({ configuredExecute, plans, fingerprint, clean })) return null;
336
+ const ledger = readDelegationLedger(cwd, env, { resolveStore, readStore, audit, readHead });
337
+ return judgeLedger(ledger, { backend: HELD_RECEIPT_BACKEND, degrades });
303
338
  };
304
339
 
305
340
  // ── the check + report core ─────────────────────────────────────────────────────────
@@ -340,6 +375,15 @@ export const buildState = ({ cwd, env = process.env, detect = detectBackends, su
340
375
  const base = resolveBase(cwd);
341
376
  const degrade = degradeRecordSet({ cwd, env, fingerprint });
342
377
  const degradedExempt = requiredBackends.filter((b) => degrade.set.has(b));
378
+ const heldSession = buildHeldSessionState({
379
+ cwd,
380
+ env,
381
+ configuredExecute: config?.[ACTIVITY]?.execute,
382
+ plans,
383
+ fingerprint,
384
+ clean,
385
+ degrades: selectHeldSessionDegrades(degrade.records),
386
+ });
343
387
  // ── the Phase-2 flow arms' state (two-tier activation, P3) ──
344
388
  // Tier 1 is store-file PRESENCE: no file ⇒ every field below stays inert and the decision is
345
389
  // byte-identical to the pre-flow checker (the unarmed fast path also reads NO plan file).
@@ -437,6 +481,7 @@ export const buildState = ({ cwd, env = process.env, detect = detectBackends, su
437
481
  evidenceReadError: degrade.readError,
438
482
  evidenceUnavailable: degrade.unavailable,
439
483
  degradedExempt,
484
+ heldSession,
440
485
  maskedUntracked: countNeverCommittableUntracked(cwd, { lstat }),
441
486
  detectionWarning,
442
487
  flowPresent,
@@ -476,7 +521,27 @@ const backendFailurePart = (b, state) => {
476
521
  if (b.state === 'probe') return `${b.backend}: only probe receipts for the current tree (CODEX_PROBE=1 / AGY_PROBE=1 relaxes the quality guards) — a probe review never attests; re-run a real one`;
477
522
  if (b.state === 'rejected') return `${b.backend}: current-tree receipts rejected — ${rejectionCause(b)} (fail-closed); inspect ${state.receiptsPath}`;
478
523
  if (b.state === 'stale') return `${b.backend}: receipts exist but none matches the current tree (edited after review) — run a fresh review`;
479
- return `${b.backend}: no receipt — run its review wrapper, or record an explicit degrade (node ${shellQuoteArg(join(dirname(fileURLToPath(import.meta.url)), 'core-evidence.mjs'))} degrade --backend ${b.backend} --reason "...")`;
524
+ return `${b.backend}: no receipt — run its review wrapper, or record an explicit degrade (node ${CORE_EVIDENCE_TOOL} degrade --backend ${b.backend} --reason "...")`;
525
+ };
526
+
527
+ const describesHeldRecovery = (state) => {
528
+ const substitution = state.heldSession.substitution;
529
+ if (substitution === null) return '';
530
+ const canRetry = substitution.folded === false;
531
+ if (state.evidenceUnavailable === true) {
532
+ const retry = canRetry ? '; or fold the retry of that thread' : '';
533
+ return ` — the evidence store is unavailable, so the degrade lane cannot be judged: repair it (inspect ${state.evidenceStorePath}) and re-check${retry}`;
534
+ }
535
+ const canDegrade = typeof substitution.postTreeDigest === 'string'
536
+ && state.fingerprint === substitution.postTreeDigest;
537
+ if (!canDegrade && !canRetry) {
538
+ return ' — no recovery lane remains: the tree moved after the substituted return and that thread is folded; re-dispatch the work under the held session';
539
+ }
540
+ const degrade = canDegrade
541
+ ? ` — mint the accepted-replacement record now, before the next edit: node ${CORE_EVIDENCE_TOOL} degrade --backend ${HELD_EXECUTE_WRAPPER} --reason "..."`
542
+ : '';
543
+ const retry = canRetry ? `${canDegrade ? '; or' : ' —'} fold the retry of that thread` : '';
544
+ return `${degrade}${retry}`;
480
545
  };
481
546
 
482
547
  // The normative --check decision (the header contract, in order). → { code, reason }.
@@ -493,6 +558,11 @@ export const decideCheck = (state) => {
493
558
  ? ` — evidence store unavailable (${state.evidenceMalformed} malformed line(s)${state.evidenceReadError ? `, read error: ${state.evidenceReadError}` : ''}); the degrade escape is denied (fail-closed) — inspect ${state.evidenceStorePath}`
494
559
  : '';
495
560
  const earlyNotes = `${malformedNote}${evidenceNote}${state.receiptsReadError ? ` — receipts store unreadable (${state.receiptsReadError}); inspect ${state.receiptsPath}` : ''}`;
561
+ if (state.heldSession !== null && state.heldSession !== undefined) {
562
+ const held = decideHeldSession(state.heldSession);
563
+ const heldRecovery = describesHeldRecovery(state);
564
+ if (held.code !== 0) return { ...held, reason: `${held.reason}${heldRecovery}${earlyNotes}`, terminal: true, heldSession: true };
565
+ }
496
566
  // Detector failure with NO configured recipe: the computed default is unknowable → fail closed.
497
567
  // An EXPLICIT configured recipe needs no detector — its obligations are readiness-independent.
498
568
  if (state.obligations.unknowable) {
@@ -649,6 +719,7 @@ const formatHuman = (state, check) => {
649
719
  if (state.fingerprint == null) lines.push(' tree: not a git work tree');
650
720
  else if (state.clean === true) lines.push(' tree: clean (nothing to review)');
651
721
  else lines.push(` tree fingerprint: ${state.fingerprint}`);
722
+ if (state.heldSession != null) lines.push(` ${decideHeldSession(state.heldSession).line}`);
652
723
  lines.push(` receipts: ${state.receiptsPath ?? '(unresolvable — no git dir)'} (${state.receiptCount} line(s)${state.malformed ? `, ${state.malformed} malformed — inspect the file` : ''})`);
653
724
  if (state.receiptsReadError) lines.push(` ⚠ receipts store unreadable (${state.receiptsReadError}) — inspect ${state.receiptsPath}`);
654
725
  if (state.evidenceUnavailable) lines.push(` ⚠ evidence store unavailable (${state.evidenceMalformed} malformed line(s)${state.evidenceReadError ? `, read error: ${state.evidenceReadError}` : ''}) — the degrade escape is denied (fail-closed)`);
@@ -700,7 +771,12 @@ attests; a malformed OR absent probe marker is rejected fail-closed — silence
700
771
  The ONLY escape for an unavailable backend under council is an explicit current-tree degrade
701
772
  record (node core-evidence.mjs degrade) — and never all backends.
702
773
 
703
- --check exits 0/1 per the normative contract in the tool header: 0 for solo / no plan in flight /
774
+ --check exits 0/1 per the normative contract in the tool header. FIRST, before any of the arms
775
+ below, a configured plan-execution.execute = delegated with a plan in flight and a dirty,
776
+ fingerprintable tree audits the delegation ledger: an absent ledger is inert; an unreadable,
777
+ malformed, foreign or audit-refused ledger, a failed HEAD read, or a SUBSTITUTED held session
778
+ exits 1 (a substitution is lifted only by a codex-exec degrade at the substituted return's own
779
+ tree, or the fold of that thread's retry). THEN: 0 for solo / no plan in flight /
704
780
  a clean tree (under a non-solo review obligation the PASS names every plan in flight and its arm) /
705
781
  not-a-git-tree / obligations satisfied (reviewed: >=1 ship-class attestation;
706
782
  council: every backend ship-class or degrade-recorded, >=1 real ship); 1 on a veto, an
@@ -728,11 +804,12 @@ export const main = (argv, ctx = {}) => {
728
804
  const cwd = ctx.cwd ?? process.cwd();
729
805
  const env = ctx.env ?? process.env;
730
806
  const detect = ctx.detect ?? detectBackends;
807
+ const build = ctx.buildState ?? buildState;
731
808
  try {
732
809
  if (argv.includes('--help') || argv.includes('-h')) return { code: 0, stdout: HELP, stderr: '' };
733
810
  const unknown = argv.find((a) => !KNOWN_ARGS.has(a));
734
811
  if (unknown !== undefined) throw fail(2, `unknown argument: ${unknown}`);
735
- const state = buildState({ cwd, env, detect, surveyVehicle: ctx.surveyVehicle, lstat: ctx.lstat, readFile: ctx.readFile });
812
+ const state = build({ cwd, env, detect, surveyVehicle: ctx.surveyVehicle, lstat: ctx.lstat, readFile: ctx.readFile });
736
813
  const check = decideCheck(state);
737
814
  // The mask advisory is NON-FAILING by contract: one notice line, never an exit-code arm.
738
815
  const advisory = maskAdvisoryLine(state);
@@ -779,6 +856,7 @@ export const mainAwait = async (argv, ctx = {}) => {
779
856
  const now = ctx.now ?? (() => Date.now());
780
857
  const sleep = ctx.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
781
858
  const pollMs = ctx.pollMs ?? AWAIT_POLL_MS;
859
+ const build = ctx.buildState ?? buildState;
782
860
  try {
783
861
  for (let i = 0; i < argv.length; i += 1) {
784
862
  const a = argv[i];
@@ -798,9 +876,10 @@ export const mainAwait = async (argv, ctx = {}) => {
798
876
  for (;;) {
799
877
  const elapsed = now() - start;
800
878
  if (elapsed >= timeoutMs) return { code: 1, stdout: '', stderr: `review-state --await: TIMEOUT after ${timeoutS}s — ${lastReason}` };
801
- const check = decideCheck(buildState({ cwd, env, detect }));
879
+ const check = decideCheck(build({ cwd, env, detect }));
802
880
  lastReason = check.reason;
803
881
  if (check.code === 0) return { code: 0, stdout: `review-state --await: READY — ${check.reason}`, stderr: '' };
882
+ if (check.heldSession === true) return { code: 1, stdout: '', stderr: `review-state --await: HELD SESSION — ${check.reason}` };
804
883
  // Decision 4 (#50): an AUTHORITATIVE veto ends the wait loudly BEFORE the deadline — the
805
884
  // dispatched review answered with a landed negative; classifying that as a timeout was the
806
885
  // misclassification this amendment closes. Only a fresh review can move it, never waiting.