bareloop 0.11.0 → 0.13.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 +252 -0
- package/README.md +3 -2
- package/bareloop.context.md +387 -9
- package/package.json +1 -1
- package/src/authorflow.js +176 -33
- package/src/authoring.js +170 -25
- package/src/authorjob.js +340 -26
- package/src/bridges.js +237 -10
- package/src/calibrate.js +597 -0
- package/src/cardauthor.js +479 -0
- package/src/declaredclose.js +108 -10
- package/src/index.js +100 -9
- package/src/job.js +27 -13
- package/src/judged.js +765 -0
- package/src/kinds.js +390 -6
- package/src/ledger.js +102 -0
- package/src/plan.js +43 -2
- package/src/planrun.js +492 -26
- package/src/readshim.js +538 -0
- package/src/reuse.js +322 -90
- package/src/reviewdoor.js +246 -0
- package/src/run.js +95 -6
- package/src/selection.js +22 -6
- package/src/tools.js +34 -2
- package/types/authorflow.d.ts +147 -9
- package/types/authoring.d.ts +54 -8
- package/types/authorjob.d.ts +36 -6
- package/types/bridges.d.ts +95 -1
- package/types/calibrate.d.ts +191 -0
- package/types/cardauthor.d.ts +352 -0
- package/types/declaredclose.d.ts +34 -5
- package/types/index.d.ts +12 -7
- package/types/job.d.ts +22 -9
- package/types/judged.d.ts +280 -0
- package/types/kinds.d.ts +151 -4
- package/types/ledger.d.ts +58 -0
- package/types/plan.d.ts +19 -2
- package/types/planrun.d.ts +91 -2
- package/types/readshim.d.ts +117 -0
- package/types/reuse.d.ts +146 -3
- package/types/reviewdoor.d.ts +81 -0
- package/types/run.d.ts +63 -1
- package/types/tools.d.ts +17 -1
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
// THE REVIEW DOOR — the signer's three doors at the END of a run (softgreen
|
|
2
|
+
// module 8; PRD v1.71 §3, design record 2026-08-17 §3 + the 2026-08-18 addenda).
|
|
3
|
+
//
|
|
4
|
+
// THE LAW, hamr verbatim: *"it's important not to change the loop self verdict."*
|
|
5
|
+
// The close mints the verdict and the ledger records it before anything here runs.
|
|
6
|
+
// What a door records is a DISPOSITION — what the person wants done about a
|
|
7
|
+
// verdict, never what the verdict is. Nothing in this file returns, writes or
|
|
8
|
+
// implies an outcome, and the run it answers has already ended.
|
|
9
|
+
//
|
|
10
|
+
// WHY THIS IS NOT `runPlan`'s job. A door opens after the run is over: a person
|
|
11
|
+
// may answer minutes or days later, from a different process, and the answer
|
|
12
|
+
// cannot ride the run's own return path (module 6 named this when it left
|
|
13
|
+
// `applyDoorDecision` exported and unwired). So the door is OPENED by the run
|
|
14
|
+
// (one record on its spine, `src/planrun.js`) and ANSWERED here, off that record.
|
|
15
|
+
//
|
|
16
|
+
// THE THREE DOORS, and what each one costs:
|
|
17
|
+
// accept — the close's MECHANICAL stages re-run against the tree as it stands
|
|
18
|
+
// (hamr's N4 ruling: an accept is not a rubber stamp), and only then is
|
|
19
|
+
// the disposition recorded and — for a held judged green — the learning
|
|
20
|
+
// credit released. A red at re-run means the tree changed under the
|
|
21
|
+
// signer: the accept is REFUSED with the stage named. Never the judged
|
|
22
|
+
// or human stages: a machine may re-prove a machine's reading, and a
|
|
23
|
+
// judgment is not re-run behind the person who rendered it.
|
|
24
|
+
// rerun — a fresh engagement (§3.4). The person's words ARE the gap; empty
|
|
25
|
+
// words are refused at the same seam every other door is refused at.
|
|
26
|
+
// It re-proves nothing here — the NEW run's close is what judges.
|
|
27
|
+
// pause — not now. Nothing runs, nothing is spent, and the same three doors are
|
|
28
|
+
// open the next time somebody looks. An unanswered door expires under
|
|
29
|
+
// the existing 60-day checkpoint TTL, and that expiry IS what `cancel`
|
|
30
|
+
// used to do (doors addendum, 2026-08-18).
|
|
31
|
+
//
|
|
32
|
+
// The registry half is module 6's (`applyDoorDecision`), called from exactly one
|
|
33
|
+
// place here so a disposition cannot be recorded twice or by two spellings.
|
|
34
|
+
|
|
35
|
+
import { REVIEW_DOOR, HUMAN_DECISIONS, mechanicalStages, normalizeHumanRuling } from './kinds.js';
|
|
36
|
+
import { closeStagesOf } from './plan.js';
|
|
37
|
+
import { isDeclaredClose, runDeclaredStages, HITL_DECISION_RED } from './declaredclose.js';
|
|
38
|
+
import { runStages } from './ralph.js';
|
|
39
|
+
import { redactSecrets } from './validate.js';
|
|
40
|
+
import { applyDoorDecision, PAUSE_TTL_MS } from './reuse.js';
|
|
41
|
+
|
|
42
|
+
/** the door's own record, off the run's spine — the LAST one, because a run that
|
|
43
|
+
* ends at a door writes exactly one and a reader that guesses which is which is a
|
|
44
|
+
* reader that will one day answer the wrong question.
|
|
45
|
+
* @param {any[]} events the run's spine, parsed
|
|
46
|
+
* @returns {any|null} */
|
|
47
|
+
export function doorRecordOf(events) {
|
|
48
|
+
if (!Array.isArray(events)) return null;
|
|
49
|
+
return events.findLast?.((e) => e?.type === REVIEW_DOOR) ?? null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* IS THE DOOR STILL OPEN? The same 60-day TTL a hitl checkpoint keeps
|
|
54
|
+
* (`PAUSE_TTL_MS`) and deliberately the same NUMBER — the door replaced `cancel`
|
|
55
|
+
* precisely because an unanswered door and an unresumed pause are one state, and
|
|
56
|
+
* two TTLs for one state is how they come to disagree.
|
|
57
|
+
*
|
|
58
|
+
* An UNREADABLE stamp refuses too: unknown is not young (F6). Reading the age is
|
|
59
|
+
* not the same as deciding on it, so this states both the answer and the arithmetic.
|
|
60
|
+
* @param {any} door the door record
|
|
61
|
+
* @param {{now?: () => number, ttlMs?: number}} [opts]
|
|
62
|
+
* @returns {{ok: boolean, ageMs: number|null, ttlMs: number, detail: string}}
|
|
63
|
+
*/
|
|
64
|
+
export function doorAgeGate(door, { now = Date.now, ttlMs = PAUSE_TTL_MS } = {}) {
|
|
65
|
+
const days = (/** @type {number} */ ms) => (ms / 86_400_000).toFixed(1);
|
|
66
|
+
const at = typeof door?.ts === 'string' ? Date.parse(door.ts) : NaN;
|
|
67
|
+
if (!Number.isFinite(at)) {
|
|
68
|
+
return { ok: false, ageMs: null, ttlMs, detail: 'the door carries no readable timestamp, and an unknown age is not a young one — the decision cannot be honoured on a record nobody can date' };
|
|
69
|
+
}
|
|
70
|
+
const ageMs = now() - at;
|
|
71
|
+
if (ageMs > ttlMs) {
|
|
72
|
+
return { ok: false, ageMs, ttlMs, detail: `the door was opened ${days(ageMs)} days ago and a door keeps for ${days(ttlMs)} days — it has expired on its own, which is exactly what nobody having to decide looks like` };
|
|
73
|
+
}
|
|
74
|
+
return { ok: true, ageMs, ttlMs, detail: `${days(ageMs)} days old, of ${days(ttlMs)}` };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** one red, one spelling @param {string} code @param {string} detail @param {string} [path] */
|
|
78
|
+
const red = (code, detail, path = 'decision') => [{ code, path, detail }];
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Answer a run's review door.
|
|
82
|
+
*
|
|
83
|
+
* Never throws and never conjures: a malformed decision, a run that opened no
|
|
84
|
+
* door, an expired one, a tree that moved and a registry that is not there all
|
|
85
|
+
* come back as named reds. The loop's verdict is untouched on every path — this
|
|
86
|
+
* function has no way to write one.
|
|
87
|
+
*
|
|
88
|
+
* @param {object} opts
|
|
89
|
+
* @param {any} opts.job the SIGNED spec the run was judged by (its close is what an accept re-proves)
|
|
90
|
+
* @param {string} opts.workdir the patient, as it stands NOW
|
|
91
|
+
* @param {any[]} opts.events the run's own spine, parsed
|
|
92
|
+
* @param {string} opts.decision one of the three doors
|
|
93
|
+
* @param {string|null} [opts.text] the rerun's words — the gap, and refused when empty
|
|
94
|
+
* @param {number} [opts.closeTimeoutMs] the per-stage bound for the accept's re-run
|
|
95
|
+
* @param {string|null} [opts.registryDir] the workflow registry, when the runner keeps one
|
|
96
|
+
* @param {string|null} [opts.name] the workflow this run wrote its row against
|
|
97
|
+
* @param {string|null} [opts.runid] the RUN's id, exactly as the bridge row records it
|
|
98
|
+
* @param {string|null} [opts.at] when the person answered
|
|
99
|
+
* @param {() => number} [opts.now] the clock, injected for the TTL
|
|
100
|
+
* @param {number} [opts.ttlMs]
|
|
101
|
+
* @param {((type: string, data: any) => any)|null} [opts.emit] the run's spine, to record the disposition on
|
|
102
|
+
* @returns {Promise<{ok: boolean, decision: string|null, next: string|null, released: boolean,
|
|
103
|
+
* doorRecorded: boolean, reds: any[], mechanical: any, door: any, text: string|null,
|
|
104
|
+
* options: string[], ttlMs: number, note: string|null}>}
|
|
105
|
+
*/
|
|
106
|
+
export async function answerReviewDoor({
|
|
107
|
+
job, workdir, events, decision, text = null, closeTimeoutMs,
|
|
108
|
+
registryDir = null, name = null, runid = null, at = null,
|
|
109
|
+
now = Date.now, ttlMs = PAUSE_TTL_MS, emit = null,
|
|
110
|
+
}) {
|
|
111
|
+
/** @type {any} */
|
|
112
|
+
const out = {
|
|
113
|
+
ok: false, decision: null, next: null, released: false, doorRecorded: false,
|
|
114
|
+
reds: [], mechanical: null, door: null, text: null, options: [...HUMAN_DECISIONS], ttlMs, note: null,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// ── 1. THE OPERATOR'S OWN INPUT, at the LIBRARY's one seam. A fourth door and
|
|
118
|
+
// an empty rerun are refused by `normalizeHumanRuling` — the same rulebook the
|
|
119
|
+
// close's human stage is answered through, so a surface cannot admit what a run
|
|
120
|
+
// refuses.
|
|
121
|
+
const norm = normalizeHumanRuling({ decision, ...(text === null ? {} : { text }) });
|
|
122
|
+
if (!norm.ok) {
|
|
123
|
+
out.reds = red(HITL_DECISION_RED, /** @type {string} */ (norm.why));
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
const ruling = /** @type {{decision: string, text: string|null}} */ (norm.ruling);
|
|
127
|
+
out.decision = ruling.decision;
|
|
128
|
+
out.text = ruling.text;
|
|
129
|
+
|
|
130
|
+
// ── 2. IS THERE A DOOR? A run that opened none has nothing to answer, and
|
|
131
|
+
// conjuring one would let a decision be recorded against a verdict nobody was
|
|
132
|
+
// ever shown the evidence for.
|
|
133
|
+
const door = doorRecordOf(events);
|
|
134
|
+
if (door === null) {
|
|
135
|
+
out.reds = red('no-door', 'this run opened no review door — its verdict stands on its own, and there is nothing here to dispose of (a green-class run opens one only when the runner asks: --review-door)', 'events');
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
out.door = door;
|
|
139
|
+
|
|
140
|
+
// ── 3. IS IT STILL OPEN? The TTL is the cancel case: nobody has to decide, and
|
|
141
|
+
// a door nobody answered simply expires.
|
|
142
|
+
const age = doorAgeGate(door, { now, ttlMs });
|
|
143
|
+
if (!age.ok) {
|
|
144
|
+
out.reds = red('door-expired', age.detail, 'door.ts');
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── 4. ACCEPT RE-PROVES THE TREE. Everything above is $0 and deterministic;
|
|
149
|
+
// this is the only branch that runs anything, and it runs the close's own
|
|
150
|
+
// mechanical stages — never a second, weaker spelling of "is it still green".
|
|
151
|
+
if (out.decision === 'accept') {
|
|
152
|
+
const proof = await proveMechanically({ job, workdir, events, closeTimeoutMs });
|
|
153
|
+
out.mechanical = proof.reading;
|
|
154
|
+
if (!proof.ok) {
|
|
155
|
+
out.reds = red('door-accept-red', proof.detail, proof.reading.stage ? `stage.${proof.reading.stage}` : 'close');
|
|
156
|
+
// NOTHING IS RECORDED on a refusal. A disposition the machine would not
|
|
157
|
+
// honour is not a disposition the record should carry: the person's answer
|
|
158
|
+
// was about a tree that no longer exists.
|
|
159
|
+
if (emit) emit('door-decision', { decision: 'accept', honoured: false, stage: proof.reading.stage ?? null, reason: 'the tree moved under the signer' });
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── 5. THE DISPOSITION IS RECORDED, and only accept releases (module 6's
|
|
165
|
+
// `recordDoor`, reached through its one on-disk seam). Every door is a datum:
|
|
166
|
+
// *"the signer's accepts double as the judge's ongoing report card"* (v1.71 §3),
|
|
167
|
+
// and a rerun over a judged green is exactly as informative as an accept.
|
|
168
|
+
if (registryDir && name && runid) {
|
|
169
|
+
// ALREADY-GREEN MINTS NOTHING, from the other side: a run that changed
|
|
170
|
+
// nothing wrote no row of its own, so there is nothing to record a door on and
|
|
171
|
+
// nothing to release. Stated rather than surfaced as a `no-row-for-run` red —
|
|
172
|
+
// the rule is the design, not a failure.
|
|
173
|
+
if (door.outcome === 'already-green') {
|
|
174
|
+
out.note = 'already-green: the run minted no row of its own, so the door records nothing and releases nothing — accept confirms a verdict, it never mints one';
|
|
175
|
+
} else {
|
|
176
|
+
const r = applyDoorDecision({ registryDir, name, runid, decision: out.decision, at });
|
|
177
|
+
if (!r.ok) {
|
|
178
|
+
out.reds = r.reds;
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
out.released = r.released;
|
|
182
|
+
out.doorRecorded = true;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
out.ok = true;
|
|
187
|
+
out.next = out.decision === 'accept' ? 'accepted' : (out.decision === 'rerun' ? 'rerun' : 'paused');
|
|
188
|
+
if (emit) {
|
|
189
|
+
emit('door-decision', {
|
|
190
|
+
decision: out.decision,
|
|
191
|
+
honoured: true,
|
|
192
|
+
...(out.text === null ? {} : { text: redactSecrets(out.text) }),
|
|
193
|
+
released: out.released,
|
|
194
|
+
recorded: out.doorRecorded,
|
|
195
|
+
meaning: 'a disposition — the verdict this run minted is untouched, and nothing here can change it',
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The accept's re-proof: the close's MECHANICAL stages, run against the tree as it
|
|
203
|
+
* stands now, through the SAME executors the run itself was judged by.
|
|
204
|
+
*
|
|
205
|
+
* The declared path measures against the run's OWN seed (`close-decl.seedRef` off
|
|
206
|
+
* the spine), never a fresh one: re-basing the baseline onto today's HEAD would
|
|
207
|
+
* compare the tree to itself and turn "nothing has changed since the run" into a
|
|
208
|
+
* green for a `files-changed` stage that means the opposite. A run whose seed is
|
|
209
|
+
* not on its spine is refused rather than guessed at.
|
|
210
|
+
* @param {{job: any, workdir: string, events: any[], closeTimeoutMs?: number}} o
|
|
211
|
+
*/
|
|
212
|
+
async function proveMechanically({ job, workdir, events, closeTimeoutMs }) {
|
|
213
|
+
const stages = mechanicalStages(closeStagesOf(job) ?? []);
|
|
214
|
+
if (!stages.length) {
|
|
215
|
+
// Honest, and named: a close with no mechanical stage has nothing a machine
|
|
216
|
+
// can re-prove, so the accept rests on the person alone. Never a silent pass
|
|
217
|
+
// dressed as a re-run.
|
|
218
|
+
return { ok: true, detail: '', reading: { ran: 0, verdict: null, stage: null, stages: [], note: 'this close has no mechanical stage — there is nothing a machine can re-prove, so the accept rests on the person' } };
|
|
219
|
+
}
|
|
220
|
+
/** @type {any} */
|
|
221
|
+
let v;
|
|
222
|
+
if (isDeclaredClose(job)) {
|
|
223
|
+
const seedRef = events.findLast?.((e) => e?.type === 'close-decl' && typeof e.seedRef === 'string')?.seedRef ?? null;
|
|
224
|
+
if (!seedRef) {
|
|
225
|
+
return { ok: false, detail: 'this run left no seed on its spine (`close-decl.seedRef`), and a declared close measures against a seed — re-proving against a guessed baseline would grade a tree nobody chose', reading: { ran: 0, verdict: null, stage: null, stages: [] } };
|
|
226
|
+
}
|
|
227
|
+
v = await runDeclaredStages(stages, redactSecrets, { timeoutMs: closeTimeoutMs, cwd: workdir, seedRef });
|
|
228
|
+
} else {
|
|
229
|
+
v = await runStages(stages, redactSecrets, { timeoutMs: closeTimeoutMs, cwd: workdir });
|
|
230
|
+
}
|
|
231
|
+
const reading = {
|
|
232
|
+
ran: stages.length,
|
|
233
|
+
verdict: v.verdict ?? null,
|
|
234
|
+
stage: v.stage ?? null,
|
|
235
|
+
stages: v.stages ?? [],
|
|
236
|
+
...(v.gap ? { gap: v.gap } : {}),
|
|
237
|
+
};
|
|
238
|
+
if (v.verdict === 'satisfied') return { ok: true, detail: '', reading };
|
|
239
|
+
return {
|
|
240
|
+
ok: false,
|
|
241
|
+
reading,
|
|
242
|
+
detail: `the close's mechanical stage "${v.stage ?? 'unknown'}" no longer passes on this tree (${v.verdict}) — `
|
|
243
|
+
+ 'the tree moved under you, so the accept is refused rather than applied to work you did not read. '
|
|
244
|
+
+ `${redactSecrets(String(v.gap ?? v.detail ?? '')).split('\n').slice(0, 3).join(' ')}`.trim(),
|
|
245
|
+
};
|
|
246
|
+
}
|
package/src/run.js
CHANGED
|
@@ -15,10 +15,27 @@ import { join } from 'node:path';
|
|
|
15
15
|
import { LiteCtx } from 'litectx';
|
|
16
16
|
import { validateJob, jobSpecHash, checkApproval } from './job.js';
|
|
17
17
|
import { runPlan } from './planrun.js';
|
|
18
|
-
import { SMOKE_STORE } from './kinds.js';
|
|
18
|
+
import { SMOKE_STORE, resolveHumanRuling } from './kinds.js';
|
|
19
|
+
import { readShimArm } from './readshim.js';
|
|
19
20
|
|
|
20
21
|
/** @typedef {{code: string, path: string, detail?: string}} Red */
|
|
21
22
|
|
|
23
|
+
/**
|
|
24
|
+
* THE ROUND RECORDS THE ONE LEDGER SUMS — as data, because two of them now exist
|
|
25
|
+
* and a hand-spelled condition per record is how a paid call comes to be free.
|
|
26
|
+
*
|
|
27
|
+
* `worker-round` is the attempt's own spend, metered per ROUND rather than per
|
|
28
|
+
* attempt-return (F12: a halted attempt's spend was invisible by 300×).
|
|
29
|
+
* `judge-round` is the CLOSE's, and it is the first spend a close has ever had:
|
|
30
|
+
* the judged floor (softgreen) buys a pinned locate call per artifact, and the
|
|
31
|
+
* budget-identity rule is that a budget funds the attempt PLUS its close. Both
|
|
32
|
+
* carry the honest `costUsd` — a null lands as `unpriced` and halts, never as $0.
|
|
33
|
+
*
|
|
34
|
+
* What is deliberately NOT here: `worker-result` / `worker-plan`, which are
|
|
35
|
+
* attempt TOTALS of the same rounds and would double-count.
|
|
36
|
+
*/
|
|
37
|
+
export const ACCOUNTED_ROUND_TYPES = Object.freeze(['worker-round', 'judge-round']);
|
|
38
|
+
|
|
22
39
|
|
|
23
40
|
/**
|
|
24
41
|
* Known-answer smoke on the primitive the run is about to trust (PRD v1.5 §4,
|
|
@@ -77,6 +94,13 @@ async function primitiveSmoke(workdir) {
|
|
|
77
94
|
* metered claude-json text provider for the toolless drafter); ignored on every Loop-driven provider
|
|
78
95
|
* @param {(type: string, data?: object) => object} opts.emit spine emitter
|
|
79
96
|
* @param {(tier: string) => any} [opts.providerFor] P: per-step model-tier provider factory (forwarded to the plan flow)
|
|
97
|
+
* @param {any} [opts.judgeProvider] SOFTGREEN — the provider a JUDGED close stage runs its
|
|
98
|
+
* locate call through, wired by the operator and PINNED to `JUDGE_MODEL` (src/judged.js:
|
|
99
|
+
* the only tier with established injection resistance upstream). Forwarded to the plan
|
|
100
|
+
* flow. It is its own seam and not `provider`/`providerFor`, because the judge tier is
|
|
101
|
+
* never a step knob and never agent-selectable; absent, a judged stage instrument-stops as
|
|
102
|
+
* a wiring gap rather than grading on whatever binding happened to be at hand. Its spend
|
|
103
|
+
* lands on this ledger as `judge-round` (see `ACCOUNTED_ROUND_TYPES`).
|
|
80
104
|
* @param {number} [opts.capRuns] shell-owned iteration cap for the plan flow's
|
|
81
105
|
* CLOSE-FIX loop (a plan STEP runs under the strike ladder instead)
|
|
82
106
|
* @param {number} [opts.strikeLimit] shell-owned strike ceiling for a plan step's ladder
|
|
@@ -133,6 +157,19 @@ async function primitiveSmoke(workdir) {
|
|
|
133
157
|
* collision walk would mint a `-2` beside the work the resume exists to continue. The
|
|
134
158
|
* fold's precedent exactly: the chain's state is DECLARED, never re-derived. Forwarded
|
|
135
159
|
* verbatim to the plan flow; absent is the cold path.
|
|
160
|
+
* @param {boolean|'cap'|'diff'} [opts.readShim=false] THE READ SHIM (src/readshim.js) —
|
|
161
|
+
* WHICH ARM to run, threaded to the plan flow. The Phase 2 pre-registration's four:
|
|
162
|
+
* `false` A0 — off (the default)
|
|
163
|
+
* `'cap'` A1 — cap + pointer + next-unseen-slice + G1, no diff
|
|
164
|
+
* `'diff'` A2 — the diff lever alone: no cap, no pointer, no G1
|
|
165
|
+
* `true` A3 — every lever
|
|
166
|
+
* Anything else THROWS below, before a token is spent: a mis-spelled arm coerced
|
|
167
|
+
* by truthiness into A3 would run one treatment under another's label and be
|
|
168
|
+
* invisible in the results afterwards.
|
|
169
|
+
* OFF is byte-identical to the pre-shim run in every observable (G1 included): the
|
|
170
|
+
* frozen A0 baseline arm has to be exactly today, so a guard firing under a disabled
|
|
171
|
+
* shim would make the baseline a treatment arm. The default-flip belongs to a
|
|
172
|
+
* paid contrast nobody has approved yet (the `layerRoot` precedent, F41).
|
|
136
173
|
* @param {boolean} [opts.layerRoot=false] Layer R (within-run ratchet) — shell
|
|
137
174
|
* territory, threaded to the plan flow. Defaults OFF
|
|
138
175
|
* (decided 2026-07-21): fixation is extinct on every current job (F41), so
|
|
@@ -145,13 +182,34 @@ async function primitiveSmoke(workdir) {
|
|
|
145
182
|
* RESUMES a paused run and forwarded verbatim to the plan flow. Absent on every
|
|
146
183
|
* ordinary run; never authored and never defaulted (a defaulted answer to "is this
|
|
147
184
|
* done?" is the rubber-stamp the class exists to prevent, 2026-08-12 §4).
|
|
185
|
+
* @param {{decision: string, text?: string|null, receivedAt?: string|null}|null} [opts.heldRuling]
|
|
186
|
+
* F102 — the answer the CHECKPOINT holds (`readResume`'s `restart.pendingDecision`):
|
|
187
|
+
* a decision a person already gave on a leg that stopped before a single round was
|
|
188
|
+
* bought for it. Forwarded verbatim; the plan flow applies it exactly as a fresh one
|
|
189
|
+
* and refuses a leg handed both. It is also what decides whether the TIME fold this
|
|
190
|
+
* run declares is the chain's or this engagement's (F103).
|
|
191
|
+
* @param {boolean|null} [opts.reviewDoor] MODULE 8 — does this run END at the review
|
|
192
|
+
* door (PRD v1.71 §3)? Forwarded verbatim to the plan flow, which owns the class
|
|
193
|
+
* default. It changes what the run RECORDS and never what it returns: the door is a
|
|
194
|
+
* disposition and the loop's verdict is not the door's to touch.
|
|
195
|
+
* @param {{text: string, fromRunid?: string|null, receivedAt?: string|null}|null} [opts.doorRerun]
|
|
196
|
+
* MODULE 8 — this leg IS the fresh engagement a signer commissioned at a previous
|
|
197
|
+
* run's review door, carrying their words. Forwarded verbatim.
|
|
148
198
|
* @returns {Promise<string>} outcome: 'green' | 'already-green' | 'escalated' |
|
|
149
199
|
* 'unapproved-spec' | 'job-red' | 'smoke-red' | 'plan-red' | 'check-red' |
|
|
150
200
|
* 'close-red' | 'close-unsupported' | 'recipe-stale' | 'branch-red' | 'pricing-red' | 'provider-red' |
|
|
151
201
|
* 'interpreter-red' | 'cap-halt' | 'wall-halt' | 'step-stalled' |
|
|
152
202
|
* 'hitl-pause' | 'hitl-decision-red' | `step-red:<id>`
|
|
153
203
|
*/
|
|
154
|
-
export async function runJob(rawSpec, { approvals, workdir, provider, nativeProvider, providerFor, emit, capRuns = 3, strikeLimit, shellCapUsd = 2, closeTimeoutMs, layerRoot = false, bridge = null, priorSpentUsd = 0, priorSpendComplete = true, priorWallMs = 0, resumeSeed = null, resumeGrades = [], resumeReplans = null, resumeBranch = null, humanRuling = null }) {
|
|
204
|
+
export async function runJob(rawSpec, { approvals, workdir, provider, nativeProvider, providerFor, judgeProvider = null, emit, capRuns = 3, strikeLimit, shellCapUsd = 2, closeTimeoutMs, layerRoot = false, readShim = false, bridge = null, priorSpentUsd = 0, priorSpendComplete = true, priorWallMs = 0, resumeSeed = null, resumeGrades = [], resumeReplans = null, resumeBranch = null, humanRuling = null, heldRuling = null, reviewDoor = null, doorRerun = null }) {
|
|
205
|
+
// THE READ SHIM's ARM, resolved at the door — the FIRST thing this entry does,
|
|
206
|
+
// before the ledger, before the approval gate, before a byte of the spec is read.
|
|
207
|
+
// An unrecognised spelling throws here at zero cost instead of being coerced by
|
|
208
|
+
// truthiness into A3 and running a whole paid battery row under an arm label it
|
|
209
|
+
// never executed — a wrong result that looks like a fine one, which is the exact
|
|
210
|
+
// blind-instrument failure this programme keeps logging. Only the guard runs
|
|
211
|
+
// here; the flag itself is threaded onward to the plan flow as written.
|
|
212
|
+
readShimArm(readShim);
|
|
155
213
|
// 0. the ledger's counters, declared FIRST so that every job-end — including
|
|
156
214
|
// the pre-token reds below — can state a real figure. An omitted `spentUsd` is
|
|
157
215
|
// not a zero: a consumer reads `undefined` and either crashes or launders it
|
|
@@ -162,6 +220,11 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
|
|
|
162
220
|
// number that enters the arbiter's arithmetic: a garbage fold is 0, never a NaN
|
|
163
221
|
// that would poison every later comparison into "no cap".
|
|
164
222
|
let spentUsd = typeof priorSpentUsd === 'number' && Number.isFinite(priorSpentUsd) && priorSpentUsd > 0 ? priorSpentUsd : 0;
|
|
223
|
+
/** F103's money half — the CHAIN's fold, kept as its own figure so `spentUsd`
|
|
224
|
+
* (the chain total, what a person reads and what the ceiling governs) and THIS
|
|
225
|
+
* ENGAGEMENT's spend can be stated side by side on every terminal. Two things
|
|
226
|
+
* counted by one number will eventually be asked to disagree. */
|
|
227
|
+
const chainFoldUsd = spentUsd;
|
|
165
228
|
let unpriced = false;
|
|
166
229
|
// RESUME (v1.46 §3): was the FOLD itself exact? `readResume` marks it false when any
|
|
167
230
|
// round inside the dead attempt came back unpriced, and that unknown does not heal by
|
|
@@ -201,7 +264,16 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
|
|
|
201
264
|
// ONE spelling of the four causes, so the terminal and the in-flight money readout can
|
|
202
265
|
// never disagree about whether the same run's figure is exact.
|
|
203
266
|
const spendComplete = () => !unpriced && !stalled && !cutMidCall && !priorFloor;
|
|
204
|
-
const spend = () => ({
|
|
267
|
+
const spend = () => ({
|
|
268
|
+
spentUsd,
|
|
269
|
+
spendComplete: spendComplete(),
|
|
270
|
+
// F103's second counter, on every terminal beside the chain total it is NOT.
|
|
271
|
+
// `spentUsd` is cumulative-so-far (this job, across engagements) and governs the
|
|
272
|
+
// ceiling; this is what THIS engagement spent. Always emitted, never conditional
|
|
273
|
+
// on there being a fold: a consumer that has to branch on field presence is a
|
|
274
|
+
// consumer that will read absence as zero.
|
|
275
|
+
engagementSpentUsd: Math.max(0, spentUsd - chainFoldUsd),
|
|
276
|
+
});
|
|
205
277
|
// 1. human-signs-always — before ANY provider call (N1 decision #1)
|
|
206
278
|
if (!checkApproval(rawSpec, approvals)) {
|
|
207
279
|
emit('job-end', { outcome: 'unapproved-spec', detail: 'no approval record matches this exact spec version', ...spend() });
|
|
@@ -214,6 +286,11 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
|
|
|
214
286
|
return 'job-red';
|
|
215
287
|
}
|
|
216
288
|
const job = /** @type {any} */ (jv.job);
|
|
289
|
+
/** F103 — the two TIME figures, resolved once here and read the same way by
|
|
290
|
+
* `runPlan`'s clock. An unreadable ruling resolves to `fresh: false` and folds as
|
|
291
|
+
* it always did; `runPlan` is where it is refused, so this never has to. */
|
|
292
|
+
const chainWallMs = typeof priorWallMs === 'number' && Number.isFinite(priorWallMs) && priorWallMs > 0 ? priorWallMs : 0;
|
|
293
|
+
const wallFold = resolveHumanRuling(humanRuling, heldRuling, doorRerun).fresh ? 0 : chainWallMs;
|
|
217
294
|
emit('job-start', {
|
|
218
295
|
job: job.job, specHash: jobSpecHash(job), budgetUsd: job.budgetUsd,
|
|
219
296
|
shape: 'plan', goal: job.goal,
|
|
@@ -226,7 +303,17 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
|
|
|
226
303
|
// A fold of $0 that is NOT exact is still a fold — the unknown is the whole of what
|
|
227
304
|
// it inherited, and it is the only field that can carry it forward.
|
|
228
305
|
...(spentUsd > 0 || priorFloor ? { priorSpentUsd: spentUsd, priorSpendComplete: !priorFloor } : {}),
|
|
229
|
-
|
|
306
|
+
// …and the TIME fold, which F103 splits in two. `priorWallMs` is what the next
|
|
307
|
+
// reader ADDS to (the bound), and it is engagement-scoped: a rerun opens a fresh
|
|
308
|
+
// engagement, so the minutes the person did not commission are not a bound on
|
|
309
|
+
// anybody. `chainWallMs` is the cumulative view and is declared whenever there is
|
|
310
|
+
// one, so nothing is lost — the readout keeps spanning the chain (the halt readout
|
|
311
|
+
// and the leg's own governor answer different questions and are never mixed).
|
|
312
|
+
// The `fresh` reading comes from the ONE resolver `runPlan`'s clock reads, never a
|
|
313
|
+
// second spelling of "is this a rerun" (that is how a record and a clock come to
|
|
314
|
+
// disagree about the same leg).
|
|
315
|
+
...(wallFold > 0 ? { priorWallMs: wallFold } : {}),
|
|
316
|
+
...(chainWallMs > 0 && chainWallMs !== wallFold ? { chainWallMs } : {}),
|
|
230
317
|
// the REPLAN ledger, on the same declaration and for the same reason — a chain of
|
|
231
318
|
// resumes must fold once. This is the DIRECT-spine half of it: `readResume`'s
|
|
232
319
|
// `direct` mode opens its one implicit window on `job-start` and reads the fold off
|
|
@@ -265,7 +352,9 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
|
|
|
265
352
|
// `account` reds the run on ANY unpriced round (a null cost is the honest
|
|
266
353
|
// unknown, never $0 — F6): per-round metering means a partially-unpriced run
|
|
267
354
|
// is caught natively, round by round, with no separate unpricedRounds tally.
|
|
268
|
-
|
|
355
|
+
// …and the CLOSE's own rounds sum into the same wallet the moment a close can
|
|
356
|
+
// spend at all (the judged floor): one ledger, never a second arithmetic.
|
|
357
|
+
if (ACCOUNTED_ROUND_TYPES.includes(type)) account(/** @type {any} */ (data)?.costUsd);
|
|
269
358
|
// W1: a `stall` is the plan flow announcing it ABANDONED a call and reissued
|
|
270
359
|
// it. The abandoned call's spend is unknowable from here (billed or not,
|
|
271
360
|
// indistinguishable), so from this point the priced sum is a floor no matter
|
|
@@ -289,7 +378,7 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
|
|
|
289
378
|
// accounts it natively (F12) and the job-end money contract is unchanged.
|
|
290
379
|
{
|
|
291
380
|
const outcome = await runPlan(job, {
|
|
292
|
-
workdir, provider, nativeProvider, providerFor, emit: meter, capRuns, ...(strikeLimit !== undefined ? { strikeLimit } : {}), closeTimeoutMs, layerRoot, bridge, priorWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling,
|
|
381
|
+
workdir, provider, nativeProvider, providerFor, judgeProvider, emit: meter, capRuns, ...(strikeLimit !== undefined ? { strikeLimit } : {}), closeTimeoutMs, layerRoot, readShim, bridge, priorWallMs: chainWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, reviewDoor, doorRerun, priorSpentUsd: chainFoldUsd,
|
|
293
382
|
remainingUsd: () => Math.min(shellCapUsd, job.budgetUsd - spentUsd),
|
|
294
383
|
isUnpriced: () => unpriced, // F6: let the plan flow bail in-flight, not just after it returns
|
|
295
384
|
spendComplete, // …and let its money-halt readout say whether the remaining it quotes is exact
|
package/src/selection.js
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
// in either direction, a percentage over 2–3 runs is fake precision, and if a score ever
|
|
27
27
|
// earns its way in its threshold is hamr's, from a measured base rate.
|
|
28
28
|
|
|
29
|
-
import { listingRow } from './bridges.js';
|
|
29
|
+
import { listingRow, QUARANTINED_CODE } from './bridges.js';
|
|
30
30
|
|
|
31
31
|
/** minutes, one decimal — the granularity a human picks a workflow at (the unit is
|
|
32
32
|
* written once, by the band) */
|
|
@@ -74,10 +74,22 @@ export function renderListing(registry) {
|
|
|
74
74
|
const reds = Array.isArray(r?.reds) ? r.reds : [];
|
|
75
75
|
const rows = bridges.map(listingRow).sort((a, b) => String(a.name).localeCompare(String(b.name)));
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
// TWO KINDS OF SKIP, and they are never written as one. An unreadable entry is a
|
|
78
|
+
// broken file; a HELD one (softgreen module 6) is a perfectly good workflow whose
|
|
79
|
+
// judged green no signer has accepted yet. Saying "could not be read" about the
|
|
80
|
+
// second would send the operator to fix a file that is not broken.
|
|
81
|
+
const broken = reds.filter((/** @type {any} */ x) => x?.code !== QUARANTINED_CODE);
|
|
82
|
+
const held = reds.filter((/** @type {any} */ x) => x?.code === QUARANTINED_CODE);
|
|
83
|
+
const skipped = [
|
|
84
|
+
broken.length
|
|
85
|
+
? [`\n\nNOTE: ${broken.length} registry entr${broken.length === 1 ? 'y' : 'ies'} could not be read and ${broken.length === 1 ? 'is' : 'are'} NOT listed — this listing is smaller than the directory:`,
|
|
86
|
+
...broken.map((/** @type {any} */ x) => ` ${x.path ?? '(unknown file)'}${x.detail ? ` — ${x.detail}` : ''}`)].join('\n')
|
|
87
|
+
: '',
|
|
88
|
+
held.length
|
|
89
|
+
? [`\n\nNOTE: ${held.length} workflow${held.length === 1 ? ' is' : 's are'} HELD and NOT offered — a judged green earns no reuse until the person who owns the job accepts it:`,
|
|
90
|
+
...held.map((/** @type {any} */ x) => ` ${x.path ?? '(unnamed)'}${x.detail ? ` — ${x.detail}` : ''}`)].join('\n')
|
|
91
|
+
: '',
|
|
92
|
+
].join('');
|
|
81
93
|
|
|
82
94
|
if (!rows.length) {
|
|
83
95
|
return `WORKFLOWS ON RECORD — none.\nThere are no workflows to reuse; a plan has to be drafted from scratch.${skipped}`;
|
|
@@ -86,7 +98,11 @@ export function renderListing(registry) {
|
|
|
86
98
|
const blocks = rows.map((row) => {
|
|
87
99
|
const g = row.greenCost;
|
|
88
100
|
return [
|
|
89
|
-
|
|
101
|
+
// an entry whose only greens are HELD has no status — but "NO-GREEN" would be
|
|
102
|
+
// false about it: it greened, and a person has simply not accepted the judged
|
|
103
|
+
// verdict yet. The distinct word is the whole difference between "this never
|
|
104
|
+
// worked" and "this is waiting on you".
|
|
105
|
+
`[${row.name ?? '(unnamed)'}] ${(row.status ?? (row.quarantinedGreens > 0 ? 'held' : 'no-green')).toUpperCase()}`,
|
|
90
106
|
` goal ${row.goal ?? '(no goal recorded)'}`,
|
|
91
107
|
` runs ${row.greens} green · ${row.reds} red${row.lastOutcome ? ` (last: ${row.lastOutcome})` : ''}`,
|
|
92
108
|
` green cost ${band(g.minUsd, g.maxUsd, usd, g.unpricedCount, row.greens, 'unpriced')}`
|
package/src/tools.js
CHANGED
|
@@ -21,7 +21,7 @@ const require = createRequire(import.meta.url);
|
|
|
21
21
|
const { createShellTools } = require('bare-agent/tools');
|
|
22
22
|
// litectx's rank-tiered render — a free function, not a ctx method (its contract's
|
|
23
23
|
// own recipe: the caller slices the chunk body, compress renders the signature tier)
|
|
24
|
-
const { compress: compressNode } = require('litectx');
|
|
24
|
+
const { compress: compressNode, StalePointerError } = require('litectx');
|
|
25
25
|
|
|
26
26
|
import { extractArtifact, priceOf } from './text.js';
|
|
27
27
|
// The isolate verbs write MODEL-AUTHORED text into `<workdir>/.litectx` — a file
|
|
@@ -243,8 +243,13 @@ const LINE_SPACE = 'Line numbers here are 0-BASED index positions from the repos
|
|
|
243
243
|
* RESULT is invisible cannot be judged: a `ctx_get` that silently reds (stale pointer,
|
|
244
244
|
* bad range) looks exactly like one that worked, and the worker's fallback to a
|
|
245
245
|
* whole-file read looks like a free choice instead of a forced one.
|
|
246
|
+
* @param {{onStalePointer?: ((req: {file: string, path: string, startLine: number, endLine: number, detail: string}) => {text: string, outcome: string}|null)|null}} [opts]
|
|
247
|
+
* `onStalePointer`: the read shim's stale-pointer serve, wired ONLY by a capping
|
|
248
|
+
* arm (see `createReadShim`). Absent — every caller today except that one — this
|
|
249
|
+
* whole seam is byte-identical to what it was, which is what keeps A0 the
|
|
250
|
+
* untouched baseline an experiment has already run against.
|
|
246
251
|
*/
|
|
247
|
-
export function createCtxTools(lc, workdir, emit) {
|
|
252
|
+
export function createCtxTools(lc, workdir, emit, { onStalePointer = null } = {}) {
|
|
248
253
|
// ctx_get's failure contract, generalised to the palette (review 2026-07-31).
|
|
249
254
|
// `emit` exists in this file because a verb whose RESULT is invisible cannot be
|
|
250
255
|
// judged — and a THROWN call is the most invisible result there is: it looks
|
|
@@ -337,6 +342,33 @@ export function createCtxTools(lc, workdir, emit) {
|
|
|
337
342
|
// StalePointerError: the file changed after indexing, so these lines now describe
|
|
338
343
|
// DIFFERENT code. Its message IS the recovery instruction — hand it to the worker.
|
|
339
344
|
const detail = String(/** @type {Error} */ (e)?.message || e);
|
|
345
|
+
// …and under a CAPPING read-shim arm, the recovery instruction is not
|
|
346
|
+
// enough. That arm's own strategy line steers the worker HERE for a whole
|
|
347
|
+
// function, and a worker that just edited the file it is working on — the
|
|
348
|
+
// normal case — gets a stale pointer and zero bytes for a paid round
|
|
349
|
+
// (measured: 5 of 10 ctx_get calls across the A1 arm's three green runs).
|
|
350
|
+
// So the shim serves the requested LINE RANGE from disk, labelled; see
|
|
351
|
+
// `serveStale`. The branch is on the TYPED error, never on the message
|
|
352
|
+
// prose — a prose sniff would fire on any failure whose wording happens
|
|
353
|
+
// to match, and litectx exports the class precisely so it need not.
|
|
354
|
+
if (onStalePointer && e instanceof StalePointerError) {
|
|
355
|
+
// The gate judged `resolve(workdir, expandHome(path))` for this call
|
|
356
|
+
// (toolAction, 'ctx_get'); the serve reads that same absolute path and
|
|
357
|
+
// no other, so it can never reach a byte the fence did not allow.
|
|
358
|
+
const served = onStalePointer({
|
|
359
|
+
file: resolve(workdir, expandHome(String(p))),
|
|
360
|
+
path: rel,
|
|
361
|
+
startLine: Number(startLine),
|
|
362
|
+
endLine: Number(endLine),
|
|
363
|
+
detail,
|
|
364
|
+
});
|
|
365
|
+
if (served && typeof served.text === 'string') {
|
|
366
|
+
// No `detail` on the event: the served TEXT is file content and the
|
|
367
|
+
// spine is append-only. Bytes and the outcome are what a reader needs.
|
|
368
|
+
emit('ctx-tool', { tool: 'ctx_get', path: rel, startLine, endLine, outcome: served.outcome, bytes: Buffer.byteLength(served.text) });
|
|
369
|
+
return served.text;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
340
372
|
emit('ctx-tool', { tool: 'ctx_get', path: rel, startLine, endLine, outcome: 'stale', bytes: 0, detail });
|
|
341
373
|
return `stale pointer: ${detail}`;
|
|
342
374
|
}
|