@sabaiway/agent-workflow-kit 14.4.0 → 14.4.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.
@@ -94,34 +94,22 @@
94
94
  // (read-only queries) to compute the fingerprint — stated honestly in the catalog. Dependency-free,
95
95
  // Node >= 22. No side effects on import (the isDirectRun idiom).
96
96
 
97
- import { lstatSync, readFileSync } from 'node:fs';
98
- import { join, dirname } from 'node:path';
99
- import { fileURLToPath } from 'node:url';
100
- import { spawnSync } from 'node:child_process';
101
- import { createHash } from 'node:crypto';
102
97
  import { detectBackends } from './detect-backends.mjs';
103
98
  import { isDirectRun } from './direct-run.mjs';
104
- import { resolveActivityRecipe, DISPLAY_ALIASES, requiredBackendsForConfiguredRecipe, composeReadiness } from './recipes.mjs';
105
- import { CONFIG_REL, fail, loadConfig } from './orchestration-config.mjs';
106
- import { resolveFlowStorePath, readFlowStore, deriveFlowOwner, readPlanFrontmatterId } from './flow-store.mjs';
107
- import { CHAIN_KIND, authoritativeFlowRecords } from './flow-record.mjs';
108
- import { selectReliedOnReceipt, evaluateVetoOverride, evaluateInternalAttestationLenses } from './flow-check.mjs';
99
+ import { CONFIG_REL, fail } from './orchestration-config.mjs';
100
+ import { RECEIPTS_BASENAME } from './core-evidence.mjs';
101
+ import { ACTIVITY, SLOT, decideCheck } from './review-state-judge.mjs';
102
+ import { buildState } from './review-state-build.mjs';
103
+ import { formatHuman, maskAdvisoryLine } from './review-state-render.mjs';
104
+ import { mainAwait } from './review-state-await.mjs';
105
+
109
106
  // Pure leaves (FLOW-READ-GRAPH-PURITY): the plan-file convention and the lexical shell quote live
110
107
  // in leaf modules so the procedures read surface reaches them without this module's write-API
111
108
  // graph; re-exported here so every historical consumer keeps its import site.
