bareloop 0.5.0 → 0.6.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 +746 -1
- package/README.md +13 -3
- package/bareloop.context.md +311 -234
- package/package.json +3 -3
- package/src/clock.js +181 -0
- package/src/exits.js +8 -2
- package/src/index.js +23 -17
- package/src/job.js +231 -108
- package/src/ledger.js +28 -3
- package/src/plan.js +356 -20
- package/src/planrun.js +978 -98
- package/src/ralph.js +226 -7
- package/src/root.js +32 -12
- package/src/run.js +61 -390
- package/src/stall.js +252 -0
- package/src/tools.js +611 -0
- package/src/validate.js +35 -250
- package/types/clock.d.ts +109 -0
- package/types/index.d.ts +4 -6
- package/types/job.d.ts +83 -8
- package/types/ledger.d.ts +3 -1
- package/types/plan.d.ts +107 -7
- package/types/planrun.d.ts +51 -3
- package/types/ralph.d.ts +62 -4
- package/types/root.d.ts +10 -2
- package/types/run.d.ts +9 -17
- package/types/stall.d.ts +80 -0
- package/types/tools.d.ts +262 -0
- package/types/validate.d.ts +21 -38
- package/src/extract.js +0 -103
- package/src/interpret.js +0 -887
- package/types/extract.d.ts +0 -25
- package/types/interpret.d.ts +0 -201
package/bareloop.context.md
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
# bareloop — Integration Guide
|
|
2
2
|
|
|
3
|
-
> **Current through Layer 2 (ACCEPTED, F47)
|
|
4
|
-
>
|
|
5
|
-
>
|
|
6
|
-
>
|
|
7
|
-
>
|
|
8
|
-
>
|
|
3
|
+
> **Current through Layer 2 (ACCEPTED, F47)**: the plan-v1 flow — a four-field job shape,
|
|
4
|
+
> agent-authored validated plans, in-run check exits — validated end-to-end on the
|
|
5
|
+
> real-model acceptance battery (3/3 conversion, 3/3 over the owned bar, the agent
|
|
6
|
+
> composing its own check exits). **The plan shape is now the ONLY shape**: the legacy
|
|
7
|
+
> operator-authored `steps[]` path, config-v1 and the draft-PR hitl step were deleted
|
|
8
|
+
> 2026-07-26 (PRD v1.32), so `interpret`, `validateConfig` and `extractRules` are gone from
|
|
9
|
+
> the public surface — a breaking change. `hitl` and `soft-green` return as a Layer 3
|
|
10
|
+
> decision, rebuilt in the plan shape rather than ported. Runs on two worker surfaces —
|
|
11
|
+
> the API `Loop` and, for
|
|
9
12
|
> `clipipe-subscription`, the CLI's native tool channel (module 4d). API sections fill in
|
|
10
13
|
> as build-ladder rungs land (PRD §10). What is settled — the boundary, the architecture,
|
|
11
14
|
> the refusals, the constraints — is settled for good. Per LIBRARY_CONVENTIONS §3 this
|
|
@@ -41,11 +44,13 @@ const spec = {
|
|
|
41
44
|
provider: 'anthropic-api',
|
|
42
45
|
cadence: { unit: 'day', every: 1 }, budgetUsd: 1.5,
|
|
43
46
|
writeScope: ['src/**'],
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
]
|
|
47
|
+
// the human signs the DESTINATION; the agent authors the road
|
|
48
|
+
goal: 'Fix any failure in src/ so the suite passes.',
|
|
49
|
+
verdictType: 'green',
|
|
50
|
+
// the close is an ORDERED LIST of named stages (PRD v1.28) — the check menu
|
|
51
|
+
// DERIVES from it; there is no separate `checks[]` field to author
|
|
52
|
+
close: [{ name: 'suite-green', cmd: 'npm test', expect: 0 }],
|
|
53
|
+
tools: ['read', 'grep', 'write', 'edit'],
|
|
49
54
|
escalation: { mode: 'decision-ready' },
|
|
50
55
|
};
|
|
51
56
|
|
|
@@ -58,17 +63,17 @@ const outcome = await runJob(spec, {
|
|
|
58
63
|
provider: new AnthropicProvider({ model: 'claude-sonnet-5' }), // key from env
|
|
59
64
|
emit: makeSpine('/path/to/checkout/run.jsonl'),
|
|
60
65
|
});
|
|
61
|
-
// '
|
|
62
|
-
// decision-ready
|
|
66
|
+
// 'green' means the operator's close passed. Anything else is a named,
|
|
67
|
+
// decision-ready stop on the spine — never a silent partial success.
|
|
63
68
|
```
|
|
64
69
|
|
|
65
70
|
## All options
|
|
66
71
|
|
|
67
|
-
Two documents, two validators — the
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
`
|
|
71
|
-
unknown-field reds.
|
|
72
|
+
Two documents, two validators — the SIGNED job spec (`validateJob`) and the AGENT-authored
|
|
73
|
+
plan (`validatePlan`). The schema is the option surface; every red lands before tokens
|
|
74
|
+
burn. The arbiter split is guarded by INEXPRESSIBILITY in both directions: the plan cannot
|
|
75
|
+
say `close`/`provider`/`budget`, and the job spec cannot say `hooks`/`loop`/`memory`, any
|
|
76
|
+
minting claim, or the shell-owned retry cap — all unknown-field reds.
|
|
72
77
|
|
|
73
78
|
**Job spec (`schema: "job-v1"`, operator-owned — the arbiter's rulebook):**
|
|
74
79
|
|
|
@@ -76,34 +81,92 @@ unknown-field reds.
|
|
|
76
81
|
|---|---|---|
|
|
77
82
|
| `job` | kebab-case slug | |
|
|
78
83
|
| `description` | non-empty string | |
|
|
79
|
-
| `provider` | `anthropic-api` | menu; part of the lineage key by definition |
|
|
84
|
+
| `provider` | `anthropic-api` \| `clipipe-subscription` | menu (`PROVIDERS`); part of the lineage key by definition. Only `anthropic-api` is guaranteed (F48); `clipipe-subscription` drives the worker natively and needs `opts.nativeProvider` |
|
|
80
85
|
| `conditions` | `{ providerPath?, closeVerbosity?, taskFraming?, scaffold? }` | declared keys only, string values — the environment label (consumed by the N3 lineage key; recorded on spines from run one) |
|
|
81
86
|
| `cadence` | `{ unit: hour\|day\|week, every: 1..30 }` | validated now, consumed at N5 (Scheduler) |
|
|
82
87
|
| `budgetUsd` | `0 < n <= shell cap` | ceiling chain: workflow ≤ job ≤ shell — each layer may tighten, never exceed |
|
|
83
|
-
| `
|
|
84
|
-
| `
|
|
85
|
-
| `steps
|
|
86
|
-
| `steps[].tools` | unique subset of `read\|grep\|write\|edit\|recall\|get` | the SPEC-side tool grant (`TOOL_MENU`, frozen; defaults to the full menu) — the drafted config cannot express mode or tools; `edit` (BA-13) is the anchored exact-once replace, judged by the SAME writeScope fence as `write`; requesting `run` (`LOCKED_TOOLS`) reds with the DISTINCT code `request-red` (locked-but-listed: the red IS the admission evidence the ledger tallies; a typo stays `invalid-value`) |
|
|
88
|
+
| `maxWallMs` | optional integer ms `>= MIN_WALL_MS` (one close timeout) | the run's wall clock. **NO DEFAULT, by ruling** — absent means time-unbounded *by explicit operator choice*, never by fallback (F45: a defaulted cap is a silent second ceiling). Enforcement is a BETWEEN-ROUND deadline, so the honest worst case is `maxWallMs + closeStages × closeTimeoutMs` — every stage of a staged close gets the FULL timeout — and all three numbers are reported (`loop.stop()` cannot cut an in-flight call — F61 measured 500ms→4,018ms). The `MIN_WALL_MS` floor is a ONE-stage number, so a spec with many stages can validate while its overshoot dwarfs its cap: the clock reports that honestly rather than the floor pretending to prevent it (stage-aware floor parked, PRD v1.39). Operator-only, tighten-only; adding or changing it changes the spec hash |
|
|
89
|
+
| `writeScope` | array of contained globs | the operator's outer fence; the plan's own scopes must fit inside it, same containment code |
|
|
90
|
+
| `steps` | RETIRED | operator-authored `steps[]` was deleted (PRD v1.32); a spec carrying it reds `shape-retired:steps` by name rather than half-running |
|
|
87
91
|
| `escalation` | `{ mode: "decision-ready" }` | the pain channel is not optional |
|
|
88
92
|
|
|
89
|
-
**The plan shape (Layer 2, design record 2026-07-21)
|
|
90
|
-
|
|
91
|
-
In the plan shape the AGENT authors the step plan at run time (gated by `validatePlan`);
|
|
92
|
-
the human signs only:
|
|
93
|
+
**The plan shape — the only shape** (Layer 2, design record 2026-07-21). The AGENT authors
|
|
94
|
+
the step plan at run time (gated by `validatePlan`); the human signs only:
|
|
93
95
|
|
|
94
96
|
| field | shape | notes |
|
|
95
97
|
|---|---|---|
|
|
96
98
|
| `goal` | non-empty text | what the agent plans against |
|
|
97
99
|
| `verdictType` | `green` \| `soft-green` \| `hitl` | declared radio, never inferred (`VERDICT_TYPES`, frozen). v1 ADMITS only `green`; declaring `soft-green`/`hitl` reds `request-red` with the type as a structured `verb` field (declared-but-locked — the tool-menu pattern) |
|
|
98
|
-
| `close` | a close object (table below) |
|
|
99
|
-
| `checks` |
|
|
100
|
-
| `tools` | optional unique subset of `TOOL_MENU` | the CEILING every plan step's grant must fit inside (
|
|
100
|
+
| `close` | **an ORDERED LIST of named stages** `[{ name, cmd, expect, judged?, gapKeep?, offer?, needs? }, ...]` (PRD v1.28), or a close object (table below) **only** for the declared-but-locked verdict classes | the destination, the only thing hand-authored; the check menu DERIVES from it (below). The plan flow executes a staged close directly and adapts a bare `predicate` object into a one-stage list; a `gold`/`rubric`/`hitl` object close validates (the declared-but-locked verdict classes still parse) but the plan flow refuses it at runtime as `close-unsupported` — it names no command to run |
|
|
101
|
+
| `checks` | **RETIRED** (PRD v1.28/v1.32) | hand-authored checks are gone, not merely discouraged: declaring `checks` reds `checks-derived` by name. The check menu is DERIVED from the close's own stages instead — see **Staged close** below. The hazard this removes is measured, not theoretical: job #5's three hand-written checks were re-implementations of three stages the close already ran, and a hand-carved copy can drift LENIENT (the worker passes the operator's ruler and fails the real inspection) |
|
|
102
|
+
| `tools` | optional unique subset of `TOOL_MENU` (14 verbs, below) | the CEILING every plan step's grant must fit inside (omitting it means the full menu — and the hash is taken over that RESOLVED form, so a `TOOL_MENU` widening flips an omitted-`tools` spec's hash and forces a re-sign; see `jobSpecHash`, MED-1); `run` is `LOCKED_TOOLS` and reds `request-red` — locked-but-listed, and the red IS the admission evidence the ledger tallies (a typo stays `invalid-value`). A ceiling of write-class and store-class verbs ONLY reds `invalid-value`: the scout surveys read-only, so it would be handed an empty menu and survey blind |
|
|
101
103
|
|
|
102
|
-
`
|
|
103
|
-
|
|
104
|
+
**`TOOL_MENU` — the worker's 14 verbs, in four components** (`src/job.js`; the verb→tool map
|
|
105
|
+
is `TOOL_BY_VERB` in `src/tools.js`). The menu is an INVENTORY: every verb is an existing
|
|
106
|
+
bare-agent or litectx primitive, none was built for it. A granted component's strategy line
|
|
107
|
+
ships with the grant (`COMPONENT_STRATEGIES` — capability without strategy is inert, F19).
|
|
104
108
|
|
|
105
|
-
|
|
106
|
-
|
|
109
|
+
| component | verbs | class |
|
|
110
|
+
|---|---|---|
|
|
111
|
+
| write | `write` · `edit` | tree writes, judged by the signed `writeScope` fence — `edit` (BA-13) is the anchored exact-once replace and goes through that SAME fence, not a weaker one |
|
|
112
|
+
| select | `read` · `grep` · `recall` · `get` · `impact` · `related` · `recent` | read-only; every one is judged as a `read` action by that same fence, so the deny list applies through every door |
|
|
113
|
+
| compress | `compress` · `peek` | read-only (a signature tier; a stash's head/tail) |
|
|
114
|
+
| isolate | `stash` · `remember` · `forget` | STORE writes (the `.litectx` store, never the tree) — their own gate action types, so a store park can never be counted as a tree write by the F32 `workerWrites` instrument. **Not read-capable** |
|
|
115
|
+
|
|
116
|
+
`run` is locked at every layer and always will be: a worker that can run commands can run
|
|
117
|
+
its own close. `ctx_recall` serves BOTH axes — code pointers, plus any note written with
|
|
118
|
+
`ctx_remember`, returned on a line labeled `memory` with the note's BODY inline (capped at
|
|
119
|
+
400 chars). A note is a conclusion, not a pointer: there is no worker verb that dereferences
|
|
120
|
+
a memory id, so a pointer-only reply would be inert. **`recall`, `get` and `impact` share ONE
|
|
121
|
+
0-based line space** — the index's own index positions, one lower than the line an editor
|
|
122
|
+
shows — and every one of the three says so in its own tool contract. The numbers are
|
|
123
|
+
interchangeable handles: `impact`'s def range dereferences through `get` verbatim, and
|
|
124
|
+
`recall` prints the same number for the same chunk. Rendering any one of them 1-based was
|
|
125
|
+
tested and REFUTED: it would print two numbers for one chunk and turn `get`'s clean
|
|
126
|
+
chunk-boundary refusal into a guessing game.
|
|
127
|
+
|
|
128
|
+
**`impact` requires `ripgrep` (`rg`) on `PATH`.** litectx's caller scan shells out to `rg` and
|
|
129
|
+
catches a missing binary exactly as it catches "no matches", so without it the verb reports
|
|
130
|
+
`0 callers, risk low` — silently, in the false-isolation direction (LC-5, open upstream).
|
|
131
|
+
Install `rg` wherever the worker runs, or leave `impact` off the spec's `tools` ceiling.
|
|
132
|
+
|
|
133
|
+
**The store is caller-owned, and it is a LIVE cross-run channel.** `runJob`/`runPlan` root ONE
|
|
134
|
+
litectx store at `<workdir>/.litectx` (`new LiteCtx({ root: workdir })`, `src/planrun.js`) — an
|
|
135
|
+
on-disk SQLite index that OUTLIVES the run. A note written with `remember` lands there as a
|
|
136
|
+
durable `fact`, and `ctx_recall` reads that tier back on any later run against the same
|
|
137
|
+
workdir: agent notes persist across runs by default. The library ships NO reset — it never
|
|
138
|
+
deletes the store, because what a run inherits is the caller's decision, not the runner's. A
|
|
139
|
+
caller who wants a COLD baseline (an inheritance-OFF control arm, a reproducible re-run, any
|
|
140
|
+
contrast between two runs) must remove `<workdir>/.litectx` itself between them; the in-repo U
|
|
141
|
+
runner does exactly that with a one-line `rmSync`. The store is a derived, self-healing cache
|
|
142
|
+
by litectx's own contract, so the only cost of deleting it is the re-index. Store writes are
|
|
143
|
+
bounded: `stash`/`remember` payloads over **64KB** (`ISOLATE_MAX_BYTES`) come back as a
|
|
144
|
+
refusal RESULT naming the actual size and the limit — worker feedback, never a throw, and
|
|
145
|
+
nothing enters the store. The bound is per-payload (aggregate growth across distinct ids is a
|
|
146
|
+
future threshold call); `forget` needs no cap — its id is a bound SQL parameter, never stored.
|
|
147
|
+
|
|
148
|
+
**Staged close — the check menu DERIVES from it (PRD v1.28, `checkMenu` in `src/job.js`).**
|
|
149
|
+
Every stage is a `predicate` command body (the same `cmd`/`expect`/`judged?`/`gapKeep?`
|
|
150
|
+
contract the single close object has always used) plus:
|
|
151
|
+
|
|
152
|
+
| stage field | shape | notes |
|
|
153
|
+
|---|---|---|
|
|
154
|
+
| `name` | unique kebab-case slug | the plan references it via `check-passes(name)`; duplicates red `duplicate-id` |
|
|
155
|
+
| `offer` | optional boolean, default offered | `offer: false` hides the stage from the derived menu — it never reaches the agent. For a stage that cannot stand alone as a ruler: a PRECONDITION (e.g. "the seed commit exists" — passes instantly, teaches nothing) or the **final grading stage** — of the six specs in `jobs/`, four hide a final `verdict` stage and two hide a `changed-from-seed` precondition instead (a per-spec convention, not a schema rule — nothing stops a stage named last from being offered except the spec author remembering to set the flag; preflight's `check-menu` event always names the full menu either way, so a forgotten flag is visible in the run's own record) |
|
|
156
|
+
| `needs` | optional non-empty array of EARLIER stage names | a stage that reads what an earlier stage built (e.g. "the public API matches what an earlier stage emitted") names its prerequisite chain; picking it via `check-passes` runs the chain first, then the stage itself. Every name must be declared before this stage (`invalid-value` otherwise); `needs` + `offer: false` together is incoherent (`invalid-value`) — a stage with a chain to run must be reachable |
|
|
157
|
+
|
|
158
|
+
The stages run in declared order as the close itself; the first red renders the verdict and
|
|
159
|
+
later stages never run. `checkMenu(close)` returns only the offerable stages (each with its
|
|
160
|
+
run chain, prerequisites first); a hidden or partial menu is an acceptable case, never a
|
|
161
|
+
failure — `check-menu` on the spine reports `hidden` + `meaning` whenever the derived menu is
|
|
162
|
+
narrower than the stage list.
|
|
163
|
+
|
|
164
|
+
**Close types and the hierarchy — the OBJECT form, which survives only for the
|
|
165
|
+
declared-but-locked verdict classes** (a close is data, never code; verdict-class laundering
|
|
166
|
+
is a named red `close-hierarchy`). The go-forward shape is the staged list above; a
|
|
167
|
+
`predicate` object is legal shorthand for a one-stage list and the plan flow adapts it, but
|
|
168
|
+
`gold`/`rubric`/`hitl` validate and then refuse at runtime (`close-unsupported`) since v1
|
|
169
|
+
admits `verdictType: green` only:
|
|
107
170
|
|
|
108
171
|
| type | fields (exact — extras red) | legal class |
|
|
109
172
|
|---|---|---|
|
|
@@ -127,7 +190,7 @@ code said.
|
|
|
127
190
|
|
|
128
191
|
It is a **floor, not a zero-check**: `node --test` reports a crashed file as ONE failing
|
|
129
192
|
test, so "zero executed" never fires. Declare it against the suite's real size
|
|
130
|
-
(`{ pattern: "^ℹ tests (\\d+)$", min: 300 }` for a
|
|
193
|
+
(`{ pattern: "^ℹ tests (\\d+)$", min: 300 }` for a 558-test suite, with
|
|
131
194
|
`--test-reporter=spec`, which prints both the counts and the failures at the end). It
|
|
132
195
|
catches *"the arbiter did not run"* — wrong tree, broken argv, a failed shared import — not
|
|
133
196
|
*"one test file is broken"*, which is an honest red the worker should fix.
|
|
@@ -173,23 +236,34 @@ middle writes; `validatePlan` gates it against the SIGNED job spec before tokens
|
|
|
173
236
|
| `steps[].tools` | non-empty unique subset of the SPEC ceiling | a verb beyond the ceiling reds `verb-escape` with the verb as structured data (overreach, distinct from the operator-side `request-red`); `run` is `verb-escape` at every layer |
|
|
174
237
|
| `steps[].rounds` | int 1..shell cap (default 40) | the step's per-attempt tool-round bound (the Gate's `maxTurns` natively) |
|
|
175
238
|
| `steps[].target` | path inside the fence | v1.18 deliverable; REQUIRED on write-granted steps |
|
|
176
|
-
| `steps[].
|
|
239
|
+
| `steps[].model` | optional; `sonnet` \| `haiku` (`STEP_MODELS`) | the step's model TIER — a menu, never a model string the agent spells (the tier→model mapping and any per-model effort params are the runner's). `opus` is deliberately absent: reserved for work the human assigns, never plan-selectable. Off-menu reds `invalid-value`. A step naming a tier when the caller supplied no `providerFor` factory is an `interpreter-red` STOP, never a silent run on the default tier |
|
|
240
|
+
| `steps[].attempts` | optional; int `1..capRuns` | TIGHTEN-only: the step's own retry cap inside `ralph`, floored against the shell's `capRuns` (default 3). Above it reds `bounds`; a plan may narrow the shell's cap, never raise it |
|
|
241
|
+
| `steps[].scope` | optional; one value from the SAME menu `tree-changed` uses | narrows this step's live WRITE fence to a subset of the signed `writeScope` — chosen from the offered scope menu (below), never authored. Off-menu reds `invalid-value` carrying the menu |
|
|
242
|
+
| `steps[].exit` | 1..2 items (`MAX_EXITS_PER_STEP`), ALL must pass (AND-only, no OR/NOT) | closed menu (`EXIT_TYPES`): `artifact-written(path, pattern?)` · `tree-changed(scope)` · `json-valid(path)` · `check-passes(name)`. **`tree-changed.scope` is a MENU, not a glob the agent authors** — the runner enumerates the signed fence entries plus the real directories beneath them (shallowest-first, capped at `MAX_SCOPE_MENU`=24; `legalScopes` in `src/plan.js`), `planPrompt` lists them verbatim, and `validatePlan` accepts membership only — the SAME array on both sides, so what was offered is what is accepted; omitted, it derives from `writeScope` alone — never a free-text fallback. An off-menu value that escapes the fence still reds `scope-escape` (the ledger's attribution class), an in-fence unoffered value reds `invalid-value` carrying the menu. `runPlan` emits `scope-menu {offered, truncated, offerableCount?, cap?}` so a capped menu is never silently complete. `check-passes` must name a stage on the close's DERIVED menu (`check-unknown` red names the offered menu — no operator authors this list, it comes straight from `checkMenu(close)`); on a write-granted step it must be paired with `tree-changed` (`exit-illegal` — the seed tree is green, a lone check would pass untouched, F17/F46). **`check-passes` also requires a write-class verb (`write`/`edit`) on the SAME step** (`exit-illegal` — BREAKING for plan authors: a read-only "verify" step carrying a check used to validate and no longer does). A failing check's gap is re-delivered to that step's OWN worker, so a step holding a check must be able to act on it; a read-only one is a mailbox with no hands and stalls to cap on a byte-identical gap (measured on 4 of 4 drafted plans). The check belongs on the step that fixes; the run's final verification is the operator's close, which the agent never authors. The rule is stated identically in the drafter prompt and the validator, so the two can never disagree — and it is suppressed when `tools` failed to parse (the step's hands are unknowable then, and the real defect already redded: one defect, one red). `artifact-written.pattern` must compile AND survive a ReDoS shape check — an unbounded quantifier over a group that itself repeats unboundedly (`(a+)+`, `(\d*)*`, even wrapped: `((a+))+`) is an `invalid-value` red (F49, catastrophic-backtracking footgun; rewrite without a repeated group inside a repeat). Exits verify FORM, not truth — progress gates; the operator's close stays the one arbiter |
|
|
177
243
|
|
|
178
|
-
Red vocabulary (
|
|
244
|
+
Red vocabulary (both validators): `parse-error`, `unknown-field`, `missing-required`,
|
|
179
245
|
`invalid-value`, `bounds`, `duplicate-id`, `close-type`, `close-hierarchy`,
|
|
180
|
-
`secret-literal`, `scope-escape`,
|
|
246
|
+
`secret-literal`, `scope-escape`,
|
|
181
247
|
`request-red` (locked-but-listed: a locked tool or verdictType — admission demand the
|
|
182
|
-
ledger tallies), plus the
|
|
183
|
-
`
|
|
184
|
-
|
|
185
|
-
|
|
248
|
+
ledger tallies), plus the plan-side reds (`verb-escape`,
|
|
249
|
+
`exit-illegal`, `check-unknown`, `step-scope-escape` — a scoped step whose `target` falls
|
|
250
|
+
outside its OWN narrowed scope (in-fence but gate-denied ground: the step's gate is built
|
|
251
|
+
from the narrowed prefix, so the pair is rejected at validation instead of burning attempts
|
|
252
|
+
on refusals). It binds WRITES, not observations: on the exit side it fires only on a
|
|
253
|
+
`tree-changed` scope DISJOINT from the step's (a scope that CONTAINS it fires exactly when
|
|
254
|
+
the narrow one does), and `artifact-written`/`json-valid` paths carry no step-scope red at
|
|
255
|
+
all — the evaluator asks nothing about who wrote the file, so naming a prior step's artifact
|
|
256
|
+
is legal and satisfiable. `job-invalid` — a plan validated against a
|
|
257
|
+
missing or non-plan-shape job fails CLOSED). `stageClose(close)` is the ONE staging
|
|
258
|
+
every check-menu consumer shares (array → itself; legacy object predicate → its
|
|
259
|
+
one-stage list, named `close`; gold/rubric/hitl → null). The `secret-literal` sweep is
|
|
186
260
|
defense-in-depth against known token shapes — env-only loading remains the law, not the
|
|
187
261
|
sweep.
|
|
188
262
|
|
|
189
263
|
## Public API
|
|
190
264
|
|
|
191
|
-
*Landed through N2 + the Layer 2 core (spine + shell +
|
|
192
|
-
|
|
265
|
+
*Landed through N2 + the Layer 2 core (spine + shell + the two validators + the plan
|
|
266
|
+
executor + extractor + runJob). Still TBD: N3
|
|
193
267
|
(contrast-bit extractor live), N4 (verdict classes — gold/rubric close EXECUTION), N5
|
|
194
268
|
(scheduler + budget ops + CLI), N6 (panel).*
|
|
195
269
|
|
|
@@ -202,12 +276,15 @@ written.
|
|
|
202
276
|
### `ralph({ middle, close?, judge?, capRuns, emit, redact?, closeTimeoutMs?, cwd?, expect?, judged?, gapKeep?, workerWrites? })` → `'green' | 'escalated'` — `src/ralph.js`
|
|
203
277
|
|
|
204
278
|
The dumb outer shell: `while close-red and under-cap: run the middle`. `close` is an argv
|
|
205
|
-
whose exit code is truth (`runClose` is also exported
|
|
279
|
+
whose exit code is truth (`runClose` is also exported — **async since 0.6: `runClose` and
|
|
280
|
+
`runStages` return Promises**; the child is awaited instead of spawnSync so a running close
|
|
281
|
+
no longer freezes the host event loop (F68); every close semantic — timeout signal, output
|
|
282
|
+
bounds, gap shape, exit bands — is byte-identical); the red gap text feeds the next
|
|
206
283
|
iteration, tail-biased when bounded (400 head + 1500 tail — the assertion diff lives at
|
|
207
284
|
the end). **`cwd` is where the close RUNS, and it is load-bearing (F8):** a close is a
|
|
208
285
|
repository command (`npm test`, `make check`) and every one of them is cwd-relative — run
|
|
209
|
-
it anywhere but the workdir and the arbiter judges the wrong tree. `
|
|
210
|
-
always
|
|
286
|
+
it anywhere but the workdir and the arbiter judges the wrong tree. `runPlan`
|
|
287
|
+
always passes the workdir. **Corollary for job authors (F15/F28):** the gap bound keeps the
|
|
211
288
|
head and the TAIL, so a close whose failures print mid-stream (a 391-subtest TAP dump) tells
|
|
212
289
|
the worker only the summary counts — pick a reporter whose failures land at the end, **or**
|
|
213
290
|
set the close's `gapKeep` (job-spec, arbiter territory) to a regex like `"^not ok"` so the
|
|
@@ -215,7 +292,21 @@ matching failure lines are preserved in a capped kept-failures block regardless
|
|
|
215
292
|
they print (F28: the first real firing delivered a gap with zero failure names). The gap
|
|
216
293
|
also combines stdout+stderr, so a stdout failure survives stderr noise.
|
|
217
294
|
`closeTimeoutMs` caps the close's wall clock (default 120s) — shell/operator
|
|
218
|
-
territory, inexpressible in any config.
|
|
295
|
+
territory, inexpressible in any config. A faulted close is asked to leave with SIGTERM and,
|
|
296
|
+
if it is still there 2s later, ended with SIGKILL: SIGTERM is a request a close can decline
|
|
297
|
+
(its own handler, a trapping wrapper), and without a second deadline the wait never resolves.
|
|
298
|
+
The verdict is untouched — the FIRST fault named the outcome and the kill only enforces it.
|
|
299
|
+
|
|
300
|
+
**The close child's environment is stripped of credential-shaped variables before it runs**
|
|
301
|
+
(`CLOSE_ENV_DENY` in `src/ralph.js`: provider keys, `AWS_*`, and any name ending
|
|
302
|
+
`_API_KEY`/`_SECRET`/`_TOKEN`/`_PASSWORD`/`_CREDENTIAL(S)`) — a close judges a tree and never
|
|
303
|
+
needs them. A COPY is stripped, never `process.env`, so the host keeps the key it spends every
|
|
304
|
+
round. This is exposure reduction, not a sandbox: worker-authored code run by the close still
|
|
305
|
+
has network and your OS user's permissions. The strip has no opt-out — no plan, config, or
|
|
306
|
+
spec field can widen it — so if your close genuinely needs a credential-shaped variable
|
|
307
|
+
(`AWS_*` regions/profiles included, e.g. a suite booting against localstack with a test DB
|
|
308
|
+
password), it will not be there and the close reds; set such values inside the close command
|
|
309
|
+
itself, not the inherited environment.
|
|
219
310
|
|
|
220
311
|
**The forbidden zone (PRD v1.11 / F17).** `runClose` returns a verdict ONLY when judgment
|
|
221
312
|
was rendered. `expect` (the signed exit code, default 0) and `judged` define the two clean
|
|
@@ -240,7 +331,7 @@ worker has written files this run — with a clean precheck baseline (`runJob` e
|
|
|
240
331
|
crash-at-precheck before any tokens), that crash is the worker's own broken edit, the most
|
|
241
332
|
recoverable red there is. The routed verdict is the DISTINCT `worker-crash` (spine event
|
|
242
333
|
`worker-crash` with the file list; never plain `crashed`, never `needs_revision`), and the
|
|
243
|
-
gap tells the worker which files it wrote and to fix or revert. `
|
|
334
|
+
gap tells the worker which files it wrote and to fix or revert. `runPlan` wires the seam
|
|
244
335
|
to the gate audit's allow-decision write/edit lines (run_id-scoped); no seam or zero writes
|
|
245
336
|
keeps the old behavior — an instrument crash still escalates `close-crashed`, never retried.
|
|
246
337
|
Measured motivation: battery pass 1 (F31) lost 4 of 7 rows to exactly this escalation.
|
|
@@ -257,48 +348,39 @@ its close.
|
|
|
257
348
|
Escalations are decision-ready (category, options, spend); cap-halt is its own
|
|
258
349
|
category, never merged with "wrong". A thrown middle is relayed by its `category`
|
|
259
350
|
property (`cap-halt`, `gate-red`, …); an unnamed throw is `interpreter-red`. Close output
|
|
260
|
-
is scrubbed at capture (an injected `redact`, wired to bareguard by `
|
|
351
|
+
is scrubbed at capture (an injected `redact`, wired to bareguard by `runPlan` with the
|
|
261
352
|
validators' full secret-shape inventory — Bearer/sk-/ghp_/github_pat_/AKIA/xox) so a
|
|
262
353
|
secret a checked command echoes never enters the append-only spine or a worker prompt —
|
|
263
354
|
a benign gap is byte-identical (secrets hard line; design law #7 intact).
|
|
264
355
|
|
|
265
|
-
### `validateConfig(input, { shellCapUsd?, jobWriteScope? })` → `{ ok, reds, config }` — `src/validate.js`
|
|
266
|
-
|
|
267
|
-
Deterministic schema-v1 predicate; never throws. Every failure is a named red
|
|
268
|
-
`{ code, path, detail? }` — reds before tokens burn. Returns the parsed config on ok
|
|
269
|
-
(single parse; `null` on any red). Pass the job spec's `writeScope` as `jobWriteScope`
|
|
270
|
-
and every workflow scope must fit inside the fence (path-boundary aware: `src2` is not
|
|
271
|
-
inside `src`) or it reds `scope-escape`; pass `min(shell cap, job budgetUsd)` as
|
|
272
|
-
`shellCapUsd` to complete the ceiling chain. Verb vocabulary bound from litectx
|
|
273
|
-
(`LOOP_SHAPES`, `SLOTS`, `VERBS` exported). `diffPaths(a, b)` returns changed JSON paths —
|
|
274
|
-
the one-knob mutation checker.
|
|
275
|
-
|
|
276
|
-
`scanSecrets(raw)` → `string[]` is the same module's text-side scan: every known
|
|
277
|
-
secret-shape match in a raw stream (a spine file, a close's output), `[]` when clean,
|
|
278
|
-
never throws. Use it instead of re-deriving a scan from `SECRET_PATTERNS` — detection
|
|
279
|
-
and redaction share ONE shape inventory, and a hand-rolled copy that misses a shape is
|
|
280
|
-
a leak on the very output it was guarding.
|
|
281
|
-
|
|
282
356
|
### `validateJob(input, { shellCapUsd? })` → `{ ok, reds, job }` — `src/job.js`
|
|
283
357
|
|
|
284
|
-
The operator-owned
|
|
285
|
-
`
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
`
|
|
291
|
-
`
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
358
|
+
The operator-owned validator — the arbiter's rulebook, and the sibling of
|
|
359
|
+
`validatePlan`, never its extension: what the human signs and what the agent authors
|
|
360
|
+
are two documents with two validators, which is what makes the arbiter inexpressible
|
|
361
|
+
from the agent's side. Validates a `job-v1` spec — see **All options** for the schema,
|
|
362
|
+
close types, and hierarchy. Never throws on JSON text or plain parsed data (the ingest
|
|
363
|
+
contract); returns the parsed spec on ok, `null` on any red. A spec carrying the retired
|
|
364
|
+
`steps[]` reds `shape-retired` by name (PRD v1.32) rather than falling through to a
|
|
365
|
+
generic unknown-field. Menus exported: `CLOSE_TYPES`, `CLASS_BY_CLOSE`, `GOLD_COMPARE`,
|
|
366
|
+
`CADENCE_UNITS`, `PROVIDERS`, `CONDITION_KEYS`, `TOOL_MENU`, `LOCKED_TOOLS`, `STORE_VERBS`,
|
|
367
|
+
`VERDICT_TYPES`, `LOCKED_VERDICTS` — plus `checkMenu` itself.
|
|
368
|
+
|
|
369
|
+
### `validatePlan(input, { job, maxStepRounds?, scopes?, capRuns? })` → `{ ok, reds, plan }` — `src/plan.js`
|
|
370
|
+
|
|
371
|
+
`validateJob`'s SIBLING in the two-document split, never a third validator over it (the
|
|
372
|
+
two-doc split's third validator never happens: plan-v1 gates the PLAN, the job spec stays
|
|
373
|
+
the arbiter's only home). Gates the AGENT-authored plan doc (`schema: "plan-v1"`) against
|
|
374
|
+
the SIGNED job spec before tokens burn — the ceiling, the fence, and the checks menu all come
|
|
297
375
|
from `job` (a missing or non-plan-shape job fails CLOSED, `job-invalid`). Never throws;
|
|
298
376
|
same `{ code, path, detail }` red shape as its siblings; `verb-escape` reds carry the
|
|
299
377
|
escaping verb as a structured `verb` field (the ledger keys on it). `maxStepRounds`
|
|
300
|
-
(default 40 — the shell's tool-mode per-attempt bound) ceilings every step's `rounds
|
|
301
|
-
|
|
378
|
+
(default 40 — the shell's tool-mode per-attempt bound) ceilings every step's `rounds`;
|
|
379
|
+
`capRuns` (default 3) ceilings every step's `attempts`; `scopes` is the offered
|
|
380
|
+
`tree-changed` scope menu — the array of scope strings the drafter prompt listed, which
|
|
381
|
+
`runJob`/`runPlan` build internally from the signed fence plus the real directories beneath
|
|
382
|
+
it (a direct `validatePlan` caller may pass its own array); omitted, it derives from the
|
|
383
|
+
signed `writeScope` alone, never a free-text fallback. Menus exported: `EXIT_TYPES`, `MAX_EXITS_PER_STEP`, `MAX_PLAN_STEPS`, `WRITE_VERBS` — plus `stageClose`.
|
|
302
384
|
|
|
303
385
|
### `snapshotScope(dir, scope)` / `evalExits(exits, { dir, snapshot?, runCheck? })` — `src/exits.js`
|
|
304
386
|
|
|
@@ -311,141 +393,147 @@ the snapshot): an identical re-write is NOT a change (F43) and git status is nev
|
|
|
311
393
|
consulted (F45). `artifact-written` rejects zero-byte files. `check-passes` delegates
|
|
312
394
|
through the `runCheck` seam (the runner wires runClose); an unwired or crashed seam
|
|
313
395
|
fails CLOSED with `fault` carrying a runClose verdict name — an instrument fault
|
|
314
|
-
escalates through `CLOSE_FAULTS`, never masquerades as worker feedback. Failing details
|
|
315
|
-
|
|
396
|
+
escalates through `CLOSE_FAULTS`, never masquerades as worker feedback. Failing details are
|
|
397
|
+
mostly counts and names — with one exception that matters: `json-valid` embeds `JSON.parse`'s
|
|
398
|
+
own message, and V8 quotes a window of the SOURCE inside it, so that detail can carry file
|
|
399
|
+
bytes the worker chose. The plan runner redacts EVERY detail (`scrub`/`SECRET_PATTERNS`, the
|
|
400
|
+
ONE inventory) at the emission boundary, before anything rides the append-only spine. The
|
|
401
|
+
module itself does not scrub: a direct `evalExits` caller wiring results into its own log
|
|
402
|
+
must apply its own redaction.
|
|
316
403
|
|
|
317
404
|
### `jobSpecHash(job)` / `checkApproval(job, approvals)` — `src/job.js`
|
|
318
405
|
|
|
319
406
|
The pure half of **human-signs-always**: an agent may draft a job spec, but no job runs
|
|
320
407
|
until a human approves that exact version. `jobSpecHash` is sha256 over canonical JSON
|
|
321
|
-
(key-order independent)
|
|
322
|
-
|
|
323
|
-
`
|
|
324
|
-
|
|
408
|
+
(key-order independent) of the **RESOLVED** spec (MED-1): an omitted `tools` is filled in
|
|
409
|
+
with the concrete current `TOOL_MENU` before canonicalization, because that is the ceiling
|
|
410
|
+
`plan.js` and `planrun.js` actually read. So any SEMANTIC edit changes the hash and an
|
|
411
|
+
edited spec is unapproved by construction — but not every BYTE edit: writing today's full
|
|
412
|
+
menu into a spec that omitted `tools` is hash-NEUTRAL, since the two spell the same
|
|
413
|
+
ceiling. The direction that matters bites instead — a `TOOL_MENU` WIDENING (a library
|
|
414
|
+
upgrade) flips the hash of an omitted-`tools` spec whose bytes never moved, straight into
|
|
415
|
+
the refuse-until-reapproved machinery. That is the point: the signature pins WHICH menu it
|
|
416
|
+
covers, so an already-signed ceiling can never grow unsigned. A spec that named its own
|
|
417
|
+
`tools` hashes exactly as before. **BREAKING** for an approval held against an
|
|
418
|
+
omitted-`tools` spec: it re-signs once. `checkApproval(job, approvals)` is a pure predicate
|
|
419
|
+
over `{ specHash, signer, ts }` records, canonicalizing the SAME resolved form (a gate
|
|
420
|
+
reading a different form than the mint would unapprove every omitted-`tools` spec); the
|
|
421
|
+
approval record lives OUTSIDE the document it signs and is shell/human territory, never
|
|
422
|
+
agent-writable. The N2 runner enforces it.
|
|
325
423
|
Reserved spine vocabulary (V7, machinery-free until job #1 surfaces one):
|
|
326
424
|
`coordination-red` — a failure between units (scope contention, step order, store
|
|
327
425
|
races), never to be folded into worker/interpreter reds.
|
|
328
426
|
|
|
329
|
-
### `
|
|
330
|
-
|
|
331
|
-
The
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
(the
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
own A/B, it ships **OFF by default (decided 2026-07-21)** — armed and correct, but not
|
|
384
|
-
default-enabled until a stuck job (Layer 2, or a manufactured-fixation probe) shows ON
|
|
385
|
-
beats OFF. Flip the default to `true` the day that evidence lands; a `root-injected` event
|
|
386
|
-
on your spine (when you pass `layerRoot: true`) is a signal worth reading, not noise.
|
|
387
|
-
|
|
388
|
-
### `runJob(spec, { approvals, workdir, provider, nativeProvider?, emit, target?, capRuns?, shellCapUsd?, closeTimeoutMs?, execCmd?, layerRoot? })` → outcome — `src/run.js`
|
|
389
|
-
|
|
390
|
-
The N2 runner — the shell's top layer; composes everything below it and interprets
|
|
391
|
-
nothing itself. Sequence: **approval gate** (human-signs-always — refuses an unapproved
|
|
392
|
-
spec before ANY provider call: `unapproved-spec`) → **primitive smoke** (litectx
|
|
393
|
-
known-answer round-trip before tokens: `smoke-red` — silent degradation throws nothing)
|
|
394
|
-
→ **sequential per-step interpret loops** under the ONE cumulative ledger (each step's
|
|
395
|
-
ceiling is `min(job budget − spent, shell cap)`; a step that cannot green stops the job:
|
|
396
|
-
`step-red:<id>`) → the **hitl step** opens a draft PR deterministically (branch → stage
|
|
397
|
-
the job fence ONLY, so spines/audit logs never enter the PR → commit → push →
|
|
398
|
-
`gh pr create --draft`) and ends `escalated` BY DESIGN, the PR URL riding the
|
|
399
|
-
decision-ready escalation; a PR failure is `pr-red` and the escalation still fires with
|
|
400
|
-
the error. Model tools never touch git — `execCmd` is the shell-owned process seam
|
|
401
|
-
(defaults to real spawnSync). **The hitl step checks the fence first**: an
|
|
402
|
-
affirmatively-clean `git status --porcelain -- <fence>` means nothing changed, so there
|
|
403
|
-
is nothing for a human to review — `pr-skipped` + `step-end: already-green`, the job ends
|
|
404
|
-
`green`, no PR and no escalation (a cadenced no-op run is silent and free). A FAILED
|
|
405
|
-
check falls through to the PR path and reds there — an unknown fence state is never a
|
|
406
|
-
green. The PR step **hands the checkout back**: the starting branch is read before
|
|
407
|
-
anything moves and restored on every path; a failed restore is a loud `workdir-red`
|
|
408
|
-
naming the stranded branch (it never un-opens a real PR). Outcomes: `green | escalated | unapproved-spec | job-red |
|
|
409
|
-
smoke-red | config-red | pricing-red | provider-red | cap-halt | close-unsupported |
|
|
410
|
-
step-red:<id>` — `provider-red` is a transport throw from the drafting call, OR a worker
|
|
411
|
-
round the API cut off mid-generation (`truncated:max_tokens`, bare-agent 0.27.0/BA-6 —
|
|
412
|
-
before which it laundered into a clean finish, F25); in both cases no verdict exists and the
|
|
413
|
-
failed round's spend is only partly known (F6). `cap-halt` is drafting spend consuming the whole job budget before a
|
|
414
|
-
valid config existed. Both are decision-ready escalations with a terminal `job-end`:
|
|
415
|
-
the spine never dangles. **Every `job-end` carries the money, on every path**:
|
|
416
|
-
`{ outcome, spentUsd, spendComplete }` (plus `step`/`cause`/`detail` where the outcome has
|
|
417
|
-
them). `spentUsd` is the accumulated sum of PRICED rounds ONLY — never an estimate from
|
|
418
|
-
token counts or averages (cap-not-estimate) — and it is stated even on the pre-token reds,
|
|
419
|
-
where the honest figure is a real `0`. `spendComplete` says whether that figure is EXACT:
|
|
420
|
-
`false` means one or more rounds came back unpriced (F6), so `spentUsd` is a FLOOR
|
|
421
|
-
("at least $X", true total unknowable) and must not be read as a total. Both fields are
|
|
422
|
-
present on all outcomes, so a consumer never branches on field presence and never has to
|
|
423
|
-
launder a missing `spentUsd` into `$0`. A text-mode job invoked without `opts.target` is a `job-red`
|
|
424
|
-
before ANY provider call (and `interpret` itself throws a TypeError for direct
|
|
425
|
-
callers) — reds-before-tokens applies to the call, not just the spec.
|
|
426
|
-
|
|
427
|
-
**The plan flow (Layer 2).** A plan-shape spec routes through the SAME `runJob` entry —
|
|
428
|
-
same approval gate, same smoke, same metered ledger, same job-end money contract
|
|
429
|
-
(`job-start` carries `shape: 'plan'` + the goal instead of a steps list; no `opts.target`
|
|
430
|
-
needed — plan steps are tool-mode by construction). The flow (`runPlan`, also exported
|
|
431
|
-
for direct callers who own their own ledger): **close precheck** (`already-green` is a
|
|
432
|
-
DISTINCT zero-token outcome; a forbidden-zone verdict escalates before spend) → **checks
|
|
433
|
-
preflight** (every SIGNED check runs once at $0 — an unrunnable check is a `check-red`
|
|
434
|
-
stop before tokens, not a fault mid-plan) → **SCOUT** (read-only by construction: the
|
|
435
|
-
write verbs are not in its menu; hard-bounded rounds) → **PLAN** (the decompose call —
|
|
427
|
+
### `runJob(spec, { approvals, workdir, provider, nativeProvider?, providerFor?, emit, capRuns?, shellCapUsd?, closeTimeoutMs?, layerRoot? })` → outcome — `src/run.js`
|
|
428
|
+
|
|
429
|
+
The runner — the shell's top layer, and the ONE entry. It composes everything below it and
|
|
430
|
+
interprets nothing itself. Sequence: **approval gate** (human-signs-always — refuses an
|
|
431
|
+
unapproved spec before ANY provider call: `unapproved-spec`) → **primitive smoke** (litectx
|
|
432
|
+
known-answer round-trip before tokens: `smoke-red` — a silent degradation throws nothing) →
|
|
433
|
+
**the plan flow** (`runPlan`, below) under the ONE cumulative ledger, whose ceiling is
|
|
434
|
+
`min(job budget − spent, shell cap)`.
|
|
435
|
+
|
|
436
|
+
Outcomes: `green | already-green | escalated | unapproved-spec | job-red | smoke-red |
|
|
437
|
+
plan-red | check-red | close-red | close-unsupported | pricing-red | provider-red |
|
|
438
|
+
interpreter-red | cap-halt | wall-halt | step-stalled | step-red:<id>`. `provider-red` is a
|
|
439
|
+
transport throw or a worker round the API cut off mid-generation (`truncated:max_tokens`,
|
|
440
|
+
BA-6 — before which it laundered into a clean finish, F25): no verdict exists and the failed
|
|
441
|
+
round's spend is only partly known (F6). `cap-halt` is the wallet; `wall-halt` is the clock
|
|
442
|
+
(F64 — a timeout derived from the run's own deadline is a governance stop, never a transport
|
|
443
|
+
casualty). **`cap-halt` reaches you from the close-fix loop too, not only from a step:** the
|
|
444
|
+
shell spells attempt-exhaustion and a money-gate halt with the same category, so both the
|
|
445
|
+
step loop and the fix loop read the WALLET to tell them apart — a drained wallet is
|
|
446
|
+
`cap-halt` (the resume-to-cap checkpoint), attempts spent with money still on the table is
|
|
447
|
+
the designed `escalated` terminal ("the close is still red"). The fix worker's gate is built
|
|
448
|
+
with the wallet at its most drained, which is exactly where a money cut would otherwise
|
|
449
|
+
masquerade as a capability read (F45). `step-stalled` is the F66 stall fuse giving up: no
|
|
450
|
+
completed round for 5 minutes,
|
|
451
|
+
three times on one call — each stall silently abandons the hung call and reissues it
|
|
452
|
+
(self-heal first); only the third throws. Inside a step it is the THIRD replan trigger
|
|
453
|
+
(with `cap-halt` and `step-variance`); outside a step it escalates under its own name, never
|
|
454
|
+
laundered into `provider-red`, and carries `spendComplete:false` (an abandoned call may
|
|
455
|
+
already have been billed). **The reissue is wall-aware:** past the run's deadline the fuse does
|
|
456
|
+
not self-heal — self-heal is what a run does with time left — and it gives up as `wall-halt`,
|
|
457
|
+
not `step-stalled`, since a replan has nothing left to re-allocate. Every one of them is a
|
|
458
|
+
decision-ready escalation with a terminal `job-end`: the spine never dangles.
|
|
459
|
+
|
|
460
|
+
**Every `job-end` carries the money, on every path**: `{ outcome, spentUsd, spendComplete }`
|
|
461
|
+
(plus `step`/`cause`/`detail` where the outcome has them). `spentUsd` is the accumulated sum
|
|
462
|
+
of PRICED rounds ONLY — never an estimate from token counts or averages (cap-not-estimate) —
|
|
463
|
+
and it is stated even on the pre-token reds, where the honest figure is a real `0`.
|
|
464
|
+
`spendComplete` says whether that figure is EXACT; `false` means `spentUsd` is a FLOOR ("at
|
|
465
|
+
least $X", true total unknowable) and must not be read as a total. Three things set it, all
|
|
466
|
+
the same class — a call whose cost is unknowable from here: a round that came back unpriced
|
|
467
|
+
(F6); a run that ABSORBED a stall, because the fuse's silent reissue may pay twice for one
|
|
468
|
+
answer and the flag is one-way whatever the outcome; and a `wall-halt` that cut a call
|
|
469
|
+
mid-flight (`cutMidCall`). A wall stop read BETWEEN iterations or steps has no call in flight,
|
|
470
|
+
so it stays exact. Both fields are present on all outcomes, so a consumer never branches on
|
|
471
|
+
field presence and never has to launder a missing `spentUsd` into `$0`.
|
|
472
|
+
|
|
473
|
+
**The plan flow (Layer 2).** `job-start` carries `shape: 'plan'` + the goal; plan steps are
|
|
474
|
+
tool-mode by construction. The flow (`runPlan`, also exported for direct callers who own
|
|
475
|
+
their own ledger): **close precheck** (`already-green` is a
|
|
476
|
+
DISTINCT zero-token outcome; a forbidden-zone verdict escalates before spend) → **check-menu
|
|
477
|
+
preflight** (`check-menu` names the derived menu, then every OFFERED stage's chain runs once
|
|
478
|
+
at $0 — an unrunnable stage is a `check-red` stop before tokens, not a fault mid-plan) →
|
|
479
|
+
**SCOUT** (read-only by construction: neither the
|
|
480
|
+
write-class nor the store-class verbs are in its menu; hard-bounded rounds) → **PLAN** (the decompose call —
|
|
436
481
|
the planner never sees the repo, only the scout blob; drafted against a schema
|
|
437
482
|
description with check NAMES only; `validatePlan` gates it, one redraft with the reds
|
|
438
483
|
fed back, then `plan-red`) → **EXECUTE** (strictly sequential micro-loops: `ralph` with
|
|
439
484
|
the exit-evaluator judge; tree snapshots at step start; the gap names every failing
|
|
440
485
|
wall — mechanical genre, F46's measured mechanism; artifacts feed forward labeled by
|
|
441
|
-
step id) → **ONE replan**, triggered by exhaustion
|
|
486
|
+
step id) → **ONE replan**, triggered by exhaustion OR variance (an instrument stop never
|
|
442
487
|
replans) → **the operator's close**, a red feeding ONE bounded fix loop judged by the
|
|
443
488
|
REAL close. `plan-executed` (the plan-as-executed record, design law #2) lands on the
|
|
444
489
|
spine on every path that executed steps. Additional outcomes: `already-green |
|
|
445
|
-
plan-red | check-red | close-red`. Worker prompts hold the v1.12 §5 contract
|
|
490
|
+
plan-red | check-red | close-red | wall-halt`. Worker prompts hold the v1.12 §5 contract
|
|
446
491
|
(mutation-proven): the absolute repo root, the step's action/target, prior artifacts,
|
|
447
492
|
the gap — NEVER the budget, the close command, a check's command, or the arbiter's books.
|
|
448
493
|
|
|
494
|
+
**Time and materials (T + A, PRD v1.27/v1.29).** `runPlan` starts ONE wall clock per run
|
|
495
|
+
(`createClock`, `src/clock.js`) from the signed `maxWallMs` and emits `wall-clock` with the
|
|
496
|
+
requested AND enforced numbers up front, plus the `closeStages` count that explains the gap
|
|
497
|
+
between them (`enforcedMs = maxWallMs + closeStages × closeTimeoutMs`, one expression — the
|
|
498
|
+
stage count comes from `stageClose`, the same staging the runner executes). Enforcement is a
|
|
499
|
+
**between-round deadline** — the only seam that exists, since `loop.stop()` cannot cut an
|
|
500
|
+
in-flight call (F61: fired at 500ms,
|
|
501
|
+
returned at 4,018ms) — so an attempt that crosses the deadline mid-flight emits `wall-bounded`,
|
|
502
|
+
is judged, and feeds its gap forward exactly like a round-bounded attempt. **The close is never
|
|
503
|
+
bounded by the wall and always runs to completion** — a deadline that kills grading leaves the
|
|
504
|
+
run unreadable after the money is spent (the F45 class, money generalized to time); what the
|
|
505
|
+
wall stops is the START of new work. Three sites decide the run-level terminal `wall-halt`, all
|
|
506
|
+
on the same clock: the step loop after a step returns, a step that would BEGIN already expired,
|
|
507
|
+
and the close-fix loop before it opens another iteration — which then stops on the verdict the
|
|
508
|
+
last close already minted (hamr: *"when time is up, keep the grade we already have and stop"*),
|
|
509
|
+
carrying that verdict, the iterations spent, and a progress `trend` (`moving` | `stalled` |
|
|
510
|
+
`unknown`, read off the last two close gaps) so the human can pick between raising `maxWallMs`
|
|
511
|
+
(resume-to-cap: the stop IS the checkpoint) and revising the goal — both spec edits, both a new
|
|
512
|
+
hash to re-sign. `cutMidCall` on the `wall-halt` record splits the deadline seen INSIDE a
|
|
513
|
+
provider call from the deadline read between them. Time reporting is
|
|
514
|
+
licensed at ~90% accuracy by ruling: imprecision is fine, reporting an unknown or unbounded
|
|
515
|
+
duration as `0` never is (F6 extended to time — unbounded reports `null`, never `Infinity`).
|
|
516
|
+
The planner (never the worker) receives a **materials** block at draft and at replan —
|
|
517
|
+
`{ balanceUsd, remainingMs }`, what is LEFT as a balance, never a rate: F57 measured a 150×
|
|
518
|
+
spread on verification-gap duration, so a per-round constant describes almost no real round
|
|
519
|
+
and a rate framing creates a rush-or-fake incentive. The replan trigger gains a second axis
|
|
520
|
+
(`step-variance`): a step consuming ≥ `VARIANCE_THRESHOLD` (0.5) of the run's REMAINING money
|
|
521
|
+
or time with its exits unmoved is pre-empted at the head of its next attempt so the planner
|
|
522
|
+
re-allocates instead. **Known and recorded (F63): that trigger fired 0 times across 18
|
|
523
|
+
archived spines / 54 steps** (near misses 0.35–0.45) — the threshold is arbiter territory,
|
|
524
|
+
deliberately not fitted to those points. The ONE-replan ceiling is unchanged.
|
|
525
|
+
|
|
526
|
+
**Per-step model tiers — `providerFor` (P).** A plan step may declare a `model` TIER
|
|
527
|
+
(`sonnet` \| `haiku`, `STEP_MODELS`). The tier menu is signed into the schema; mapping a tier
|
|
528
|
+
onto a real model (and any per-model provider params) is the RUNNER's territory, so both
|
|
529
|
+
`runJob` and `runPlan` take `providerFor?: (tier: string) => provider`. It is called ONLY for
|
|
530
|
+
a step that declared a tier, once per such step — the scout, the plan drafter and every
|
|
531
|
+
untiered step stay on `opts.provider`, so a caller that never plans tiers can omit it
|
|
532
|
+
entirely. Cache per tier on your side if you want one instance reused. **A plan naming a tier
|
|
533
|
+
when no factory was supplied is an `interpreter-red` STOP**, named as a wiring gap rather than
|
|
534
|
+
a plan defect: running the default tier as if the choice had been honoured is the F50
|
|
535
|
+
blind-instrument class, and the whole point of the field is that the choice is observable.
|
|
536
|
+
|
|
449
537
|
**Two worker surfaces — API and native clipipe (BA-16, module 4d).** The plan flow is
|
|
450
538
|
provider-agnostic: the close, the checks, and the exit evaluator are commands and form
|
|
451
539
|
checks, so ONLY the worker differs. `job.provider === 'clipipe-subscription'` drives tools
|
|
@@ -467,17 +555,14 @@ per-attempt bound and surfaces `max_turns` as a BOUNDED attempt (judged, gap fed
|
|
|
467
555
|
the native analog of `loop.stop()`. Live-validated green end-to-end on the real CLI; the
|
|
468
556
|
Loop path is untouched.
|
|
469
557
|
|
|
470
|
-
**Resume-to-cap (close-first
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
pays ZERO provider calls. So the resume story is: a `cap-halt` stop is the checkpoint
|
|
479
|
-
(the workdir + the closes), the human raises `budgetUsd` (a new spec hash — re-sign),
|
|
480
|
-
and the rerun picks up exactly where the budget died.
|
|
558
|
+
**Resume-to-cap (close-first):** the job's close runs FIRST, before any tokens
|
|
559
|
+
(`close-precheck` on the spine, output scrubbed at capture like every close). Already-green
|
|
560
|
+
is a DISTINCT zero-token outcome, never plain `green`: nothing was done, so it mints no
|
|
561
|
+
learning credit and runs no on-green retention. A close that cannot RUN is a `broken-close`
|
|
562
|
+
escalation before any provider call. So the resume story is: a `cap-halt` or `wall-halt`
|
|
563
|
+
stop IS the checkpoint (the workdir + the close), the human raises `budgetUsd` or
|
|
564
|
+
`maxWallMs` (a new spec hash — re-sign), and the rerun picks up where the budget died,
|
|
565
|
+
paying zero provider calls for anything already green.
|
|
481
566
|
|
|
482
567
|
**Unpriced is never free (F6/F12):** the ledger meters **per ROUND** (`worker-round`, at
|
|
483
568
|
bare-agent's `onLlmResult` seam) — money is counted as it is spent, never at the end of an
|
|
@@ -485,19 +570,11 @@ attempt: a tool-mode attempt that HALTS never returns, so accounting its result
|
|
|
485
570
|
the whole attempt's spend (a real run bought $1.4375 of tokens and the ledger reported
|
|
486
571
|
$0.0048). Any round whose cost is the honest null halts `pricing-red`, decision-ready; a
|
|
487
572
|
null never accumulates as $0, so the hard cap cannot be gamed by an unpriced provider path.
|
|
488
|
-
The
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
The **÷ steps** is F40: the config is drafted once and re-validated at every step against
|
|
494
|
-
what is LEFT, so a ceiling sized to the whole pot goes stale the moment step 1 spends and
|
|
495
|
-
the next step reds `bounds` on an unchanged config with money still available. A per-step
|
|
496
|
-
share still fits after its predecessors have spent (the shares sum to `budgetUsd − reserve`).
|
|
497
|
-
Single-predicate-step jobs are arithmetically unchanged. This is a DRAFTING bound, not an
|
|
498
|
-
enforcement one: cap-not-estimate is intact, and a step that genuinely needs more than its
|
|
499
|
-
share still cap-halts cleanly with a resume point rather than silently borrowing from the
|
|
500
|
-
steps after it.
|
|
573
|
+
The plan and scout calls are metered on that same priced path — a drafting call is a
|
|
574
|
+
provider call and is never accounted around the ledger (F6). The scout runs under its own
|
|
575
|
+
reserved round bound, with the LAST round reserved for a toolless summary: the bound halts
|
|
576
|
+
a scout mid-tool-use, and text is its only deliverable, so without that reservation it
|
|
577
|
+
spends its whole allowance exploring and reports nothing (F59 — measured on 15 of 18 runs).
|
|
501
578
|
|
|
502
579
|
**What the worker is told (tool mode):** the absolute repository root (F10 — bare-agent's
|
|
503
580
|
shell tools resolve relative paths against the PROCESS cwd, so a worker with no root is
|
|
@@ -508,14 +585,7 @@ re-run with the close's verdict; without this a model one-shots and can eat the
|
|
|
508
585
|
reads before ever writing). It is NOT allowed to read the run's own machinery (F14): the gate
|
|
509
586
|
audit, the smoke store and the litectx store are denied — the agent neither authors the
|
|
510
587
|
arbiter nor reads its books. N2 bounds (honest): `gold`/`rubric` closes refuse
|
|
511
|
-
`close-unsupported` (execution lands at N4)
|
|
512
|
-
steps.
|
|
513
|
-
|
|
514
|
-
### `extractRules({ config, provider, priorRules, revisionDiff? })` — `src/extract.js`
|
|
515
|
-
|
|
516
|
-
One sealed LLM call distilling a lineage's rules from ledger facts after a green run.
|
|
517
|
-
`MAX_RULES`/`MAX_RULE_CHARS` bounds enforced mechanically post-call, rejected whole.
|
|
518
|
-
Malformed output is a red as data (`rules: null`) — the caller keeps prior rules.
|
|
588
|
+
`close-unsupported` (execution lands at N4).
|
|
519
589
|
|
|
520
590
|
### `updateLedger({ ledgerFile, spineFiles })` → `{ appended, fold }` — `src/ledger.js`
|
|
521
591
|
|
|
@@ -532,9 +602,14 @@ failures can't derive), `runtime-red`, `provider-red`, `pricing-red` (F6), possi
|
|
|
532
602
|
`capability-gap` (cap-halt + request-red in one spine), `broken-close` (consumer-attributed),
|
|
533
603
|
`request-red` (admission demand for a locked verb — keyed on the red's structured
|
|
534
604
|
`verb` field, prose-quoted verb as legacy fallback), `retention-red`, `config-red`
|
|
535
|
-
(drafting friction — attributed to bareloop's own schema/prompt). Deliberate exclusions
|
|
536
|
-
|
|
537
|
-
|
|
605
|
+
(drafting friction — attributed to bareloop's own schema/prompt). Deliberate exclusions
|
|
606
|
+
(`EXCLUDED_ESCALATIONS`, a runtime set — anything outside classified ∪ excluded is
|
|
607
|
+
counted as unmapped, never dropped): `cap-halt`/`wall-halt` (budget stories, money and
|
|
608
|
+
time), `step-stalled` (the stall fuse firing is our governance, not an observed provider
|
|
609
|
+
failure), `step-variance` (a planning story), `gate-red`/`smoke-red` (governance working
|
|
610
|
+
as intended / already counted), `hitl-close`/`close-unsupported` (by design),
|
|
611
|
+
`close-timeout`/`close-killed`/`close-crashed` (the arbiter's own named terminals, F17);
|
|
612
|
+
`close-verdict`/`artifact-red` stay worker stories, `pr-red` operator environment.
|
|
538
613
|
`suggestedAsk` on every row is a template seed for an upstream ask — filing stays human;
|
|
539
614
|
status rows (`open → filed → fixed → consumed`) are human-appended, and the fold shows
|
|
540
615
|
the latest per key. Pure pieces exported for custom folds: `classifyIncidents(events,
|
|
@@ -545,8 +620,10 @@ panel reads the same file at N6.
|
|
|
545
620
|
|
|
546
621
|
Three layers. An **outer shell** (dumb, permanent): per-run budget cap via bareguard,
|
|
547
622
|
retry cap, verdict collection, escalation routing — stateless across runs; nothing inside
|
|
548
|
-
negotiates with it. An **emergent middle**: the authored
|
|
549
|
-
|
|
623
|
+
negotiates with it. An **emergent middle**: the AGENT-authored plan — bounded steps, each
|
|
624
|
+
with its granted verbs, its round and attempt caps, its narrowed write scope and its
|
|
625
|
+
form-checkable exits — schema-validated by `validatePlan` before tokens burn (the
|
|
626
|
+
operator-authored `steps[]` shape is deleted, PRD v1.32). A **floor**: append-only
|
|
550
627
|
JSONL spine (single source for every UI), litectx store per job, per-run ledger. Built on
|
|
551
628
|
the bare suite: bareagent, bareguard, litectx, barebrowse, baremobile — the full surface
|
|
552
629
|
is disclosed to the authoring agent; only admitted verbs are callable per job.
|