@sabaiway/agent-workflow-kit 5.1.0 → 5.2.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/SKILL.md +13 -1
  3. package/bridges/antigravity-cli-bridge/SKILL.md +14 -3
  4. package/bridges/antigravity-cli-bridge/bin/agy-review.sh +220 -30
  5. package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +264 -8
  6. package/bridges/antigravity-cli-bridge/bin/agy.sh +12 -2
  7. package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +18 -0
  8. package/bridges/antigravity-cli-bridge/capability.json +19 -13
  9. package/bridges/antigravity-cli-bridge/references/driving-agy.md +3 -2
  10. package/bridges/codex-cli-bridge/SKILL.md +8 -5
  11. package/bridges/codex-cli-bridge/bin/codex-exec.sh +3 -2
  12. package/bridges/codex-cli-bridge/bin/codex-review.sh +205 -34
  13. package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +276 -5
  14. package/bridges/codex-cli-bridge/capability.json +8 -6
  15. package/bridges/codex-cli-bridge/references/driving-codex.md +2 -2
  16. package/bridges/codex-cli-bridge/references/sandbox-and-flags.md +2 -2
  17. package/capability.json +1 -1
  18. package/package.json +1 -1
  19. package/references/modes/flow-writer.md +37 -0
  20. package/references/modes/gates.md +4 -4
  21. package/references/modes/procedures.md +4 -2
  22. package/references/modes/receipt-deadline.md +16 -0
  23. package/references/modes/review-state.md +1 -1
  24. package/references/modes/set-flow.md +22 -0
  25. package/tools/cheap-agents.mjs +8 -2
  26. package/tools/commands.mjs +24 -2
  27. package/tools/commit-guard.mjs +44 -9
  28. package/tools/core-evidence.mjs +25 -22
  29. package/tools/detect-backends.mjs +32 -11
  30. package/tools/doc-parity.mjs +21 -6
  31. package/tools/flow-check.mjs +806 -0
  32. package/tools/flow-record.mjs +795 -0
  33. package/tools/flow-store-read.mjs +114 -0
  34. package/tools/flow-store.mjs +1178 -0
  35. package/tools/flow-writer.mjs +1265 -0
  36. package/tools/fs-read-nofollow.mjs +128 -0
  37. package/tools/gates-declaration.mjs +184 -0
  38. package/tools/gates-init.mjs +59 -17
  39. package/tools/orchestration-config.mjs +87 -10
  40. package/tools/orchestration-write.mjs +3 -3
  41. package/tools/plan-files.mjs +35 -0
  42. package/tools/procedures.mjs +75 -11
  43. package/tools/receipt-deadline.mjs +242 -0
  44. package/tools/recipes.mjs +21 -0
  45. package/tools/repo-lex.mjs +22 -0
  46. package/tools/review-state.mjs +240 -80
  47. package/tools/run-gates.mjs +361 -139
  48. package/tools/set-flow.mjs +465 -0
  49. package/tools/velocity-profile.mjs +8 -2
@@ -66,6 +66,20 @@
66
66
  // bridge's capability.json roles.review.contract.receipt, and the bash twin lives in both
67
67
  // wrappers — cross-checked by test/review-fingerprint-parity.test.mjs.
68
68
  //
69
+ // Phase-2 flow arms (flow-orchestration Plan 3, #43/#61/#68/#48 — two-tier activation, P3): with
70
+ // NO flow store file the decision is byte-identical to the pre-flow checker; a present-but-
71
+ // malformed store FAILS the dirty-tree check closed; a valid store with no adoption changes
72
+ // nothing. Under an ARMED store (an adoption record exists): (a) a STALE receipt lifts to CURRENT
73
+ // through an unbroken declared-path bookkeeping-delta chain (#61, labeled in the PASS reason);
74
+ // (b) a standing veto lifted by a valid maintainer-override PASSES with the durable label in the
75
+ // check reason and the human report (#38/#56); (c) the SOLO obligations class (no reviewers
76
+ // configured — configured solo or the computed solo default) gains the reduced internal-only
77
+ // floor as an OBLIGATION (#34/#43): every in-flight plan must be covered by an adopted chain
78
+ // (planId + content digest + owner, P13/P19) AND carry an internal-attestation at the current
79
+ // tree; an uncovered plan REFUSES, never relaxes (#68), and a standing current-tree veto still
80
+ // blocks the floor (#48). A configured reviewed/council NEVER takes the reduced floor — its
81
+ // missing backends keep the degrade-record bar (the full degraded-council set arms in Phase 4).
82
+ //
69
83
  // HUMAN residual (accepted, documented): `git commit --no-verify` skips any pre-commit gate, and