112
- import { PLANS_REL, isScratchPlanName, plansInFlight } from './plan-files.mjs';
113
- import { escapeForDisplay, shellQuoteArg } from './repo-lex.mjs';
114
- import { GIT_MAX_BUFFER, resolveGitLocation, stripGitLocationEnv, withGitPath } from './git-env.mjs';
115
- import { readDelegationLedger } from './dispatch-store-read.mjs';
116
- import {
117
- decideHeldSession, HELD_EXECUTE_WRAPPER, HELD_RECEIPT_BACKEND, judgeLedger,
118
- } from './held-session.mjs';
119
-
120
- export { PLANS_REL, isScratchPlanName, plansInFlight, shellQuoteArg };
121
- // The canonical review-domain primitives live in the core-evidence DAG bottom (ONE home for the
122
- // fingerprint, the receipt read path, and the attesting predicate); this module RE-EXPORTS its
123
- // historical public API from there and consumes the degrade records the same store owns.
124
- import {
109
+ export { PLANS_REL, isScratchPlanName, plansInFlight } from './plan-files.mjs';
110
+ export { shellQuoteArg } from './repo-lex.mjs';
111
+ export { requiredBackendsForConfiguredRecipe } from './recipes.mjs';
112
+ export {
125
113
  RECEIPTS_BASENAME,
126
114
  computeFingerprintPayload,
127
115
  computeTreeFingerprint,
@@ -129,644 +117,21 @@ import {
129
117
  isNeverCommittableStat,
130
118
  resolveReceiptsPath,
131
119
  readReceipts,
132
- summarizeReviewReceiptsForTree,
133
- resolveBase,
134
- isShipVerdict,
135
- resolveEvidencePath,
136
- readEvidence,
137
- authoritativeOfKind,
138
120
  } from './core-evidence.mjs';
139
-
140
121
  export {
141
- RECEIPTS_BASENAME,
142
- computeFingerprintPayload,
143
- computeTreeFingerprint,
144
- isTreeClean,
145
- isNeverCommittableStat,
146
- resolveReceiptsPath,
147
- readReceipts,
148
- };
149
-
150
- const ACTIVITY = 'plan-execution';
151
- const SLOT = 'review';
152
- const CORE_EVIDENCE_TOOL = shellQuoteArg(join(dirname(fileURLToPath(import.meta.url)), 'core-evidence.mjs'));
153
- const DISPATCH_TOOL = shellQuoteArg(join(dirname(fileURLToPath(import.meta.url)), 'dispatch.mjs'));
154
- // --await (BUGFREE-3 / AD-049, item (d)) bounds + poll cadence. The default timeout is generous —
155
- // a real grounded bridge review can take minutes — and every value is overridable (--timeout / the
156
- // injectable clock) so hermetic tests never spend wall-clock.
157
- export const DEFAULT_AWAIT_TIMEOUT_S = 900;
158
- export const AWAIT_POLL_MS = 5000;
159
-
160
- // ── git plumbing (read-only queries; injectable for tests) ─────────────────────────
161
-
162
- const gitRaw = (args, cwd, env) =>
163
- spawnSync('git', args, { cwd, env: withGitPath(env ?? process.env), maxBuffer: GIT_MAX_BUFFER, windowsHide: true });
164
-
165
- // stdout Buffer of a git query, or null when git fails (not a repo / git absent).
166
- const gitBuf = (args, cwd, env) => {
167
- const r = gitRaw(args, cwd, env);
168
- if (r.error || r.status !== 0) return null;
169
- return r.stdout;
170
- };
171
-
172
- const gitLine = (args, cwd, env) => {
173
- const buf = gitBuf(args, cwd, env);
174
- return buf == null ? null : buf.toString('utf8').replace(/\r?\n$/, '');
175
- };
176
-
177
- // The canonical fingerprint / clean check / never-committable filter live in core-evidence.mjs
178
- // (the ONE home, re-exported above); this module keeps only its own presentation plumbing.
179
-
180
- // ── the sandbox-masks advisory (D lane, AD-044 Plan 4 Phase 1.5) ────────────────────
181
-
182
- // Count the never-committable untracked paths the STANDARD walk still shows. The review domain
183
- // ignores them by construction; this count only feeds ONE non-failing advisory line naming the
184
- // cosmetic sandbox-masks apply — an applied managed block hides the paths from --exclude-standard,
185
- // so the advisory disappears exactly when the status noise does (no standing detector).
186
- export const countNeverCommittableUntracked = (cwd, { lstat = lstatSync, env } = {}) => {
187
- const top = gitLine(['rev-parse', '--show-toplevel'], cwd, env);
188
- if (top == null) return 0;
189
- const untrackedZ = gitBuf(['ls-files', '--others', '--exclude-standard', '-z'], top, env);
190
- if (untrackedZ == null) return 0;
191
- return untrackedZ
192
- .toString('utf8')
193
- .split('\0')
194
- .filter(Boolean)
195
- .filter((rel) => {
196
- try {
197
- return isNeverCommittableStat(lstat(join(top, rel)));
198
- } catch {
199
- return false;
200
- }
201
- }).length;
202
- };
203
-
204
- const maskAdvisoryLine = (state) =>
205
- state.maskedUntracked > 0
206
- ? `notice: ${state.maskedUntracked} never-committable untracked path(s) (device/FIFO/socket) are ignored by the review domain — hide them from git status: node ${shellQuoteArg(join(dirname(fileURLToPath(import.meta.url)), 'sandbox-masks.mjs'))} --cwd ${shellQuoteArg(state.root)} --apply`
207
- : '';
208
-
209
- // ── plan-in-flight detector (the AD-038 naming convention; documented in queue.md) ─────
210
-
211
- // The clean-tree PASS wording, split so the doc-parity registry can bind the forward-looking half
212
- // to the mode doc — the notice is the contract, not decoration.
213
- export const CLEAN_TREE_PASS = 'the working tree is clean — nothing to review';
214
- export const LATENT_ARM_NOTICE = 'this gate arms as soon as the tree is dirty';
215
-
216
- const UNSAFE_INLINE_SCALARS = /[\u007f-\u009f\u2028\u2029]/gu;
217
- // A plan FILENAME reaches a one-line gate report. JSON quoting handles the C0 control bytes; the C1
218
- // range (DEL + U+0080..U+009F, including the U+0085 line break) and the two Unicode line separators
219
- // survive JSON.stringify and can still break a line, so they are escaped explicitly — every escape a
220
- // valid JSON \uXXXX, so the token round-trips through JSON.parse. The plan set stays STRING-typed for
221
- // its consumers (procedures / worktrees / the JSON surface); a name carrying INVALID UTF-8 bytes is a
222
- // stated residual (queue: PARALLEL-TRACK-REDESIGN — two such names could collapse in the DISPLAY only,
223
- // never in the gate decision, which keys on the receipt fingerprint, not the name).
224
- export const quoteReportName = (name) =>
225
- JSON.stringify(name).replace(
226
- UNSAFE_INLINE_SCALARS,
227
- (value) => `\\u${value.codePointAt(0).toString(16).padStart(4, '0')}`,
228
- );
229
-
230
-
231
- // The in-flight-plan → adopted-chain coverage map (P13/P19): ONE bounded read + ONE hash per plan
232
- // file; coverage requires the frontmatter planId, the FULL content digest, and the owner to match
233
- // the adoption record — a mismatch on any axis refuses internal-only arming by name.
234
- export const computePlanAdoptionCoverage = ({ root, plans, records, owner, readFile = readFileSync }) =>
235
- plans.map((plan) => {
236
- let bytes;
237
- try {
238
- bytes = readFile(join(root, PLANS_REL, plan));
239
- } catch (err) {
240
- return { plan, planId: null, covered: false, reason: `the plan file is unreadable (${(err && err.code) || (err && err.message) || err})` };
241
- }
242
- const buf = typeof bytes === 'string' ? Buffer.from(bytes) : bytes;
243
- const planId = readPlanFrontmatterId(buf.toString('utf8'));
244
- if (planId == null) return { plan, planId: null, covered: false, reason: 'no frontmatter planId — plan filenames are never chain identity (#58); adopt the plan through the flow writer' };
245
- const adoptionRecord = records.find((r) => r.kind === CHAIN_KIND && r.purpose === 'adoption' && r.planId === planId);
246
- if (adoptionRecord === undefined) return { plan, planId, covered: false, reason: `no adoption record for planId "${planId}"` };
247
- if (adoptionRecord.planDigest !== createHash('sha256').update(buf).digest('hex')) {
248
- return { plan, planId, covered: false, reason: 'the plan content no longer matches its adoption record (edited after adoption)' };
249
- }
250
- if (adoptionRecord.owner !== owner) {
251
- return { plan, planId, covered: false, reason: `the adoption is owned by "${adoptionRecord.owner}", not this worktree ("${owner}") — a foreign-owner adoption never arms internal-only here` };
252
- }
253
- return { plan, planId, covered: true };
254
- });
255
-
256
- // ── receipts (path + reader re-exported from the core-evidence one-home above) ──────
257
-
258
- // Per-backend receipt status for the current fingerprint, over the ONE shared attesting-receipt
259
- // predicate — the LATEST NORMAL (probe-free, marker-valid, current-fingerprint) receipt is
260
- // selected FIRST and THEN judged, so a later unknown/ungrounded receipt never lets an earlier
261
- // SHIP survive:
262
- // current — the latest normal receipt ATTESTS (grounded + recognized verdict);
263
- // its verdict rides — ONLY ship-class satisfies, a recognized negative
264
- // is an authoritative veto;
265
- // ungrounded — the latest normal receipt carries grounded:false;
266
- // unrecognized-verdict — the latest normal receipt carries a verdict outside the closed
267
- // vocabulary (an unknown verdict never attests — fail closed);
268
- // probe — current receipts exist and EVERY one is a well-formed probe (D3);
269
- // rejected — current receipts exist, none normal, and >=1 marker malformed/absent;
270
- // stale — receipts exist, none for the current fingerprint (edited after review);
271
- // missing — no receipt from this backend at all.
272
- export const backendReceiptStatus = (receipts, backend, fingerprint) => {
273
- const own = receipts.filter((r) => r.backend === backend);
274
- const summary = summarizeReviewReceiptsForTree(own, fingerprint);
275
- const counts = {
276
- probeExcluded: summary.probeExcluded,
277
- markerRejected: summary.markerRejected,
278
- unmarkedRejected: summary.unmarkedRejected,
279
- postureRejected: summary.postureRejected,
280
- deliveryRejected: summary.deliveryRejected,
281
- };
282
- if (summary.state === 'current') {
283
- return { state: 'current', verdict: summary.receipt.verdict ?? 'unknown', shipClass: isShipVerdict(summary.receipt.verdict), grounded: true, timestamp: summary.receipt.timestamp ?? null, ...counts };
284
- }
285
- if (summary.state === 'ungrounded' || summary.state === 'unrecognized-verdict') {
286
- return { state: summary.state, verdict: summary.receipt.verdict ?? 'unknown', shipClass: false, grounded: summary.receipt.grounded === true, timestamp: summary.receipt.timestamp ?? null, ...counts };
287
- }
288
- if (summary.state === 'probe' || summary.state === 'rejected') {
289
- return { state: summary.state, verdict: null, shipClass: false, grounded: null, timestamp: null, ...counts };
290
- }
291
- return { state: own.length > 0 ? 'stale' : 'missing', verdict: null, shipClass: false, grounded: null, timestamp: null, ...counts };
292
- };
293
-
294
- // ── obligations from the CONFIGURED recipe + the D3(b) degrade-record escape ────────
295
-
296
- // Obligations derive from the CONFIGURED recipe — the RAW orchestration.json value — never the
297
- // readiness-degraded effective recipe (a computed readiness-degrade NEVER silently becomes solo).
298
- // The derivation is homed in recipes.mjs (cycle-free) so flow-check's exact-coverage rungs consume
299
- // the SAME consumed backend set; re-exported here for this module's historical public API.
300
- export { requiredBackendsForConfiguredRecipe };
301
-
302
- // degradeRecordSet — the D3(b) escape: an EXPLICIT per-backend, per-tree degrade RECORD in the
303
- // core-evidence store is the ONLY exemption lane. Fail-closed: an unreadable/malformed store
304
- // DENIES every exemption (surfaced), but never fails a tree whose receipts independently satisfy
305
- // the gate. A stale-fingerprint record never exempts (the authoritative record per {backend,
306
- // fingerprint} must attest THIS tree).
307
- export const degradeRecordSet = ({ cwd, env = process.env, fingerprint }) => {
308
- const storePath = resolveEvidencePath(cwd, env);
309
- const read = storePath ? readEvidence(storePath) : { records: [], malformed: 0, malformedReasons: [] };
310
- const unavailable = (read.malformed ?? 0) > 0 || read.readError != null;
311
- const records = unavailable ? [] : authoritativeOfKind(read.records, 'degrade');
312
- const set = unavailable || fingerprint == null
313
- ? new Set()
314
- : new Set(records.filter((r) => r.fingerprint === fingerprint).map((r) => r.backend));
315
- return { set, records, storePath, malformed: read.malformed ?? 0, readError: read.readError ?? null, unavailable };
316
- };
317
-
318
- export const selectHeldSessionDegrades = (records) =>
319
- records.filter((record) => record.kind === 'degrade' && record.backend === HELD_EXECUTE_WRAPPER);
320
-
321
- export const shouldReadHeldSession = ({ configuredExecute, plans, fingerprint, clean }) =>
322
- configuredExecute === 'delegated' && plans.length > 0 && fingerprint !== null && clean === false;
323
-
324
- export const buildHeldSessionState = ({
325
- cwd,
326
- env,
327
- configuredExecute,
328
- plans,
329
- fingerprint,
330
- clean,
331
- degrades,
332
- resolveStore,
333
- readStore,
334
- audit,
335
- readHead,
336
- }) => {
337
- if (!shouldReadHeldSession({ configuredExecute, plans, fingerprint, clean })) return null;
338
- const ledger = readDelegationLedger(cwd, env, { resolveStore, readStore, audit, readHead });
339
- return judgeLedger(ledger, { backend: HELD_RECEIPT_BACKEND, degrades });
340
- };
341
-
342
- // ── the check + report core ─────────────────────────────────────────────────────────
343
-
344
- // buildState({ cwd, env, detect }) → everything both renders need. Pure I/O at the edges.
345
- // EVERY project-relative read (orchestration config, docs/plans, receipts) anchors at the git
346
- // work-tree ROOT when one exists — the fingerprint is root-anchored, so a subdirectory invocation
347
- // must read the same config/plans or a dirty unreceipted tree could false-PASS as "no plan in
348
- // flight". Outside a git tree the cwd is the only anchor (and --check exits 0).
349
- export const buildState = ({ cwd, env = process.env, detect = detectBackends, surveyVehicle, lstat = lstatSync, readFile = readFileSync } = {}) => {
350
- // WHICH repository, first; the config/plan anchor is the stripped discovery's top (from a subdirectory the bare cwd would miss docs/plans — decideCheck refuses on an unanchored root).
351
- const location = resolveGitLocation(cwd, { env });
352
- const anchoredTop = location.top ?? gitLine(['rev-parse', '--show-toplevel'], cwd, stripGitLocationEnv(env));
353
- const root = anchoredTop ?? cwd;
354
- const { config, source: configSource } = loadConfig(root);
355
- // A bridge-detector throw reaches the hook, never a catch that would also lose the surveyed
356
- // executor vehicle: bridge readiness goes unknown (fail closed below), the carrier stays known.
357
- let detectionWarning = null;
358
- const onDetectError = (err) => {
359
- detectionWarning = `backend detection failed (${(err && err.message) || err}) — bridge readiness unknown.`;
360
- };
361
- const detection = composeReadiness(root, { detect, surveyVehicle, onDetectError });
362
- // The resolver stays for DISPLAY only; the OBLIGATIONS come from the configured recipe (never the readiness-degraded effective one — no silent solo).
363
- const resolved = resolveActivityRecipe({ config: config ?? {}, readiness: detection, activity: ACTIVITY, slot: SLOT });
364
- const obligations = requiredBackendsForConfiguredRecipe({ config: config ?? {}, readiness: detection, detectionFailed: detectionWarning != null });
365
- const requiredBackends = obligations.backends;
366
- const plans = plansInFlight(root);
367
- // The injected lstat threads through EVERY stat-dependent computation (fingerprint, clean, the
368
- // mask count) — a partial injection would let a test observe an inconsistent state.
369
- const fingerprint = location.state === 'work-tree' ? computeTreeFingerprint(cwd, { lstat, env }) : null;
370
- const clean = fingerprint == null ? null : isTreeClean(cwd, { lstat, env });
371
- const receiptsPath = resolveReceiptsPath(cwd, env);
372
- const receiptsRead = receiptsPath ? readReceipts(receiptsPath) : { receipts: [], malformed: 0 };
373
- const { receipts, malformed } = receiptsRead;
374
- const receiptsReadError = receiptsRead.readError ?? null;
375
- const backends = requiredBackends.map((b) => ({ backend: b, ...backendReceiptStatus(receipts, b, fingerprint) }));
376
- // The D3(b) degrade escape: read the core-evidence store ONLY here, ONLY for the exemption —
377
- // the gate never otherwise depends on it (an unavailable store denies the exemption CLOSED,
378
- // never fails a tree whose receipts independently satisfy the gate).
379
- const base = resolveBase(cwd);
380
- const degrade = degradeRecordSet({ cwd, env, fingerprint });
381
- const degradedExempt = requiredBackends.filter((b) => degrade.set.has(b));
382
- const heldSession = buildHeldSessionState({
383
- cwd,
384
- env,
385
- configuredExecute: [config?.[ACTIVITY]?.execute, config?.task?.execute].find((recipe) => recipe === 'delegated'),
386
- plans,
387
- fingerprint,
388
- clean,
389
- degrades: selectHeldSessionDegrades(degrade.records),
390
- });
391
- // ── the Phase-2 flow arms' state (two-tier activation, P3) ──
392
- // Tier 1 is store-file PRESENCE: no file ⇒ every field below stays inert and the decision is
393
- // byte-identical to the pre-flow checker (the unarmed fast path also reads NO plan file).
394
- const flowPath = resolveFlowStorePath(cwd, env);
395
- const flowStat = (() => {
396
- if (flowPath == null) return null;
397
- try {
398
- return lstat(flowPath);
399
- } catch (err) {
400
- return err && err.code === 'ENOENT' ? null : 'unstatable';
401
- }
402
- })();
403
- const flowPresent = flowStat !== null;
404
- const flowRead = flowPresent && flowStat !== 'unstatable' ? readFlowStore(flowPath) : null;
405
- const flowBrokenReason = !flowPresent ? null
406
- : flowStat === 'unstatable' ? 'the store leaf cannot be stat-ed (fail closed)'
407
- : flowRead.readError != null ? `read error: ${flowRead.readError}`
408
- : flowRead.malformed > 0 ? `${flowRead.malformed} malformed line(s) (${flowRead.malformedReasons[0]})`
409
- : null;
410
- const flowArmed = flowPresent && flowBrokenReason == null
411
- && flowRead.records.some((r) => r.kind === CHAIN_KIND && r.purpose === 'adoption');
412
- const flowOwner = flowArmed ? deriveFlowOwner(cwd) : null;
413
- const planCoverage = flowArmed
414
- ? computePlanAdoptionCoverage({ root, plans, records: flowRead.records, owner: flowOwner, readFile })
415
- : [];
416
- // The lens-substitution rung (#15/#3): an attestation whose lens set claims a review provider's
417
- // slot without a then-active down-mark never counts — and its refusal is NAMED, never silent
418
- // (lensRefusedAttestations feeds the internal-only floor's failure reason).
419
- const lensRefusedAttestations = [];
420
- const attestedPlanIds = flowArmed
421
- ? [...new Set(authoritativeFlowRecords(flowRead.records)
422
- .filter((r) => r.kind === 'internal-attestation' && r.base === base && r.fingerprint === fingerprint)
423
- .filter((r) => {
424
- const lensCheck = evaluateInternalAttestationLenses({ record: r, records: flowRead.records, providerBackends: Object.values(DISPLAY_ALIASES) });
425
- if (!lensCheck.ok) {
426
- lensRefusedAttestations.push({ planId: r.planId, reason: lensCheck.reason });
427
- return false;
428
- }
429
- return true;
430
- })
431
- .map((r) => r.planId))]
432
- : [];
433
- const flowConfig = config?.flow ?? null;
434
- // #61: the SHARED relied-on selector (one home with the flow-check coverage rung) — an unbroken
435
- // declared-path bookkeeping-delta chain lifts the backend's last receipt; any break, fork, or
436
- // cap exhaustion stays stale.
437
- const liftStale = (row, backend) => {
438
- if (!flowArmed || row.state !== 'stale') return row;
439
- const selected = selectReliedOnReceipt({
440
- receipts, backend, tree: { base, fingerprint },
441
- records: flowRead.records,
442
- declaredPaths: [flowConfig?.debtQueue, flowConfig?.convergenceSummary].filter((p) => typeof p === 'string'),
443
- refreshCap: flowConfig?.councilRounds,
444
- });
445
- if (selected.receipt == null || selected.lifted === 0) return row;
446
- return {
447
- ...row, state: 'current', verdict: selected.receipt.verdict ?? 'unknown',
448
- shipClass: isShipVerdict(selected.receipt.verdict), grounded: true,
449
- timestamp: selected.receipt.timestamp ?? null, deltaLift: selected.lifted,
450
- };
451
- };
452
- // #56/#38: a standing CURRENT-tree veto consults its override instance; a delta-lifted veto has
453
- // no override lane here (the bound set pins the veto receipt's own tree).
454
- const consumeOverride = (row) => {
455
- if (!flowArmed || !(row.state === 'current' && !row.shipClass) || row.deltaLift != null) return row;
456
- const own = receipts.filter((r) => r.backend === row.backend);
457
- const summary = summarizeReviewReceiptsForTree(own, fingerprint);
458
- if (summary.state !== 'current') return row;
459
- const lift = evaluateVetoOverride({ records: flowRead.records, vetoReceipt: summary.receipt, tree: { base, fingerprint } });
460
- return lift.lifted ? { ...row, overrideLabel: lift.label } : row;
461
- };
462
- const armedBackends = backends.map((b) => consumeOverride(liftStale(b, b.backend)));
463
- // The solo-class veto probe (#48): under an armed solo recipe every review-capable backend's
464
- // CURRENT-tree receipt is still consulted, so a recipe flip to solo never buries a standing veto.
465
- const soloVetoRows = flowArmed && obligations.recipe === 'solo'
466
- ? Object.values(DISPLAY_ALIASES).map((b) => consumeOverride(liftStale({ backend: b, ...backendReceiptStatus(receipts, b, fingerprint) }, b)))
467
- : [];
468
- return {
469
- resolved,
470
- configSource,
471
- obligations,
472
- requiredBackends,
473
- backends: armedBackends,
474
- plans,
475
- root,
476
- rootAnchored: anchoredTop !== null,
477
- location,
478
- fingerprint,
479
- clean,
480
- receiptsPath,
481
- receiptCount: receipts.length,
482
- malformed,
483
- receiptsReadError,
484
- base,
485
- evidenceStorePath: degrade.storePath,
486
- evidenceMalformed: degrade.malformed,
487
- evidenceReadError: degrade.readError,
488
- evidenceUnavailable: degrade.unavailable,
489
- degradedExempt,
490
- heldSession,
491
- maskedUntracked: countNeverCommittableUntracked(cwd, { lstat, env }),
492
- detectionWarning,
493
- flowPresent,
494
- flowArmed,
495
- flowBrokenReason,
496
- flowOwner,
497
- planCoverage,
498
- attestedPlanIds,
499
- lensRefusedAttestations,
500
- soloVetoRows,
501
- };
502
- };
503
-
504
- // Why a backend's current-tree receipts were all rejected — the two causes read differently and
505
- // have different recoveries (fix the file vs refresh the bridge), so they are never collapsed.
506
- const rejectionCause = (b) => {
507
- const parts = [];
508
- if (b.markerRejected > 0) parts.push(`${b.markerRejected} with a malformed probe marker`);
509
- if (b.unmarkedRejected > 0) {
510
- parts.push(`${b.unmarkedRejected} with no probe marker — silence is not a declaration, so the probe status is untrustworthy; re-run the review with a bridge that marks its runs`);
511
- }
512
- if ((b.postureRejected ?? 0) > 0) {
513
- parts.push(`${b.postureRejected} with an absent/invalid run posture (D5) — a pre-posture wrapper minted it; re-run the review on the current bridge`);
514
- }
515
- if ((b.deliveryRejected ?? 0) > 0) {
516
- parts.push(`${b.deliveryRejected} with an absent/invalid delivery declaration (D8b) — the receipt never declared HOW the change set reached the model, so delivery was not proven; re-run the review on the current bridge`);
517
- }
518
- return parts.join(' + ');
519
- };
520
-
521
- // One failing backend row → its stated recovery. Shared by the council per-backend arm and the
522
- // reviewed closest-recovery listing.
523
- const backendFailurePart = (b, state) => {
524
- if (b.state === 'current') return `${b.backend}: latest recognized verdict is ${JSON.stringify(b.verdict)} — a recognized negative is an authoritative veto; fold and re-review`;
525
- if (b.state === 'unrecognized-verdict') return `${b.backend}: the latest normal receipt carries an unrecognized verdict (${JSON.stringify(b.verdict)}) — an unknown verdict never attests (fail-closed); re-run the review`;
526
- if (b.state === 'ungrounded') return `${b.backend}: the latest normal receipt is ungrounded — re-run grounded (--facts)`;
527
- 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`;
528
- if (b.state === 'rejected') return `${b.backend}: current-tree receipts rejected — ${rejectionCause(b)} (fail-closed); inspect ${escapeForDisplay(state.receiptsPath)}`;
529
- if (b.state === 'stale') return `${b.backend}: receipts exist but none matches the current tree (edited after review) — run a fresh review`;
530
- return `${b.backend}: no receipt — run its review wrapper, or record an explicit degrade (node ${CORE_EVIDENCE_TOOL} degrade --backend ${b.backend} --reason "...")`;
531
- };
532
-
533
- const describesHeldRecovery = (state) => {
534
- const substitution = state.heldSession.substitution;
535
- if (substitution === null) return '';
536
- if (substitution.baseKind === 'checkpoint') {
537
- return ` — a checkpoint-based thread: no core-evidence degrade lifts it; record its ledger degrade (node ${DISPATCH_TOOL} degrade --wave <wave> --nonce ${shellQuoteArg(substitution.nonce)} --step-class code --rationale "...") then retry it (cap permitting) or open a new thread on the held session, and fold that`;
538
- }
539
- const canRetry = substitution.folded === false;
540
- if (state.evidenceUnavailable === true) {
541
- const retry = canRetry ? '; or fold the retry of that thread' : '';
542
- return ` — the evidence store is unavailable, so the degrade lane cannot be judged: repair it (inspect ${escapeForDisplay(state.evidenceStorePath)}) and re-check${retry}`;
543
- }
544
- const canDegrade = typeof substitution.postTreeDigest === 'string'
545
- && state.fingerprint === substitution.postTreeDigest;
546
- if (!canDegrade && !canRetry) {
547
- 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';
548
- }
549
- const degrade = canDegrade
550
- ? ` — mint the accepted-replacement record now, before the next edit: node ${CORE_EVIDENCE_TOOL} degrade --backend ${HELD_EXECUTE_WRAPPER} --reason "..."`
551
- : '';
552
- const retry = canRetry ? `${canDegrade ? '; or' : ' —'} fold the retry of that thread` : '';
553
- return `${degrade}${retry}`;
554
- };
555
-
556
- // The normative --check decision (the header contract, in order). → { code, reason }.
557
- // Obligations come from the CONFIGURED recipe; satisfaction is SHIP-CLASS ONLY; a recognized
558
- // negative on the latest normal receipt VETOES; an explicit current-tree degrade record is the
559
- // only escape for an unavailable backend under council — and never all backends (>=1 ship-class
560
- // attestation is required whenever >=1 backend is configured).
561
- export const decideCheck = (state) => {
562
- // Store diagnostics ride EVERY check line, early exits (and the unknowable arm) included — a
563
- // malformed receipt line, an unavailable evidence store, or an unreadable receipts store is
564
- // never hidden behind any exit.
565
- const malformedNote = state.malformed > 0 ? ` — ${state.malformed} malformed receipt line(s) ignored; inspect ${escapeForDisplay(state.receiptsPath)}` : '';
566
- const evidenceNote = state.evidenceUnavailable
567
- ? ` — evidence store unavailable (${state.evidenceMalformed} malformed line(s)${state.evidenceReadError ? `, read error: ${state.evidenceReadError}` : ''}); the degrade escape is denied (fail-closed) — inspect ${escapeForDisplay(state.evidenceStorePath)}`
568
- : '';
569
- const earlyNotes = `${malformedNote}${evidenceNote}${state.receiptsReadError ? ` — receipts store unreadable (${state.receiptsReadError}); inspect ${escapeForDisplay(state.receiptsPath)}` : ''}`;
570
- // The git LOCATION (hand-built states carry none; the fingerprint decides there) rules below the solo arm: not-a-repository passes, the four other non-work-tree states refuse by name.
571
- const location = state.location ?? { state: state.fingerprint == null ? 'not-a-repository' : 'work-tree', cause: 'no location was resolved for a hand-built state' };
572
- if (state.heldSession !== null && state.heldSession !== undefined) {
573
- const held = decideHeldSession(state.heldSession);
574
- const heldRecovery = describesHeldRecovery(state);
575
- if (held.code !== 0) return { ...held, reason: `${held.reason}${heldRecovery}${earlyNotes}`, terminal: true, heldSession: true };
576
- }
577
- // Detector failure with NO configured recipe: the computed default is unknowable → fail closed.
578
- // An EXPLICIT configured recipe needs no detector — its obligations are readiness-independent.
579
- if (state.obligations.unknowable) {
580
- return { code: 1, reason: `cannot verify receipts — ${state.detectionWarning} No configured ${ACTIVITY}.${SLOT} recipe: the computed default is unknowable while the detector is down (fail closed).${earlyNotes}` };
581
- }
582
- if (state.obligations.recipe === 'solo' && !state.flowArmed && state.flowBrokenReason == null) {
583
- const why = state.obligations.source === 'config'
584
- ? `configured ${ACTIVITY}.${SLOT} recipe is solo`
585
- : `no reviewer backend is ready — the computed ${ACTIVITY}.${SLOT} default is solo`;
586
- return { code: 0, reason: `${why} — no receipt required${earlyNotes}` };
587
- }
588
- // An UNANCHORED root (git dead everywhere): the bare-cwd plan inventory cannot say "no plan".
589
- if (location.state !== 'work-tree' && location.state !== 'not-a-repository' && state.rootAnchored === false) return { code: 1, reason: `the git location is ${location.state} — ${escapeForDisplay(location.cause)} — and the project root cannot be anchored, so the plan arm cannot decide (fail closed)${earlyNotes}` };
590
- if (state.plans.length === 0) return { code: 0, reason: `no plan in flight (docs/plans/ holds no active plan) — no receipt required${earlyNotes}` };
591
- if (location.state !== 'work-tree' && location.state !== 'not-a-repository') return { code: 1, reason: `the git location is ${location.state} — ${escapeForDisplay(location.cause)} — refusing to judge receipts for a tree git cannot name (fail closed)${earlyNotes}` };
592
- if (location.state === 'not-a-repository') return { code: 0, reason: `not a git repository (git's own answer: ${escapeForDisplay(location.cause)}) — nothing to fingerprint${earlyNotes}` };
593
- if (state.fingerprint == null || state.clean === null) return { code: 1, reason: `the tree ${state.fingerprint == null ? 'fingerprint' : 'clean check'} is undecidable in a work tree (a git query failed or was killed) — refusing to judge receipts (fail closed)${earlyNotes}` };
594
- // A clean tree PASSES, but never silently: >=1 plan IS in flight here, and this gate arms the
595
- // moment the tree turns dirty — naming each file makes the arm discoverable BEFORE it blocks.
596
- if (state.clean === true) {
597
- // Each name is byte-preserving and quoted because this reason is one gate-report line.
598
- const named = state.plans.map((p) => quoteReportName(p)).join(', ');
599
- return { code: 0, reason: `${CLEAN_TREE_PASS} — ${state.plans.length} plan(s) in flight: ${named} — ${LATENT_ARM_NOTICE}${earlyNotes}` };
600
- }
601
- // Tier-1 fail-closed (P3): a PRESENT flow store must read clean before the decision leans on
602
- // receipts — the flow arms cannot be evaluated over a store of unknown content.
603
- if (state.flowBrokenReason != null) {
604
- return { code: 1, reason: `the flow store is unavailable (${state.flowBrokenReason}) — the flow arms fail closed; inspect the store${earlyNotes}` };
605
- }
606
- // The no-reviewers-configured floor (#34/#43): an ARMED flow makes the reduced record set an
607
- // OBLIGATION for the solo class — every in-flight plan covered by an adopted chain AND
608
- // internally attested at the current tree; a standing current-tree veto still blocks (#48),
609
- // whatever the recipe consults, so a recipe flip to solo can never bury one.
610
- if (state.obligations.recipe === 'solo') {
611
- if (state.receiptsReadError != null) {
612
- return { code: 1, reason: `the receipts store is unreadable (${state.receiptsReadError}) — "no standing veto" cannot be established, so the internal-only floor fails closed; inspect ${escapeForDisplay(state.receiptsPath)}${earlyNotes}` };
613
- }
614
- const standing = state.soloVetoRows.filter((b) => (b.state === 'current' && !b.shipClass && !b.overrideLabel) || b.state === 'unrecognized-verdict');
615
- if (standing.length > 0) {
616
- // `veto` marks the AUTHORITATIVE-veto class only (a recognized negative) — the Decision-4
617
- // --await early exit keys on it; an unrecognized verdict stays a plain fail-closed refusal.
618
- const authoritative = standing.some((b) => b.state === 'current' && !b.shipClass);
619
- return { code: 1, reason: `${standing.map((b) => backendFailurePart(b, state)).join('; ')} — a standing veto blocks the internal-only floor (#48)${earlyNotes}`, ...(authoritative ? { veto: true } : {}) };
620
- }
621
- const uncovered = state.planCoverage.filter((p) => !p.covered);
622
- if (uncovered.length > 0) {
623
- return { code: 1, reason: `internal-only arming refused (#68): ${uncovered.map((p) => `plan ${quoteReportName(p.plan)} — ${p.reason}`).join('; ')} — an uncovered in-flight plan is a refusal, never a relaxation${earlyNotes}` };
624
- }
625
- const missing = state.planCoverage.filter((p) => !state.attestedPlanIds.includes(p.planId));
626
- if (missing.length > 0) {
627
- // A lens-refused attestation is a NAMED cause, never a silent gap — the refusal rides the
628
- // floor's failure reason for exactly the plans it starved.
629
- const lensNotes = (state.lensRefusedAttestations ?? []).filter((x) => missing.some((p) => p.planId === x.planId));
630
- const lensSuffix = lensNotes.length > 0 ? `; ${lensNotes.map((x) => x.reason).join('; ')}` : '';
631
- return { code: 1, reason: `internal-only floor: no internal-attestation record at the current tree for plan(s) ${missing.map((p) => quoteReportName(p.plan)).join(', ')} — the reduced record set (#34/#43) requires one per covered plan (minting rides the Plan-4 round machinery)${lensSuffix}${earlyNotes}` };
632
- }
633
- const floorLabels = [
634
- ...state.soloVetoRows.filter((b) => b.deltaLift != null).map((b) => `${b.backend}: receipt lifted to CURRENT through an unbroken bookkeeping-delta chain (${b.deltaLift} link(s), #61)`),
635
- ...state.soloVetoRows.filter((b) => b.overrideLabel).map((b) => b.overrideLabel),
636
- 'internal-only (downgraded class)',
637
- ];
638
- const liftedTail = floorLabels.length > 1 ? ` — ${floorLabels.slice(0, -1).join('; ')}` : '';
639
- return {
640
- code: 0,
641
- reason: `internal-only floor satisfied (DOWNGRADED class, #28/#43): the solo recipe under an ARMED flow consumes the reduced record set — every in-flight plan (${state.planCoverage.map((p) => quoteReportName(p.plan)).join(', ')}) is covered by an adopted chain and internally attested at the current tree; self-authored review, disclosed${liftedTail}${earlyNotes}`,
642
- flowLabels: floorLabels,
643
- };
644
- }
645
- const exempt = new Set(state.degradedExempt);
646
- const satisfied = state.backends.filter((b) => b.state === 'current' && b.shipClass);
647
- const vetoed = state.backends.filter((b) => b.state === 'current' && !b.shipClass && !b.overrideLabel);
648
- const overridden = state.backends.filter((b) => b.state === 'current' && !b.shipClass && b.overrideLabel);
649
- // The marker note is PATH-AWARE (no silent rejections on any exit): it counts every backend's
650
- // untrusted-marker exclusions EXCEPT those whose printed part already names them — i.e. only a
651
- // PRINTED `rejected` row (backendFailurePart's rejectionCause) suppresses its own counts; a
652
- // printed veto/unrecognized/ungrounded row does not, and success paths suppress nothing.
653
- const notesFor = (printed) => {
654
- const total = state.backends
655
- .filter((b) => !(printed.has(b.backend) && b.state === 'rejected'))
656
- .reduce((n, b) => n + (b.markerRejected ?? 0) + (b.unmarkedRejected ?? 0), 0);
657
- const markerNote = total > 0
658
- ? ` — ${total} receipt(s) rejected: an untrustworthy probe marker (malformed, or absent — silence is not a declaration) — fail-closed; inspect ${escapeForDisplay(state.receiptsPath)}`
659
- : '';
660
- return `${markerNote}${earlyNotes}`;
661
- };
662
- const NONE_PRINTED = new Set();
663
- // UNCONDITIONAL refusals, checked BEFORE minShip/exemptions: a recognized NEGATIVE (the
664
- // authoritative veto) and an UNRECOGNIZED verdict (fail closed) — another backend's SHIP never
665
- // masks them and a degrade record never lifts them (the backend demonstrably ran).
666
- const unrecognized = state.backends.filter((b) => b.state === 'unrecognized-verdict');
667
- const unconditional = [...vetoed, ...unrecognized];
668
- if (unconditional.length > 0) {
669
- // `veto` marks the AUTHORITATIVE-veto class only (Decision 4): --await ends the wait loudly on
670
- // it — a landed negative is the dispatched review's ANSWER, never a condition to out-wait.
671
- return { code: 1, reason: `${unconditional.map((b) => backendFailurePart(b, state)).join('; ')}${notesFor(new Set(unconditional.map((b) => b.backend)))}`, ...(vetoed.length > 0 ? { veto: true } : {}) };
672
- }
673
- // The armed-flow PASS labels (#61/#38): the delta-lift and override facts ride every PASS reason
674
- // and the flowLabels field the commit-guard PASS line consumes. Empty (and absent) unarmed.
675
- const flowNotes = [
676
- ...state.backends.filter((b) => b.deltaLift != null)
677
- .map((b) => `${b.backend}: receipt lifted to CURRENT through an unbroken bookkeeping-delta chain (${b.deltaLift} link(s), #61)`),
678
- ...overridden.map((b) => b.overrideLabel),
679
- ];
680
- const flowSuffix = flowNotes.length > 0 ? ` — ${flowNotes.join('; ')}` : '';
681
- const pass = (reason) => (state.flowArmed ? { code: 0, reason, flowLabels: flowNotes } : { code: 0, reason });
682
- const perBackendFailing = state.backends.filter((b) => !(b.state === 'current' && b.shipClass) && !exempt.has(b.backend) && !b.overrideLabel);
683
- // Never all degraded: >=1 ship-class attestation whenever >=1 backend is configured. An
684
- // already-exempt backend renders its own honest part — never the "record an explicit degrade"
685
- // recovery it has already taken.
686
- if (satisfied.length < state.obligations.minShip) {
687
- const failing = state.backends.filter((b) => !(b.state === 'current' && b.shipClass) && !b.overrideLabel);
688
- const allExempt = failing.length > 0 && failing.every((b) => exempt.has(b.backend));
689
- const head = failing.length === 0
690
- ? 'every configured backend is veto-overridden for this tree — >=1 non-overridden ship-class attestation is required; run at least one real review'
691
- : allExempt
692
- ? `every configured backend is ${overridden.length > 0 ? 'degrade-recorded or veto-overridden' : 'degrade-recorded'} for this tree — never all degraded: >=1 ${overridden.length > 0 ? 'non-degraded, non-overridden' : 'non-degraded'} ship-class attestation is required; run at least one real review`
693
- : failing
694
- .map((b) => (exempt.has(b.backend)
695
- ? `${b.backend}: degrade-recorded for this tree — a degrade never counts toward the >=1 ship-class floor; run a real review on another backend`
696
- : backendFailurePart(b, state)))
697
- .join('; ');
698
- // Only backends that actually rendered their backendFailurePart suppress their counts — an
699
- // exempt row prints the degrade string (no rejectionCause), so its exclusions stay named.
700
- return { code: 1, reason: `${head}${notesFor(new Set(failing.filter((b) => !exempt.has(b.backend)).map((b) => b.backend)))}` };
701
- }
702
- if (state.obligations.perBackend) {
703
- // Council: EVERY configured backend must attest ship-class OR carry a current-tree degrade
704
- // record OR a lifted maintainer-override.
705
- if (perBackendFailing.length > 0) {
706
- return { code: 1, reason: `${perBackendFailing.map((b) => backendFailurePart(b, state)).join('; ')}${notesFor(new Set(perBackendFailing.map((b) => b.backend)))}` };
707
- }
708
- if (exempt.size === 0 && overridden.length === 0) {
709
- return pass(`every configured backend attests ship-class for the current tree (${state.requiredBackends.join(' + ')})${flowSuffix}${notesFor(NONE_PRINTED)}`);
710
- }
711
- const clauses = [`council satisfied: ship-class attestation(s) from ${satisfied.map((b) => b.backend).join(' + ')}`];
712
- if (exempt.size > 0) clauses.push(`degrade-recorded for this tree: ${[...exempt].join(', ')}`);
713
- return pass(`${clauses.join('; ')}${flowSuffix}${notesFor(NONE_PRINTED)}`);
714
- }
715
- // Reviewed: >=1 ship-class attestation from any review-capable backend satisfies.
716
- return pass(`reviewed satisfied: ship-class attestation from ${satisfied.map((b) => b.backend).join(' + ')} for the current tree${flowSuffix}${notesFor(NONE_PRINTED)}`);
717
- };
718
-
719
- // ── rendering ───────────────────────────────────────────────────────────────────────
720
-
721
- // The glyph reflects SATISFACTION, not bare state: a `current` row carrying a recognized
722
- // NEGATIVE is an authoritative veto and renders ✗.
723
- const STATE_GLYPH = { 'unrecognized-verdict': '✗', ungrounded: '✗', probe: '✗', rejected: '✗', stale: '✗', missing: '✗' };
724
- const glyphFor = (b) => (b.state === 'current' ? (b.shipClass ? '✓' : '✗') : STATE_GLYPH[b.state]);
725
-
726
- const formatHuman = (state, check) => {
727
- const src = state.obligations.source === 'config' ? `from ${CONFIG_REL}` : 'computed default';
728
- const lines = [
729
- `review-state — ${ACTIVITY}.${SLOT} = ${state.obligations.recipe ?? '(unknowable)'} (${src})${state.requiredBackends.length ? ` → ${state.requiredBackends.join(' + ')}${state.obligations.perBackend ? '' : ' (any one, ship-class)'}` : ''}`,
730
- ];
731
- if (state.detectionWarning) lines.push(` ⚠ ${state.detectionWarning}`);
732
- lines.push(` plan in flight: ${state.plans.length ? state.plans.map((name) => quoteReportName(name)).join(', ') : '(none)'}`);
733
- if (state.fingerprint == null) lines.push(` tree: ${state.location != null && state.location.state !== 'work-tree' ? `git location ${state.location.state} — ${escapeForDisplay(state.location.cause)}` : 'the fingerprint is undecidable (a git query failed or was killed)'}`);
734
- else if (state.clean === true) lines.push(' tree: clean (nothing to review)');
735
- else lines.push(` tree fingerprint: ${state.fingerprint}`);
736
- if (state.heldSession != null) lines.push(` ${decideHeldSession(state.heldSession).line}`);
737
- lines.push(` receipts: ${state.receiptsPath == null ? '(unresolvable — no git dir)' : escapeForDisplay(state.receiptsPath)} (${state.receiptCount} line(s)${state.malformed ? `, ${state.malformed} malformed — inspect the file` : ''})`);
738
- if (state.receiptsReadError) lines.push(` ⚠ receipts store unreadable (${state.receiptsReadError}) — inspect ${escapeForDisplay(state.receiptsPath)}`);
739
- 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)`);
740
- const exempt = new Set(state.degradedExempt);
741
- for (const b of state.backends) {
742
- const exemptTag = exempt.has(b.backend) ? ' — degrade-recorded for the current tree (core-evidence store)' : '';
743
- const detail =
744
- b.state === 'current'
745
- ? `current (verdict: ${JSON.stringify(b.verdict)}${b.shipClass ? ', ship-class' : ' — a recognized negative, an authoritative VETO'}, grounded, ${b.timestamp ?? '?'})`
746
- : b.state === 'unrecognized-verdict'
747
- ? `latest normal receipt carries an unrecognized verdict (${JSON.stringify(b.verdict)}) — never attests (fail-closed)`
748
- : b.state === 'ungrounded'
749
- ? `ungrounded latest normal receipt (verdict: ${JSON.stringify(b.verdict)}) — a grounded fresh run is required`
750
- : b.state === 'probe'
751
- ? 'only probe receipts for the current tree (quality guards relaxed) — a probe review never attests'
752
- : b.state === 'rejected'
753
- ? `current-tree receipts rejected — ${rejectionCause(b)} (fail-closed)`
754
- : b.state === 'stale'
755
- ? 'stale — no receipt matches the current tree (edited after review)'
756
- : 'missing — no receipt from this backend';
757
- const excludedTag = b.probeExcluded || b.markerRejected || b.unmarkedRejected
758
- ? ` [excluded: ${b.probeExcluded} probe, ${b.markerRejected} malformed-marker, ${b.unmarkedRejected} unmarked]`
759
- : '';
760
- const liftTag = b.deltaLift != null ? ` — lifted to CURRENT through ${b.deltaLift} bookkeeping-delta link(s) (#61)` : '';
761
- const overrideTag = b.overrideLabel ? ` — ${b.overrideLabel}` : '';
762
- // ⊘ only where the escape actually applies: a PRODUCED receipt outranks the record — a
763
- // negative/unknown verdict keeps its ✗ (a degrade lifts neither; a maintainer-override does).
764
- const escapeApplies = (exempt.has(b.backend) && b.state !== 'current' && b.state !== 'unrecognized-verdict') || Boolean(b.overrideLabel);
765
- lines.push(` ${escapeApplies ? '⊘' : glyphFor(b)} ${b.backend}: ${detail}${liftTag}${overrideTag}${excludedTag}${exemptTag}`);
766
- }
767
- lines.push(` check: ${check.code === 0 ? 'PASS' : 'FAIL'} — ${check.reason}`);
768
- return lines.join('\n');
769
- };
122
+ CLEAN_TREE_PASS,
123
+ LATENT_ARM_NOTICE,
124
+ quoteReportName,
125
+ backendReceiptStatus,
126
+ degradeRecordSet,
127
+ selectHeldSessionDegrades,
128
+ shouldReadHeldSession,
129
+ buildHeldSessionState,
130
+ decideCheck,
131
+ } from './review-state-judge.mjs';
132
+ export { countNeverCommittableUntracked, buildState } from './review-state-build.mjs';
133
+ export { computePlanAdoptionCoverage } from './review-state-flow.mjs';
134
+ export { DEFAULT_AWAIT_TIMEOUT_S, AWAIT_POLL_MS, mainAwait } from './review-state-await.mjs';
770
135
 
771
136
  const HELP = `review-state — read-only review-receipt checker (agent-workflow family, AD-038).
772
137
 
@@ -839,71 +204,6 @@ export const main = (argv, ctx = {}) => {
839
204
  }
840
205
  };
841
206
 
842
- // ── --await: block until the configured review obligations are SATISFIED ───────────────
843
- // It waits until `--check` would pass: under `reviewed`, a ship-class attestation from any
844
- // review-capable backend; under `council`, every configured backend ship-class OR carrying a
845
- // current-tree degrade RECORD in the core-evidence store (never all backends) — once such a
846
- // record lands, --await stops waiting for that backend and returns READY (before, it waited
847
- // forever for a receipt that never comes). It inherits everything for FREE — it polls the SAME
848
- // decideCheck(buildState()) `--check` computes. The completion signal is the RECEIPT/RECORD
849
- // (i.e. `--check` would PASS), NEVER a process event — a harness "completed" notification fires
850
- // early and a bridge's output late-flushes, so polling state is the durable mechanization of
851
- // receipts-not-pgrep. Stays read-only (it only re-reads state — now the evidence store too, a
852
- // few KB per tick); the clock is injectable (ctx.now / ctx.sleep / ctx.pollMs) so hermetic tests
853
- // never spend wall-clock.
854
-
855
- const AWAIT_ALLOWED_ARGS = new Set(['--await', '--timeout']);
856
-
857
- const parseAwaitTimeoutS = (argv) => {
858
- const i = argv.indexOf('--timeout');
859
- if (i === -1) return DEFAULT_AWAIT_TIMEOUT_S;
860
- const raw = argv[i + 1];
861
- if (!raw || !/^\d+$/.test(raw) || Number(raw) < 1) throw fail(2, '--timeout requires a positive integer number of seconds');
862
- return Number(raw);
863
- };
864
-
865
- export const mainAwait = async (argv, ctx = {}) => {
866
- const cwd = ctx.cwd ?? process.cwd();
867
- const env = ctx.env ?? process.env;
868
- const detect = ctx.detect ?? detectBackends;
869
- const now = ctx.now ?? (() => Date.now());
870
- const sleep = ctx.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
871
- const pollMs = ctx.pollMs ?? AWAIT_POLL_MS;
872
- const build = ctx.buildState ?? buildState;
873
- try {
874
- for (let i = 0; i < argv.length; i += 1) {
875
- const a = argv[i];
876
- if (a === '--timeout') { i += 1; continue; } // its value is consumed by parseAwaitTimeoutS
877
- if (!AWAIT_ALLOWED_ARGS.has(a)) throw fail(2, `--await accepts only --timeout <s> (got ${a})`);
878
- }
879
- const timeoutS = parseAwaitTimeoutS(argv);
880
- const timeoutMs = timeoutS * 1000;
881
- const start = now();
882
- // Poll the SAME normative decision --check computes: ready == `--check` would pass (solo / no
883
- // plan / a clean tree / not-a-git-tree all resolve instantly — nothing to await). Re-read state
884
- // every poll so a landed receipt (or a tree edit that re-staled one) is seen fresh. The DEADLINE
885
- // is checked BEFORE readiness (codex council R2): once elapsed reaches the timeout the await is
886
- // over, so a receipt that only lands AT/after the deadline never flips it to READY; and each
887
- // sleep is BOUNDED to the remaining time so a full poll interval can never overshoot the timeout.
888
- let lastReason = 'no poll completed before the deadline';
889
- for (;;) {
890
- const elapsed = now() - start;
891
- if (elapsed >= timeoutMs) return { code: 1, stdout: '', stderr: `review-state --await: TIMEOUT after ${timeoutS}s — ${lastReason}` };
892
- const check = decideCheck(build({ cwd, env, detect }));
893
- lastReason = check.reason;
894
- if (check.code === 0) return { code: 0, stdout: `review-state --await: READY — ${check.reason}`, stderr: '' };
895
- if (check.heldSession === true) return { code: 1, stdout: '', stderr: `review-state --await: HELD SESSION — ${check.reason}` };
896
- // Decision 4 (#50): an AUTHORITATIVE veto ends the wait loudly BEFORE the deadline — the
897
- // dispatched review answered with a landed negative; classifying that as a timeout was the
898
- // misclassification this amendment closes. Only a fresh review can move it, never waiting.
899
- if (check.veto === true) return { code: 1, stdout: '', stderr: `review-state --await: VETO — ${check.reason}` };
900
- await sleep(Math.min(pollMs, timeoutMs - elapsed));
901
- }
902
- } catch (err) {
903
- return { code: err.exitCode ?? 1, stdout: '', stderr: `review-state: ${err.message}` };
904
- }
905
- };
906
-
907
207
  const emitResult = (r) => {
908
208
  // Exact writes + a natural exit: process.exit() can truncate unflushed piped stdio.
909
209
  if (r.stdout) process.stdout.write(r.stdout.endsWith('\n') ? r.stdout : `${r.stdout}\n`);