bareloop 0.21.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +292 -0
- package/README.md +1 -1
- package/bareloop.context.md +177 -24
- package/package.json +3 -3
- package/src/authorflow.js +7 -2
- package/src/authorjob.js +15 -1
- package/src/authorscout.js +83 -9
- package/src/bundle.js +40 -46
- package/src/calibrate.js +10 -0
- package/src/cli.js +52 -19
- package/src/close-integrity.js +325 -0
- package/src/closetimeout.js +165 -0
- package/src/declaredclose.js +9 -2
- package/src/index.js +14 -0
- package/src/job.js +84 -5
- package/src/judged.js +108 -5
- package/src/kinds.js +12 -0
- package/src/plan.js +5 -1
- package/src/planrun.js +392 -46
- package/src/providers.js +176 -0
- package/src/ralph.js +40 -7
- package/src/ratelimit.js +116 -0
- package/src/reuse.js +12 -2
- package/src/reviewdoor.js +26 -6
- package/src/run.js +83 -8
- package/src/transport.js +29 -12
- package/src/validate.js +59 -0
- package/types/authorscout.d.ts +27 -2
- package/types/close-integrity.d.ts +196 -0
- package/types/closetimeout.d.ts +126 -0
- package/types/declaredclose.d.ts +6 -1
- package/types/index.d.ts +1 -0
- package/types/job.d.ts +4 -0
- package/types/judged.d.ts +7 -2
- package/types/kinds.d.ts +9 -0
- package/types/planrun.d.ts +17 -2
- package/types/providers.d.ts +104 -0
- package/types/ralph.d.ts +19 -5
- package/types/ratelimit.d.ts +59 -0
- package/types/reuse.d.ts +2 -0
- package/types/reviewdoor.d.ts +3 -1
- package/types/run.d.ts +33 -2
- package/types/transport.d.ts +10 -4
- package/types/validate.d.ts +48 -0
package/bareloop.context.md
CHANGED
|
@@ -70,6 +70,19 @@ inherited rule carries the green that minted it and the contrast that attributed
|
|
|
70
70
|
with `anthropic-api` on cost, and budgets don't transfer between them.
|
|
71
71
|
`job.provider === 'clipipe-subscription'` with no `nativeProvider` supplied is
|
|
72
72
|
`interpreter-red`, never a silent fall-back to the metered API (`src/planrun.js:1073`).
|
|
73
|
+
`provider: 'openai-api'` (PRD item 28) takes the ordinary `Loop` path against an
|
|
74
|
+
OpenAI-shaped endpoint, reads `OPENAI_API_KEY`, and accepts an optional job-level
|
|
75
|
+
`baseUrl` (https, or http on loopback only; credentials in the URL are a red). Its one
|
|
76
|
+
admitted model is `deepseek-chat`, which earned the slot with a real paid green — no
|
|
77
|
+
endpoint or model enters the menu without its own clean paid probe. The provider table
|
|
78
|
+
lives in `src/providers.js` (`resolveProvider`/`makeProvider`/`buildRunnerProviders`);
|
|
79
|
+
an unknown provider name THROWS there, it never falls back to a default. Per-model
|
|
80
|
+
request-key gating lives in that table: `deepseek-chat` sets bare-agent's
|
|
81
|
+
`legacyMaxTokens`, because DeepSeek silently ignores `max_completion_tokens` and an
|
|
82
|
+
output cap that does not bind is a money hazard. The JUDGE stays pinned to
|
|
83
|
+
`anthropic-api` and `JUDGE_MODEL` whatever the worker's provider is. The bundle runner
|
|
84
|
+
(`bareloop run`) is `ANTHROPIC_API_KEY`-only and REFUSES at $0, naming the key it would
|
|
85
|
+
have needed, rather than constructing another provider with the wrong key.
|
|
73
86
|
- **Reuse — where a workflow comes from:** a plain `runJob` always drafts cold. Passing
|
|
74
87
|
`bridge` starts from one standalone bridge file (`src/reuse.js`'s envelope, `## The reuse
|
|
75
88
|
ENVELOPE and runReuse` below). The CLI's `--registry <dir>` / library-level `registryDir`
|
|
@@ -85,7 +98,7 @@ inherited rule carries the green that minted it and the contrast that attributed
|
|
|
85
98
|
|
|
86
99
|
```js
|
|
87
100
|
import { runJob, jobSpecHash, makeSpine } from 'bareloop';
|
|
88
|
-
import { AnthropicProvider } from 'bare-agent';
|
|
101
|
+
import { AnthropicProvider } from 'bare-agent/providers';
|
|
89
102
|
|
|
90
103
|
const spec = {
|
|
91
104
|
schema: 'job-v1', job: 'my-maintainer',
|
|
@@ -130,11 +143,12 @@ minting claim, or the shell-owned retry cap — all unknown-field reds.
|
|
|
130
143
|
|---|---|---|
|
|
131
144
|
| `job` | kebab-case slug | |
|
|
132
145
|
| `description` | non-empty string | |
|
|
133
|
-
| `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` |
|
|
146
|
+
| `provider` | `anthropic-api` \| `openai-api` \| `clipipe-subscription` | menu (`PROVIDERS`); part of the lineage key by definition. Only `anthropic-api` is guaranteed (F48); `openai-api` (PRD item 28) goes through the provider factory (`src/providers.js`), reads `OPENAI_API_KEY`, and admits only `deepseek-chat` today — see "Worker surface" above for its `baseUrl` field and per-model gating; `clipipe-subscription` drives the worker natively and needs `opts.nativeProvider` |
|
|
134
147
|
| `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) |
|
|
135
148
|
| `cadence` | `{ unit: hour\|day\|week, every: 1..30 }` | validated now, consumed at N5 (Scheduler) |
|
|
136
149
|
| `budgetUsd` | `0 < n <= shell cap` | ceiling chain: workflow ≤ job ≤ shell — each layer may tighten, never exceed |
|
|
137
|
-
| `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:
|
|
150
|
+
| `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, un-autoset-default number, so a spec with many stages OR a raised/autoset `closeTimeoutMs` can validate while its overshoot dwarfs its cap: `runPlan` adds a RUN-START check (PRD item 27/M3) that refuses `wall-under-close-timeout` at $0 once the EFFECTIVE per-stage ceiling is known (autoset or signed), rather than a silent clamp. Operator-only, tighten-only; adding or changing it changes the spec hash |
|
|
151
|
+
| `closeTimeoutMs` | optional integer ms `>= CLOSE_TIMEOUT_FLOOR_MS` (120,000 — hamr's arbiter floor, 2026-09-07) | PRD item 27/M3's SIGNED override for the close's own per-stage wall-clock cap. Absent (the normal case): the run autosets it from a $0 seed timing pass — `max(FLOOR, K × slowest measured stage)`, `K = 5` (hamr, 2026-09-07: "2 and 5 are fine"). Present: the operator's own number wins outright and may legally sit ABOVE *or below* the autoset estimate (the rates-passthrough shape, F113 — never tighten-only). Arbiter territory exactly like `budgetUsd`/`maxWallMs`/`close[].sha256`: the authoring pipeline's `assembleSpec` refuses a draft carrying it |
|
|
138
152
|
| `model` | optional non-empty string, exact provider model id (e.g. `"claude-sonnet-5"`) | the WORKER's model, build-list #3. Absent means today's runner default; present it wins over a `--model` flag outright, and a flag naming a different id is refused (`resolveWorkerModel`, `src/job.js`) rather than silently overridden. Part of the signed hash like every other field. Worker only — the judge model stays library-pinned (`JUDGE_MODEL`) pending recalibration |
|
|
139
153
|
| `writeScope` | array of contained globs | the operator's outer fence; the plan's own scopes must fit inside it, same containment code |
|
|
140
154
|
| `steps` | RETIRED | operator-authored `steps[]` was deleted (PRD v1.32); a spec carrying it reds `shape-retired:steps` by name rather than half-running |
|
|
@@ -391,7 +405,7 @@ Append-only JSONL event emitter bound to one file. `seq` monotonic per spine, `t
|
|
|
391
405
|
last. Consumers are pure listeners; nothing reads the file back. Returns each event as
|
|
392
406
|
written.
|
|
393
407
|
|
|
394
|
-
### `ralph({ middle, close?, judge?, capRuns?, ladder?, emit, redact?, closeTimeoutMs?, cwd?, expect?, judged?, gapKeep?, workerWrites? })` → `'green' | 'escalated'` — `src/ralph.js`
|
|
408
|
+
### `ralph({ middle, close?, judge?, capRuns?, ladder?, emit, redact?, closeTimeoutMs?, cwd?, expect?, judged?, gapKeep?, workerWrites?, closeDir? })` → `'green' | 'escalated'` — `src/ralph.js`
|
|
395
409
|
|
|
396
410
|
The dumb outer shell: `while close-red and not exhausted: run the middle`. **Exhaustion has
|
|
397
411
|
two alternative rules and you supply exactly one** — `capRuns` (a fixed number of middle runs)
|
|
@@ -422,11 +436,23 @@ set the close's `gapKeep` (job-spec, arbiter territory) to a regex like `"^not o
|
|
|
422
436
|
matching failure lines are preserved in a capped kept-failures block regardless of where
|
|
423
437
|
they print (F28: the first real firing delivered a gap with zero failure names). The gap
|
|
424
438
|
also combines stdout+stderr, so a stdout failure survives stderr noise.
|
|
425
|
-
`closeTimeoutMs` caps the close's wall clock (default 120s
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
439
|
+
`closeTimeoutMs` caps the close's wall clock (default 120s when nothing else supplies a
|
|
440
|
+
value — `runClose`'s own bare default) — shell/operator territory, inexpressible in any
|
|
441
|
+
config. A faulted close is asked to leave with SIGTERM and, if it is still there 2s later,
|
|
442
|
+
ended with SIGKILL: SIGTERM is a request a close can decline (its own handler, a trapping
|
|
443
|
+
wrapper), and without a second deadline the wait never resolves. The verdict is untouched —
|
|
444
|
+
the FIRST fault named the outcome and the kill only enforces it. **As of PRD item 27/M3, a
|
|
445
|
+
direct `ralph`/`runClose`/`runStages` caller still owns this number outright (`closeTimeoutMs
|
|
446
|
+
?? 120_000`); the AUTOSET/override resolution lives one level up, in `runPlan`/`runJob`
|
|
447
|
+
(see `resolveCloseTimeoutMs` below) — `ralph` itself never runs a timing pass.**
|
|
448
|
+
|
|
449
|
+
`closeDir` (PRD item 27/M3 Part B, optional) is the close's own BOOKS DIRECTORY — pristine
|
|
450
|
+
copies, calibration thresholds, logs — deliberately outside the patient tree (the worker
|
|
451
|
+
must never read the arbiter's books, v1.12). Forwarded to every close invocation as the
|
|
452
|
+
env var `BARELOOP_CLOSE_DIR` (set on a COPY of `process.env`, after `CLOSE_ENV_DENY`
|
|
453
|
+
stripping — a `BARELOOP_` name matches none of that denylist's rules and is never stripped).
|
|
454
|
+
A close script that reads this variable and gets none must instrument-stop itself; nothing
|
|
455
|
+
in the runner invents a fallback directory.
|
|
430
456
|
|
|
431
457
|
**The close child's environment is stripped of credential-shaped variables before it runs**
|
|
432
458
|
(`CLOSE_ENV_DENY` in `src/ralph.js`: provider keys, `AWS_*`, any name ending
|
|
@@ -958,13 +984,33 @@ Reserved spine vocabulary (V7, machinery-free until job #1 surfaces one):
|
|
|
958
984
|
`coordination-red` — a failure between units (scope contention, step order, store
|
|
959
985
|
races), never to be folded into worker/interpreter reds.
|
|
960
986
|
|
|
961
|
-
### `runJob(spec, { approvals, workdir, provider, nativeProvider?, providerFor?, emit, capRuns?, strikeLimit?, shellCapUsd?, closeTimeoutMs?, layerRoot?, readShim?, scout?, bridge?, priorSpentUsd?, priorSpendComplete?, priorWallMs?, resumeSeed?, resumeGrades?, resumeReplans?, resumeBranch?, humanRuling?, heldRuling?, reviewDoor?, doorRerun? })` → outcome — `src/run.js`
|
|
987
|
+
### `runJob(spec, { approvals, workdir, provider, nativeProvider?, providerFor?, emit, capRuns?, strikeLimit?, shellCapUsd?, closeTimeoutMs?, closeDir?, layerRoot?, readShim?, scout?, bridge?, priorSpentUsd?, priorSpendComplete?, priorWallMs?, resumeSeed?, resumeGrades?, resumeReplans?, resumeBranch?, humanRuling?, heldRuling?, reviewDoor?, doorRerun?, resumable? })` → outcome — `src/run.js`
|
|
988
|
+
|
|
989
|
+
**`closeTimeoutMs` (PRD item 27/M3) is now OPTIONAL for every real caller.** Omit it (both
|
|
990
|
+
`src/cli.js`'s bundle runner and `scripts/run-u.mjs` do) and the run AUTOSETS the per-stage
|
|
991
|
+
close ceiling from a $0 seed timing pass, or reads the job spec's own signed override —
|
|
992
|
+
"guesstimate + loud sign + customer override", the same shape rates-passthrough already
|
|
993
|
+
uses (F113). Passing an explicit value here still wins outright over both (kept for
|
|
994
|
+
backward compatibility and test control) — see `resolveCloseTimeoutMs` further down.
|
|
995
|
+
|
|
996
|
+
**`closeDir` (PRD item 27/M3 Part B, optional but effectively required for any
|
|
997
|
+
hand-authored close that reads its own books)** — the close's own directory (pristine
|
|
998
|
+
copies, calibration thresholds, logs), outside the patient tree. `src/cli.js` sets it to
|
|
999
|
+
`<bundleDir>/runs/<runid>/close/`; `scripts/run-u.mjs` sets it to its existing `spineDir`. A
|
|
1000
|
+
close script whose source mentions `BARELOOP_CLOSE_DIR` and gets none here refuses the run
|
|
1001
|
+
at $0 (`close-dir-required`, before any provider call) — see `checkCloseDirRequired` below.
|
|
962
1002
|
|
|
963
1003
|
The last seven are the RESUME fold and are documented under *Resuming a killed run* below; they
|
|
964
1004
|
default to `0` / `true` / `0` / `null` / `[]` / `null` / `null`, so a fresh run passes none of them.
|
|
965
1005
|
Three of them are folds of a bound the operator SIGNED (money, wall, replans) and one is a
|
|
966
1006
|
readout seed (grades) — the distinction matters and is spelled out there.
|
|
967
1007
|
|
|
1008
|
+
`resumable` (default `true`, PRD item 27(c)/F130) says whether THIS runner supports
|
|
1009
|
+
`--resume` at all — `run-u.mjs` leaves it at the default (byte-identical to before this
|
|
1010
|
+
flag existed); the exported bundle CLI (`src/cli.js`) passes `false` so the run's
|
|
1011
|
+
escalation tail says "resume is `run-u`-only in v1" instead of naming a flag it does not
|
|
1012
|
+
implement.
|
|
1013
|
+
|
|
968
1014
|
The runner — the shell's top layer, and the ONE entry. It composes everything below it and
|
|
969
1015
|
interprets nothing itself. Sequence: **approval gate** (human-signs-always — refuses an
|
|
970
1016
|
unapproved spec before ANY provider call: `unapproved-spec`) → **primitive smoke** (litectx
|
|
@@ -975,7 +1021,18 @@ known-answer round-trip before tokens: `smoke-red` — a silent degradation thro
|
|
|
975
1021
|
Outcomes: `green | already-green | escalated | unapproved-spec | job-red | smoke-red |
|
|
976
1022
|
plan-red | check-red | close-red | close-unsupported | recipe-stale | branch-red | pricing-red |
|
|
977
1023
|
provider-red | interpreter-red | cap-halt | wall-halt | step-stalled | hitl-pause |
|
|
978
|
-
hitl-decision-red | step-red:<id
|
|
1024
|
+
hitl-decision-red | step-red:<id> | runner-drained`.
|
|
1025
|
+
|
|
1026
|
+
**`runner-drained` (F140/PRD item 28(c))** is a distinct class from every outcome above: it is
|
|
1027
|
+
NOT `runJob`'s own return value, it is a `beforeExit` backstop's terminal, emitted from outside
|
|
1028
|
+
the normal control flow the instant node drains with no `job-end` at all — a provider promise
|
|
1029
|
+
that never settled (BA-25's class; bare-agent 0.42.0 closed the known drop cases per F141, but
|
|
1030
|
+
"an instrument fires on absence" stays a permanent class, not a fixed bug). It means: exits
|
|
1031
|
+
non-zero, `spendComplete:false` always (a drained run's in-flight spend is unknowable — the
|
|
1032
|
+
floor already banked is honest, an exact-looking total would not be, F6). **It is deliberately
|
|
1033
|
+
NOT in the resumable/checkpoint set** (`CHECKPOINT_OUTCOMES`/`RESUMABLE_HALTS`, `src/reuse.js`)
|
|
1034
|
+
— an unknown in-flight state, unlike a clean `cap-halt`/`wall-halt`/`provider-red`, is not a
|
|
1035
|
+
known-safe resume point.
|
|
979
1036
|
|
|
980
1037
|
**`readShim` (default `false`) — the capped read seam, per ARM.** The value names which levers
|
|
981
1038
|
run — the four arms of the frozen Phase 2 pre-registration:
|
|
@@ -1158,17 +1215,36 @@ round of this attempt being priced repairs nothing about the one before it); and
|
|
|
1158
1215
|
reporting with the rest end of gate"). Every worker Loop (scout, drafter, each step, the fix
|
|
1159
1216
|
worker — the ONE seam in `planrun.js`) gets exactly ONE extra attempt, and only for a
|
|
1160
1217
|
transport-class throw — fetch itself throwing with no HTTP response at all (a TLS fault,
|
|
1161
|
-
`ECONNRESET`/`EPIPE`/`ETIMEDOUT`, `fetch failed` wrapping a network cause)
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1218
|
+
`ECONNRESET`/`EPIPE`/`ETIMEDOUT`, `fetch failed` wrapping a network cause), OR (bare-agent
|
|
1219
|
+
0.42.0, BA-25) a response body cut after headers but before it completed — the provider's own
|
|
1220
|
+
`guardResponseSettles` rejects that as a retryable `ProviderError` tagged
|
|
1221
|
+
`context.bound:'transport'`, which `isTransportFailure` also recognizes (F141). An HTTP response
|
|
1222
|
+
(4xx/5xx generally) is never retried here and rides bare-agent's own unchanged policy. The budget
|
|
1223
|
+
is fixed (`TRANSPORT_MAX_ATTEMPTS` in `src/transport.js`) — never a job-spec or CLI knob,
|
|
1224
|
+
tighten-only doctrine. Each retry emits a report-only `transport-retry` spine record (`{phase,
|
|
1225
|
+
attempt, error, recovered}`, no cost field — the ledger is unaffected); the FIRST attempt threw
|
|
1226
|
+
before any usage figure came back, so **any** transport retry floors `spendComplete` for the rest
|
|
1227
|
+
of the run even when the retry recovers and the run finishes green — the same one-way flag class
|
|
1228
|
+
as `stalled` and `cutMidCall`. A wall stop read BETWEEN iterations or steps has no call in flight,
|
|
1169
1229
|
so it stays exact. Both fields are present on all outcomes, so a consumer never branches on
|
|
1170
1230
|
field presence and never has to launder a missing `spentUsd` into `$0`.
|
|
1171
1231
|
|
|
1232
|
+
A **rate-limit retry** (PRD item 28's parked a/b ruling, hamr verbatim: "do a/b as a fallback, and
|
|
1233
|
+
verify/validate + no regression"; F143/`src/ratelimit.js`) rides the SAME seam as a second,
|
|
1234
|
+
INDEPENDENT one-shot budget: an HTTP 429 gets ONE retry that honours the vendor's own stated
|
|
1235
|
+
wait, capped at 60s (`RATE_LIMIT_MAX_WAIT_MS` — a longer stated wait is not honoured at all, no
|
|
1236
|
+
retry). This is a FALLBACK, not the primary answer — the primary answer (F139) is picking a
|
|
1237
|
+
worker model with adequate rate headroom; this only softens the failure mode when that was not
|
|
1238
|
+
enough. The wall always wins here too: before sleeping, a retry that would cost more than
|
|
1239
|
+
`clock.remainingMs()` is refused and the error rides straight through, exactly like the transport
|
|
1240
|
+
ladder's own wall carve-out. Each retry emits a report-only `rate-limit-retry` spine record
|
|
1241
|
+
(`{phase, attempt, statedMs, waitedMs, error, recovered}`, no cost field). Unlike the transport
|
|
1242
|
+
retry, a rate-limit retry does **not** floor `spendComplete`: a 429 is a REFUSAL — the vendor
|
|
1243
|
+
rejected the request before processing it, so nothing can have been billed (a stated assumption,
|
|
1244
|
+
not independently measured against a real provider). The two budgets are independent — a
|
|
1245
|
+
transport throw and a 429 may both occur, and both retry, across one call's lifetime, but each at
|
|
1246
|
+
most once.
|
|
1247
|
+
|
|
1172
1248
|
**The plan flow (Layer 2).** `job-start` carries `shape: 'plan'` + the goal; plan steps are
|
|
1173
1249
|
tool-mode by construction. The flow (`runPlan`, also exported for direct callers who own
|
|
1174
1250
|
their own ledger): **close precheck** (`already-green` is a
|
|
@@ -1601,7 +1677,7 @@ package makes). Which runs open one is `doorOpens(job, reviewDoor)`:
|
|
|
1601
1677
|
territory.
|
|
1602
1678
|
|
|
1603
1679
|
**Answering one — `answerReviewDoor({ job, workdir, events, decision, text?, closeTimeoutMs?,
|
|
1604
|
-
registryDir?, name?, runid?, at?, now?, emit? })`** (`src/reviewdoor.js`). The answer arrives
|
|
1680
|
+
closeDir?, registryDir?, name?, runid?, at?, now?, emit? })`** (`src/reviewdoor.js`). The answer arrives
|
|
1605
1681
|
after the run has ended, possibly days later and from another process, so it cannot ride the
|
|
1606
1682
|
run's return path. This is that seam. It never throws and never conjures — a fourth door, an
|
|
1607
1683
|
empty `rerun`, a run that opened no door (`no-door`), an expired one (`door-expired`), a tree
|
|
@@ -1612,7 +1688,11 @@ that moved (`door-accept-red`) and a missing registry all come back as named red
|
|
|
1612
1688
|
a person) before it is honoured: hamr's ruling that an accept is not a rubber stamp, and the
|
|
1613
1689
|
answer to a tree that can move in the 60 days a door keeps. A red REFUSES the accept with the
|
|
1614
1690
|
stage named and records **nothing**. A pass records the disposition and, over a held judged
|
|
1615
|
-
green, RELEASES the credit through module 6's `applyDoorDecision`.
|
|
1691
|
+
green, RELEASES the credit through module 6's `applyDoorDecision`. `scripts/run-u.mjs` resolves
|
|
1692
|
+
the re-run's ceiling itself via `resolveCloseTimeoutMs` before calling this, and refuses outright
|
|
1693
|
+
(F137) as a named `close-timing-red` door stop — `answerReviewDoor` never called, nothing
|
|
1694
|
+
recorded — if that timing pass itself times out, rather than falling through to `runClose`'s
|
|
1695
|
+
library default.
|
|
1616
1696
|
- **`rerun`** carries the person's words back as a directive (`next: 'rerun'`) and re-proves
|
|
1617
1697
|
nothing: the new run's close is what judges. Empty or whitespace text is refused at the same
|
|
1618
1698
|
seam every other door is.
|
|
@@ -2481,7 +2561,9 @@ nothing here to check it against).
|
|
|
2481
2561
|
|
|
2482
2562
|
Enforcement is **local only** — wired into `npm test` (see `package.json`'s `test`
|
|
2483
2563
|
script), never into `.github/workflows/*` (CI already runs `npm test`, so the rule is
|
|
2484
|
-
enforced there without an ask-first CI edit).
|
|
2564
|
+
enforced there without an ask-first CI edit). `npm test` itself now runs `node --test`
|
|
2565
|
+
through `scripts/hermetic.mjs`, which strips this machine's `HOME`/git identity so a
|
|
2566
|
+
test that leans on either fails locally instead of only on CI (F136). The check itself lives outside the
|
|
2485
2567
|
published package, at `scripts/prompt-commit-check.mjs` (pure decision logic in
|
|
2486
2568
|
`scripts/promptcommitlib.mjs`, `scripts/` is not in `package.json`'s `files`, so none of
|
|
2487
2569
|
this ships):
|
|
@@ -2546,11 +2628,77 @@ written by `bareloop run`, step 6 below) is real and live but is not in the froz
|
|
|
2546
2628
|
layout table at all. `history.jsonl`'s row also carries `bundleHash` and `approveHash`
|
|
2547
2629
|
(the POC-fact correction, below) beyond the fields the original layout table named.
|
|
2548
2630
|
|
|
2631
|
+
#### `src/close-integrity.js` — close-absolute-path (M1) AND close-bytes signature (M2)
|
|
2632
|
+
|
|
2633
|
+
Shared by `src/bundle.js` (export time) and a $0 run-start precheck inside `runPlan`
|
|
2634
|
+
(`src/planrun.js`) — every job, not only exported bundles, refuses at $0 (before the
|
|
2635
|
+
close-first precheck, before any provider call) when a close script's CONTENT bakes in an
|
|
2636
|
+
absolute path that exists on disk. Two spellings of the same rule is exactly how F129
|
|
2637
|
+
shipped an export-only guard while every non-exported job stayed exposed.
|
|
2638
|
+
|
|
2639
|
+
**Scope (widened 2026-09-06, PRD item 27/M2, an orchestrator audit):** a close script is
|
|
2640
|
+
addressable — visible to `readCloseScripts`, `checkCloseAbsolutePaths`, the sha256
|
|
2641
|
+
fingerprint, and `signCloseScripts` — when its `cmd` is EITHER an interpreter
|
|
2642
|
+
(`node`/`sh`/`bash`/`python`/`python3`/`npx`) followed by a non-empty argv[1], OR a bare
|
|
2643
|
+
cmd whose argv[0] ITSELF is an absolute path (a `.sh`/other directly-executable close
|
|
2644
|
+
script, no interpreter prefix — e.g. `jobs/aurora-testgen-cold.json`'s close). A relative
|
|
2645
|
+
bare executable (`true`, `pytest`, `npm test`) names no file at all and stays out of
|
|
2646
|
+
scope, on both halves, exactly as before. The shape test itself
|
|
2647
|
+
(`closeScriptCandidateToken`, `src/validate.js`) is shared with `src/job.js`'s sha256
|
|
2648
|
+
demand so the demand and the detectors can never drift apart about which stages are in
|
|
2649
|
+
scope.
|
|
2650
|
+
|
|
2651
|
+
| function | args → returns | notes |
|
|
2652
|
+
|---|---|---|
|
|
2653
|
+
| `absolutePathLiteralsOf(source)` | close script text → `string[]` | the scan itself (moved here from `src/bundle.js`, which now imports it) |
|
|
2654
|
+
| `readCloseScripts(spec, cwd)` | resolved job spec + cwd → `{ stage, path, bytes, sha256 }[]` | resolves every addressable `close[].cmd` (see scope above) to its absolute path and bytes off disk; a relative path resolves against `cwd`. Never throws — a missing/unreadable script reports `bytes: null`. `sha256` (M2, additive) is the stage's own signed field verbatim, or `null` |
|
|
2655
|
+
| `checkCloseAbsolutePaths(spec, cwd)` | resolved job spec + cwd → `{ ok: true } \| { ok: false, reds: { stage, path, literal }[] }` | the run-start check `runPlan` calls before the close-first precheck |
|
|
2656
|
+
| `hashCloseScriptBytes(text)` | script text → sha256 hex | the ONE formula every M2 consumer uses (minting, the run-start check, the runtime re-verify, the export mismatch check) — hashing the utf8-decoded text agrees with hashing the raw file bytes because a close script is always valid UTF-8 |
|
|
2657
|
+
| `checkCloseByteSignature(spec, cwd)` | resolved job spec + cwd → `{ ok: true } \| { ok: false, reds: { stage, path, expected, actual }[] }` | the $0 run-start integrity check (PRD item 27): every signed `close[].sha256` must match the script's bytes off disk RIGHT NOW. `runPlan` calls this alongside `checkCloseAbsolutePaths`, before the close-first precheck and before any provider call — a mismatch reds `close-tampered`. An unreadable signed script reads `actual: null` and is STILL a red (never silently "fine") |
|
|
2658
|
+
| `checkStageByteSignature(stages, cwd)` | a close stage chain + cwd → same shape as above | the narrower re-verify `runPlan`'s `runCloseStages` wrapper runs before EVERY close execution (precheck, preflight, check-passes, and the close-fix loop all go through this one seam) — a mismatch short-circuits to a `close-tampered` verdict in the same shape `runClose`/`runStages` render, so it rides out through the existing `CLOSE_FAULTS` machinery exactly like `crashed`/`timed-out`/`killed` |
|
|
2659
|
+
| `signCloseScripts(spec, cwd)` | resolved job spec + cwd → `{ ok, spec, changes: {stage,path,old,new}[], missing: {stage,path}[] }` | the PURE minting helper: fills `close[].sha256` from disk for every addressable stage. Never writes anything itself — `scripts/sign-close.mjs` is the thin CLI wrapper that gates on `--write`. A `closeDecl` spec (no script path to sign) returns `ok:false` unchanged |
|
|
2660
|
+
| `checkCloseDirRequired(spec, cwd, closeDir)` | resolved job spec + cwd + the runner's `closeDir` (or `null`) → `{ ok: true } \| { ok: false, reds: { stage, path }[] }` | PRD item 27/M3 Part B: scans the same addressable-script bytes `readCloseScripts` reads for the literal substring `BARELOOP_CLOSE_DIR` — a script that mentions it needs a real `closeDir` or it would run against `undefined`. A script that never mentions it carries no demand |
|
|
2661
|
+
| `CLOSE_DIR_ENV_VAR` | `'BARELOOP_CLOSE_DIR'` | the ONE spelling `checkCloseDirRequired`'s scan and `runClose`'s env-set (`src/ralph.js`) both use |
|
|
2662
|
+
|
|
2663
|
+
#### `src/closetimeout.js` — close timeout: autoset + signed override (M3)
|
|
2664
|
+
|
|
2665
|
+
hamr's ruling (2026-09-06): *"both … autoset and can be override, same like api pricing"*
|
|
2666
|
+
— the guesstimate-plus-loud-sign-plus-customer-override shape (F113), never a hidden
|
|
2667
|
+
defaulted knob. Arbiter constants, hamr 2026-09-07 verbatim: **floor = 120,000 ms; K = 5
|
|
2668
|
+
("2 and 5 are fine")** — never agent- or adopter-settable.
|
|
2669
|
+
|
|
2670
|
+
| export | shape | notes |
|
|
2671
|
+
|---|---|---|
|
|
2672
|
+
| `CLOSE_TIMEOUT_FLOOR_MS` | `120_000` | the floor, hamr's arbiter constant |
|
|
2673
|
+
| `CLOSE_TIMEOUT_K` | `5` | the multiplier, hamr's arbiter constant |
|
|
2674
|
+
| `LIBRARY_DEFAULT_CLOSE_TIMEOUT_MS` | `120_000` | `runClose`'s own un-autoset default (`src/ralph.js`'s `timeoutMs = 120_000`) — named separately from the floor above even though the two numbers agree today, because they are different claims |
|
|
2675
|
+
| `TIMING_PREFLIGHT_CEILING_MS` | `600_000` (library default × K) | the GENEROUS provisional ceiling every stage runs under DURING the timing pass itself — never the autoset/override ceiling the run then uses, which is only known after the pass completes |
|
|
2676
|
+
| `timeCloseStages(stages, {cwd, redact})` | → `{ perStage: {name,ms,exitCode,timedOut}[], slowestMs, slowestName, anyTimedOut }` | runs EVERY stage once, ignoring verdicts (first-red-wins stays for the real verdict precheck) — a declared/kind stage (no `.cmd`) is skipped, contributing no reading rather than a false zero. A stage that never finishes within `TIMING_PREFLIGHT_CEILING_MS` reads `timedOut: true` |
|
|
2677
|
+
| `computeCloseTimeoutCeiling({slowestMs, overrideMs?})` | → `{ ceilingMs, source: 'estimated'\|'override' }` | `max(FLOOR, K × slowestMs)`, unless `overrideMs` is given, in which case it wins outright (may sit above OR below the estimate — not tighten-only) |
|
|
2678
|
+
| `resolveCloseTimeoutMs({job, stages, cwd, redact})` | → `{ closeTimeoutMs, source, timing, timedOut }` | the ONE resolver `src/planrun.js` and `scripts/run-u.mjs` BOTH call (never two independent spellings of this precedence): a signed `job.closeTimeoutMs` wins with NO timing pass run at all; absent, the pass runs and the ceiling autosets. `timedOut: true` means a stage never finished the pass — the caller reds `close-timing-red` rather than trusting any number |
|
|
2679
|
+
| `closeTimeoutBanner({ceilingMs, source, slowestMs?, slowestName?})` | → the printed line | ONE spelling for `estimated`/`override`/`explicit` (the last is the pre-M3 shell/test runtime-option escape hatch, never the signed field) — printed on every run so a silent default can never hide behind this feature the way the OLD 120s/900s defaults did |
|
|
2680
|
+
|
|
2681
|
+
**`scripts/sign-close.mjs`** — `node scripts/sign-close.mjs <jobs/x.json> [--write]` or
|
|
2682
|
+
`--all` for every `jobs/*.json`. Prints the old→new `jobSpecHash` and each stage's
|
|
2683
|
+
old→new sha256; NEVER writes unless `--write` is passed (a dry run is the default). The
|
|
2684
|
+
agent never runs this with `--write` on its own authority — minting a close-bytes
|
|
2685
|
+
signature is arbiter territory exactly like a budget or a wall clock, the same as the
|
|
2686
|
+
authoring pipeline's own refusal (`src/authorjob.js`'s `assembleSpec`, which refuses a
|
|
2687
|
+
draft carrying `sha256` the same way it refuses `close`/`closeDecl`/`verdictType`).
|
|
2688
|
+
|
|
2689
|
+
**`close[].sha256`** — a signed spec field (PRD item 27/M2): hex sha256 of the bytes of
|
|
2690
|
+
the script file the stage's `cmd` names (only for an addressable cmd, per the scope
|
|
2691
|
+
above). `validateJob` (`src/job.js`) demands it as `missing-required` on every addressable
|
|
2692
|
+
stage and shape-checks it (`invalid-value`, 64 lowercase hex chars) even where not
|
|
2693
|
+
demanded. It sits INSIDE the signed spec, so `jobSpecHash` covers it with no
|
|
2694
|
+
hash-function change — re-signing a job (minting or changing this field) moves the spec
|
|
2695
|
+
hash and needs re-approval, exactly like any other semantic edit.
|
|
2696
|
+
|
|
2549
2697
|
#### `src/bundle.js` — pure, no provider/process calls
|
|
2550
2698
|
|
|
2551
2699
|
| function | args → returns | notes |
|
|
2552
2700
|
|---|---|---|
|
|
2553
|
-
| `exportBundle(o)` | `{ spec, closeScripts, registryDir, outDir, bareloopVersion }` → `{ ok, reds, dir, bundleHash, manifest }` | mints the directory above. Every check runs before anything touches disk — a red means nothing was written |
|
|
2701
|
+
| `exportBundle(o)` | `{ spec, closeScripts, registryDir, outDir, bareloopVersion }` → `{ ok, reds, dir, bundleHash, manifest }` | mints the directory above. Every check runs before anything touches disk — a red means nothing was written. **F132:** a stage carrying a signed `close[].sha256` gets it RE-SIGNED in the written `spec.json` over the RELOCATED bytes (post import-rewrite) it actually packs — not copied verbatim from the source spec. Without this, a script whose import gets rewritten (`../src/x.js` → `'bareloop'`) ships with a signature that covers the wrong bytes, and the bundle refuses itself as `close-tampered` at $0 on its very first run (the manifest hash and the spec signature disagree over two different byte strings for the same file) |
|
|
2554
2702
|
| `bundleHash(dir)` | `dir` → sha256 hex | recomputes from what is on disk right now; throws only if `dir` itself is not a directory |
|
|
2555
2703
|
| `readBundle(dir)` | `dir` → `{ ok, reds, spec, manifest, bridges, blessing, history }` | never throws; every failure is a typed red. Runs the tamper check (below) whenever both a manifest and a spec parsed |
|
|
2556
2704
|
| `resolveBundleSpec(bundle, bundleDir)` | a `readBundle` result + the bundle dir → `{ spec, approveHash }` | the in-memory `$BARELOOP_BUNDLE` → absolute `bundleDir` substitution; `approveHash` is `jobSpecHash` of the *substituted* spec, never the unresolved one |
|
|
@@ -2572,7 +2720,12 @@ layout table at all. `history.jsonl`'s row also carries `bundleHash` and `approv
|
|
|
2572
2720
|
| `close-script-collision` | `exportBundle` | two different close script paths share one basename — a flat `close/` directory would silently clobber one |
|
|
2573
2721
|
| `close-import-unparsed` | `exportBundle` | a close script's import is a shape this module cannot verify at all — default import, `* as ns`, mixed default+named/namespace, a bare side-effect import, a dynamic `import()`, or a relative import that does not point into `src/` (a sibling file never ships in `close/`). Fails safe rather than silently accepting what it cannot read |
|
|
2574
2722
|
| `close-import-unexported` | `exportBundle` | a close script imports a name from bareloop's own `src` that `src/index.js` does not export |
|
|
2575
|
-
| `close-absolute-path` | `exportBundle` (F129) | a close script's source bakes in a quoted string literal that is an absolute POSIX path which `existsSync` finds real on
|
|
2723
|
+
| `close-absolute-path` | `exportBundle` (F129) AND `runPlan` (PRD item 27, every job) | a close script's source bakes in a quoted string literal that is an absolute POSIX path which `existsSync` finds real on this machine and which is not under an allow-listed system prefix (`/usr/`, `/bin/`, `/sbin/`, `/lib/`, `/lib64/`, `/dev/`, `/etc/`, `/proc/`, `/sys/`, `/opt/`). **A close judges `process.cwd()` — the runner's cwd — never a path baked into the script (F8/F129):** the live defect this catches is a hardcoded `WORKDIR` making a close judge the ORIGINAL patient checkout instead of the fresh worktree it was actually pointed at, minting a fake `already-green` at $0. A nonexistent absolute-looking literal is a NAME, not a proven hazard, and is not flagged — the check is deliberately monotone and simple, not clever (see `absolutePathLiteralsOf`'s JSDoc, `src/close-integrity.js`, for its named limits: string concatenation and cross-machine-only paths are invisible to it). `runPlan` reds this at run start, before the close-first precheck and before any provider call — $0, nothing spent |
|
|
2724
|
+
| `close-sha-mismatch` | `exportBundle` (PRD item 27/M2) | a `close[].sha256` the spec carries does not match the bytes of the script actually being packed into the bundle — the two signatures (this one, and `bundleHash`'s manifest hash over the rewritten script) cover the same bytes by two different paths and must never drift apart silently (N4's own hazard, one layer up). Only fires when a value is PRESENT and wrong; a spec with no sha256 at all reds `missing-required` upstream, at `validateJob` |
|
|
2725
|
+
| `close-tampered` | `runPlan` (PRD item 27/M2) | a close script's bytes no longer match its signed `sha256` — checked at run start (alongside `close-absolute-path`, before the close-first precheck and before any provider call) AND before EVERY close run thereafter (`runCloseStages`'s wrapper in `src/planrun.js`, the one seam the precheck/preflight/check-passes/close-fix-loop all share). Distinct from `close-red` (a judged "no" from a working close) and from `close-crashed`/`close-timeout`/`close-killed` (the close ran and hit an instrument fault): this is decided BEFORE the close is even spawned, and it means the close that ran would not have been the one the operator approved. Registered in `CLOSE_FAULTS` (`src/ralph.js`) so it rides out through the same forbidden-zone machinery as every other close fault — never retried, never fed back as a gap |
|
|
2726
|
+
| `close-dir-required` | `runPlan` (PRD item 27/M3 Part B) | a close script's source mentions `BARELOOP_CLOSE_DIR` (it reads its own books directory) but the runner was given no `closeDir` at all — `checkCloseDirRequired` (`src/close-integrity.js`) reuses the same `readCloseScripts` reader the other two integrity checks share. $0, run start, before the close-first precheck and before any provider call |
|
|
2727
|
+
| `close-timing-red` | `runPlan` (PRD item 27/M3) | the $0 timing preflight (`resolveCloseTimeoutMs`/`timeCloseStages`, `src/closetimeout.js`) ran every close stage once, ignoring verdicts, and at least one never finished within the provisional ceiling (`TIMING_PREFLIGHT_CEILING_MS`, the library default × K = 600s) — the close cannot run on this machine in a boundable time. Only reachable when NEITHER a signed `job.closeTimeoutMs` NOR a caller-passed runtime override is present (both skip the pass entirely). `scripts/run-u.mjs`'s review door hits the SAME red on its own `accept` timing pass (F137) — it refuses the door outright rather than calling `answerReviewDoor` at all, so nothing is recorded |
|
|
2728
|
+
| `wall-under-close-timeout` | `runPlan` (PRD item 27/M3) | `job.maxWallMs` is set and is under the EFFECTIVE per-stage close timeout just resolved (autoset or signed) — a budget under one close cannot fund its own close. `validateJob`'s own `MIN_WALL_MS` floor only bounds the un-autoset library default; this run-start check is the honest shape once the real ceiling is known, rather than a silent clamp |
|
|
2576
2729
|
| `bundle-missing` | `readBundle` | `dir` is not a directory at all |
|
|
2577
2730
|
| `manifest-invalid` / `spec-invalid` | `readBundle` | `manifest.json`/`spec.json` could not be read or parsed |
|
|
2578
2731
|
| `bundle-tampered` | `readBundle` | manifest and spec both parsed, but the recomputed `bundleHash` (from what's really on disk) does not match the stored one — **the load-bearing check, N4 below** |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bareloop",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Workflows that earn their own design, with receipts — agent-authored workflows for repeated, long, verifiable jobs that improve across runs under an un-gameable gate.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -20,13 +20,13 @@
|
|
|
20
20
|
"node": ">=20"
|
|
21
21
|
},
|
|
22
22
|
"scripts": {
|
|
23
|
-
"test": "node
|
|
23
|
+
"test": "node scripts/hermetic.mjs && node scripts/prompt-commit-check.mjs --range origin/main..HEAD",
|
|
24
24
|
"typecheck": "tsc --noEmit",
|
|
25
25
|
"build:types": "tsc",
|
|
26
26
|
"prepublishOnly": "npm run build:types"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"bare-agent": "^0.
|
|
29
|
+
"bare-agent": "^0.42.0",
|
|
30
30
|
"bareguard": "^0.13.0",
|
|
31
31
|
"litectx": "^0.32.0"
|
|
32
32
|
},
|
package/src/authorflow.js
CHANGED
|
@@ -82,7 +82,7 @@ import {
|
|
|
82
82
|
GENRE_LANGUAGES, TYPES_GENRE_TEMPLATE, classGuards, genreEnv, genreOwnedEnvNames, genreInstruments,
|
|
83
83
|
validateDeclaration, envCapableKind, classMenu, VERDICT_CLASSES, LIVE_CLASSES,
|
|
84
84
|
} from './authoring.js';
|
|
85
|
-
import { buildSeedListing, cleanEntry, SURVEY_CAUSES } from './authorscout.js';
|
|
85
|
+
import { buildSeedListing, cleanEntry, SURVEY_CAUSES, AUTHOR_CALL_TIMEOUT_MS } from './authorscout.js';
|
|
86
86
|
import { extractArtifact, priceOf, scrubRaw, tallyCalls, capStop } from './text.js';
|
|
87
87
|
import { redactSecrets } from './validate.js';
|
|
88
88
|
|
|
@@ -1147,7 +1147,12 @@ export function makeLoopGenerate(provider, { system = AUTHOR_SYSTEM, maxTokens =
|
|
|
1147
1147
|
...t,
|
|
1148
1148
|
execute: async (/** @type {any} */ a) => { const r = await t.execute(a); loop.stop(); return r; },
|
|
1149
1149
|
}));
|
|
1150
|
-
|
|
1150
|
+
// F152/PRD 30.4 — the SAME idle bound `authorscout.js` reuses (this pipeline
|
|
1151
|
+
// has no wall clock of its own): a live endpoint that accepts and never
|
|
1152
|
+
// answers hangs the process forever otherwise. `...opts` still spreads AFTER
|
|
1153
|
+
// it, so a caller that ever needs a tighter number for one call may pass it —
|
|
1154
|
+
// tighten-only, and nothing does today.
|
|
1155
|
+
return loop.run(messages, wired, { cacheMessages: true, maxTokens, timeoutMs: AUTHOR_CALL_TIMEOUT_MS, ...opts });
|
|
1151
1156
|
};
|
|
1152
1157
|
}
|
|
1153
1158
|
|
package/src/authorjob.js
CHANGED
|
@@ -654,7 +654,21 @@ export async function authorCloseForJob({
|
|
|
654
654
|
/** The fields `assembleSpec` writes, and therefore the fields a DRAFT must not
|
|
655
655
|
* carry. Named as data so the refusal and the fold can never disagree about
|
|
656
656
|
* which half of the spec is which. */
|
|
657
|
-
|
|
657
|
+
// `sha256` (PRD item 27/M2) rides alongside `close`/`closeDecl`/`verdictType`:
|
|
658
|
+
// it is arbiter territory exactly like a budget or a wall clock — the agent
|
|
659
|
+
// never mints a close-bytes signature (only `signCloseScripts`/hamr's own
|
|
660
|
+
// re-sign does), so a draft carrying the field at all is refused the same
|
|
661
|
+
// way one carrying a raw `close` array is. A `closeDecl` spec has no script
|
|
662
|
+
// path to sign in the first place (M1's own kind executor, never a shipped
|
|
663
|
+
// script), so this can only ever fire on a smuggled/legacy-shaped draft —
|
|
664
|
+
// named here anyway, because "arbiter fields are refused by name" is the
|
|
665
|
+
// rule, not "arbiter fields that are currently reachable are refused".
|
|
666
|
+
// `closeTimeoutMs` (PRD item 27/M3) joins the same list for the same reason:
|
|
667
|
+
// it is arbiter territory exactly like the timeout floor/multiplier
|
|
668
|
+
// themselves — the agent never mints a close-timeout override (only the
|
|
669
|
+
// operator's own signed spec edit does), so a draft carrying it is refused
|
|
670
|
+
// the same way one carrying a raw `close` array is.
|
|
671
|
+
export const AUTHORED_SPEC_FIELDS = Object.freeze(['close', 'closeDecl', 'verdictType', 'sha256', 'closeTimeoutMs']);
|
|
658
672
|
|
|
659
673
|
/**
|
|
660
674
|
* Fold an authored close into the OPERATOR's own half of the spec.
|
package/src/authorscout.js
CHANGED
|
@@ -55,11 +55,37 @@ import { TOOL_MENU, WRITE_VERBS, STORE_VERBS } from './job.js';
|
|
|
55
55
|
import { TOOL_BY_VERB, CTX_TOOLS, createCtxTools, toolAction, strategyFor } from './tools.js';
|
|
56
56
|
import { redactSecrets, SECRET_PATTERNS } from './validate.js';
|
|
57
57
|
import { extractArtifact, priceOf, scrubRaw, stampRaw, tallyCalls, capStop } from './text.js';
|
|
58
|
+
import { PROVIDER_TIMEOUT_MS } from './clock.js';
|
|
58
59
|
|
|
59
60
|
const require = createRequire(import.meta.url);
|
|
60
61
|
const { Loop, wireGate } = require('bare-agent');
|
|
61
62
|
const { createShellTools } = require('bare-agent/tools');
|
|
62
63
|
|
|
64
|
+
/**
|
|
65
|
+
* F152/PRD 30.4 — the scout's per-call IDLE bound. Every `loop.run()` in this
|
|
66
|
+
* module carried NO time bound at all: a live endpoint that accepts and never
|
|
67
|
+
* answers hangs the process forever (measured) — an open socket is an active
|
|
68
|
+
* handle, so node never drains and no backstop can fire.
|
|
69
|
+
*
|
|
70
|
+
* UNLIKE the run path (`callBounds()`, src/planrun.js:1518), this pipeline has
|
|
71
|
+
* NO wall clock to derive a bound FROM — the authoring pipeline is genuinely
|
|
72
|
+
* clockless (`src/authorjob.js`/`src/authorflow.js`/`scripts/run-author.mjs`
|
|
73
|
+
* carry a money ceiling, `ceilingUsd`/`capStop`, and never a `maxWallMs`).
|
|
74
|
+
* Rather than inventing a fresh number, this reuses the EXACT ceiling the run
|
|
75
|
+
* path itself falls back to when ITS OWN wall is unbounded
|
|
76
|
+
* (`PROVIDER_TIMEOUT_MS`, `src/clock.js` — BA-18's idle timeout, already an
|
|
77
|
+
* approved "no wall present" default one layer up): an authoring-time call is
|
|
78
|
+
* the identical situation, one call earlier.
|
|
79
|
+
*
|
|
80
|
+
* It bounds SOCKET INACTIVITY only (reset by every byte, per BA-18) — a call
|
|
81
|
+
* that is merely slow keeps streaming and is untouched; only a truly silent
|
|
82
|
+
* endpoint trips it. TIGHTEN-ONLY, like every cap in this file: this is an
|
|
83
|
+
* operator-set ceiling fixed here, not a param a caller or a signed spec may
|
|
84
|
+
* widen (a defaulted cap is a silent second ceiling, and there is no clock
|
|
85
|
+
* whose narrower reading could ever tighten it further).
|
|
86
|
+
*/
|
|
87
|
+
export const AUTHOR_CALL_TIMEOUT_MS = PROVIDER_TIMEOUT_MS;
|
|
88
|
+
|
|
63
89
|
/** src/planrun.js:42 — the survey's hard round bound. */
|
|
64
90
|
export const AUTHOR_SCOUT_ROUNDS = 8;
|
|
65
91
|
/** src/planrun.js:44 — the survey is a PROMPT INGREDIENT for the authoring call. */
|
|
@@ -348,7 +374,7 @@ const defaultLoop = (/** @type {any} */ { system, policy, onLlmResult, provider
|
|
|
348
374
|
*
|
|
349
375
|
* @param {{workdir: string, provider?: any,
|
|
350
376
|
* rounds?: number, minBytes?: number, blobMax?: number, maxTokens?: number, ctx?: boolean,
|
|
351
|
-
* attempts?: number, ceilingUsd?: number|null,
|
|
377
|
+
* attempts?: number, ceilingUsd?: number|null, callTimeoutMs?: number,
|
|
352
378
|
* onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number}) => void,
|
|
353
379
|
* createLoop?: (o: {system: string, policy: any, onLlmResult: any, provider: any}) => any,
|
|
354
380
|
* createSurveyor?: (o: {workdir: string, granted: readonly string[], ctx: boolean}) => Promise<any>}} o
|
|
@@ -365,10 +391,18 @@ export async function runAuthorScout({
|
|
|
365
391
|
ctx = true,
|
|
366
392
|
attempts = SCOUT_ATTEMPTS,
|
|
367
393
|
ceilingUsd = null,
|
|
394
|
+
// F152/PRD 30.4 — TIGHTEN-ONLY, the same clamp shape `attempts` carries just
|
|
395
|
+
// below: an operator (a test included) may buy a SHORTER idle bound and never
|
|
396
|
+
// a longer one. A garbage/absent value floors at the module ceiling
|
|
397
|
+
// (`AUTHOR_CALL_TIMEOUT_MS`), never at "no bound at all".
|
|
398
|
+
callTimeoutMs = AUTHOR_CALL_TIMEOUT_MS,
|
|
368
399
|
onCall = () => {},
|
|
369
400
|
createLoop = defaultLoop,
|
|
370
401
|
createSurveyor = defaultSurveyor,
|
|
371
402
|
}) {
|
|
403
|
+
const callTimeout = Number.isFinite(Number(callTimeoutMs)) && Number(callTimeoutMs) > 0
|
|
404
|
+
? Math.min(Math.trunc(Number(callTimeoutMs)), AUTHOR_CALL_TIMEOUT_MS)
|
|
405
|
+
: AUTHOR_CALL_TIMEOUT_MS;
|
|
372
406
|
const granted = AUTHOR_SCOUT_VERBS;
|
|
373
407
|
const system = SCOUT_SYSTEM + strategyFor([...granted]);
|
|
374
408
|
const surveyor = await createSurveyor({ workdir, granted, ctx });
|
|
@@ -404,6 +438,33 @@ export async function runAuthorScout({
|
|
|
404
438
|
// an ABSENT nobody can act on.
|
|
405
439
|
const allowed = Math.max(1, Math.min(Math.trunc(Number(attempts)) || 1, SCOUT_ATTEMPTS));
|
|
406
440
|
|
|
441
|
+
/**
|
|
442
|
+
* F152/PRD 30.4 — ONE call seam for every `loop.run()` below, so a REJECTED
|
|
443
|
+
* call (a `TimeoutError` off `AUTHOR_CALL_TIMEOUT_MS` included — bare-agent
|
|
444
|
+
* 0.42 rejects every known transport/idle-timeout casualty, F141) lands in the
|
|
445
|
+
* SAME place a call that merely RESOLVED with an error already did: as an
|
|
446
|
+
* `{error}` shape, read by `record()` (cost null, same as a `provider-red`
|
|
447
|
+
* locate call — a rejected call has no knowable cost) and by `classifySurvey`
|
|
448
|
+
* under the EXISTING `call-failed` cause. Without this seam a rejection would
|
|
449
|
+
* escape uncaught to `run-author.mjs`'s top-level crash handler instead of the
|
|
450
|
+
* honest, already-typed terminal this module was built to report through.
|
|
451
|
+
* @param {Promise<any>} p @returns {Promise<any>}
|
|
452
|
+
*/
|
|
453
|
+
const settled = async (p) => {
|
|
454
|
+
try { return await p; } catch (e) {
|
|
455
|
+
// ONLY the call's own deadline lands here. Swallowing every throw would
|
|
456
|
+
// launder a budget HaltError, a programming bug, or any other named
|
|
457
|
+
// terminal into "the survey call failed" — the exact class of dishonesty
|
|
458
|
+
// this repo refuses (an unknown reported as a known). A timeout is the one
|
|
459
|
+
// throw this seam exists for: it IS a call that produced nothing, which is
|
|
460
|
+
// what `call-failed` already means. Everything else re-raises and is
|
|
461
|
+
// relayed after cleanup, as it was before this seam existed.
|
|
462
|
+
const err = /** @type {any} */ (e);
|
|
463
|
+
if (err?.code !== 'ETIMEDOUT' && err?.name !== 'TimeoutError') throw e;
|
|
464
|
+
return { error: String(err?.message ?? err) };
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
|
|
407
468
|
try {
|
|
408
469
|
/** @type {Survey|null} */
|
|
409
470
|
let survey = null;
|
|
@@ -455,8 +516,8 @@ export async function runAuthorScout({
|
|
|
455
516
|
};
|
|
456
517
|
loop = createLoop({ system, policy: surveyor.policy, onLlmResult: metered, provider });
|
|
457
518
|
|
|
458
|
-
const r = await loop.run([{ role: 'user', content: scoutPrompt(workdir) }], surveyor.tools,
|
|
459
|
-
{ cacheMessages: true, maxTokens });
|
|
519
|
+
const r = await settled(loop.run([{ role: 'user', content: scoutPrompt(workdir) }], surveyor.tools,
|
|
520
|
+
{ cacheMessages: true, maxTokens, timeoutMs: callTimeout }));
|
|
460
521
|
verdictAt = record(label, attempt, r);
|
|
461
522
|
blob = redactSecrets(String(r?.text ?? '')).slice(0, blobMax);
|
|
462
523
|
lastMsgs = Array.isArray(r?.msgs) && r.msgs.length ? r.msgs : null;
|
|
@@ -493,8 +554,16 @@ export async function runAuthorScout({
|
|
|
493
554
|
if (recoveryHalt) budgetStop = recoveryHalt;
|
|
494
555
|
if (recoveryWanted && !recoveryHalt) {
|
|
495
556
|
const recovery = createLoop({ system, policy: surveyor.policy, onLlmResult: surveyor.onLlmResult, provider });
|
|
496
|
-
|
|
497
|
-
|
|
557
|
+
// F147 (second instance; sibling fix: src/planrun.js `askFrom`, commit
|
|
558
|
+
// 031ffe1): `r.msgs` is a prior `loop.run()`'s returned transcript,
|
|
559
|
+
// which bare-agent already prepended `system` to (Loop.run, loop.js
|
|
560
|
+
// ~482-486). `recovery` below is built with the same `system` and
|
|
561
|
+
// would prepend it again, leaving two identical system messages at
|
|
562
|
+
// index 0/1 — real OpenAI tolerates it, vLLM-class OpenAI-compatible
|
|
563
|
+
// backends 400 on it. Strip the leading one before re-running.
|
|
564
|
+
const rMsgs = r.msgs[0]?.role === 'system' ? r.msgs.slice(1) : r.msgs;
|
|
565
|
+
s2 = await settled(recovery.run([...rMsgs, { role: 'user', content: SCOUT_RECOVERY_PROMPT }], [],
|
|
566
|
+
{ cacheMessages: true, maxTokens, timeoutMs: callTimeout }));
|
|
498
567
|
const recoveryAt = record('author-scout-recovery', attempt, s2);
|
|
499
568
|
const t = redactSecrets(String(s2?.text ?? '')).slice(0, blobMax);
|
|
500
569
|
// the verdict follows the blob that WON, never the call that came last
|
|
@@ -518,10 +587,15 @@ export async function runAuthorScout({
|
|
|
518
587
|
// THE RE-ASK. Toolless by construction, over the survey's own
|
|
519
588
|
// conversation, naming only what failed mechanically.
|
|
520
589
|
const reask = createLoop({ system, policy: surveyor.policy, onLlmResult: surveyor.onLlmResult, provider });
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
590
|
+
// F147 (second instance; sibling fix: src/planrun.js `askFrom`, commit
|
|
591
|
+
// 031ffe1): `lastMsgs` is a prior `loop.run()`'s returned transcript
|
|
592
|
+
// (system already prepended by bare-agent); `reask` is built with the
|
|
593
|
+
// same `system` and would prepend it again. Strip the leading one.
|
|
594
|
+
const askMsgs = lastMsgs[0]?.role === 'system' ? lastMsgs.slice(1) : lastMsgs;
|
|
595
|
+
const rr = await settled(reask.run(
|
|
596
|
+
[...askMsgs, { role: 'user', content: scoutReaskTurn(/** @type {Survey} */ (survey).reason ?? '') }], [],
|
|
597
|
+
{ cacheMessages: true, maxTokens, timeoutMs: callTimeout },
|
|
598
|
+
));
|
|
525
599
|
verdictAt = record(label, attempt, rr);
|
|
526
600
|
blob = redactSecrets(String(rr?.text ?? '')).slice(0, blobMax);
|
|
527
601
|
if (Array.isArray(rr?.msgs) && rr.msgs.length) lastMsgs = rr.msgs;
|