@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.
- package/CHANGELOG.md +55 -0
- package/SKILL.md +13 -1
- package/bridges/antigravity-cli-bridge/SKILL.md +14 -3
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +220 -30
- package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +264 -8
- package/bridges/antigravity-cli-bridge/bin/agy.sh +12 -2
- package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +18 -0
- package/bridges/antigravity-cli-bridge/capability.json +19 -13
- package/bridges/antigravity-cli-bridge/references/driving-agy.md +3 -2
- package/bridges/codex-cli-bridge/SKILL.md +8 -5
- package/bridges/codex-cli-bridge/bin/codex-exec.sh +3 -2
- package/bridges/codex-cli-bridge/bin/codex-review.sh +205 -34
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +276 -5
- package/bridges/codex-cli-bridge/capability.json +8 -6
- package/bridges/codex-cli-bridge/references/driving-codex.md +2 -2
- package/bridges/codex-cli-bridge/references/sandbox-and-flags.md +2 -2
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/flow-writer.md +37 -0
- package/references/modes/gates.md +4 -4
- package/references/modes/procedures.md +4 -2
- package/references/modes/receipt-deadline.md +16 -0
- package/references/modes/review-state.md +1 -1
- package/references/modes/set-flow.md +22 -0
- package/tools/cheap-agents.mjs +8 -2
- package/tools/commands.mjs +24 -2
- package/tools/commit-guard.mjs +44 -9
- package/tools/core-evidence.mjs +25 -22
- package/tools/detect-backends.mjs +32 -11
- package/tools/doc-parity.mjs +21 -6
- package/tools/flow-check.mjs +806 -0
- package/tools/flow-record.mjs +795 -0
- package/tools/flow-store-read.mjs +114 -0
- package/tools/flow-store.mjs +1178 -0
- package/tools/flow-writer.mjs +1265 -0
- package/tools/fs-read-nofollow.mjs +128 -0
- package/tools/gates-declaration.mjs +184 -0
- package/tools/gates-init.mjs +59 -17
- package/tools/orchestration-config.mjs +87 -10
- package/tools/orchestration-write.mjs +3 -3
- package/tools/plan-files.mjs +35 -0
- package/tools/procedures.mjs +75 -11
- package/tools/receipt-deadline.mjs +242 -0
- package/tools/recipes.mjs +21 -0
- package/tools/repo-lex.mjs +22 -0
- package/tools/review-state.mjs +240 -80
- package/tools/run-gates.mjs +361 -139
- package/tools/set-flow.mjs +465 -0
- package/tools/velocity-profile.mjs +8 -2
package/tools/review-state.mjs
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
199
|
-
//
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
|
|
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
|
-
|
|
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 =
|
|
462
|
-
?
|
|
463
|
-
:
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
|
474
|
-
|
|
475
|
-
if (
|
|
476
|
-
return { code: 1, reason: `${
|
|
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
|
|
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
|
-
|
|
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
|
|
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 ✗ (
|
|
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) {
|