bareloop 0.21.0 → 0.23.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 +292 -0
- package/README.md +1 -1
- package/bareloop.context.md +177 -24
- package/package.json +3 -3
- package/src/authorflow.js +7 -2
- package/src/authorjob.js +15 -1
- package/src/authorscout.js +83 -9
- package/src/bundle.js +40 -46
- package/src/calibrate.js +10 -0
- package/src/cli.js +52 -19
- package/src/close-integrity.js +325 -0
- package/src/closetimeout.js +165 -0
- package/src/declaredclose.js +9 -2
- package/src/index.js +14 -0
- package/src/job.js +84 -5
- package/src/judged.js +108 -5
- package/src/kinds.js +12 -0
- package/src/plan.js +5 -1
- package/src/planrun.js +392 -46
- package/src/providers.js +176 -0
- package/src/ralph.js +40 -7
- package/src/ratelimit.js +116 -0
- package/src/reuse.js +12 -2
- package/src/reviewdoor.js +26 -6
- package/src/run.js +83 -8
- package/src/transport.js +29 -12
- package/src/validate.js +59 -0
- package/types/authorscout.d.ts +27 -2
- package/types/close-integrity.d.ts +196 -0
- package/types/closetimeout.d.ts +126 -0
- package/types/declaredclose.d.ts +6 -1
- package/types/index.d.ts +1 -0
- package/types/job.d.ts +4 -0
- package/types/judged.d.ts +7 -2
- package/types/kinds.d.ts +9 -0
- package/types/planrun.d.ts +17 -2
- package/types/providers.d.ts +104 -0
- package/types/ralph.d.ts +19 -5
- package/types/ratelimit.d.ts +59 -0
- package/types/reuse.d.ts +2 -0
- package/types/reviewdoor.d.ts +3 -1
- package/types/run.d.ts +33 -2
- package/types/transport.d.ts +10 -4
- package/types/validate.d.ts +48 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// Close timeout — PRD item 27/M3, `docs/product/CLOSE-INTEGRITY-BUILD.md`.
|
|
2
|
+
//
|
|
3
|
+
// hamr's ruling (2026-09-06, verbatim): "both … autoset and can be override,
|
|
4
|
+
// same like api pricing" — the guesstimate-plus-loud-sign-plus-customer-
|
|
5
|
+
// override shape, never a hidden knob. Today `runClose`'s own default
|
|
6
|
+
// (`src/ralph.js:221`, `timeoutMs = 120_000`) is a defaulted cap with no
|
|
7
|
+
// operator-set floor — the same shape the wall's no-default rule already
|
|
8
|
+
// forbids for `maxWallMs`. This module is the fix: measure the close's own
|
|
9
|
+
// stages once at $0, before any provider call, and derive a per-stage ceiling
|
|
10
|
+
// from what was actually measured, unless the signed spec names one itself.
|
|
11
|
+
//
|
|
12
|
+
// CONSTANTS BELOW ARE ARBITER CONSTANTS — hamr set them 2026-09-07, verbatim:
|
|
13
|
+
// "floor = 120,000 ms; K = 5 ('2 and 5 are fine')." Never agent- or
|
|
14
|
+
// adopter-settable; a build changing them is arbiter territory, not a tuning
|
|
15
|
+
// knob a drafted spec or a runner script can reach for.
|
|
16
|
+
|
|
17
|
+
import { runClose } from './ralph.js';
|
|
18
|
+
|
|
19
|
+
/** hamr, 2026-09-07: the floor, verbatim. Also the value `runClose`'s own
|
|
20
|
+
* un-autoset default has always used — kept as its own named constant here
|
|
21
|
+
* (not re-exported from ralph.js) because the two are different CLAIMS: one is
|
|
22
|
+
* "what runClose does if nobody says otherwise", the other is "the arbiter's
|
|
23
|
+
* chosen floor for autoset" — they happen to be the same number today, and a
|
|
24
|
+
* future change to either must not silently move the other. */
|
|
25
|
+
export const CLOSE_TIMEOUT_FLOOR_MS = 120_000;
|
|
26
|
+
/** hamr, 2026-09-07, verbatim: "K = 5 ('2 and 5 are fine')." */
|
|
27
|
+
export const CLOSE_TIMEOUT_K = 5;
|
|
28
|
+
/** `runClose`'s own shipped default when nobody passes a timeout at all
|
|
29
|
+
* (`src/ralph.js:221`) — the ceiling the $0 timing PASS ITSELF runs each
|
|
30
|
+
* stage under is a multiple of this, never of the floor above (the two are
|
|
31
|
+
* the same number today; named separately on purpose, see the constant
|
|
32
|
+
* above). */
|
|
33
|
+
export const LIBRARY_DEFAULT_CLOSE_TIMEOUT_MS = 120_000;
|
|
34
|
+
/**
|
|
35
|
+
* The generous PROVISIONAL ceiling the timing preflight runs every stage
|
|
36
|
+
* under, so "a genuinely hung stage still ends" (build spec, M3 §2) rather
|
|
37
|
+
* than blocking the timing pass forever. Deliberately the library default × K
|
|
38
|
+
* (600s) — NOT the autoset/override ceiling this module computes, which is
|
|
39
|
+
* only known after the pass completes.
|
|
40
|
+
*/
|
|
41
|
+
export const TIMING_PREFLIGHT_CEILING_MS = LIBRARY_DEFAULT_CLOSE_TIMEOUT_MS * CLOSE_TIMEOUT_K;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The $0 timing preflight (PRD item 27, M3 §2): run every close STAGE once,
|
|
45
|
+
* ignoring verdicts — first-red-wins stays for the real verdict precheck
|
|
46
|
+
* (`src/planrun.js`'s `check-preflight`); this is a separate, sequential pass
|
|
47
|
+
* (F131/F68: never two closes in flight against one tree) whose only purpose
|
|
48
|
+
* is TIMING. A declared-close stage (no `.cmd`, a kind executor) has nothing
|
|
49
|
+
* a command runner can spawn and is skipped — it contributes no reading, not
|
|
50
|
+
* a zero one (a silent zero would understate the real close's later cost).
|
|
51
|
+
*
|
|
52
|
+
* @param {any[]} stages a close stage chain (`runStages`'s own shape)
|
|
53
|
+
* @param {{cwd?: string, redact?: (s: string) => string, ceilingMs?: number}} [opts]
|
|
54
|
+
* `ceilingMs` (test seam only — never wired to any spec/runner surface):
|
|
55
|
+
* overrides `TIMING_PREFLIGHT_CEILING_MS` so a hang-detection test does not
|
|
56
|
+
* have to wait 10 real minutes. Production callers never pass it.
|
|
57
|
+
* @returns {Promise<{
|
|
58
|
+
* perStage: {name: string, ms: number, exitCode: number|null, timedOut: boolean}[],
|
|
59
|
+
* slowestMs: number, slowestName: string|null, anyTimedOut: boolean,
|
|
60
|
+
* }>}
|
|
61
|
+
*/
|
|
62
|
+
export async function timeCloseStages(stages, { cwd, redact = (s) => s, ceilingMs = TIMING_PREFLIGHT_CEILING_MS } = {}) {
|
|
63
|
+
/** @type {{name: string, ms: number, exitCode: number|null, timedOut: boolean}[]} */
|
|
64
|
+
const perStage = [];
|
|
65
|
+
for (const st of Array.isArray(stages) ? stages : []) {
|
|
66
|
+
if (!st || typeof st !== 'object' || typeof st.cmd !== 'string') continue; // declared/kind stage — nothing to spawn
|
|
67
|
+
const start = Date.now();
|
|
68
|
+
// Sequential by construction (F68), same discipline as `runStages`: a
|
|
69
|
+
// stage's own timing pass is never in flight beside another close.
|
|
70
|
+
// eslint-disable-next-line no-await-in-loop
|
|
71
|
+
const r = await runClose(st.cmd.trim().split(/\s+/), redact, {
|
|
72
|
+
timeoutMs: ceilingMs, cwd, expect: st.expect, judged: st.judged, gapKeep: st.gapKeep,
|
|
73
|
+
});
|
|
74
|
+
const ms = Date.now() - start;
|
|
75
|
+
// `runClose` only reports `exitCode` on a NON-satisfied verdict (a
|
|
76
|
+
// satisfied close's exit code is implied by `expect`, never re-stated) —
|
|
77
|
+
// this timing pass records it either way, since "which exit code did
|
|
78
|
+
// this stage return" is a cheap, honest thing to keep for `close-timing`.
|
|
79
|
+
const exitCode = typeof r.exitCode === 'number' ? r.exitCode
|
|
80
|
+
: (r.verdict === 'satisfied' && typeof st.expect === 'number' ? st.expect : null);
|
|
81
|
+
perStage.push({
|
|
82
|
+
name: typeof st.name === 'string' ? st.name : st.cmd,
|
|
83
|
+
ms,
|
|
84
|
+
exitCode,
|
|
85
|
+
timedOut: r.verdict === 'timed-out',
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
let slowest = null;
|
|
89
|
+
for (const s of perStage) if (slowest === null || s.ms > slowest.ms) slowest = s;
|
|
90
|
+
return {
|
|
91
|
+
perStage,
|
|
92
|
+
slowestMs: slowest ? slowest.ms : 0,
|
|
93
|
+
slowestName: slowest ? slowest.name : null,
|
|
94
|
+
anyTimedOut: perStage.some((s) => s.timedOut),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The ceiling formula (PRD item 27, M3): `max(FLOOR, K × slowest measured
|
|
100
|
+
* stage)`, unless an explicit override is given — the override is the
|
|
101
|
+
* OPERATOR'S number (the rates-passthrough shape, F113: guesstimate + loud
|
|
102
|
+
* sign + customer override), so it may legally sit ABOVE or BELOW the
|
|
103
|
+
* autoset estimate; this is not tighten-only.
|
|
104
|
+
* @param {{slowestMs: number, overrideMs?: number|null}} args
|
|
105
|
+
* @returns {{ ceilingMs: number, source: 'estimated'|'override' }}
|
|
106
|
+
*/
|
|
107
|
+
export function computeCloseTimeoutCeiling({ slowestMs, overrideMs = null }) {
|
|
108
|
+
if (typeof overrideMs === 'number' && Number.isFinite(overrideMs)) {
|
|
109
|
+
return { ceilingMs: overrideMs, source: 'override' };
|
|
110
|
+
}
|
|
111
|
+
return { ceilingMs: Math.max(CLOSE_TIMEOUT_FLOOR_MS, CLOSE_TIMEOUT_K * slowestMs), source: 'estimated' };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* ONE resolver for "what per-stage close timeout does this job get", shared
|
|
116
|
+
* by `src/planrun.js` (the in-run path) and `scripts/run-u.mjs` (which must
|
|
117
|
+
* know the answer BEFORE it spawns the outside watchdog — F67 — so the
|
|
118
|
+
* watchdog's own stale/grace windows are sized off the real ceiling rather
|
|
119
|
+
* than a hardcoded guess). Two callers computing this independently is
|
|
120
|
+
* exactly the class of drift this whole rung exists to close (F129's own
|
|
121
|
+
* lesson, one level up): this function is the only place the precedence is
|
|
122
|
+
* spelled out.
|
|
123
|
+
*
|
|
124
|
+
* Precedence: a signed spec override (`job.closeTimeoutMs`) wins outright,
|
|
125
|
+
* with NO timing pass run (the pass would waste real machine time and, on a
|
|
126
|
+
* deliberately slow/hung stage, block for up to `TIMING_PREFLIGHT_CEILING_MS`
|
|
127
|
+
* for no purpose). Absent that, every stage is timed once (F131's autoset
|
|
128
|
+
* design) and the ceiling is `max(FLOOR, K × slowest)`.
|
|
129
|
+
* @param {{job: any, stages: any[], cwd: string, redact?: (s: string) => string, ceilingMs?: number}} args
|
|
130
|
+
* `ceilingMs` — see `timeCloseStages`; test seam only.
|
|
131
|
+
* @returns {Promise<{
|
|
132
|
+
* closeTimeoutMs: number|null, source: 'estimated'|'override',
|
|
133
|
+
* timing: Awaited<ReturnType<typeof timeCloseStages>>|null, timedOut: boolean,
|
|
134
|
+
* }>} `closeTimeoutMs: null, timedOut: true` means a stage never finished the
|
|
135
|
+
* timing pass — the caller reds `close-timing-red` rather than trusting any
|
|
136
|
+
* number here.
|
|
137
|
+
*/
|
|
138
|
+
export async function resolveCloseTimeoutMs({ job, stages, cwd, redact = (s) => s, ceilingMs }) {
|
|
139
|
+
const signedOverride = typeof job?.closeTimeoutMs === 'number' ? job.closeTimeoutMs : null;
|
|
140
|
+
if (signedOverride !== null) {
|
|
141
|
+
return { closeTimeoutMs: signedOverride, source: 'override', timing: null, timedOut: false };
|
|
142
|
+
}
|
|
143
|
+
const timing = await timeCloseStages(stages, { cwd, redact, ...(ceilingMs !== undefined ? { ceilingMs } : {}) });
|
|
144
|
+
if (timing.anyTimedOut) return { closeTimeoutMs: null, source: 'estimated', timing, timedOut: true };
|
|
145
|
+
const computed = computeCloseTimeoutCeiling({ slowestMs: timing.slowestMs });
|
|
146
|
+
return { closeTimeoutMs: computed.ceilingMs, source: computed.source, timing, timedOut: false };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The printed banner (build spec M3 §2's exact wording) — ONE spelling, so
|
|
151
|
+
* the run-u tail and the bundle CLI can never drift into two announcements of
|
|
152
|
+
* the same number. `source` accepts 'explicit' too: a caller-passed
|
|
153
|
+
* `closeTimeoutMs` runtime option (the pre-M3 shell/test knob, kept for
|
|
154
|
+
* backward compatibility and test control — never the signed spec field) is
|
|
155
|
+
* a real override but not a SIGNED one, so it earns its own honest word
|
|
156
|
+
* rather than being folded into 'signed override'.
|
|
157
|
+
* @param {{ceilingMs: number, source: 'estimated'|'override'|'explicit', slowestMs?: number, slowestName?: string|null}} args
|
|
158
|
+
* @returns {string}
|
|
159
|
+
*/
|
|
160
|
+
export function closeTimeoutBanner({ ceilingMs, source, slowestMs, slowestName }) {
|
|
161
|
+
const seconds = Math.round(ceilingMs / 1000);
|
|
162
|
+
if (source === 'override') return `close timeout: ${seconds}s per stage (signed override)`;
|
|
163
|
+
if (source === 'explicit') return `close timeout: ${seconds}s per stage (explicit runner override)`;
|
|
164
|
+
return `close timeout: ${seconds}s per stage (estimated from seed timing: slowest ${slowestName ?? '<none>'} ${slowestMs ?? 0}ms × ${CLOSE_TIMEOUT_K}, floor ${Math.round(CLOSE_TIMEOUT_FLOOR_MS / 1000)}s)`;
|
|
165
|
+
}
|
package/src/declaredclose.js
CHANGED
|
@@ -567,13 +567,14 @@ function translate(r, redact) {
|
|
|
567
567
|
* seedTrees?: any, gapCap?: number, maxBuffer?: number, baselineMode?: 'auto'|'worktree',
|
|
568
568
|
* humanRuling?: {decision: string, text?: string|null}|null,
|
|
569
569
|
* judgeLoop?: ((o: {system: string}) => any)|null,
|
|
570
|
-
* onJudgeCost?: ((c: any) => void)|null
|
|
570
|
+
* onJudgeCost?: ((c: any) => void)|null,
|
|
571
|
+
* callBounds?: (() => {timeoutMs?: number, deadlineMs?: number})|null}} [opts]
|
|
571
572
|
* @returns {Promise<any>}
|
|
572
573
|
*/
|
|
573
574
|
export async function runDeclaredStages(stages, redact = (s) => s, opts = {}) {
|
|
574
575
|
const {
|
|
575
576
|
timeoutMs, cwd, seedRef, seedTrees: shared, gapCap, maxBuffer, baselineMode, humanRuling = null,
|
|
576
|
-
judgeLoop = null, onJudgeCost = null,
|
|
577
|
+
judgeLoop = null, onJudgeCost = null, callBounds = null,
|
|
577
578
|
} = opts;
|
|
578
579
|
if (!isNonEmptyString(cwd) || !isNonEmptyString(seedRef)) {
|
|
579
580
|
// Refuse rather than guess. A declared close measures against a SEED; a
|
|
@@ -611,6 +612,12 @@ export async function runDeclaredStages(stages, redact = (s) => s, opts = {}) {
|
|
|
611
612
|
// until this kind, and a budget must fund the attempt PLUS its close.
|
|
612
613
|
judgeLoop,
|
|
613
614
|
onJudgeCost,
|
|
615
|
+
// F152/PRD 30.4 — the SAME per-call time bounds every worker round already
|
|
616
|
+
// carries (`callBounds()`, src/planrun.js:1518), so the judged stage's own
|
|
617
|
+
// locate call cannot hang the process on a live silent endpoint. `null` is
|
|
618
|
+
// the honest absence (an adopter/test that wires no clock through), never a
|
|
619
|
+
// defaulted number invented here.
|
|
620
|
+
callBounds,
|
|
614
621
|
seedTrees,
|
|
615
622
|
};
|
|
616
623
|
try {
|
package/src/index.js
CHANGED
|
@@ -262,6 +262,20 @@ export { PROMPT_REGISTERS, isPromptFile } from './promptregisters.js';
|
|
|
262
262
|
export {
|
|
263
263
|
exportBundle, bundleHash, readBundle, resolveBundleSpec, checkEnvelope, bless, verifyBlessing, appendHistory, checkBundleDeps,
|
|
264
264
|
} from './bundle.js';
|
|
265
|
+
// Close integrity (PRD item 27 / M1, `docs/product/CLOSE-INTEGRITY-BUILD.md`)
|
|
266
|
+
// — the ONE `close-absolute-path` detector shared by `exportBundle` (above)
|
|
267
|
+
// and `runPlan`'s run-start precheck. `readCloseScripts` is the pure reader
|
|
268
|
+
// M2's sha256 fingerprint will reuse.
|
|
269
|
+
export { readCloseScripts, checkCloseAbsolutePaths, absolutePathLiteralsOf } from './close-integrity.js';
|
|
270
|
+
// Close integrity M2 (PRD item 27, `docs/product/CLOSE-INTEGRITY-BUILD.md`) —
|
|
271
|
+
// the close-BYTES half. `hashCloseScriptBytes`/`checkCloseByteSignature` are
|
|
272
|
+
// the fingerprint the run-start precheck and `exportBundle` both compare
|
|
273
|
+
// against; `checkStageByteSignature` is the narrower re-verify `runCloseStages`
|
|
274
|
+
// (src/planrun.js) runs before every close run; `signCloseScripts` is the
|
|
275
|
+
// pure minting helper `scripts/sign-close.mjs` wraps (never writes itself).
|
|
276
|
+
export {
|
|
277
|
+
hashCloseScriptBytes, checkCloseByteSignature, checkStageByteSignature, signCloseScripts,
|
|
278
|
+
} from './close-integrity.js';
|
|
265
279
|
// Export M2 (`docs/product/EXPORT-BUILD.md`) — the CLI's one entry point,
|
|
266
280
|
// exported as `cliMain` (never `main`, a name generic enough to collide with
|
|
267
281
|
// an adopter's own). `bin/bareloop.mjs` is the only other caller; this export
|
package/src/job.js
CHANGED
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
// judgment call. Minting policy is product doctrine, not job-authorable.
|
|
15
15
|
|
|
16
16
|
import { createHash } from 'node:crypto';
|
|
17
|
-
import { globToPrefix, scopeContained, isObj, isNonEmptyString, sweepSecretLiterals, hasNestedQuantifier } from './validate.js';
|
|
17
|
+
import { globToPrefix, scopeContained, isObj, isNonEmptyString, sweepSecretLiterals, hasNestedQuantifier, closeScriptCandidateToken } from './validate.js';
|
|
18
18
|
import { validateCloseDecl, DECLARED_CLOSE_CLASSES } from './declaredclose.js';
|
|
19
|
+
import { CLOSE_TIMEOUT_FLOOR_MS } from './closetimeout.js';
|
|
19
20
|
|
|
20
21
|
// The menus below ARE the close-authoring hierarchy (PRD §7) and ship frozen:
|
|
21
22
|
// they are read at call time, so a mutable export would let adopter code
|
|
@@ -132,7 +133,11 @@ export const CADENCE_UNITS = Object.freeze(['hour', 'day', 'week']);
|
|
|
132
133
|
* subscription — its costUsd axis is NOTIONAL (API-equivalent value, not
|
|
133
134
|
* billed), so it is a DISTINCT condition: rows never pool with anthropic-api
|
|
134
135
|
* rows on the cost axis, and budgets sized for one do not transfer. */
|
|
135
|
-
|
|
136
|
+
/** PRD item 28 (2026-09-06/09-09): `openai-api` — an OpenAI-shaped provider,
|
|
137
|
+
* constructed via `src/providers.js`'s factory. hamr's ruling (PRD 30.7):
|
|
138
|
+
* `deepseek-chat` is THE secondary provider (one, not a menu of half-tested
|
|
139
|
+
* models); the factory's tier table is where that lives, not this menu. */
|
|
140
|
+
export const PROVIDERS = Object.freeze(['anthropic-api', 'openai-api', 'clipipe-subscription']);
|
|
136
141
|
/** V3 environment label: declared keys only — every field is a lineage-key
|
|
137
142
|
* candidate at N3. `provider` is part of the key by definition (top-level,
|
|
138
143
|
* not duplicated here). */
|
|
@@ -140,7 +145,7 @@ export const CONDITION_KEYS = Object.freeze(['providerPath', 'closeVerbosity', '
|
|
|
140
145
|
// `steps` stays in the field list ONLY so a retired spec reds by name
|
|
141
146
|
// (`shape-retired`) instead of falling through to a generic unknown-field —
|
|
142
147
|
// the operator gets told what happened, not just that something is wrong.
|
|
143
|
-
const JOB_FIELDS = ['schema', 'job', 'description', 'provider', 'conditions', 'cadence', 'budgetUsd', 'maxWallMs', 'model', 'writeScope', 'steps', 'escalation', 'goal', 'verdictType', 'close', 'closeDecl', 'checks', 'tools'];
|
|
148
|
+
const JOB_FIELDS = ['schema', 'job', 'description', 'provider', 'baseUrl', 'conditions', 'cadence', 'budgetUsd', 'maxWallMs', 'closeTimeoutMs', 'model', 'writeScope', 'steps', 'escalation', 'goal', 'verdictType', 'close', 'closeDecl', 'checks', 'tools'];
|
|
144
149
|
/** the four-field plan shape's core (decision 5) — presence of any of these
|
|
145
150
|
* declares the shape; `tools` (the ceiling) rides the shape but alone does not
|
|
146
151
|
* declare it, so a legacy spec carrying it gets a pointed red, not a conflict */
|
|
@@ -148,7 +153,7 @@ const PLAN_CORE_FIELDS = ['goal', 'verdictType', 'close', 'closeDecl', 'checks']
|
|
|
148
153
|
/** exact field set per close type — anything else is an unknown-field red
|
|
149
154
|
* (freeform code, script bodies, and minting claims all land there) */
|
|
150
155
|
const CLOSE_FIELDS = {
|
|
151
|
-
predicate: ['type', 'cmd', 'expect', 'judged', 'gapKeep'],
|
|
156
|
+
predicate: ['type', 'cmd', 'expect', 'judged', 'gapKeep', 'sha256'],
|
|
152
157
|
gold: ['type', 'expected', 'compare'],
|
|
153
158
|
rubric: ['type', 'criteria'],
|
|
154
159
|
hitl: ['type', 'prompt'],
|
|
@@ -240,6 +245,21 @@ export function validateJob(input, { shellCapUsd = 2 } = {}) {
|
|
|
240
245
|
`integer milliseconds >= ${MIN_WALL_MS} (one close timeout). Enforcement is a between-round deadline, measured as maxWallMs + closeStages x closeTimeoutMs (design addendum 1: loop.stop() cannot cut an in-flight call; W5: every close stage runs under the full timeout), so a budget under one close cannot fund its own close and the advertised number would be more wrong than right`);
|
|
241
246
|
}
|
|
242
247
|
|
|
248
|
+
// CLOSE TIMEOUT (PRD item 27/M3, hamr 2026-09-06: "both … autoset and can be
|
|
249
|
+
// override, same like api pricing"). OPTIONAL and WITH NO CEILING: absent
|
|
250
|
+
// means the runner autosets it from a $0 seed timing pass
|
|
251
|
+
// (`src/closetimeout.js`); present is the OPERATOR's own number and — like a
|
|
252
|
+
// rates-passthrough override (F113) — it may legally sit ABOVE the autoset
|
|
253
|
+
// estimate, so this is not tighten-only. The one bound that IS enforced is
|
|
254
|
+
// the floor every close timeout has always had (`CLOSE_TIMEOUT_FLOOR_MS`,
|
|
255
|
+
// hamr's arbiter constant, 2026-09-07): a value below it could never fund
|
|
256
|
+
// even a fast close, exactly the "advertised number more wrong than right"
|
|
257
|
+
// failure `maxWallMs` already guards above.
|
|
258
|
+
if (spec.closeTimeoutMs !== undefined
|
|
259
|
+
&& !(typeof spec.closeTimeoutMs === 'number' && Number.isInteger(spec.closeTimeoutMs) && spec.closeTimeoutMs >= CLOSE_TIMEOUT_FLOOR_MS)) {
|
|
260
|
+
red('bounds', 'closeTimeoutMs', `integer milliseconds >= ${CLOSE_TIMEOUT_FLOOR_MS} (the arbiter's floor) — a signed override may sit above the autoset estimate, never below the floor every close timeout has always had`);
|
|
261
|
+
}
|
|
262
|
+
|
|
243
263
|
// MODEL — the worker's provider model id (build-list #3, hamr's GO
|
|
244
264
|
// 2026-08-30). OPTIONAL, exactly like budgetUsd/maxWallMs before it: absent
|
|
245
265
|
// means today's behaviour (the runner's own default), present it is part
|
|
@@ -251,6 +271,37 @@ export function validateJob(input, { shellCapUsd = 2 } = {}) {
|
|
|
251
271
|
red('invalid-value', 'model', 'non-empty string — exact provider model id (e.g. "claude-sonnet-5")');
|
|
252
272
|
}
|
|
253
273
|
|
|
274
|
+
// BASE URL — PRD item 28, ruling (d), 2026-09-09: admitted in v1 of
|
|
275
|
+
// openai-api (an OpenAI-compatible gateway/endpoint), OPTIONAL and part of
|
|
276
|
+
// the signed hash like `model` above. This validator has no network
|
|
277
|
+
// access, so it checks SHAPE only: `https://` for a real endpoint, or
|
|
278
|
+
// `http://` restricted to a loopback host (a local OpenAI-compatible
|
|
279
|
+
// server, the only legitimate reason to skip TLS) — and never a
|
|
280
|
+
// credential embedded IN the URL itself. A URL travels into logs and
|
|
281
|
+
// spine records far more casually than a header ever would, and secrets
|
|
282
|
+
// never enter the tree/spine (hard line #3) — a bareUrl carrying
|
|
283
|
+
// `user:pass@host` is exactly that leak, just spelled differently.
|
|
284
|
+
if (spec.baseUrl !== undefined) {
|
|
285
|
+
if (!isNonEmptyString(spec.baseUrl)) {
|
|
286
|
+
red('invalid-value', 'baseUrl', 'non-empty string — an https:// URL (http:// admitted only to 127.0.0.1/localhost/::1)');
|
|
287
|
+
} else {
|
|
288
|
+
/** @type {URL|null} */
|
|
289
|
+
let u = null;
|
|
290
|
+
try { u = new URL(spec.baseUrl); } catch { /* u stays null — caught below */ }
|
|
291
|
+
if (!u) {
|
|
292
|
+
red('invalid-value', 'baseUrl', 'must be a parseable URL');
|
|
293
|
+
} else if (u.username !== '' || u.password !== '') {
|
|
294
|
+
red('invalid-value', 'baseUrl', 'no embedded credentials (user:pass@host) — secrets never enter a signed spec');
|
|
295
|
+
} else if (u.protocol === 'https:') {
|
|
296
|
+
// admitted
|
|
297
|
+
} else if (u.protocol === 'http:' && (u.hostname === '127.0.0.1' || u.hostname === 'localhost' || u.hostname === '::1')) {
|
|
298
|
+
// admitted — a local OpenAI-compatible endpoint, the one case that legitimately skips TLS
|
|
299
|
+
} else {
|
|
300
|
+
red('invalid-value', 'baseUrl', 'https:// required (http:// admitted only to 127.0.0.1/localhost/::1)');
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
254
305
|
// 3. the outer write fence — operator law (interview decision #4), same
|
|
255
306
|
// containment rules as the workflow layer THROUGH THE SAME CODE (F9: two
|
|
256
307
|
// transforms would let a fence validate green and mean something else).
|
|
@@ -356,6 +407,34 @@ function predicateBody(o, at, red) {
|
|
|
356
407
|
// refusal beats silent misparse (N2 design default)
|
|
357
408
|
else if (/["']/.test(o.cmd)) red('invalid-value', `${at}.cmd`, 'quote characters are inexpressible: cmd runs as whitespace-split argv, no shell');
|
|
358
409
|
else if (o.cmd !== o.cmd.trim()) red('invalid-value', `${at}.cmd`, 'leading/trailing whitespace — argv splits on whitespace and an empty argv[0] cannot spawn; honest refusal beats a silent misparse');
|
|
410
|
+
// Close-bytes signature (PRD item 27/M2, F129's general fix's own general
|
|
411
|
+
// fix — N4): the signature must cover the close SCRIPT'S CONTENT, not only
|
|
412
|
+
// its path (`cmd`). Demanded exactly where a byte-exact fingerprint can be
|
|
413
|
+
// minted against something: `cmd` names an addressable script by the
|
|
414
|
+
// SAME shape test `src/close-integrity.js`'s detectors use
|
|
415
|
+
// (`closeScriptCandidateToken`, src/validate.js — `node <path> …`, OR a
|
|
416
|
+
// bare directly-executable absolute path such as a `.sh` wrapper; widened
|
|
417
|
+
// 2026-09-06 after an orchestrator audit found the bare form silently
|
|
418
|
+
// invisible to both this demand and the byte/path detectors — a blind
|
|
419
|
+
// instrument). A cmd naming no such script (most test-fixture closes:
|
|
420
|
+
// `true`, `npx`, `pytest`, a relative bare executable — or a
|
|
421
|
+
// missing/malformed cmd, already reded above) carries no demand — a stage
|
|
422
|
+
// with nothing to hash gets no `missing-required` for a field that could
|
|
423
|
+
// never be minted honestly. `validateJob` stays pure (no fs): this checks
|
|
424
|
+
// the FIELD'S SHAPE only, never the bytes on disk (that is the runner's
|
|
425
|
+
// job, `checkCloseByteSignature`/`checkStageByteSignature`).
|
|
426
|
+
const isScriptCmd = closeScriptCandidateToken(o.cmd) !== null;
|
|
427
|
+
if (isScriptCmd) {
|
|
428
|
+
if (o.sha256 === undefined) {
|
|
429
|
+
red('missing-required', `${at}.sha256`, 'hex sha256 of the script file bytes `cmd` names — mint it with signCloseScripts (src/close-integrity.js) / scripts/sign-close.mjs, never hand-typed');
|
|
430
|
+
} else if (typeof o.sha256 !== 'string' || !/^[0-9a-f]{64}$/.test(o.sha256)) {
|
|
431
|
+
red('invalid-value', `${at}.sha256`, '64 lowercase hex chars — sha256 of the script file bytes, never the spec, never the path');
|
|
432
|
+
}
|
|
433
|
+
} else if (o.sha256 !== undefined && (typeof o.sha256 !== 'string' || !/^[0-9a-f]{64}$/.test(o.sha256))) {
|
|
434
|
+
// not demanded for a non-script cmd, but a PRESENT value is still
|
|
435
|
+
// shape-checked — a malformed string is a typo either way
|
|
436
|
+
red('invalid-value', `${at}.sha256`, '64 lowercase hex chars — sha256 of the script file bytes, never the spec, never the path');
|
|
437
|
+
}
|
|
359
438
|
if (!Number.isInteger(o.expect)) red('invalid-value', `${at}.expect`, 'integer exit code');
|
|
360
439
|
// The judgment-rendered signal (PRD v1.11, optional). Exit code alone
|
|
361
440
|
// cannot separate "the suite ran and failed" from "the suite crashed at
|
|
@@ -492,7 +571,7 @@ export function checkMenu(close) {
|
|
|
492
571
|
}
|
|
493
572
|
|
|
494
573
|
/** the fields a close STAGE may carry — anything else is a smuggle channel */
|
|
495
|
-
const STAGE_FIELDS = ['name', 'cmd', 'expect', 'judged', 'gapKeep', 'offer', 'needs', 'direction'];
|
|
574
|
+
const STAGE_FIELDS = ['name', 'cmd', 'expect', 'judged', 'gapKeep', 'offer', 'needs', 'direction', 'sha256'];
|
|
496
575
|
|
|
497
576
|
/**
|
|
498
577
|
* Validate the staged close. Every stage is a predicate BODY under the same
|
package/src/judged.js
CHANGED
|
@@ -565,6 +565,43 @@ export function validateFacts(facts) {
|
|
|
565
565
|
|
|
566
566
|
// ── THE LOCATE PROMPT ───────────────────────────────────────────────────────
|
|
567
567
|
|
|
568
|
+
/**
|
|
569
|
+
* THE ONE SHAPE INVENTORY (PRD 30.8), stated ONCE and never twice. It drives
|
|
570
|
+
* BOTH the LOCATE prompt's prose ("declared with ...") AND the completeness
|
|
571
|
+
* detector's regex — the same standing rule the secret-shape scrubber
|
|
572
|
+
* follows (one inventory drives detection, never two spellings that can
|
|
573
|
+
* drift). A shape added here rides both sides in the same commit; a shape
|
|
574
|
+
* added to only a hand-typed prompt string or only a hand-typed regex is
|
|
575
|
+
* exactly the blind-instrument class this repo keeps paying for.
|
|
576
|
+
*
|
|
577
|
+
* Two BASE shapes, each carrying its own independent `export`/`async`
|
|
578
|
+
* modifiers (the modifiers sit in different POSITIONS in the two shapes —
|
|
579
|
+
* `async function foo(` vs `const foo = async () =>` — so they cannot be
|
|
580
|
+
* factored into one shared prefix):
|
|
581
|
+
* - `function name(...)` — optionally `export`, optionally `async`,
|
|
582
|
+
* in either order before `function`.
|
|
583
|
+
* - `const name = (...) =>` — optionally `export` before `const`,
|
|
584
|
+
* optionally `async` after `=`.
|
|
585
|
+
*
|
|
586
|
+
* Restricted to column-0 lines only, in both the prompt's prose and the
|
|
587
|
+
* detector: a nested function or a function expression returned from one is
|
|
588
|
+
* indented, and excluding it is the SAME cure PROMPT_HEAD already states
|
|
589
|
+
* (the POC's nested-function cure).
|
|
590
|
+
* @type {ReadonlyArray<{prose: string, match: (line: string) => string|null}>}
|
|
591
|
+
*/
|
|
592
|
+
const FN_SHAPES = Object.freeze([
|
|
593
|
+
{
|
|
594
|
+
prose: '`function name(...)` (optionally `async`, optionally `export`, in either order)',
|
|
595
|
+
match: (/** @type {string} */ line) =>
|
|
596
|
+
line.match(/^(?:(?:export|async)\s+){0,2}function\s+([A-Za-z_$][\w$]*)\s*\(/)?.[1] ?? null,
|
|
597
|
+
},
|
|
598
|
+
{
|
|
599
|
+
prose: '`const name = (...) =>` (optionally `export` before `const`, optionally `async` after `=`)',
|
|
600
|
+
match: (/** @type {string} */ line) =>
|
|
601
|
+
line.match(/^(?:export\s+)?const\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s+)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>/)?.[1] ?? null,
|
|
602
|
+
},
|
|
603
|
+
]);
|
|
604
|
+
|
|
568
605
|
/** The spine every card shares. Both POC cures live here rather than on a rule,
|
|
569
606
|
* because both are properties of ENUMERATING functions and not of any one
|
|
570
607
|
* question about them: the nested-function clause (the POC counted a returned
|
|
@@ -573,8 +610,7 @@ export function validateFacts(facts) {
|
|
|
573
610
|
const PROMPT_HEAD =
|
|
574
611
|
'You LOCATE FACTS in a source file. You never decide whether anything passes, and you never judge — '
|
|
575
612
|
+ 'another system does that.\n'
|
|
576
|
-
+
|
|
577
|
-
+ 'or `export function name(...)`), report:\n'
|
|
613
|
+
+ `For EVERY top-level function in the FILE (declared with ${FN_SHAPES.map((s) => s.prose).join(', or ')}), report:\n`
|
|
578
614
|
+ 'Count ONLY functions declared at the top level of the module — never a function nested inside '
|
|
579
615
|
+ 'another function, and never a function expression returned from one.\n'
|
|
580
616
|
+ ' "name": the function name\n'
|
|
@@ -596,6 +632,43 @@ export function locatePrompt(card) {
|
|
|
596
632
|
return `${PROMPT_HEAD}${asks}\n${PROMPT_TAIL}`;
|
|
597
633
|
}
|
|
598
634
|
|
|
635
|
+
/**
|
|
636
|
+
* THE COMPLETENESS MIRROR of `PROMPT_HEAD`'s own contract (PRD 30.8): which
|
|
637
|
+
* top-level function declarations the artifact ACTUALLY holds, read off the
|
|
638
|
+
* SAME `FN_SHAPES` inventory the prompt's prose is built from, restricted to
|
|
639
|
+
* column-0 lines.
|
|
640
|
+
*
|
|
641
|
+
* This is a HEURISTIC, never a parser, and it is used for exactly one thing:
|
|
642
|
+
* telling `decide()` when LOCATE's `functions` list is short of what the file
|
|
643
|
+
* holds. It never grades a rule and it never adds a fact `decide()` acts on
|
|
644
|
+
* beyond "this name exists at the top level" — the missing function's rule
|
|
645
|
+
* violations, if any, stay unknown, which is exactly why the miss is UNSURE
|
|
646
|
+
* and not a manufactured red on a specific rule.
|
|
647
|
+
*
|
|
648
|
+
* TWO DIRECTIONS, both handled but not symmetrically by design:
|
|
649
|
+
* - a shape THIS inventory does not enumerate is a FALSE NEGATIVE — the
|
|
650
|
+
* check stays silent about that function, same as it stays silent with
|
|
651
|
+
* no `artifactText` at all. Widening `FN_SHAPES` closes the hole; nothing
|
|
652
|
+
* here manufactures a claim about a shape it was never told to look for.
|
|
653
|
+
* - a column-0 line that only LOOKS like a declaration (e.g. inside a
|
|
654
|
+
* template literal) is a FALSE POSITIVE and reds a real judged close —
|
|
655
|
+
* that is the SAFE side of the fail-safe tiebreak (unsure is red), and it
|
|
656
|
+
* is never silently swallowed the way a false negative would be.
|
|
657
|
+
* @param {string} artifactText @returns {string[]}
|
|
658
|
+
*/
|
|
659
|
+
function topLevelFunctionNames(artifactText) {
|
|
660
|
+
/** @type {string[]} */
|
|
661
|
+
const names = [];
|
|
662
|
+
for (const line of artifactText.split('\n')) {
|
|
663
|
+
if (/^\s/.test(line)) continue; // indented: nested, or a returned function expression — never top level
|
|
664
|
+
for (const shape of FN_SHAPES) {
|
|
665
|
+
const name = shape.match(line);
|
|
666
|
+
if (name) { names.push(name); break; }
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
return names;
|
|
670
|
+
}
|
|
671
|
+
|
|
599
672
|
/** the default toolless seam — one spelling of how this repo drives a judge.
|
|
600
673
|
* A `Loop`, NOT `provider.generate()`: the POC measured that a bare provider
|
|
601
674
|
* result carries no `costUsd` at all, and under F6 an unpriced call is a red,
|
|
@@ -628,12 +701,23 @@ export const defaultJudgeLoop = ({ provider, system }) => {
|
|
|
628
701
|
* On every route out, red or clean, `onCost` fires exactly once with the honest
|
|
629
702
|
* read: a paid call that leaves no meter record is F12 wearing a judge's coat.
|
|
630
703
|
* @param {{artifactText: string, card: any, loopFactory: (o: {provider?: any, system: string}) => any,
|
|
631
|
-
* maxTokens?: number, attempt?: number, onCost?: (c: {costUsd: number|null, unpricedRounds: number}) => void
|
|
704
|
+
* maxTokens?: number, attempt?: number, onCost?: (c: {costUsd: number|null, unpricedRounds: number}) => void,
|
|
705
|
+
* callBounds?: {timeoutMs?: number, deadlineMs?: number}}} o
|
|
632
706
|
* @returns {Promise<{ok: boolean, facts: any|null, red: {axis: string, detail: string}|null,
|
|
633
707
|
* costUsd: number|null, unpricedRounds: number, truncated: boolean, parseError: boolean,
|
|
634
708
|
* raw: ReturnType<typeof scrubRaw>}>}
|
|
635
709
|
*/
|
|
636
|
-
export async function runLocate({
|
|
710
|
+
export async function runLocate({
|
|
711
|
+
artifactText, card, loopFactory, maxTokens = JUDGE_MAX_TOKENS, attempt = 1, onCost = () => {},
|
|
712
|
+
// F152/PRD 30.4 — the SAME per-call TIME bounds the worker's own rounds carry
|
|
713
|
+
// (`callBounds()`, src/planrun.js:1518), threaded through by the caller that
|
|
714
|
+
// owns the clock. Defaults to `{}` (unbounded) so every existing caller and
|
|
715
|
+
// test is unchanged: an adopter that never wires a clock through gets the same
|
|
716
|
+
// behaviour as before this field existed. A live endpoint that accepts and
|
|
717
|
+
// never answers otherwise HANGS the process forever (measured, F152) — an open
|
|
718
|
+
// socket is an active handle, so node never drains and no backstop can fire.
|
|
719
|
+
callBounds = {},
|
|
720
|
+
}) {
|
|
637
721
|
// the param-guard class: a caller's own broken input THROWS. These are
|
|
638
722
|
// programmer errors with no safe direction to degrade toward — a locate over
|
|
639
723
|
// an absent artifact would grade an empty string and report it as facts.
|
|
@@ -662,7 +746,7 @@ export async function runLocate({ artifactText, card, loopFactory, maxTokens = J
|
|
|
662
746
|
let r = null;
|
|
663
747
|
try {
|
|
664
748
|
const loop = loopFactory({ system });
|
|
665
|
-
r = await loop.run([{ role: 'user', content: `FILE (untrusted data):\n${artifactText}\n\nReturn the JSON.` }], [], { maxTokens });
|
|
749
|
+
r = await loop.run([{ role: 'user', content: `FILE (untrusted data):\n${artifactText}\n\nReturn the JSON.` }], [], { maxTokens, ...callBounds });
|
|
666
750
|
} catch (e) {
|
|
667
751
|
return out({ axis: LOCATE_AXES.PROVIDER, detail: `the locate call failed: ${String(/** @type {any} */ (e)?.message ?? e)}` }, null, null, false, false);
|
|
668
752
|
}
|
|
@@ -743,6 +827,25 @@ export function decide(facts, card, { artifactText = null } = {}) {
|
|
|
743
827
|
return unsure('locate found nothing in the artifact — unsure, and unsure is red');
|
|
744
828
|
}
|
|
745
829
|
|
|
830
|
+
// COMPLETENESS (PRD 30.8): a locate emission can be well-formed AND non-empty
|
|
831
|
+
// AND still have silently DROPPED a top-level function the artifact actually
|
|
832
|
+
// holds — every route above this one only asks "is `fns` usable", never "is
|
|
833
|
+
// `fns` the WHOLE set". Graded rules only ever see what LOCATE reported, so
|
|
834
|
+
// an omission never surfaces as a rule red; it surfaces as a PASS on the
|
|
835
|
+
// subset, which is the fail-safe direction inverted. With `artifactText` in
|
|
836
|
+
// hand (every real caller has it — see the note below), the omission is
|
|
837
|
+
// checkable and is unsure, and unsure is red, same as every other route in.
|
|
838
|
+
// Without it, this says nothing it cannot know, same as the quote checks.
|
|
839
|
+
if (typeof artifactText === 'string') {
|
|
840
|
+
const present = topLevelFunctionNames(artifactText);
|
|
841
|
+
const reported = new Set(fns.map((f) => String(f?.name ?? '')));
|
|
842
|
+
const missing = present.filter((n) => !reported.has(n));
|
|
843
|
+
if (missing.length > 0) {
|
|
844
|
+
return unsure(`locate omitted top-level function(s) the artifact contains: ${missing.join(', ')} — a pass on `
|
|
845
|
+
+ 'the reported subset is not a pass on the file, and unsure is red');
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
746
849
|
const lines = typeof artifactText === 'string'
|
|
747
850
|
? new Set(artifactText.split('\n').map((l) => l.trim()).filter(Boolean))
|
|
748
851
|
: null;
|
package/src/kinds.js
CHANGED
|
@@ -260,6 +260,7 @@ export function mechanicalStages(stages) {
|
|
|
260
260
|
* judgeLoop?: (o: {system: string}) => any,
|
|
261
261
|
* onJudgeCost?: (c: {stage: string|null, kind: string|null, path: string, attempt: number,
|
|
262
262
|
* label: string, model: string, costUsd: number|null, unpricedRounds: number}) => void,
|
|
263
|
+
* callBounds?: () => {timeoutMs?: number, deadlineMs?: number},
|
|
263
264
|
* onStage?: (row: {stage: string|null, kind: string|null, verdict: string, durationMs: number}) => void}} Ctx
|
|
264
265
|
* The arbiter's half of a stage run. Every field here is operator/runner
|
|
265
266
|
* territory: a declaration parameterises kinds, never the contracts.
|
|
@@ -275,6 +276,11 @@ export function mechanicalStages(stages) {
|
|
|
275
276
|
* funded honestly: this executor cannot reach the run's ledger, so every locate
|
|
276
277
|
* call's cost is reported OUT through it, once per call, on every route including
|
|
277
278
|
* the red ones — a paid call that leaves no meter record is F12 in a judge's coat.
|
|
279
|
+
* `callBounds` is F152/PRD 30.4's per-call TIME bound, read fresh before every
|
|
280
|
+
* locate attempt — the same derivation the run's own worker rounds carry
|
|
281
|
+
* (`callBounds()`, src/planrun.js). Absent is the honest unbounded default:
|
|
282
|
+
* an adopter or test that wires no clock through behaves exactly as before
|
|
283
|
+
* this field existed.
|
|
278
284
|
* `onStage` is a REPORTING seam and nothing else: the seed read spawns a real
|
|
279
285
|
* toolchain per stage (a suite is the slow one) and a whole read is one opaque
|
|
280
286
|
* await, so the shell gets told as each stage lands. It decides nothing, it is
|
|
@@ -1781,6 +1787,12 @@ async function runJudgedFloor(stage, ctx) {
|
|
|
1781
1787
|
card: p.card,
|
|
1782
1788
|
loopFactory,
|
|
1783
1789
|
attempt,
|
|
1790
|
+
// F152/PRD 30.4 — read fresh per attempt, the same way `planrun.js`'s own
|
|
1791
|
+
// worker rounds re-read `callBounds()` at every call site rather than
|
|
1792
|
+
// once: the remaining time shrinks between attempts. Absent (no clock
|
|
1793
|
+
// wired through) is `{}`, the honest unbounded default `runLocate` itself
|
|
1794
|
+
// already carries.
|
|
1795
|
+
callBounds: typeof ctx.callBounds === 'function' ? ctx.callBounds() : {},
|
|
1784
1796
|
onCost: (c) => {
|
|
1785
1797
|
if (typeof ctx.onJudgeCost !== 'function') return;
|
|
1786
1798
|
ctx.onJudgeCost({
|
package/src/plan.js
CHANGED
|
@@ -177,7 +177,11 @@ export { hasNestedQuantifier };
|
|
|
177
177
|
export function stageClose(close) {
|
|
178
178
|
if (Array.isArray(close)) return close;
|
|
179
179
|
if (!isObj(close) || close.type !== 'predicate') return null;
|
|
180
|
-
|
|
180
|
+
// `sha256` (M2, PRD item 27) rides through exactly like every other
|
|
181
|
+
// predicate-body field — a single-object predicate close is byte-signable
|
|
182
|
+
// the same way a staged close's stages are, and `checkCloseByteSignature`
|
|
183
|
+
// reads this field off whatever `closeStagesOf` hands back.
|
|
184
|
+
return [{ name: 'close', cmd: close.cmd, expect: close.expect, judged: close.judged, gapKeep: close.gapKeep, sha256: close.sha256 }];
|
|
181
185
|
}
|
|
182
186
|
|
|
183
187
|
/**
|