@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.
- package/CHANGELOG.md +19 -0
- package/SKILL.md +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/tools/review-state-await.mjs +77 -0
- package/tools/review-state-build.mjs +163 -0
- package/tools/review-state-flow.mjs +110 -0
- package/tools/review-state-judge.mjs +331 -0
- package/tools/review-state-render.mjs +67 -0
- package/tools/review-state.mjs +24 -724
package/tools/review-state.mjs
CHANGED
|
@@ -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 {
|
|
105
|
-
import {
|
|
106
|
-
import {
|
|
107
|
-
import {
|
|
108
|
-
import {
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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`);
|