70
84
  // deleting/editing the receipt file forges state — receipts live in the git dir (never committable)
71
85
  // as an honest self-discipline mechanism, not a security boundary.
@@ -74,13 +88,24 @@
74
88
  // (read-only queries) to compute the fingerprint — stated honestly in the catalog. Dependency-free,
75
89
  // Node >= 22. No side effects on import (the isDirectRun idiom).
76
90
 
77
- import { readdirSync, lstatSync } from 'node:fs';
91
+ import { lstatSync, readFileSync } from 'node:fs';
78
92
  import { join, dirname } from 'node:path';
79
93
  import { pathToFileURL, fileURLToPath } from 'node:url';
80
94
  import { spawnSync } from 'node:child_process';
95
+ import { createHash } from 'node:crypto';
81
96
  import { detectBackends, READY } from './detect-backends.mjs';
82
- import { resolveActivityRecipe, DISPLAY_ALIASES } from './recipes.mjs';
97
+ import { resolveActivityRecipe, DISPLAY_ALIASES, requiredBackendsForConfiguredRecipe } from './recipes.mjs';
83
98
  import { CONFIG_REL, fail, loadConfig } from './orchestration-config.mjs';
99
+ import { resolveFlowStorePath, readFlowStore, deriveFlowOwner, readPlanFrontmatterId } from './flow-store.mjs';
100
+ import { CHAIN_KIND, authoritativeFlowRecords } from './flow-record.mjs';
101
+ import { selectReliedOnReceipt, evaluateVetoOverride, evaluateInternalAttestationLenses } from './flow-check.mjs';
102
+ // Pure leaves (FLOW-READ-GRAPH-PURITY): the plan-file convention and the lexical shell quote live
103
+ // in leaf modules so the procedures read surface reaches them without this module's write-API
104
+ // graph; re-exported here so every historical consumer keeps its import site.
105
+ import { PLANS_REL, isScratchPlanName, plansInFlight } from './plan-files.mjs';
106
+ import { shellQuoteArg } from './repo-lex.mjs';
107
+
108
+ export { PLANS_REL, isScratchPlanName, plansInFlight, shellQuoteArg };
84
109
  // The canonical review-domain primitives live in the core-evidence DAG bottom (ONE home for the
85
110
  // fingerprint, the receipt read path, and the attesting predicate); this module RE-EXPORTS its
86
111
  // historical public API from there and consumes the degrade records the same store owns.
@@ -110,7 +135,6 @@ export {
110
135
  readReceipts,
111
136
  };
112
137
 
113
- export const PLANS_REL = 'docs/plans';
114
138
  const ACTIVITY = 'plan-execution';
115
139
  const SLOT = 'review';
116
140
  const GIT_MAX_BUFFER = 256 * 1024 * 1024; // a full-tree diff can be large; never truncate silently
@@ -164,11 +188,6 @@ export const countNeverCommittableUntracked = (cwd, { lstat = lstatSync } = {})
164
188
  }).length;
165
189
  };
166
190
 
