@sabaiway/agent-workflow-kit 2.1.0 → 3.1.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 +94 -1
- package/README.md +7 -5
- package/SKILL.md +16 -8
- package/bin/install.mjs +11 -2
- package/bridges/antigravity-cli-bridge/SKILL.md +8 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review-honesty.test.mjs +163 -0
- package/bridges/antigravity-cli-bridge/bin/agy-review-model-screen.test.mjs +76 -0
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +80 -15
- package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +171 -22
- package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +59 -41
- package/bridges/antigravity-cli-bridge/capability.json +3 -2
- package/bridges/codex-cli-bridge/SKILL.md +8 -1
- package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +67 -22
- package/bridges/codex-cli-bridge/bin/codex-review-honesty.test.mjs +156 -0
- package/bridges/codex-cli-bridge/bin/codex-review.sh +71 -10
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +158 -27
- package/bridges/codex-cli-bridge/capability.json +3 -2
- package/capability.json +1 -1
- package/migrations/3.0.0-hardened-core-loop.md +44 -0
- package/migrations/README.md +1 -1
- package/package.json +2 -2
- package/references/hooks/gate-approve.mjs +1 -1
- package/references/modes/bootstrap.md +3 -3
- package/references/modes/commit-guard.md +19 -0
- package/references/modes/core-evidence.md +15 -0
- package/references/modes/coverage-check.md +12 -0
- package/references/modes/doc-parity.md +7 -6
- package/references/modes/gates.md +7 -6
- package/references/modes/grounding.md +1 -2
- package/references/modes/hook.md +1 -1
- package/references/modes/recommendations.md +1 -0
- package/references/modes/review-state.md +3 -3
- package/references/modes/upgrade.md +6 -6
- package/references/modes/velocity.md +2 -2
- package/references/modes/worktrees.md +120 -0
- package/references/scripts/archive-decisions.mjs +3 -3
- package/references/scripts/archive-decisions.test.mjs +5 -5
- package/references/scripts/check-docs-size-cli.test.mjs +41 -0
- package/references/scripts/check-docs-size.mjs +46 -29
- package/references/scripts/install-git-hooks-repo-exec.test.mjs +82 -0
- package/references/scripts/install-git-hooks.mjs +90 -18
- package/references/scripts/install-git-hooks.test.mjs +102 -0
- package/references/scripts/migrate-gates-branches.test.mjs +157 -0
- package/references/scripts/migrate-gates.mjs +395 -0
- package/references/scripts/migrate-gates.test.mjs +284 -0
- package/references/templates/agent_rules.md +2 -2
- package/tools/ack-write.mjs +1 -1
- package/tools/atomic-write.mjs +2 -2
- package/tools/autonomy-config.mjs +1 -1
- package/tools/autonomy-doctor.mjs +2 -2
- package/tools/autonomy-write.mjs +1 -1
- package/tools/bridge-settings-read.mjs +1 -1
- package/tools/bridge-settings.mjs +1 -1
- package/tools/changed-surface.mjs +16 -77
- package/tools/cheap-agents.mjs +2 -2
- package/tools/commands.mjs +26 -10
- package/tools/commit-guard.mjs +167 -0
- package/tools/core-evidence.mjs +914 -0
- package/tools/coverage-check.mjs +260 -0
- package/tools/delegation.mjs +1 -1
- package/tools/detect-backends.mjs +3 -3
- package/tools/doc-parity.mjs +11 -27
- package/tools/engine-source.mjs +1 -1
- package/tools/family-members.mjs +1 -1
- package/tools/family-registry.mjs +1 -1
- package/tools/fs-safe.mjs +1 -1
- package/tools/gate-hook.mjs +1 -1
- package/tools/{seed-gates.mjs → gates-init.mjs} +58 -52
- package/tools/grounding.mjs +19 -96
- package/tools/hide-footprint.mjs +1 -1
- package/tools/inject-methodology.mjs +72 -41
- package/tools/known-footprint.mjs +1 -1
- package/tools/labels.mjs +1 -1
- package/tools/lcov.mjs +6 -10
- package/tools/lens-region.mjs +1 -1
- package/tools/manifest/validate.mjs +19 -0
- package/tools/migrate-adr-store.mjs +5 -5
- package/tools/orchestration-config.mjs +1 -1
- package/tools/orchestration-write.mjs +1 -1
- package/tools/presentation.mjs +1 -1
- package/tools/procedures.mjs +3 -3
- package/tools/recipes.mjs +62 -11
- package/tools/recommendations.mjs +97 -14
- package/tools/release-scan.mjs +10 -3
- package/tools/renderers.mjs +1 -1
- package/tools/review-state.mjs +291 -343
- package/tools/run-gates.mjs +216 -92
- package/tools/sandbox-masks.mjs +14 -14
- package/tools/semver-lite.mjs +1 -1
- package/tools/set-autonomy.mjs +1 -1
- package/tools/set-recipe.mjs +2 -2
- package/tools/setup-backends.mjs +1 -1
- package/tools/surface.mjs +1 -1
- package/tools/uninstall.mjs +1 -1
- package/tools/velocity-profile.mjs +9 -9
- package/tools/view-model.mjs +1 -1
- package/tools/worktrees.mjs +2292 -0
- package/references/modes/fold-completeness.md +0 -30
- package/references/modes/review-ledger.md +0 -34
- package/references/templates/verification-profile.json +0 -10
- package/tools/fold-completeness-run.mjs +0 -1120
- package/tools/fold-completeness.mjs +0 -672
- package/tools/review-ledger-core.mjs +0 -428
- package/tools/review-ledger-write.mjs +0 -647
- package/tools/review-ledger.mjs +0 -630
- package/tools/sarif.mjs +0 -52
- package/tools/verification-profile.mjs +0 -219
package/tools/review-state.mjs
CHANGED
|
@@ -1,32 +1,39 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// review-state.mjs — the read-only review-receipt checker behind `/agent-workflow-kit review-state`
|
|
3
|
-
// (AD-038). It makes "reviewed ≠ shipped" mechanically
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
3
|
+
// (AD-038; HARDENED by the strip-the-kit core, D3(b)). It makes "reviewed ≠ shipped" mechanically
|
|
4
|
+
// detectable: the bridge review wrappers append one receipt line per successful review; this tool
|
|
5
|
+
// derives the review OBLIGATIONS from the CONFIGURED `plan-execution.review` recipe (the raw
|
|
6
|
+
// orchestration.json value — never the readiness-degraded effective recipe: a computed
|
|
7
|
+
// readiness-degrade NEVER silently becomes solo), recomputes the CURRENT canonical
|
|
8
|
+
// uncommitted-state fingerprint, and judges — per configured backend — the LATEST NORMAL receipt.
|
|
9
|
+
// `--check` turns the report into a gate exit code (declare it in docs/ai/gates.json).
|
|
10
10
|
//
|
|
11
11
|
// Normative `--check` exit contract (the single home of this list — SKILL.md points here):
|
|
12
|
-
// exit 0 when the
|
|
13
|
-
//
|
|
14
|
-
// top-level .md that is not queue.md and not scratch by the naming convention:
|
|
15
|
-
// EXECUTE- / FEEDBACK-, or a name containing PROMPT / prompt / handoff); when
|
|
16
|
-
// clean (nothing to review); when the cwd is not a git work tree
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
// the
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
12
|
+
// exit 0 when the CONFIGURED plan-execution.review recipe is solo (or the computed default is —
|
|
13
|
+
// absent config with no reviewer backend ready); when no plan is in flight (docs/plans/
|
|
14
|
+
// holds no top-level .md that is not queue.md and not scratch by the naming convention:
|
|
15
|
+
// prefixes EXECUTE- / FEEDBACK-, or a name containing PROMPT / prompt / handoff); when
|
|
16
|
+
// the tree is clean (nothing to review); when the cwd is not a git work tree; and when
|
|
17
|
+
// the obligations are SATISFIED: under `reviewed`, >=1 backend's latest normal receipt
|
|
18
|
+
// attests SHIP-CLASS (ship / ship with nits) for the current tree; under `council`,
|
|
19
|
+
// EVERY review-capable backend attests ship-class OR carries an explicit current-tree
|
|
20
|
+
// degrade record in the core-evidence store — and NEVER all backends (>=1 non-degraded
|
|
21
|
+
// ship-class attestation is required whenever >=1 backend is configured).
|
|
22
|
+
// exit 1 on an authoritative VETO (any configured backend's latest normal receipt carries a
|
|
23
|
+
// recognized NEGATIVE verdict — revise / rethink / rework — for the current tree; a
|
|
24
|
+
// degrade record never lifts a veto); on an UNRECOGNIZED verdict on the latest normal
|
|
25
|
+
// receipt (an `unknown` verdict never attests — fail closed, so a later unknown never
|
|
26
|
+
// lets an earlier SHIP survive); on a missing/stale/ungrounded/probe-only backend under
|
|
27
|
+
// council without a current-tree degrade record; when every configured backend is
|
|
28
|
+
// degrade-recorded (all-degraded refused); and, with NO configured recipe, when the
|
|
29
|
+
// backend detector is down (the computed default is unknowable — fail closed). An
|
|
30
|
+
// unreadable/malformed evidence store DENIES the degrade escape (fail-closed) but NEVER
|
|
31
|
+
// fails a tree whose receipts independently satisfy the gate (surfaced either way).
|
|
32
|
+
// Selection is LATEST-NORMAL-FIRST: among current-fingerprint, marker-valid, probe-free receipts
|
|
33
|
+
// the LATEST is selected and THEN judged (the verdict-vocabulary arm first, then grounding — an
|
|
34
|
+
// unrecognized verdict is an unconditional refusal that grounding never reclassifies) — so a
|
|
35
|
+
// revise-class latest VETOES an earlier ship-class one, and a probe/forged-marker receipt written
|
|
36
|
+
// after a real review never becomes the authoritative verdict.
|
|
30
37
|
// Informational receipts NEVER satisfy (nor fail) the tree check: plan/diff-mode receipts
|
|
31
38
|
// (artifact ≠ "code") and continuations (fresh:false — agy --continue/--conversation cannot attest
|
|
32
39
|
// a folded tree; only a fresh grounded re-run mints a gate-satisfying receipt).
|
|
@@ -61,28 +68,44 @@
|
|
|
61
68
|
//
|
|
62
69
|
// Read-only: never writes, never commits, never runs a subscription CLI. It DOES spawn `git`
|
|
63
70
|
// (read-only queries) to compute the fingerprint — stated honestly in the catalog. Dependency-free,
|
|
64
|
-
// Node >=
|
|
71
|
+
// Node >= 22. No side effects on import (the isDirectRun idiom).
|
|
65
72
|
|
|
66
|
-
import {
|
|
73
|
+
import { readdirSync, lstatSync } from 'node:fs';
|
|
67
74
|
import { join, dirname } from 'node:path';
|
|
68
75
|
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
69
76
|
import { spawnSync } from 'node:child_process';
|
|
70
|
-
import {
|
|
71
|
-
import {
|
|
72
|
-
import { resolveActivityRecipe, planRecipe, DISPLAY_ALIASES } from './recipes.mjs';
|
|
77
|
+
import { detectBackends, READY } from './detect-backends.mjs';
|
|
78
|
+
import { resolveActivityRecipe, DISPLAY_ALIASES } from './recipes.mjs';
|
|
73
79
|
import { CONFIG_REL, fail, loadConfig } from './orchestration-config.mjs';
|
|
74
|
-
// The
|
|
75
|
-
//
|
|
80
|
+
// The canonical review-domain primitives live in the core-evidence DAG bottom (ONE home for the
|
|
81
|
+
// fingerprint, the receipt read path, and the attesting predicate); this module RE-EXPORTS its
|
|
82
|
+
// historical public API from there and consumes the degrade records the same store owns.
|
|
76
83
|
import {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
84
|
+
RECEIPTS_BASENAME,
|
|
85
|
+
computeFingerprintPayload,
|
|
86
|
+
computeTreeFingerprint,
|
|
87
|
+
isTreeClean,
|
|
88
|
+
isNeverCommittableStat,
|
|
89
|
+
resolveReceiptsPath,
|
|
90
|
+
readReceipts,
|
|
82
91
|
summarizeReviewReceiptsForTree,
|
|
83
|
-
|
|
92
|
+
resolveBase,
|
|
93
|
+
isShipVerdict,
|
|
94
|
+
resolveEvidencePath,
|
|
95
|
+
readEvidence,
|
|
96
|
+
authoritativeOfKind,
|
|
97
|
+
} from './core-evidence.mjs';
|
|
98
|
+
|
|
99
|
+
export {
|
|
100
|
+
RECEIPTS_BASENAME,
|
|
101
|
+
computeFingerprintPayload,
|
|
102
|
+
computeTreeFingerprint,
|
|
103
|
+
isTreeClean,
|
|
104
|
+
isNeverCommittableStat,
|
|
105
|
+
resolveReceiptsPath,
|
|
106
|
+
readReceipts,
|
|
107
|
+
};
|
|
84
108
|
|
|
85
|
-
export const RECEIPTS_BASENAME = 'agent-workflow-review-receipts.jsonl';
|
|
86
109
|
export const PLANS_REL = 'docs/plans';
|
|
87
110
|
const ACTIVITY = 'plan-execution';
|
|
88
111
|
const SLOT = 'review';
|
|
@@ -110,111 +133,8 @@ const gitLine = (args, cwd) => {
|
|
|
110
133
|
return buf == null ? null : buf.toString('utf8').replace(/\r?\n$/, '');
|
|
111
134
|
};
|
|
112
135
|
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
// First 8 KiB contain a NUL byte → binary (git's own heuristic; mirrors the wrappers' is_binary).
|
|
116
|
-
const isBinaryFile = (path) => {
|
|
117
|
-
let fd;
|
|
118
|
-
try {
|
|
119
|
-
fd = openSync(path, 'r');
|
|
120
|
-
const buf = Buffer.alloc(8192);
|
|
121
|
-
const n = readSync(fd, buf, 0, 8192, 0);
|
|
122
|
-
return buf.subarray(0, n).includes(0);
|
|
123
|
-
} catch {
|
|
124
|
-
return false;
|
|
125
|
-
} finally {
|
|
126
|
-
if (fd !== undefined) closeSync(fd);
|
|
127
|
-
}
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
// The never-committable untracked stat classes (Decision 1, AD-044 Plan 4): character/block
|
|
131
|
-
// devices, FIFOs, sockets — git content can never carry them, so they are excluded from the ENTIRE
|
|
132
|
-
// review domain (fingerprint payload, clean check; the wrappers' bash twin filters the assembled
|
|
133
|
-
// payload identically). lstat-keyed by design: the sandbox mask class surfaces exactly where the
|
|
134
|
-
// dirent LIES (readdir says file, lstat says char device). A null stat (vanished path) is NOT in
|
|
135
|
-
// the class — it keeps its name-only note, like directories (gitlinks) and symlinks.
|
|
136
|
-
export const isNeverCommittableStat = (stat) =>
|
|
137
|
-
stat != null &&
|
|
138
|
-
(stat.isCharacterDevice() || stat.isBlockDevice() || stat.isFIFO() || stat.isSocket());
|
|
139
|
-
|
|
140
|
-
// The canonical payload bytes: staged diff + unstaged diff + the untracked-not-ignored section —
|
|
141
|
-
// byte-identical to the wrappers' emit_fingerprint_payload (same git invocations, same headers,
|
|
142
|
-
// same ls-files ordering), emitted from the work-tree ROOT. Returns null outside a git work tree.
|
|
143
|
-
// The lstat is injectable ONLY so tests can prove the never-committable filter non-vacuously (a
|
|
144
|
-
// lying lstat over a git-visible fixture path — the sandbox mechanism itself); production callers
|
|
145
|
-
// never pass it.
|
|
146
|
-
export const computeFingerprintPayload = (cwd, { lstat = lstatSync } = {}) => {
|
|
147
|
-
const top = gitLine(['rev-parse', '--show-toplevel'], cwd);
|
|
148
|
-
if (top == null) return null;
|
|
149
|
-
const staged = gitBuf(['diff', '--cached', '--no-ext-diff'], top);
|
|
150
|
-
const unstaged = gitBuf(['diff', '--no-ext-diff'], top);
|
|
151
|
-
const untrackedZ = gitBuf(['ls-files', '--others', '--exclude-standard', '-z'], top);
|
|
152
|
-
if (staged == null || unstaged == null || untrackedZ == null) return null;
|
|
153
|
-
const chunks = [staged, unstaged];
|
|
154
|
-
for (const rel of untrackedZ.toString('utf8').split('\0').filter(Boolean)) {
|
|
155
|
-
const full = join(top, rel);
|
|
156
|
-
let stat = null;
|
|
157
|
-
try {
|
|
158
|
-
stat = lstat(full);
|
|
159
|
-
} catch {
|
|
160
|
-
stat = null;
|
|
161
|
-
}
|
|
162
|
-
if (isNeverCommittableStat(stat)) continue;
|
|
163
|
-
if (stat?.isSymbolicLink()) {
|
|
164
|
-
let target = '?';
|
|
165
|
-
try {
|
|
166
|
-
target = readlinkSync(full);
|
|
167
|
-
} catch {
|
|
168
|
-
target = '?';
|
|
169
|
-
}
|
|
170
|
-
chunks.push(Buffer.from(`untracked-symlink:${rel} -> ${target}\n`));
|
|
171
|
-
} else if (!stat?.isFile()) {
|
|
172
|
-
chunks.push(Buffer.from(`untracked-nonregular:${rel}\n`));
|
|
173
|
-
} else if (isBinaryFile(full)) {
|
|
174
|
-
chunks.push(Buffer.from(`untracked-binary:${rel}\n`));
|
|
175
|
-
} else {
|
|
176
|
-
chunks.push(Buffer.from(`untracked:${rel}\n`));
|
|
177
|
-
chunks.push(readFileSync(full));
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
return Buffer.concat(chunks);
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
// sha256 hex of the canonical payload, or null outside a git work tree.
|
|
184
|
-
export const computeTreeFingerprint = (cwd, fsx) => {
|
|
185
|
-
const payload = computeFingerprintPayload(cwd, fsx);
|
|
186
|
-
return payload == null ? null : createHash('sha256').update(payload).digest('hex');
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
// Clean = nothing staged, nothing unstaged, no REVIEWABLE untracked-not-ignored paths (the
|
|
190
|
-
// wrappers' no-diff preflight). Never-committable untracked paths (device/FIFO/socket) do not
|
|
191
|
-
// count as dirty — same filter as the fingerprint, so the two can never disagree about a
|
|
192
|
-
// masks-only tree. An lstat failure keeps the path in the domain (dirty), mirroring the
|
|
193
|
-
// fingerprint's null-stat note. null when not decidable (not a git work tree). Anchored at the
|
|
194
|
-
// work-tree ROOT like the fingerprint: `git ls-files --others` is cwd-SCOPED, so a subdirectory
|
|
195
|
-
// invocation would otherwise miss root/sibling untracked paths and report a dirty tree as clean
|
|
196
|
-
// (codex R1 finding).
|
|
197
|
-
export const isTreeClean = (cwd, { lstat = lstatSync } = {}) => {
|
|
198
|
-
const top = gitLine(['rev-parse', '--show-toplevel'], cwd);
|
|
199
|
-
if (top == null) return null;
|
|
200
|
-
const staged = gitRaw(['diff', '--cached', '--quiet'], top);
|
|
201
|
-
const unstaged = gitRaw(['diff', '--quiet'], top);
|
|
202
|
-
if (staged.error || unstaged.error || staged.status > 1 || unstaged.status > 1) return null;
|
|
203
|
-
const untrackedZ = gitBuf(['ls-files', '--others', '--exclude-standard', '-z'], top);
|
|
204
|
-
if (untrackedZ == null) return null;
|
|
205
|
-
const reviewable = untrackedZ
|
|
206
|
-
.toString('utf8')
|
|
207
|
-
.split('\0')
|
|
208
|
-
.filter(Boolean)
|
|
209
|
-
.filter((rel) => {
|
|
210
|
-
try {
|
|
211
|
-
return !isNeverCommittableStat(lstat(join(top, rel)));
|
|
212
|
-
} catch {
|
|
213
|
-
return true;
|
|
214
|
-
}
|
|
215
|
-
});
|
|
216
|
-
return staged.status === 0 && unstaged.status === 0 && reviewable.length === 0;
|
|
217
|
-
};
|
|
136
|
+
// The canonical fingerprint / clean check / never-committable filter live in core-evidence.mjs
|
|
137
|
+
// (the ONE home, re-exported above); this module keeps only its own presentation plumbing.
|
|
218
138
|
|
|
219
139
|
// ── the sandbox-masks advisory (D lane, AD-044 Plan 4 Phase 1.5) ────────────────────
|
|
220
140
|
|
|
@@ -242,7 +162,7 @@ export const countNeverCommittableUntracked = (cwd, { lstat = lstatSync } = {})
|
|
|
242
162
|
|
|
243
163
|
// Shell-quote one argument for a COPY-PASTE advisory command: plain safe tokens stay bare; anything
|
|
244
164
|
// else rides single quotes (a space/metacharacter path must never render a dead or unsafe paste —
|
|
245
|
-
//
|
|
165
|
+
// Exported for the sandbox-masks probe, which renders the same apply one-liner.
|
|
246
166
|
export const shellQuoteArg = (s) => (/^[A-Za-z0-9_/.\-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`);
|
|
247
167
|
|
|
248
168
|
const maskAdvisoryLine = (state) =>
|
|
@@ -277,49 +197,22 @@ export const plansInFlight = (cwd, readdir = readdirSync) => {
|
|
|
277
197
|
.sort();
|
|
278
198
|
};
|
|
279
199
|
|
|
280
|
-
// ── receipts
|
|
281
|
-
|
|
282
|
-
export const resolveReceiptsPath = (cwd, env = process.env) => {
|
|
283
|
-
if (env.AW_REVIEW_RECEIPTS) return env.AW_REVIEW_RECEIPTS;
|
|
284
|
-
const gitDir = gitLine(['rev-parse', '--absolute-git-dir'], cwd);
|
|
285
|
-
return gitDir == null ? null : join(gitDir, RECEIPTS_BASENAME);
|
|
286
|
-
};
|
|
287
|
-
|
|
288
|
-
// Parse the receipt file → { receipts, malformed }. Absent file → empty (not an error: no review
|
|
289
|
-
// ever ran). A malformed line is counted + reported, never silently dropped.
|
|
290
|
-
export const readReceipts = (path, readFile = readFileSync) => {
|
|
291
|
-
let raw;
|
|
292
|
-
try {
|
|
293
|
-
raw = readFile(path, 'utf8');
|
|
294
|
-
} catch {
|
|
295
|
-
return { receipts: [], malformed: 0 };
|
|
296
|
-
}
|
|
297
|
-
const receipts = [];
|
|
298
|
-
let malformed = 0;
|
|
299
|
-
for (const line of raw.split('\n')) {
|
|
300
|
-
if (line.trim() === '') continue;
|
|
301
|
-
try {
|
|
302
|
-
const parsed = JSON.parse(line);
|
|
303
|
-
if (parsed && typeof parsed === 'object' && typeof parsed.backend === 'string') receipts.push(parsed);
|
|
304
|
-
else malformed += 1;
|
|
305
|
-
} catch {
|
|
306
|
-
malformed += 1;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
return { receipts, malformed };
|
|
310
|
-
};
|
|
200
|
+
// ── receipts (path + reader re-exported from the core-evidence one-home above) ──────
|
|
311
201
|
|
|
312
202
|
// Per-backend receipt status for the current fingerprint, over the ONE shared attesting-receipt
|
|
313
|
-
// predicate
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
//
|
|
203
|
+
// predicate — the LATEST NORMAL (probe-free, marker-valid, current-fingerprint) receipt is
|
|
204
|
+
// selected FIRST and THEN judged, so a later unknown/ungrounded receipt never lets an earlier
|
|
205
|
+
// SHIP survive:
|
|
206
|
+
// current — the latest normal receipt ATTESTS (grounded + recognized verdict);
|
|
207
|
+
// its verdict rides — ONLY ship-class satisfies, a recognized negative
|
|
208
|
+
// is an authoritative veto;
|
|
209
|
+
// ungrounded — the latest normal receipt carries grounded:false;
|
|
210
|
+
// unrecognized-verdict — the latest normal receipt carries a verdict outside the closed
|
|
211
|
+
// vocabulary (an unknown verdict never attests — fail closed);
|
|
212
|
+
// probe — current receipts exist and EVERY one is a well-formed probe (D3);
|
|
213
|
+
// rejected — current receipts exist, none normal, and >=1 marker malformed/absent;
|
|
214
|
+
// stale — receipts exist, none for the current fingerprint (edited after review);
|
|
215
|
+
// missing — no receipt from this backend at all.
|
|
323
216
|
export const backendReceiptStatus = (receipts, backend, fingerprint) => {
|
|
324
217
|
const own = receipts.filter((r) => r.backend === backend);
|
|
325
218
|
const summary = summarizeReviewReceiptsForTree(own, fingerprint);
|
|
@@ -327,56 +220,60 @@ export const backendReceiptStatus = (receipts, backend, fingerprint) => {
|
|
|
327
220
|
probeExcluded: summary.probeExcluded,
|
|
328
221
|
markerRejected: summary.markerRejected,
|
|
329
222
|
unmarkedRejected: summary.unmarkedRejected,
|
|
223
|
+
postureRejected: summary.postureRejected,
|
|
330
224
|
};
|
|
331
225
|
if (summary.state === 'current') {
|
|
332
|
-
return { state: 'current', verdict: summary.receipt.verdict ?? 'unknown', grounded: true, timestamp: summary.receipt.timestamp ?? null, ...counts };
|
|
226
|
+
return { state: 'current', verdict: summary.receipt.verdict ?? 'unknown', shipClass: isShipVerdict(summary.receipt.verdict), grounded: true, timestamp: summary.receipt.timestamp ?? null, ...counts };
|
|
333
227
|
}
|
|
334
|
-
if (summary.state === 'ungrounded') {
|
|
335
|
-
return { state:
|
|
228
|
+
if (summary.state === 'ungrounded' || summary.state === 'unrecognized-verdict') {
|
|
229
|
+
return { state: summary.state, verdict: summary.receipt.verdict ?? 'unknown', shipClass: false, grounded: summary.receipt.grounded === true, timestamp: summary.receipt.timestamp ?? null, ...counts };
|
|
336
230
|
}
|
|
337
231
|
if (summary.state === 'probe' || summary.state === 'rejected') {
|
|
338
|
-
return { state: summary.state, verdict: null, grounded: null, timestamp: null, ...counts };
|
|
232
|
+
return { state: summary.state, verdict: null, shipClass: false, grounded: null, timestamp: null, ...counts };
|
|
233
|
+
}
|
|
234
|
+
return { state: own.length > 0 ? 'stale' : 'missing', verdict: null, shipClass: false, grounded: null, timestamp: null, ...counts };
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// ── obligations from the CONFIGURED recipe + the D3(b) degrade-record escape ────────
|
|
238
|
+
|
|
239
|
+
// Obligations derive from the CONFIGURED recipe — the RAW orchestration.json value — never the
|
|
240
|
+
// readiness-degraded effective recipe: the resolver degrades council→reviewed→solo BEFORE any
|
|
241
|
+
// check would see the missing backend, which would silently drop an obligation (a computed
|
|
242
|
+
// readiness-degrade NEVER silently becomes solo). The resolver stays for display/diagnostics
|
|
243
|
+
// only. The computed DEFAULT (absent config) is readiness-aware by design — a default never
|
|
244
|
+
// mints an unsatisfiable obligation; an EXPLICIT configured recipe never degrades here.
|
|
245
|
+
// solo → no obligation (the existing honest exit-0 contract);
|
|
246
|
+
// reviewed → ONE ship-class attestation from ANY review-capable backend (minShip 1);
|
|
247
|
+
// council → EVERY review-capable backend attests ship-class OR carries a current-tree degrade
|
|
248
|
+
// record — and NEVER all degraded (minShip 1 stands whenever >=1 backend is configured).
|
|
249
|
+
export const requiredBackendsForConfiguredRecipe = ({ config, readiness = [], detectionFailed = false } = {}) => {
|
|
250
|
+
const configured = config?.[ACTIVITY]?.[SLOT];
|
|
251
|
+
const providers = Object.values(DISPLAY_ALIASES); // every review-capable backend, codex first
|
|
252
|
+
if (configured == null && detectionFailed) {
|
|
253
|
+
// No config + no readiness signal: the computed default is UNKNOWABLE — fail closed upstream.
|
|
254
|
+
return { recipe: null, source: 'default', backends: [], minShip: 0, perBackend: false, unknowable: true };
|
|
339
255
|
}
|
|
340
|
-
|
|
256
|
+
const anyReady = readiness.some((b) => b.readiness === READY);
|
|
257
|
+
const recipe = configured ?? (anyReady ? 'reviewed' : 'solo');
|
|
258
|
+
const source = configured != null ? 'config' : 'default';
|
|
259
|
+
if (recipe === 'solo') return { recipe, source, backends: [], minShip: 0, perBackend: false, unknowable: false };
|
|
260
|
+
if (recipe === 'council') return { recipe, source, backends: providers, minShip: 1, perBackend: true, unknowable: false };
|
|
261
|
+
return { recipe, source, backends: providers, minShip: 1, perBackend: false, unknowable: false };
|
|
341
262
|
};
|
|
342
263
|
|
|
343
|
-
//
|
|
344
|
-
|
|
345
|
-
//
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
const empty = new Set();
|
|
357
|
-
if (plans.length !== 1) return empty; // (i) ambiguous loop → exemption suppressed (no fail-closed exit-1 arm)
|
|
358
|
-
if (readError || malformed > 0) return empty; // fail-closed: a corrupt ledger denies the exemption
|
|
359
|
-
if (currentFingerprint == null) return empty;
|
|
360
|
-
const loop = plans[0].replace(/\.md$/, '');
|
|
361
|
-
const rounds = filterSegmentRecords(records, { activity: ACTIVITY, loop, base }).filter((r) => r.kind === 'round');
|
|
362
|
-
if (rounds.length === 0) return empty; // (ii) empty segment → nothing recorded yet
|
|
363
|
-
if (!roundSequenceIntact(rounds)) return empty; // (ii) corrupt sequence → fail closed
|
|
364
|
-
const latest = rounds[rounds.length - 1];
|
|
365
|
-
if (latest.fingerprint !== currentFingerprint) return empty; // (iv) the degrade must attest THIS tree
|
|
366
|
-
// Mirror decideStop's PRESENCE discipline (review-ledger.mjs): EVERY recipe-named backend must be IN
|
|
367
|
-
// the latest round (allPresent) — a backend absent from the round reviewed nothing there, so a stray
|
|
368
|
-
// current receipt for a NON-recorded backend can never justify the exemption (codex R1: else a
|
|
369
|
-
// degrade-only round `[{agy degraded}]` + any current codex receipt would exempt agy, disagreeing
|
|
370
|
-
// with review-ledger, whose decideStop fails allPresent on the absent codex).
|
|
371
|
-
const entryFor = (rb) => latest.backends.find((b) => b.backend === rb);
|
|
372
|
-
if (!requiredBackends.every((rb) => entryFor(rb) !== undefined)) return empty;
|
|
373
|
-
const receiptCurrent = new Set(backends.filter((b) => b.state === 'current').map((b) => b.backend));
|
|
374
|
-
// (v) >=1 non-degraded recipe-named backend PRESENT in the latest round with a current grounded
|
|
375
|
-
// receipt — never all degraded (mirrors decideStop's nonDegradedReq >= 1, plus review-state's own
|
|
376
|
-
// "it really reviewed" = a current receipt).
|
|
377
|
-
if (!requiredBackends.some((rb) => { const e = entryFor(rb); return e && !e.degraded && receiptCurrent.has(rb); })) return empty;
|
|
378
|
-
// (iii) exempt each recipe-named backend the latest round records degraded.
|
|
379
|
-
return new Set(requiredBackends.filter((rb) => { const e = entryFor(rb); return e && e.degraded === true; }));
|
|
264
|
+
// degradeRecordSet — the D3(b) escape: an EXPLICIT per-backend, per-tree degrade RECORD in the
|
|
265
|
+
// core-evidence store is the ONLY exemption lane. Fail-closed: an unreadable/malformed store
|
|
266
|
+
// DENIES every exemption (surfaced), but never fails a tree whose receipts independently satisfy
|
|
267
|
+
// the gate. A stale-fingerprint record never exempts (the authoritative record per {backend,
|
|
268
|
+
// fingerprint} must attest THIS tree).
|
|
269
|
+
export const degradeRecordSet = ({ cwd, env = process.env, fingerprint }) => {
|
|
270
|
+
const storePath = resolveEvidencePath(cwd, env);
|
|
271
|
+
const read = storePath ? readEvidence(storePath) : { records: [], malformed: 0, malformedReasons: [] };
|
|
272
|
+
const unavailable = (read.malformed ?? 0) > 0 || read.readError != null;
|
|
273
|
+
const set = unavailable || fingerprint == null
|
|
274
|
+
? new Set()
|
|
275
|
+
: new Set(authoritativeOfKind(read.records, 'degrade').filter((r) => r.fingerprint === fingerprint).map((r) => r.backend));
|
|
276
|
+
return { set, storePath, malformed: read.malformed ?? 0, readError: read.readError ?? null, unavailable };
|
|
380
277
|
};
|
|
381
278
|
|
|
382
279
|
// ── the check + report core ─────────────────────────────────────────────────────────
|
|
@@ -385,7 +282,7 @@ export const degradedExemptSet = ({ records, readError, malformed, base, plans,
|
|
|
385
282
|
// EVERY project-relative read (orchestration config, docs/plans, receipts) anchors at the git
|
|
386
283
|
// work-tree ROOT when one exists — the fingerprint is root-anchored, so a subdirectory invocation
|
|
387
284
|
// must read the same config/plans or a dirty unreceipted tree could false-PASS as "no plan in
|
|
388
|
-
// flight"
|
|
285
|
+
// flight". Outside a git tree the cwd is the only anchor (and --check exits 0).
|
|
389
286
|
export const buildState = ({ cwd, env = process.env, detect = detectBackends, lstat = lstatSync } = {}) => {
|
|
390
287
|
const root = gitLine(['rev-parse', '--show-toplevel'], cwd) ?? cwd;
|
|
391
288
|
const { config, source: configSource } = loadConfig(root);
|
|
@@ -394,29 +291,33 @@ export const buildState = ({ cwd, env = process.env, detect = detectBackends, ls
|
|
|
394
291
|
try {
|
|
395
292
|
detection = detect();
|
|
396
293
|
} catch (err) {
|
|
397
|
-
detectionWarning = `backend detection failed (${(err && err.message) || err}) —
|
|
294
|
+
detectionWarning = `backend detection failed (${(err && err.message) || err}) — readiness unknown.`;
|
|
398
295
|
}
|
|
296
|
+
// The resolver stays for DISPLAY/diagnostics only; the OBLIGATIONS come from the configured
|
|
297
|
+
// recipe (never the readiness-degraded effective one — no silent solo).
|
|
399
298
|
const resolved = resolveActivityRecipe({ config: config ?? {}, readiness: detection, activity: ACTIVITY, slot: SLOT });
|
|
400
|
-
const {
|
|
401
|
-
const requiredBackends =
|
|
299
|
+
const obligations = requiredBackendsForConfiguredRecipe({ config: config ?? {}, readiness: detection, detectionFailed: detectionWarning != null });
|
|
300
|
+
const requiredBackends = obligations.backends;
|
|
402
301
|
const plans = plansInFlight(root);
|
|
403
302
|
// The injected lstat threads through EVERY stat-dependent computation (fingerprint, clean, the
|
|
404
|
-
// mask count) — a partial injection would let a test observe an inconsistent state
|
|
303
|
+
// mask count) — a partial injection would let a test observe an inconsistent state.
|
|
405
304
|
const fingerprint = computeTreeFingerprint(cwd, { lstat });
|
|
406
305
|
const clean = fingerprint == null ? null : isTreeClean(cwd, { lstat });
|
|
407
306
|
const receiptsPath = resolveReceiptsPath(cwd, env);
|
|
408
|
-
const
|
|
307
|
+
const receiptsRead = receiptsPath ? readReceipts(receiptsPath) : { receipts: [], malformed: 0 };
|
|
308
|
+
const { receipts, malformed } = receiptsRead;
|
|
309
|
+
const receiptsReadError = receiptsRead.readError ?? null;
|
|
409
310
|
const backends = requiredBackends.map((b) => ({ backend: b, ...backendReceiptStatus(receipts, b, fingerprint) }));
|
|
410
|
-
// The
|
|
411
|
-
//
|
|
412
|
-
// whose receipts independently satisfy the gate
|
|
311
|
+
// The D3(b) degrade escape: read the core-evidence store ONLY here, ONLY for the exemption —
|
|
312
|
+
// the gate never otherwise depends on it (an unavailable store denies the exemption CLOSED,
|
|
313
|
+
// never fails a tree whose receipts independently satisfy the gate).
|
|
413
314
|
const base = resolveBase(cwd);
|
|
414
|
-
const
|
|
415
|
-
const
|
|
416
|
-
const degradedExempt = [...degradedExemptSet({ records, readError: ledgerReadError, malformed: ledgerMalformed, base, plans, currentFingerprint: fingerprint, requiredBackends, backends })];
|
|
315
|
+
const degrade = degradeRecordSet({ cwd, env, fingerprint });
|
|
316
|
+
const degradedExempt = requiredBackends.filter((b) => degrade.set.has(b));
|
|
417
317
|
return {
|
|
418
318
|
resolved,
|
|
419
319
|
configSource,
|
|
320
|
+
obligations,
|
|
420
321
|
requiredBackends,
|
|
421
322
|
backends,
|
|
422
323
|
plans,
|
|
@@ -426,10 +327,12 @@ export const buildState = ({ cwd, env = process.env, detect = detectBackends, ls
|
|
|
426
327
|
receiptsPath,
|
|
427
328
|
receiptCount: receipts.length,
|
|
428
329
|
malformed,
|
|
330
|
+
receiptsReadError,
|
|
429
331
|
base,
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
332
|
+
evidenceStorePath: degrade.storePath,
|
|
333
|
+
evidenceMalformed: degrade.malformed,
|
|
334
|
+
evidenceReadError: degrade.readError,
|
|
335
|
+
evidenceUnavailable: degrade.unavailable,
|
|
433
336
|
degradedExempt,
|
|
434
337
|
maskedUntracked: countNeverCommittableUntracked(cwd, { lstat }),
|
|
435
338
|
detectionWarning,
|
|
@@ -444,83 +347,120 @@ const rejectionCause = (b) => {
|
|
|
444
347
|
if (b.unmarkedRejected > 0) {
|
|
445
348
|
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`);
|
|
446
349
|
}
|
|
350
|
+
if ((b.postureRejected ?? 0) > 0) {
|
|
351
|
+
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`);
|
|
352
|
+
}
|
|
447
353
|
return parts.join(' + ');
|
|
448
354
|
};
|
|
449
355
|
|
|
356
|
+
// One failing backend row → its stated recovery. Shared by the council per-backend arm and the
|
|
357
|
+
// reviewed closest-recovery listing.
|
|
358
|
+
const backendFailurePart = (b, state) => {
|
|
359
|
+
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`;
|
|
360
|
+
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`;
|
|
361
|
+
if (b.state === 'ungrounded') return `${b.backend}: the latest normal receipt is ungrounded — re-run grounded (--facts)`;
|
|
362
|
+
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`;
|
|
363
|
+
if (b.state === 'rejected') return `${b.backend}: current-tree receipts rejected — ${rejectionCause(b)} (fail-closed); inspect ${state.receiptsPath}`;
|
|
364
|
+
if (b.state === 'stale') return `${b.backend}: receipts exist but none matches the current tree (edited after review) — run a fresh review`;
|
|
365
|
+
return `${b.backend}: no receipt — run its review wrapper, or record an explicit degrade (node ${shellQuoteArg(join(dirname(fileURLToPath(import.meta.url)), 'core-evidence.mjs'))} degrade --backend ${b.backend} --reason "...")`;
|
|
366
|
+
};
|
|
367
|
+
|
|
450
368
|
// The normative --check decision (the header contract, in order). → { code, reason }.
|
|
369
|
+
// Obligations come from the CONFIGURED recipe; satisfaction is SHIP-CLASS ONLY; a recognized
|
|
370
|
+
// negative on the latest normal receipt VETOES; an explicit current-tree degrade record is the
|
|
371
|
+
// only escape for an unavailable backend under council — and never all backends (>=1 ship-class
|
|
372
|
+
// attestation is required whenever >=1 backend is configured).
|
|
451
373
|
export const decideCheck = (state) => {
|
|
452
|
-
//
|
|
453
|
-
//
|
|
454
|
-
//
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
374
|
+
// Store diagnostics ride EVERY check line, early exits (and the unknowable arm) included — a
|
|
375
|
+
// malformed receipt line, an unavailable evidence store, or an unreadable receipts store is
|
|
376
|
+
// never hidden behind any exit.
|
|
377
|
+
const malformedNote = state.malformed > 0 ? ` — ${state.malformed} malformed receipt line(s) ignored; inspect ${state.receiptsPath}` : '';
|
|
378
|
+
const evidenceNote = state.evidenceUnavailable
|
|
379
|
+
? ` — evidence store unavailable (${state.evidenceMalformed} malformed line(s)${state.evidenceReadError ? `, read error: ${state.evidenceReadError}` : ''}); the degrade escape is denied (fail-closed) — inspect ${state.evidenceStorePath}`
|
|
380
|
+
: '';
|
|
381
|
+
const earlyNotes = `${malformedNote}${evidenceNote}${state.receiptsReadError ? ` — receipts store unreadable (${state.receiptsReadError}); inspect ${state.receiptsPath}` : ''}`;
|
|
382
|
+
// Detector failure with NO configured recipe: the computed default is unknowable → fail closed.
|
|
383
|
+
// An EXPLICIT configured recipe needs no detector — its obligations are readiness-independent.
|
|
384
|
+
if (state.obligations.unknowable) {
|
|
385
|
+
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}` };
|
|
461
386
|
}
|
|
462
|
-
if (state.
|
|
463
|
-
const why = state.
|
|
464
|
-
? `
|
|
465
|
-
: `
|
|
466
|
-
return { code: 0, reason: `${why} — no receipt required` };
|
|
387
|
+
if (state.obligations.recipe === 'solo') {
|
|
388
|
+
const why = state.obligations.source === 'config'
|
|
389
|
+
? `configured ${ACTIVITY}.${SLOT} recipe is solo`
|
|
390
|
+
: `no reviewer backend is ready — the computed ${ACTIVITY}.${SLOT} default is solo`;
|
|
391
|
+
return { code: 0, reason: `${why} — no receipt required${earlyNotes}` };
|
|
467
392
|
}
|
|
468
|
-
if (state.plans.length === 0) return { code: 0, reason:
|
|
469
|
-
if (state.fingerprint == null) return { code: 0, reason:
|
|
470
|
-
if (state.clean === true) return { code: 0, reason:
|
|
471
|
-
// A malformed receipt line is never silently ignored (No-silent-failures Hard Constraint): it
|
|
472
|
-
// cannot fail the gate by itself (a forged/corrupt line must not brick commits), but the check
|
|
473
|
-
// line always names it so a PASS over a partially-corrupt file is visible.
|
|
474
|
-
const malformedNote = state.malformed > 0 ? ` — ${state.malformed} malformed receipt line(s) ignored; inspect ${state.receiptsPath}` : '';
|
|
475
|
-
// The review-ledger is consulted ONLY for the degraded exemption; a corrupt ledger DENIES it
|
|
476
|
-
// (fail-closed) but never fails a tree the receipts independently satisfy — surfaced either way
|
|
477
|
-
// (No-silent-failures; Decision 3).
|
|
478
|
-
const ledgerNote = state.ledgerReadError
|
|
479
|
-
? ` — review ledger unreadable (${state.ledgerReadError}); degraded exemption unavailable (fail-closed) — inspect ${state.ledgerPath}`
|
|
480
|
-
: state.ledgerMalformed > 0
|
|
481
|
-
? ` — review ledger has ${state.ledgerMalformed} malformed line(s); degraded exemption unavailable (fail-closed) — inspect ${state.ledgerPath}`
|
|
482
|
-
: '';
|
|
483
|
-
// The degraded exemption (AD-050): a backend recorded degraded for the current tree is excluded from
|
|
484
|
-
// `failing` (it reviewed nothing to receipt — MIRRORS decideStop excluding a degraded backend). It
|
|
485
|
-
// stays verdict-blind: the exemption proves the degrade was RECORDED, never that the tree converged.
|
|
393
|
+
if (state.plans.length === 0) return { code: 0, reason: `no plan in flight (docs/plans/ holds no active plan) — no receipt required${earlyNotes}` };
|
|
394
|
+
if (state.fingerprint == null) return { code: 0, reason: `not a git work tree — nothing to fingerprint${earlyNotes}` };
|
|
395
|
+
if (state.clean === true) return { code: 0, reason: `the working tree is clean — nothing to review${earlyNotes}` };
|
|
486
396
|
const exempt = new Set(state.degradedExempt);
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
//
|
|
490
|
-
//
|
|
491
|
-
//
|
|
492
|
-
//
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
397
|
+
const satisfied = state.backends.filter((b) => b.state === 'current' && b.shipClass);
|
|
398
|
+
const vetoed = state.backends.filter((b) => b.state === 'current' && !b.shipClass);
|
|
399
|
+
// The marker note is PATH-AWARE (no silent rejections on any exit): it counts every backend's
|
|
400
|
+
// untrusted-marker exclusions EXCEPT those whose printed part already names them — i.e. only a
|
|
401
|
+
// PRINTED `rejected` row (backendFailurePart's rejectionCause) suppresses its own counts; a
|
|
402
|
+
// printed veto/unrecognized/ungrounded row does not, and success paths suppress nothing.
|
|
403
|
+
const notesFor = (printed) => {
|
|
404
|
+
const total = state.backends
|
|
405
|
+
.filter((b) => !(printed.has(b.backend) && b.state === 'rejected'))
|
|
406
|
+
.reduce((n, b) => n + (b.markerRejected ?? 0) + (b.unmarkedRejected ?? 0), 0);
|
|
407
|
+
const markerNote = total > 0
|
|
408
|
+
? ` — ${total} receipt(s) rejected: an untrustworthy probe marker (malformed, or absent — silence is not a declaration) — fail-closed; inspect ${state.receiptsPath}`
|
|
409
|
+
: '';
|
|
410
|
+
return `${markerNote}${earlyNotes}`;
|
|
411
|
+
};
|
|
412
|
+
const NONE_PRINTED = new Set();
|
|
413
|
+
// UNCONDITIONAL refusals, checked BEFORE minShip/exemptions: a recognized NEGATIVE (the
|
|
414
|
+
// authoritative veto) and an UNRECOGNIZED verdict (fail closed) — another backend's SHIP never
|
|
415
|
+
// masks them and a degrade record never lifts them (the backend demonstrably ran).
|
|
416
|
+
const unrecognized = state.backends.filter((b) => b.state === 'unrecognized-verdict');
|
|
417
|
+
const unconditional = [...vetoed, ...unrecognized];
|
|
418
|
+
if (unconditional.length > 0) {
|
|
419
|
+
return { code: 1, reason: `${unconditional.map((b) => backendFailurePart(b, state)).join('; ')}${notesFor(new Set(unconditional.map((b) => b.backend)))}` };
|
|
420
|
+
}
|
|
421
|
+
// Never all degraded: >=1 ship-class attestation whenever >=1 backend is configured. An
|
|
422
|
+
// already-exempt backend renders its own honest part — never the "record an explicit degrade"
|
|
423
|
+
// recovery it has already taken.
|
|
424
|
+
if (satisfied.length < state.obligations.minShip) {
|
|
425
|
+
const failing = state.backends.filter((b) => !(b.state === 'current' && b.shipClass));
|
|
426
|
+
const allExempt = failing.length > 0 && failing.every((b) => exempt.has(b.backend));
|
|
427
|
+
const head = allExempt
|
|
428
|
+
? `every configured backend is degrade-recorded for this tree — never all degraded: >=1 non-degraded ship-class attestation is required; run at least one real review`
|
|
429
|
+
: failing
|
|
430
|
+
.map((b) => (exempt.has(b.backend)
|
|
431
|
+
? `${b.backend}: degrade-recorded for this tree — a degrade never counts toward the >=1 ship-class floor; run a real review on another backend`
|
|
432
|
+
: backendFailurePart(b, state)))
|
|
433
|
+
.join('; ');
|
|
434
|
+
// Only backends that actually rendered their backendFailurePart suppress their counts — an
|
|
435
|
+
// exempt row prints the degrade string (no rejectionCause), so its exclusions stay named.
|
|
436
|
+
return { code: 1, reason: `${head}${notesFor(new Set(failing.filter((b) => !exempt.has(b.backend)).map((b) => b.backend)))}` };
|
|
437
|
+
}
|
|
438
|
+
if (state.obligations.perBackend) {
|
|
439
|
+
// Council: EVERY configured backend must attest ship-class OR carry a current-tree degrade record.
|
|
440
|
+
const failing = state.backends.filter((b) => !(b.state === 'current' && b.shipClass) && !exempt.has(b.backend));
|
|
441
|
+
if (failing.length > 0) {
|
|
442
|
+
return { code: 1, reason: `${failing.map((b) => backendFailurePart(b, state)).join('; ')}${notesFor(new Set(failing.map((b) => b.backend)))}` };
|
|
443
|
+
}
|
|
501
444
|
if (exempt.size === 0) {
|
|
502
|
-
return { code: 0, reason: `every
|
|
445
|
+
return { code: 0, reason: `every configured backend attests ship-class for the current tree (${state.requiredBackends.join(' + ')})${notesFor(NONE_PRINTED)}` };
|
|
503
446
|
}
|
|
504
|
-
return { code: 0, reason: `
|
|
447
|
+
return { code: 0, reason: `council satisfied: ship-class attestation(s) from ${satisfied.map((b) => b.backend).join(' + ')}; degrade-recorded for this tree: ${[...exempt].join(', ')}${notesFor(NONE_PRINTED)}` };
|
|
505
448
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
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`;
|
|
509
|
-
if (b.state === 'rejected') return `${b.backend}: current-tree receipts rejected — ${rejectionCause(b)} (fail-closed); inspect ${state.receiptsPath}`;
|
|
510
|
-
if (b.state === 'stale') return `${b.backend}: receipts exist but none matches the current tree (edited after review) — run a fresh review`;
|
|
511
|
-
return `${b.backend}: no receipt — run its review wrapper`;
|
|
512
|
-
});
|
|
513
|
-
return { code: 1, reason: `${parts.join('; ')}${markerNote}${malformedNote}${ledgerNote}` };
|
|
449
|
+
// Reviewed: >=1 ship-class attestation from any review-capable backend satisfies.
|
|
450
|
+
return { code: 0, reason: `reviewed satisfied: ship-class attestation from ${satisfied.map((b) => b.backend).join(' + ')} for the current tree${notesFor(NONE_PRINTED)}` };
|
|
514
451
|
};
|
|
515
452
|
|
|
516
453
|
// ── rendering ───────────────────────────────────────────────────────────────────────
|
|
517
454
|
|
|
518
|
-
|
|
455
|
+
// The glyph reflects SATISFACTION, not bare state: a `current` row carrying a recognized
|
|
456
|
+
// NEGATIVE is an authoritative veto and renders ✗.
|
|
457
|
+
const STATE_GLYPH = { 'unrecognized-verdict': '✗', ungrounded: '✗', probe: '✗', rejected: '✗', stale: '✗', missing: '✗' };
|
|
458
|
+
const glyphFor = (b) => (b.state === 'current' ? (b.shipClass ? '✓' : '✗') : STATE_GLYPH[b.state]);
|
|
519
459
|
|
|
520
460
|
const formatHuman = (state, check) => {
|
|
521
|
-
const src = state.
|
|
461
|
+
const src = state.obligations.source === 'config' ? `from ${CONFIG_REL}` : 'computed default';
|
|
522
462
|
const lines = [
|
|
523
|
-
`review-state — ${ACTIVITY}.${SLOT} = ${state.
|
|
463
|
+
`review-state — ${ACTIVITY}.${SLOT} = ${state.obligations.recipe ?? '(unknowable)'} (${src})${state.requiredBackends.length ? ` → ${state.requiredBackends.join(' + ')}${state.obligations.perBackend ? '' : ' (any one, ship-class)'}` : ''}`,
|
|
524
464
|
];
|
|
525
465
|
if (state.detectionWarning) lines.push(` ⚠ ${state.detectionWarning}`);
|
|
526
466
|
lines.push(` plan in flight: ${state.plans.length ? state.plans.join(', ') : '(none)'}`);
|
|
@@ -528,27 +468,32 @@ const formatHuman = (state, check) => {
|
|
|
528
468
|
else if (state.clean === true) lines.push(' tree: clean (nothing to review)');
|
|
529
469
|
else lines.push(` tree fingerprint: ${state.fingerprint}`);
|
|
530
470
|
lines.push(` receipts: ${state.receiptsPath ?? '(unresolvable — no git dir)'} (${state.receiptCount} line(s)${state.malformed ? `, ${state.malformed} malformed — inspect the file` : ''})`);
|
|
531
|
-
if (state.
|
|
532
|
-
|
|
471
|
+
if (state.receiptsReadError) lines.push(` ⚠ receipts store unreadable (${state.receiptsReadError}) — inspect ${state.receiptsPath}`);
|
|
472
|
+
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)`);
|
|
533
473
|
const exempt = new Set(state.degradedExempt);
|
|
534
474
|
for (const b of state.backends) {
|
|
535
|
-
const exemptTag = exempt.has(b.backend) ? ' —
|
|
475
|
+
const exemptTag = exempt.has(b.backend) ? ' — degrade-recorded for the current tree (core-evidence store)' : '';
|
|
536
476
|
const detail =
|
|
537
477
|
b.state === 'current'
|
|
538
|
-
? `current (verdict: ${b.verdict}, grounded, ${b.timestamp ?? '?'})`
|
|
539
|
-
: b.state === '
|
|
540
|
-
? `
|
|
541
|
-
: b.state === '
|
|
542
|
-
?
|
|
543
|
-
: b.state === '
|
|
544
|
-
?
|
|
545
|
-
: b.state === '
|
|
546
|
-
?
|
|
547
|
-
:
|
|
478
|
+
? `current (verdict: ${JSON.stringify(b.verdict)}${b.shipClass ? ', ship-class' : ' — a recognized negative, an authoritative VETO'}, grounded, ${b.timestamp ?? '?'})`
|
|
479
|
+
: b.state === 'unrecognized-verdict'
|
|
480
|
+
? `latest normal receipt carries an unrecognized verdict (${JSON.stringify(b.verdict)}) — never attests (fail-closed)`
|
|
481
|
+
: b.state === 'ungrounded'
|
|
482
|
+
? `ungrounded latest normal receipt (verdict: ${JSON.stringify(b.verdict)}) — a grounded fresh run is required`
|
|
483
|
+
: b.state === 'probe'
|
|
484
|
+
? 'only probe receipts for the current tree (quality guards relaxed) — a probe review never attests'
|
|
485
|
+
: b.state === 'rejected'
|
|
486
|
+
? `current-tree receipts rejected — ${rejectionCause(b)} (fail-closed)`
|
|
487
|
+
: b.state === 'stale'
|
|
488
|
+
? 'stale — no receipt matches the current tree (edited after review)'
|
|
489
|
+
: 'missing — no receipt from this backend';
|
|
548
490
|
const excludedTag = b.probeExcluded || b.markerRejected || b.unmarkedRejected
|
|
549
491
|
? ` [excluded: ${b.probeExcluded} probe, ${b.markerRejected} malformed-marker, ${b.unmarkedRejected} unmarked]`
|
|
550
492
|
: '';
|
|
551
|
-
|
|
493
|
+
// ⊘ only where the escape actually applies: a PRODUCED receipt outranks the record — a
|
|
494
|
+
// negative/unknown verdict keeps its ✗ (the degrade lifts neither).
|
|
495
|
+
const escapeApplies = exempt.has(b.backend) && b.state !== 'current' && b.state !== 'unrecognized-verdict';
|
|
496
|
+
lines.push(` ${escapeApplies ? '⊘' : glyphFor(b)} ${b.backend}: ${detail}${excludedTag}${exemptTag}`);
|
|
552
497
|
}
|
|
553
498
|
lines.push(` check: ${check.code === 0 ? 'PASS' : 'FAIL'} — ${check.reason}`);
|
|
554
499
|
return lines.join('\n');
|
|
@@ -559,23 +504,25 @@ const HELP = `review-state — read-only review-receipt checker (agent-workflow
|
|
|
559
504
|
Usage:
|
|
560
505
|
node review-state.mjs [--check] [--json]
|
|
561
506
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
the review-payload domain), reads the
|
|
565
|
-
(<git dir>/${RECEIPTS_BASENAME}; AW_REVIEW_RECEIPTS
|
|
566
|
-
|
|
567
|
-
(
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
507
|
+
Derives the review OBLIGATIONS from the CONFIGURED ${ACTIVITY}.${SLOT} recipe (${CONFIG_REL} raw
|
|
508
|
+
value — never the readiness-degraded effective recipe), recomputes the canonical uncommitted-state
|
|
509
|
+
fingerprint (staged + unstaged + untracked-not-ignored — the review-payload domain), reads the
|
|
510
|
+
receipt file the review wrappers append to (<git dir>/${RECEIPTS_BASENAME}; AW_REVIEW_RECEIPTS
|
|
511
|
+
overrides), and judges each configured backend's LATEST NORMAL receipt: only SHIP-CLASS verdicts
|
|
512
|
+
(ship / ship with nits) satisfy; a recognized negative (revise / rethink / rework) is an
|
|
513
|
+
authoritative VETO; an unrecognized verdict never attests (fail closed). Plan/diff-mode receipts
|
|
514
|
+
and continuations (fresh:false) are informational-only, and a PROBE receipt (probe:true) never
|
|
515
|
+
attests; a malformed OR absent probe marker is rejected fail-closed — silence is not a declaration.
|
|
516
|
+
The ONLY escape for an unavailable backend under council is an explicit current-tree degrade
|
|
517
|
+
record (node core-evidence.mjs degrade) — and never all backends.
|
|
572
518
|
|
|
573
519
|
--check exits 0/1 per the normative contract in the tool header: 0 for solo / no plan in flight /
|
|
574
|
-
a clean tree / not-a-git-tree /
|
|
575
|
-
|
|
576
|
-
a
|
|
577
|
-
|
|
578
|
-
|
|
520
|
+
a clean tree / not-a-git-tree / obligations satisfied (reviewed: >=1 ship-class attestation;
|
|
521
|
+
council: every backend ship-class or degrade-recorded, >=1 real ship); 1 on a veto, an
|
|
522
|
+
unrecognized verdict, a missing/stale/ungrounded/probe-only backend without a degrade record,
|
|
523
|
+
an all-degraded tree, or a down detector with no configured recipe.
|
|
524
|
+
Declare it as a project gate by hand (docs/ai/gates.json) or via the
|
|
525
|
+
explicit-consent init preview (tools/gates-init.mjs) — never without consent.
|
|
579
526
|
|
|
580
527
|
Read-only: never writes, never commits, never runs a subscription CLI; spawns read-only git queries.
|
|
581
528
|
Sandbox-safe: runs fully inside an OS sandbox (fs + git reads, no network) — the D4 sandbox lane.
|
|
@@ -611,17 +558,18 @@ export const main = (argv, ctx = {}) => {
|
|
|
611
558
|
}
|
|
612
559
|
};
|
|
613
560
|
|
|
614
|
-
// ── --await: block until
|
|
615
|
-
//
|
|
616
|
-
//
|
|
617
|
-
//
|
|
618
|
-
//
|
|
619
|
-
//
|
|
620
|
-
// `--check`
|
|
621
|
-
//
|
|
622
|
-
//
|
|
623
|
-
//
|
|
624
|
-
//
|
|
561
|
+
// ── --await: block until the configured review obligations are SATISFIED ───────────────
|
|
562
|
+
// It waits until `--check` would pass: under `reviewed`, a ship-class attestation from any
|
|
563
|
+
// review-capable backend; under `council`, every configured backend ship-class OR carrying a
|
|
564
|
+
// current-tree degrade RECORD in the core-evidence store (never all backends) — once such a
|
|
565
|
+
// record lands, --await stops waiting for that backend and returns READY (before, it waited
|
|
566
|
+
// forever for a receipt that never comes). It inherits everything for FREE — it polls the SAME
|
|
567
|
+
// decideCheck(buildState()) `--check` computes. The completion signal is the RECEIPT/RECORD
|
|
568
|
+
// (i.e. `--check` would PASS), NEVER a process event — a harness "completed" notification fires
|
|
569
|
+
// early and a bridge's output late-flushes, so polling state is the durable mechanization of
|
|
570
|
+
// receipts-not-pgrep. Stays read-only (it only re-reads state — now the evidence store too, a
|
|
571
|
+
// few KB per tick); the clock is injectable (ctx.now / ctx.sleep / ctx.pollMs) so hermetic tests
|
|
572
|
+
// never spend wall-clock.
|
|
625
573
|
|
|
626
574
|
const AWAIT_ALLOWED_ARGS = new Set(['--await', '--timeout']);
|
|
627
575
|
|
|
@@ -670,7 +618,7 @@ export const mainAwait = async (argv, ctx = {}) => {
|
|
|
670
618
|
};
|
|
671
619
|
|
|
672
620
|
const emitResult = (r) => {
|
|
673
|
-
// Exact writes + a natural exit: process.exit() can truncate unflushed piped stdio
|
|
621
|
+
// Exact writes + a natural exit: process.exit() can truncate unflushed piped stdio.
|
|
674
622
|
if (r.stdout) process.stdout.write(r.stdout.endsWith('\n') ? r.stdout : `${r.stdout}\n`);
|
|
675
623
|
if (r.stderr) process.stderr.write(r.stderr.endsWith('\n') ? r.stderr : `${r.stderr}\n`);
|
|
676
624
|
process.exitCode = r.code;
|