167
- // Shell-quote one argument for a COPY-PASTE advisory command: plain safe tokens stay bare; anything
168
- // else rides single quotes (a space/metacharacter path must never render a dead or unsafe paste —
169
- // Exported for the sandbox-masks probe, which renders the same apply one-liner.
170
- export const shellQuoteArg = (s) => (/^[A-Za-z0-9_/.\-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`);
171
-
172
191
  const maskAdvisoryLine = (state) =>
173
192
  state.maskedUntracked > 0
174
193
  ? `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`
@@ -195,30 +214,31 @@ export const quoteReportName = (name) =>
195
214
  (value) => `\\u${value.codePointAt(0).toString(16).padStart(4, '0')}`,
196
215
  );
197
216
 
198
- // Scratch by the naming convention: EXECUTE-/FEEDBACK- prefixes, or a name carrying PROMPT/prompt/
199
- // handoff. queue.md is the series index, never a plan.
200
- export const isScratchPlanName = (name) =>
201
- name === 'queue.md' ||
202
- name.startsWith('EXECUTE-') ||
203
- name.startsWith('FEEDBACK-') ||
204
- name.includes('PROMPT') ||
205
- name.includes('prompt') ||
206
- name.includes('handoff');
207
-
208
- // The in-flight plan files: top-level docs/plans/*.md minus queue.md minus scratch. [] when the
209
- // directory is absent (no plans → nothing in flight). STRING-typed for every consumer.
210
- export const plansInFlight = (cwd, readdir = readdirSync) => {
211
- let entries;
212
- try {
213
- entries = readdir(join(cwd, PLANS_REL), { withFileTypes: true });
214
- } catch {
215
- return [];
216
- }
217
- return entries
218
- .filter((e) => e.isFile() && e.name.endsWith('.md') && !isScratchPlanName(e.name))
219
- .map((e) => e.name)
220
- .sort();
221
- };
217
+
218
+ // The in-flight-plan adopted-chain coverage map (P13/P19): ONE bounded read + ONE hash per plan
219
+ // file; coverage requires the frontmatter planId, the FULL content digest, and the owner to match
220
+ // the adoption record — a mismatch on any axis refuses internal-only arming by name.
221
+ export const computePlanAdoptionCoverage = ({ root, plans, records, owner, readFile = readFileSync }) =>
222
+ plans.map((plan) => {
223
+ let bytes;
224
+ try {
225
+ bytes = readFile(join(root, PLANS_REL, plan));
226
+ } catch (err) {
227
+ return { plan, planId: null, covered: false, reason: `the plan file is unreadable (${(err && err.code) || (err && err.message) || err})` };
228
+ }
229
+ const buf = typeof bytes === 'string' ? Buffer.from(bytes) : bytes;
230
+ const planId = readPlanFrontmatterId(buf.toString('utf8'));
231
+ 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' };
232
+ const adoptionRecord = records.find((r) => r.kind === CHAIN_KIND && r.purpose === 'adoption' && r.planId === planId);
233
+ if (adoptionRecord === undefined) return { plan, planId, covered: false, reason: `no adoption record for planId "${planId}"` };
234
+ if (adoptionRecord.planDigest !== createHash('sha256').update(buf).digest('hex')) {
235
+ return { plan, planId, covered: false, reason: 'the plan content no longer matches its adoption record (edited after adoption)' };
236
+ }
237
+ if (adoptionRecord.owner !== owner) {
238
+ 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` };
239
+ }
240
+ return { plan, planId, covered: true };
241
+ });
222
242
 
223
243
  // ── receipts (path + reader re-exported from the core-evidence one-home above) ──────
224
244
 
@@ -261,29 +281,10 @@ export const backendReceiptStatus = (receipts, backend, fingerprint) => {
261
281
  // ── obligations from the CONFIGURED recipe + the D3(b) degrade-record escape ────────
262
282
 
263
283
  // Obligations derive from the CONFIGURED recipe — the RAW orchestration.json value — never the
264
- // readiness-degraded effective recipe: the resolver degrades council→reviewed→solo BEFORE any
265
- // check would see the missing backend, which would silently drop an obligation (a computed
266
- // readiness-degrade NEVER silently becomes solo). The resolver stays for display/diagnostics
267
- // only. The computed DEFAULT (absent config) is readiness-aware by design — a default never
268
- // mints an unsatisfiable obligation; an EXPLICIT configured recipe never degrades here.
269
- // solo → no obligation (the existing honest exit-0 contract);
270
- // reviewed → ONE ship-class attestation from ANY review-capable backend (minShip 1);
271
- // council → EVERY review-capable backend attests ship-class OR carries a current-tree degrade
272
- // record — and NEVER all degraded (minShip 1 stands whenever >=1 backend is configured).
273
- export const requiredBackendsForConfiguredRecipe = ({ config, readiness = [], detectionFailed = false } = {}) => {
274
- const configured = config?.[ACTIVITY]?.[SLOT];
275
- const providers = Object.values(DISPLAY_ALIASES); // every review-capable backend, codex first
276
- if (configured == null && detectionFailed) {
277
- // No config + no readiness signal: the computed default is UNKNOWABLE — fail closed upstream.
278
- return { recipe: null, source: 'default', backends: [], minShip: 0, perBackend: false, unknowable: true };
279
- }
280
- const anyReady = readiness.some((b) => b.readiness === READY);
281
- const recipe = configured ?? (anyReady ? 'reviewed' : 'solo');
282
- const source = configured != null ? 'config' : 'default';
283
- if (recipe === 'solo') return { recipe, source, backends: [], minShip: 0, perBackend: false, unknowable: false };
284
- if (recipe === 'council') return { recipe, source, backends: providers, minShip: 1, perBackend: true, unknowable: false };
285
- return { recipe, source, backends: providers, minShip: 1, perBackend: false, unknowable: false };
286
- };
284
+ // readiness-degraded effective recipe (a computed readiness-degrade NEVER silently becomes solo).
285
+ // The derivation is homed in recipes.mjs (cycle-free) so flow-check's exact-coverage rungs consume
286
+ // the SAME consumed backend set; re-exported here for this module's historical public API.
287
+ export { requiredBackendsForConfiguredRecipe };
287
288
 
288
289
  // degradeRecordSet — the D3(b) escape: an EXPLICIT per-backend, per-tree degrade RECORD in the
289
290
  // core-evidence store is the ONLY exemption lane. Fail-closed: an unreadable/malformed store
@@ -307,7 +308,7 @@ export const degradeRecordSet = ({ cwd, env = process.env, fingerprint }) => {
307
308
  // work-tree ROOT when one exists — the fingerprint is root-anchored, so a subdirectory invocation
308
309
  // must read the same config/plans or a dirty unreceipted tree could false-PASS as "no plan in
309
310
  // flight". Outside a git tree the cwd is the only anchor (and --check exits 0).
310
- export const buildState = ({ cwd, env = process.env, detect = detectBackends, lstat = lstatSync } = {}) => {
311
+ export const buildState = ({ cwd, env = process.env, detect = detectBackends, lstat = lstatSync, readFile = readFileSync } = {}) => {
311
312
  const root = gitLine(['rev-parse', '--show-toplevel'], cwd) ?? cwd;
312
313
  const { config, source: configSource } = loadConfig(root);
313
314
  let detection = [];
@@ -338,12 +339,89 @@ export const buildState = ({ cwd, env = process.env, detect = detectBackends, ls
338
339
  const base = resolveBase(cwd);
339
340
  const degrade = degradeRecordSet({ cwd, env, fingerprint });
340
341
  const degradedExempt = requiredBackends.filter((b) => degrade.set.has(b));
342
+ // ── the Phase-2 flow arms' state (two-tier activation, P3) ──
343
+ // Tier 1 is store-file PRESENCE: no file ⇒ every field below stays inert and the decision is
344
+ // byte-identical to the pre-flow checker (the unarmed fast path also reads NO plan file).
345
+ const flowPath = resolveFlowStorePath(cwd, env);
346
+ const flowStat = (() => {
347
+ if (flowPath == null) return null;
348
+ try {
349
+ return lstat(flowPath);
350
+ } catch (err) {
351
+ return err && err.code === 'ENOENT' ? null : 'unstatable';
352
+ }
353
+ })();
354
+ const flowPresent = flowStat !== null;
355
+ const flowRead = flowPresent && flowStat !== 'unstatable' ? readFlowStore(flowPath) : null;
356
+ const flowBrokenReason = !flowPresent ? null
357
+ : flowStat === 'unstatable' ? 'the store leaf cannot be stat-ed (fail closed)'
358
+ : flowRead.readError != null ? `read error: ${flowRead.readError}`
359
+ : flowRead.malformed > 0 ? `${flowRead.malformed} malformed line(s) (${flowRead.malformedReasons[0]})`
360
+ : null;
361
+ const flowArmed = flowPresent && flowBrokenReason == null
362
+ && flowRead.records.some((r) => r.kind === CHAIN_KIND && r.purpose === 'adoption');
363
+ const flowOwner = flowArmed ? deriveFlowOwner(cwd) : null;
364
+ const planCoverage = flowArmed
365
+ ? computePlanAdoptionCoverage({ root, plans, records: flowRead.records, owner: flowOwner, readFile })
366
+ : [];
367
+ // The lens-substitution rung (#15/#3): an attestation whose lens set claims a review provider's
368
+ // slot without a then-active down-mark never counts — and its refusal is NAMED, never silent
369
+ // (lensRefusedAttestations feeds the internal-only floor's failure reason).
370
+ const lensRefusedAttestations = [];
371
+ const attestedPlanIds = flowArmed
372
+ ? [...new Set(authoritativeFlowRecords(flowRead.records)
373
+ .filter((r) => r.kind === 'internal-attestation' && r.base === base && r.fingerprint === fingerprint)
374
+ .filter((r) => {
375
+ const lensCheck = evaluateInternalAttestationLenses({ record: r, records: flowRead.records, providerBackends: Object.values(DISPLAY_ALIASES) });
376
+ if (!lensCheck.ok) {
377
+ lensRefusedAttestations.push({ planId: r.planId, reason: lensCheck.reason });
378
+ return false;
379
+ }
380
+ return true;
381
+ })
382
+ .map((r) => r.planId))]
383
+ : [];
384
+ const flowConfig = config?.flow ?? null;
385
+ // #61: the SHARED relied-on selector (one home with the flow-check coverage rung) — an unbroken
386
+ // declared-path bookkeeping-delta chain lifts the backend's last receipt; any break, fork, or
387
+ // cap exhaustion stays stale.
388
+ const liftStale = (row, backend) => {
389
+ if (!flowArmed || row.state !== 'stale') return row;
390
+ const selected = selectReliedOnReceipt({
391
+ receipts, backend, tree: { base, fingerprint },
392
+ records: flowRead.records,
393
+ declaredPaths: [flowConfig?.debtQueue, flowConfig?.convergenceSummary].filter((p) => typeof p === 'string'),
394
+ refreshCap: flowConfig?.councilRounds,
395
+ });
396
+ if (selected.receipt == null || selected.lifted === 0) return row;
397
+ return {
398
+ ...row, state: 'current', verdict: selected.receipt.verdict ?? 'unknown',
399
+ shipClass: isShipVerdict(selected.receipt.verdict), grounded: true,
400
+ timestamp: selected.receipt.timestamp ?? null, deltaLift: selected.lifted,
401
+ };
402
+ };
403
+ // #56/#38: a standing CURRENT-tree veto consults its override instance; a delta-lifted veto has
404
+ // no override lane here (the bound set pins the veto receipt's own tree).
405
+ const consumeOverride = (row) => {
406
+ if (!flowArmed || !(row.state === 'current' && !row.shipClass) || row.deltaLift != null) return row;
407
+ const own = receipts.filter((r) => r.backend === row.backend);
408
+ const summary = summarizeReviewReceiptsForTree(own, fingerprint);
409
+ if (summary.state !== 'current') return row;
410
+ const lift = evaluateVetoOverride({ records: flowRead.records, vetoReceipt: summary.receipt, tree: { base, fingerprint } });
411
+ return lift.lifted ? { ...row, overrideLabel: lift.label } : row;
412
+ };
413
+ const armedBackends = backends.map((b) => consumeOverride(liftStale(b, b.backend)));
414
+ // The solo-class veto probe (#48): under an armed solo recipe every review-capable backend's
415
+ // CURRENT-tree receipt is still consulted, so a recipe flip to solo never buries a standing veto.
416
+ const soloVetoRows = flowArmed && obligations.recipe === 'solo'
417
+ ? Object.values(DISPLAY_ALIASES).map((b) => consumeOverride(liftStale({ backend: b, ...backendReceiptStatus(receipts, b, fingerprint) }, b)))
418
+ : [];
341
419
  return {
342
420
  resolved,
343
421
  configSource,
344
422
  obligations,
345
423
  requiredBackends,
346
- backends,
424
+ backends: armedBackends,
347
425
  plans,
348
426
  root,
349
427
  fingerprint,
@@ -360,6 +438,15 @@ export const buildState = ({ cwd, env = process.env, detect = detectBackends, ls
360
438
  degradedExempt,
361
439
  maskedUntracked: countNeverCommittableUntracked(cwd, { lstat }),
362
440
  detectionWarning,
441
+ anyReviewerReady: detection.some((b) => b.readiness === READY),
442
+ flowPresent,
443
+ flowArmed,
444
+ flowBrokenReason,
445
+ flowOwner,
446
+ planCoverage,
447
+ attestedPlanIds,
448
+ lensRefusedAttestations,
449
+ soloVetoRows,
363
450
  };
364
451
  };
365
452
 
@@ -411,7 +498,7 @@ export const decideCheck = (state) => {
411
498
  if (state.obligations.unknowable) {
412
499
  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}` };
413
500
  }
414
- if (state.obligations.recipe === 'solo') {
501
+ if (state.obligations.recipe === 'solo' && !state.flowArmed && state.flowBrokenReason == null) {
415
502
  const why = state.obligations.source === 'config'
416
503
  ? `configured ${ACTIVITY}.${SLOT} recipe is solo`
417
504
  : `no reviewer backend is ready — the computed ${ACTIVITY}.${SLOT} default is solo`;
@@ -427,9 +514,54 @@ export const decideCheck = (state) => {
427
514
  const named = state.plans.map((p) => quoteReportName(p)).join(', ');
428
515
  return { code: 0, reason: `${CLEAN_TREE_PASS} — ${state.plans.length} plan(s) in flight: ${named} — ${LATENT_ARM_NOTICE}${earlyNotes}` };
429
516
  }
517
+ // Tier-1 fail-closed (P3): a PRESENT flow store must read clean before the decision leans on
518
+ // receipts — the flow arms cannot be evaluated over a store of unknown content.
519
+ if (state.flowBrokenReason != null) {
520
+ return { code: 1, reason: `the flow store is unavailable (${state.flowBrokenReason}) — the flow arms fail closed; inspect the store${earlyNotes}` };
521
+ }
522
+ // The no-reviewers-configured floor (#34/#43): an ARMED flow makes the reduced record set an
523
+ // OBLIGATION for the solo class — every in-flight plan covered by an adopted chain AND
524
+ // internally attested at the current tree; a standing current-tree veto still blocks (#48),
525
+ // whatever the recipe consults, so a recipe flip to solo can never bury one.
526
+ if (state.obligations.recipe === 'solo') {
527
+ if (state.receiptsReadError != null) {
528
+ 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 ${state.receiptsPath}${earlyNotes}` };
529
+ }
530
+ const standing = state.soloVetoRows.filter((b) => (b.state === 'current' && !b.shipClass && !b.overrideLabel) || b.state === 'unrecognized-verdict');
531
+ if (standing.length > 0) {
532
+ // `veto` marks the AUTHORITATIVE-veto class only (a recognized negative) — the Decision-4
533
+ // --await early exit keys on it; an unrecognized verdict stays a plain fail-closed refusal.
534
+ const authoritative = standing.some((b) => b.state === 'current' && !b.shipClass);
535
+ return { code: 1, reason: `${standing.map((b) => backendFailurePart(b, state)).join('; ')} — a standing veto blocks the internal-only floor (#48)${earlyNotes}`, ...(authoritative ? { veto: true } : {}) };
536
+ }
537
+ const uncovered = state.planCoverage.filter((p) => !p.covered);
538
+ if (uncovered.length > 0) {
539
+ 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}` };
540
+ }
541
+ const missing = state.planCoverage.filter((p) => !state.attestedPlanIds.includes(p.planId));
542
+ if (missing.length > 0) {
543
+ // A lens-refused attestation is a NAMED cause, never a silent gap — the refusal rides the
544
+ // floor's failure reason for exactly the plans it starved.
545
+ const lensNotes = (state.lensRefusedAttestations ?? []).filter((x) => missing.some((p) => p.planId === x.planId));
546
+ const lensSuffix = lensNotes.length > 0 ? `; ${lensNotes.map((x) => x.reason).join('; ')}` : '';
547
+ 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}` };
548
+ }
549
+ const floorLabels = [
550
+ ...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)`),
551
+ ...state.soloVetoRows.filter((b) => b.overrideLabel).map((b) => b.overrideLabel),
552
+ 'internal-only (downgraded class)',
553
+ ];
554
+ const liftedTail = floorLabels.length > 1 ? ` — ${floorLabels.slice(0, -1).join('; ')}` : '';
555
+ return {
556
+ code: 0,
557
+ 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}`,
558
+ flowLabels: floorLabels,
559
+ };
560
+ }
430
561
  const exempt = new Set(state.degradedExempt);
431
562
  const satisfied = state.backends.filter((b) => b.state === 'current' && b.shipClass);
432
- const vetoed = state.backends.filter((b) => b.state === 'current' && !b.shipClass);
563
+ const vetoed = state.backends.filter((b) => b.state === 'current' && !b.shipClass && !b.overrideLabel);
564
+ const overridden = state.backends.filter((b) => b.state === 'current' && !b.shipClass && b.overrideLabel);
433
565
  // The marker note is PATH-AWARE (no silent rejections on any exit): it counts every backend's
434
566
  // untrusted-marker exclusions EXCEPT those whose printed part already names them — i.e. only a
435
567
  // PRINTED `rejected` row (backendFailurePart's rejectionCause) suppresses its own counts; a
@@ -450,38 +582,54 @@ export const decideCheck = (state) => {
450
582
  const unrecognized = state.backends.filter((b) => b.state === 'unrecognized-verdict');
451
583
  const unconditional = [...vetoed, ...unrecognized];
452
584
  if (unconditional.length > 0) {
453
- return { code: 1, reason: `${unconditional.map((b) => backendFailurePart(b, state)).join('; ')}${notesFor(new Set(unconditional.map((b) => b.backend)))}` };
585
+ // `veto` marks the AUTHORITATIVE-veto class only (Decision 4): --await ends the wait loudly on
586
+ // it — a landed negative is the dispatched review's ANSWER, never a condition to out-wait.
587
+ return { code: 1, reason: `${unconditional.map((b) => backendFailurePart(b, state)).join('; ')}${notesFor(new Set(unconditional.map((b) => b.backend)))}`, ...(vetoed.length > 0 ? { veto: true } : {}) };
454
588
  }
589
+ // The armed-flow PASS labels (#61/#38): the delta-lift and override facts ride every PASS reason
590
+ // and the flowLabels field the commit-guard PASS line consumes. Empty (and absent) unarmed.
591
+ const flowNotes = [
592
+ ...state.backends.filter((b) => b.deltaLift != null)
593
+ .map((b) => `${b.backend}: receipt lifted to CURRENT through an unbroken bookkeeping-delta chain (${b.deltaLift} link(s), #61)`),
594
+ ...overridden.map((b) => b.overrideLabel),
595
+ ];
596
+ const flowSuffix = flowNotes.length > 0 ? ` — ${flowNotes.join('; ')}` : '';
597
+ const pass = (reason) => (state.flowArmed ? { code: 0, reason, flowLabels: flowNotes } : { code: 0, reason });
598
+ const perBackendFailing = state.backends.filter((b) => !(b.state === 'current' && b.shipClass) && !exempt.has(b.backend) && !b.overrideLabel);
455
599
  // Never all degraded: >=1 ship-class attestation whenever >=1 backend is configured. An
456
600
  // already-exempt backend renders its own honest part — never the "record an explicit degrade"
457
601
  // recovery it has already taken.
458
602
  if (satisfied.length < state.obligations.minShip) {
459
- const failing = state.backends.filter((b) => !(b.state === 'current' && b.shipClass));
603
+ const failing = state.backends.filter((b) => !(b.state === 'current' && b.shipClass) && !b.overrideLabel);
460
604
  const allExempt = failing.length > 0 && failing.every((b) => exempt.has(b.backend));
461
- const head = allExempt
462
- ? `every configured backend is degrade-recorded for this tree — never all degraded: >=1 non-degraded ship-class attestation is required; run at least one real review`
463
- : failing
464
- .map((b) => (exempt.has(b.backend)
465
- ? `${b.backend}: degrade-recorded for this tree — a degrade never counts toward the >=1 ship-class floor; run a real review on another backend`
466
- : backendFailurePart(b, state)))
467
- .join('; ');
605
+ const head = failing.length === 0
606
+ ? 'every configured backend is veto-overridden for this tree — >=1 non-overridden ship-class attestation is required; run at least one real review'
607
+ : allExempt
608
+ ? `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`
609
+ : failing
610
+ .map((b) => (exempt.has(b.backend)
611
+ ? `${b.backend}: degrade-recorded for this tree — a degrade never counts toward the >=1 ship-class floor; run a real review on another backend`
612
+ : backendFailurePart(b, state)))
613
+ .join('; ');
468
614
  // Only backends that actually rendered their backendFailurePart suppress their counts — an
469
615
  // exempt row prints the degrade string (no rejectionCause), so its exclusions stay named.
470
616
  return { code: 1, reason: `${head}${notesFor(new Set(failing.filter((b) => !exempt.has(b.backend)).map((b) => b.backend)))}` };
471
617
  }
472
618
  if (state.obligations.perBackend) {
473
- // Council: EVERY configured backend must attest ship-class OR carry a current-tree degrade record.
474
- const failing = state.backends.filter((b) => !(b.state === 'current' && b.shipClass) && !exempt.has(b.backend));
475
- if (failing.length > 0) {
476
- return { code: 1, reason: `${failing.map((b) => backendFailurePart(b, state)).join('; ')}${notesFor(new Set(failing.map((b) => b.backend)))}` };
619
+ // Council: EVERY configured backend must attest ship-class OR carry a current-tree degrade
620
+ // record OR a lifted maintainer-override.
621
+ if (perBackendFailing.length > 0) {
622
+ return { code: 1, reason: `${perBackendFailing.map((b) => backendFailurePart(b, state)).join('; ')}${notesFor(new Set(perBackendFailing.map((b) => b.backend)))}` };
477
623
  }
478
- if (exempt.size === 0) {
479
- return { code: 0, reason: `every configured backend attests ship-class for the current tree (${state.requiredBackends.join(' + ')})${notesFor(NONE_PRINTED)}` };
624
+ if (exempt.size === 0 && overridden.length === 0) {
625
+ return pass(`every configured backend attests ship-class for the current tree (${state.requiredBackends.join(' + ')})${flowSuffix}${notesFor(NONE_PRINTED)}`);
480
626
  }
481
- return { code: 0, reason: `council satisfied: ship-class attestation(s) from ${satisfied.map((b) => b.backend).join(' + ')}; degrade-recorded for this tree: ${[...exempt].join(', ')}${notesFor(NONE_PRINTED)}` };
627
+ const clauses = [`council satisfied: ship-class attestation(s) from ${satisfied.map((b) => b.backend).join(' + ')}`];
628
+ if (exempt.size > 0) clauses.push(`degrade-recorded for this tree: ${[...exempt].join(', ')}`);
629
+ return pass(`${clauses.join('; ')}${flowSuffix}${notesFor(NONE_PRINTED)}`);
482
630
  }
483
631
  // Reviewed: >=1 ship-class attestation from any review-capable backend satisfies.
484
- return { code: 0, reason: `reviewed satisfied: ship-class attestation from ${satisfied.map((b) => b.backend).join(' + ')} for the current tree${notesFor(NONE_PRINTED)}` };
632
+ return pass(`reviewed satisfied: ship-class attestation from ${satisfied.map((b) => b.backend).join(' + ')} for the current tree${flowSuffix}${notesFor(NONE_PRINTED)}`);
485
633
  };
486
634
 
487
635
  // ── rendering ───────────────────────────────────────────────────────────────────────
@@ -524,10 +672,12 @@ const formatHuman = (state, check) => {
524
672
  const excludedTag = b.probeExcluded || b.markerRejected || b.unmarkedRejected
525
673
  ? ` [excluded: ${b.probeExcluded} probe, ${b.markerRejected} malformed-marker, ${b.unmarkedRejected} unmarked]`
526
674
  : '';
675
+ const liftTag = b.deltaLift != null ? ` — lifted to CURRENT through ${b.deltaLift} bookkeeping-delta link(s) (#61)` : '';
676
+ const overrideTag = b.overrideLabel ? ` — ${b.overrideLabel}` : '';
527
677
  // ⊘ only where the escape actually applies: a PRODUCED receipt outranks the record — a
528
- // negative/unknown verdict keeps its ✗ (the degrade lifts neither).
529
- const escapeApplies = exempt.has(b.backend) && b.state !== 'current' && b.state !== 'unrecognized-verdict';
530
- lines.push(` ${escapeApplies ? '⊘' : glyphFor(b)} ${b.backend}: ${detail}${excludedTag}${exemptTag}`);
678
+ // negative/unknown verdict keeps its ✗ (a degrade lifts neither; a maintainer-override does).
679
+ const escapeApplies = (exempt.has(b.backend) && b.state !== 'current' && b.state !== 'unrecognized-verdict') || Boolean(b.overrideLabel);
680
+ lines.push(` ${escapeApplies ? '⊘' : glyphFor(b)} ${b.backend}: ${detail}${liftTag}${overrideTag}${excludedTag}${exemptTag}`);
531
681
  }
532
682
  lines.push(` check: ${check.code === 0 ? 'PASS' : 'FAIL'} — ${check.reason}`);
533
683
  return lines.join('\n');
@@ -556,6 +706,12 @@ not-a-git-tree / obligations satisfied (reviewed: >=1 ship-class attestation;
556
706
  council: every backend ship-class or degrade-recorded, >=1 real ship); 1 on a veto, an
557
707
  unrecognized verdict, a missing/stale/ungrounded/probe-only backend without a degrade record,
558
708
  an all-degraded tree, or a down detector with no configured recipe.
709
+ Under an ARMED flow store (flow-orchestration Plan 3): a stale receipt can lift to CURRENT
710
+ through an unbroken bookkeeping-delta chain (#61), a maintainer-override can lift a standing
711
+ veto (#38/#56), and the SOLO obligations class gains the reduced internal-only floor as an
712
+ OBLIGATION (covered + internally attested plans; a standing veto still blocks, #43/#68/#48) —
713
+ each labeled in the check reason; a present-but-malformed flow store fails the dirty-tree
714
+ check closed; no store file means byte-identical pre-flow behavior.
559
715
  Declare it as a project gate by hand (docs/ai/gates.json) or via the
560
716
  explicit-consent init preview (tools/gates-init.mjs) — never without consent.
561
717
 
@@ -576,7 +732,7 @@ export const main = (argv, ctx = {}) => {
576
732
  if (argv.includes('--help') || argv.includes('-h')) return { code: 0, stdout: HELP, stderr: '' };
577
733
  const unknown = argv.find((a) => !KNOWN_ARGS.has(a));
578
734
  if (unknown !== undefined) throw fail(2, `unknown argument: ${unknown}`);
579
- const state = buildState({ cwd, env, detect, lstat: ctx.lstat });
735
+ const state = buildState({ cwd, env, detect, lstat: ctx.lstat, readFile: ctx.readFile });
580
736
  const check = decideCheck(state);
581
737
  // The mask advisory is NON-FAILING by contract: one notice line, never an exit-code arm.
582
738
  const advisory = maskAdvisoryLine(state);
@@ -645,6 +801,10 @@ export const mainAwait = async (argv, ctx = {}) => {
645
801
  const check = decideCheck(buildState({ cwd, env, detect }));
646
802
  lastReason = check.reason;
647
803
  if (check.code === 0) return { code: 0, stdout: `review-state --await: READY — ${check.reason}`, stderr: '' };
804
+ // Decision 4 (#50): an AUTHORITATIVE veto ends the wait loudly BEFORE the deadline — the
805
+ // dispatched review answered with a landed negative; classifying that as a timeout was the
806
+ // misclassification this amendment closes. Only a fresh review can move it, never waiting.
807
+ if (check.veto === true) return { code: 1, stdout: '', stderr: `review-state --await: VETO — ${check.reason}` };
648
808
  await sleep(Math.min(pollMs, timeoutMs - elapsed));
649
809
  }
650
810
  } catch (err) {