bareloop 0.26.0 → 0.28.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 +271 -0
- package/bareloop.context.md +308 -38
- package/package.json +4 -3
- package/src/authorflow.js +255 -33
- package/src/authorjob.js +12 -9
- package/src/authorreadout.js +323 -0
- package/src/authorrun.js +1337 -0
- package/src/authorscout.js +16 -11
- package/src/cli.js +130 -8
- package/src/index.js +14 -2
- package/src/interviewrun.js +842 -0
- package/src/job.js +42 -26
- package/src/planrun.js +82 -3
- package/src/promptregisters.js +8 -0
- package/src/providers.js +36 -0
- package/src/replayio.js +153 -0
- package/src/reuse.js +1 -1
- package/src/run.js +1 -1
- package/src/source.js +138 -1
- package/src/text.js +45 -0
- package/src/u-patient.js +73 -0
- package/src/u-readout.js +451 -0
- package/src/userrun.js +2273 -0
- package/src/validate.js +25 -1
- package/types/authorflow.d.ts +49 -7
- package/types/authorreadout.d.ts +172 -0
- package/types/authorrun.d.ts +37 -0
- package/types/index.d.ts +3 -2
- package/types/interviewrun.d.ts +46 -0
- package/types/job.d.ts +21 -0
- package/types/planrun.d.ts +43 -3
- package/types/providers.d.ts +28 -0
- package/types/replayio.d.ts +93 -0
- package/types/reuse.d.ts +2 -2
- package/types/run.d.ts +2 -2
- package/types/source.d.ts +45 -0
- package/types/text.d.ts +35 -0
- package/types/u-patient.d.ts +39 -0
- package/types/u-readout.d.ts +247 -0
- package/types/userrun.d.ts +134 -0
- package/types/validate.d.ts +22 -0
package/bareloop.context.md
CHANGED
|
@@ -72,8 +72,10 @@ inherited rule carries the green that minted it and the contrast that attributed
|
|
|
72
72
|
(`sonnet` → `gemini-2.5-pro`, `haiku` → `gemini-2.5-flash`). It is
|
|
73
73
|
**ADMITTED-PENDING-PROBE**: it has ZERO runs, and the probe rule is not waived for it —
|
|
74
74
|
`PROBE_STATUS` (`src/providers.js`) is the machine-readable form of that debt, and
|
|
75
|
-
`probeWarningLines(provider)` renders the loud launch-time marker `
|
|
76
|
-
prints
|
|
75
|
+
`probeWarningLines(provider)` renders the loud launch-time marker `src/userrun.js`
|
|
76
|
+
prints (also called from `src/interviewrun.js` at interview time — PANEL-BUILD.md P0
|
|
77
|
+
lifted both call sites out of the `scripts/run-u.mjs`/`scripts/run-interview.mjs` scripts
|
|
78
|
+
that used to hold them). That table is read to WARN, never to REFUSE. Each entry also declares its own
|
|
77
79
|
`endpointKey` — the CONSTRUCTOR option name it reads an endpoint from — because
|
|
78
80
|
bare-agent's providers do not agree on one spelling (`baseUrl` for anthropic/openai/
|
|
79
81
|
gemini, `url` for Ollama) and none of them validate unknown option names, so a
|
|
@@ -94,7 +96,8 @@ inherited rule carries the green that minted it and the contrast that attributed
|
|
|
94
96
|
- **Reuse — where a workflow comes from:** a plain `runJob` always drafts cold. Passing
|
|
95
97
|
`bridge` starts from one standalone bridge file (`src/reuse.js`'s envelope, `## The reuse
|
|
96
98
|
ENVELOPE and runReuse` below). The CLI's `--registry <dir>` / library-level `registryDir`
|
|
97
|
-
(`src/reuse.js`, `scripts/run-u.mjs`
|
|
99
|
+
(`src/reuse.js`, `src/userrun.js` — lifted out of `scripts/run-u.mjs` by PANEL-BUILD.md P0)
|
|
100
|
+
instead reads/writes dated rows in an
|
|
98
101
|
operator-supplied directory of plain JSON files — no database, no default location; a
|
|
99
102
|
named-but-missing registry is a red, never conjured (`## The reuse registry (Layer 3)`
|
|
100
103
|
below).
|
|
@@ -234,7 +237,7 @@ contract the single close object has always used) plus:
|
|
|
234
237
|
| `name` | unique kebab-case slug | the plan references it via `check-passes(name)`; duplicates red `duplicate-id` |
|
|
235
238
|
| `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 fourteen specs in `jobs/`, five hide a final `verdict` stage and seven hide a `changed-from-seed` precondition instead (the remaining two use an object-form close with no staged `offer` flags) (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) |
|
|
236
239
|
| `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 |
|
|
237
|
-
| `direction` | optional `'up'` \| `'down'`, allow-list validated (anything else is `invalid-value`) | which way this stage's numeric series is READING as progress — `'down'` (fewer is better: typecheck errors, suppressions) or `'up'` (more is better: a fault-detection rate). **Absent means `'down'`** — every close built before this field existed is a count-shaped close, so the silent default is behaviour-identical for all of them (F120). Never inferred from the goal text and never LLM-judged — prose the agent writes must never influence its own halt rule, the same reasoning that keeps every close hand-authored or declared, never judged. Consulted by `isBetter(stage,a,b)` in `src/trend.js`, the one place the close-fix governor's `countImproved` and `best`-tracking read direction; `scripts/run-u.mjs` prints each stage's direction at the close-stage banner so a silent default is never invisible |
|
|
240
|
+
| `direction` | optional `'up'` \| `'down'`, allow-list validated (anything else is `invalid-value`) | which way this stage's numeric series is READING as progress — `'down'` (fewer is better: typecheck errors, suppressions) or `'up'` (more is better: a fault-detection rate). **Absent means `'down'`** — every close built before this field existed is a count-shaped close, so the silent default is behaviour-identical for all of them (F120). Never inferred from the goal text and never LLM-judged — prose the agent writes must never influence its own halt rule, the same reasoning that keeps every close hand-authored or declared, never judged. Consulted by `isBetter(stage,a,b)` in `src/trend.js`, the one place the close-fix governor's `countImproved` and `best`-tracking read direction; `src/userrun.js` (lifted out of `scripts/run-u.mjs` by PANEL-BUILD.md P0) prints each stage's direction at the close-stage banner so a silent default is never invisible |
|
|
238
241
|
|
|
239
242
|
The stages run in declared order as the close itself; the first red renders the verdict and
|
|
240
243
|
later stages never run. `checkMenu(close)` returns only the offerable stages (each with its
|
|
@@ -538,6 +541,7 @@ contract); returns the parsed spec on ok, `null` on any red. A spec carrying the
|
|
|
538
541
|
generic unknown-field. Menus exported: `CLOSE_TYPES`, `CLASS_BY_CLOSE`, `GOLD_COMPARE`,
|
|
539
542
|
`CADENCE_UNITS`, `PROVIDERS`, `CONDITION_KEYS`, `TOOL_MENU`, `LOCKED_TOOLS`, `STORE_VERBS`,
|
|
540
543
|
`VERDICT_TYPES`, `LOCKED_VERDICTS` — plus `checkMenu` itself.
|
|
544
|
+
|
|
541
545
|
`globToPrefix(scope)` and `scanSecrets(text)` are exported too — the write-scope prefix
|
|
542
546
|
mapping and the one secret-shape sweep `validateJob` itself is built from (`src/validate.js`).
|
|
543
547
|
|
|
@@ -615,9 +619,9 @@ a step, or test it without a provider.
|
|
|
615
619
|
| interview | `runInterview({ verdictType, answers, repoPath })` → `{ ok, answers, verdictType, refusal, reds }` | PURE — no model, no repo, no clock. The interview ADMITS only what is on the menu (`MENU_CLASSES`): nothing is locked today (`LOCKED_CLASSES` is empty), and any pick off the menu returns a refusal rather than `ok:true` — ask `MENU_CLASSES` which classes can be authored, never this sentence. The interview asks NOTHING about a genre and NOTHING about the repository. **Ask the library for the set and its numbers — never hardcode a count or a slot number**: the green set has lost several slots since it was frozen (D13's genre confirm; the repo question, dropped 2026-08-15 because `repoPath` is mandatory structured input; and, at PRD item 33 M3 piece 3, the old "which files change/read" question, replaced by Source and Destination — MECHANICAL fields, proven against the machine, never a numbered answer), and it renumbers CONTIGUOUSLY from 1 each time rather than leaving a gap. Today the green set is THREE questions (Goal, Success, Guardrails); the `soft-green` set is those three plus one more — Judge Examples (one example to pass, one to fail, and why) — the judged floor's one required input, compiled into the signed rubric card and the frozen calibration set. `verdictType` and `repoPath` are STRUCTURED input, never parsed out of prose. Answers are scrubbed at INGEST |
|
|
616
620
|
| survey | `runAuthorScout({ workdir, provider, attempts?, ceilingUsd? })` → `{ state, facts, reason, meta, raw, raws, calls, budgetStop }` | a bounded READ-ONLY LLM survey. Read-only by MENU CONSTRUCTION (`AUTHOR_SCOUT_VERBS` = the full menu minus write-class and store-class verbs), 8 rounds, F59's reserved toolless final round. **`state: 'ABSENT'` means the scout did not complete — never "no special facts are needed"**, and a parsed `{}` is one of its five ABSENT routes. Up to `SCOUT_ATTEMPTS` (**3**, hardcoded — PRD v1.58) attempts: `attempts` is TIGHTEN-ONLY and CLAMPS rather than throws (floor **1**, because a scout that never ran is an ABSENT nobody can act on), the direction every cap in this system runs in. **A retry fires on the typed MALFORMED class alone** (`SCOUT_RETRY_CAUSES` = `empty` \| `unparseable`); what is excluded is excluded for a reason — `call-failed` covers transport and `truncated:max_tokens`, both provider-red with NO redraft; `short` is F59's cut-off population and already has its own instrument INSIDE the attempt; `non-object` and `empty-object` are valid JSON with wrong or vacuous content, which is a SEMANTIC failure, and F38/F39 measured what re-asking one buys (the same distribution, sampled twice) — that is the self-healing line, and it is not crossed here. The re-ask is TOOLLESS over the conversation the survey already produced (the repository was already read; only the emission was unreadable) and names the mechanical parse error and nothing else. **There is no JSON repair behind it and there never will be** — a repairer decides what the model MEANT and writes it down as though the model had said it, in the one artefact whose whole job is to be honest about what a repository contains. `meta.attempts` / `meta.attemptsAllowed` record what was paid for; every attempt is metered under its own label (`author-scout`, `author-scout#2`, …) and leaves its raw behind. **`ceilingUsd` is the operator's money ceiling** (PRD v1.62) — checked BETWEEN attempts and before F59's reserved round, so no paid call escapes it; `null`/absent is UNBOUNDED, and a non-null non-finite value THROWS (the shared `capStop` seam — see `authorClose` below). When it ends the ladder, `budgetStop` names which stop (`cap-halt` \| `pricing-red`) and a survey that was never asked for carries the typed cause `not-funded` — deliberately OUTSIDE `SCOUT_RETRY_CAUSES`, because a retry is precisely what the cap forbade |
|
|
617
621
|
| listing | `buildSeedListing({ workdir, seedRef, sourcePaths, testPaths })` | mechanical, `$0`, no model. `files` is the WHOLE tree (what the validator judges paths against); `block` is scoped to the survey's own paths and capped in ANNOUNCED tiers (what the prompt carries). Handing the validator the scoped half would make a job scoped to `src/` read as whole-tree and silently disarm the one-population law |
|
|
618
|
-
| authoring | `authorClose({ workdir, seedRef, lang, verdictType, answers, scout, listing, generate, ceilingUsd? })` | the grounded loop: author → validate → run EVERY stage at the seed → feed the MEASURED results back → revise, bounded at `MAX_REVISIONS` (2) — a passed `maxRevisions` is TIGHTEN-ONLY, so a caller may LOWER that ceiling and never buy more revise rounds than the constant (floor **0**: one authoring call with no revise round is a legal ask, and unlimited revising would launder thrash as adaptation) — early-stop on an unchanged declaration. The declaration is emitted through a SCHEMA-FORCED TOOL CALL (`declare_close`), never parsed out of prose; the feedback is EXECUTION OUTPUT only — no model ever reviews another model's close. The return carries **`raws`**: what every model call actually SAID, the scout's attempts absorbed together with the declaration's, in the cost book's own order and under the same labels it meters. Each is `{ label, attempt, bytes, trimmed, text, cause, reason }` through the ONE persist helper (`scrubRaw` — redacted over the ONE `SECRET_PATTERNS` inventory, since a raw is the record most likely in this system to carry a live credential; bounded at `RAW_PERSIST_MAX` (8000) with the trim ANNOUNCING its own full size, and the cut walked back off a continuation byte so a multi-byte character is never split). `raws` is present on EVERY path **including the `$0` preflight refusals** — the path that spends nothing more is exactly the one whose evidence used to vanish with the process. `iterations` records what each turn MEANT (declaration, validation, seed read); `raws` records what it SAID, and a malformation is only ever visible in the second. **`ceilingUsd` is the same operator ceiling the survey took** — the scout's spend is ABSORBED into this loop's cost book before its first call is weighed, so spend already incurred folds in and re-entering cannot silently widen it. It is checked immediately before EVERY paid call (the author call, each revise, and each malformed-emission retry). **A MALFORMED ceiling is an ERROR, not a silent unbounded run** (v1.64 §3): the seam every ceiling read goes through (`capStop`, `src/text.js`) THROWS on a non-null non-finite value — `'2.50'`, `NaN`, `Infinity`, `true`, `{}` — before the first paid call, and the throw propagates uncaught rather than being caught into an ABSENT survey, because a caught one would hand you the very silent-unbounded run it exists to prevent. `null`/omitted is the STATED operator choice for unbounded; `0` and negatives are finite, legal, and cap-halt immediately. The advertised ceiling and the enforced ceiling can therefore never be different numbers. A `cap-halt` here can arrive with `ok:true` — a close that was already validated and measured survives the money stop, exactly as a late provider casualty does |
|
|
622
|
+
| authoring | `authorClose({ workdir, seedRef, lang, verdictType, answers, scout, listing, generate, ceilingUsd? })` | the grounded loop: author → validate → run EVERY stage at the seed → feed the MEASURED results back → revise, bounded at `MAX_REVISIONS` (2) — a passed `maxRevisions` is TIGHTEN-ONLY, so a caller may LOWER that ceiling and never buy more revise rounds than the constant (floor **0**: one authoring call with no revise round is a legal ask, and unlimited revising would launder thrash as adaptation) — early-stop on an unchanged declaration. The declaration is emitted through a SCHEMA-FORCED TOOL CALL (`declare_close`), never parsed out of prose; the feedback is EXECUTION OUTPUT only — no model ever reviews another model's close. The return carries **`raws`**: what every model call actually SAID, the scout's attempts absorbed together with the declaration's, in the cost book's own order and under the same labels it meters. Each is `{ label, attempt, bytes, trimmed, text, cause, reason }` through the ONE persist helper (`scrubRaw` — redacted over the ONE `SECRET_PATTERNS` inventory, since a raw is the record most likely in this system to carry a live credential; bounded at `RAW_PERSIST_MAX` (8000) with the trim ANNOUNCING its own full size, and the cut walked back off a continuation byte so a multi-byte character is never split). `raws` is present on EVERY path **including the `$0` preflight refusals** — the path that spends nothing more is exactly the one whose evidence used to vanish with the process. `iterations` records what each turn MEANT (declaration, validation, seed read); `raws` records what it SAID, and a malformation is only ever visible in the second. **`ceilingUsd` is the same operator ceiling the survey took** — the scout's spend is ABSORBED into this loop's cost book before its first call is weighed, so spend already incurred folds in and re-entering cannot silently widen it. It is checked immediately before EVERY paid call (the author call, each revise, and each malformed-emission retry). **A MALFORMED ceiling is an ERROR, not a silent unbounded run** (v1.64 §3): the seam every ceiling read goes through (`capStop`, `src/text.js`) THROWS on a non-null non-finite value — `'2.50'`, `NaN`, `Infinity`, `true`, `{}` — before the first paid call, and the throw propagates uncaught rather than being caught into an ABSENT survey, because a caught one would hand you the very silent-unbounded run it exists to prevent. `null`/omitted is the STATED operator choice for unbounded; `0` and negatives are finite, legal, and cap-halt immediately. The advertised ceiling and the enforced ceiling can therefore never be different numbers. A `cap-halt` here can arrive with `ok:true` — a close that was already validated and measured survives the money stop, exactly as a late provider casualty does. **F176 (fixed):** "kept the last accepted revision" is not the same claim as "kept the best revision" — a later revise round can VALIDATE fine yet MEASURE worse (every stage instrument-stops rather than reading a real red/green verdict). `declaration`/`seedRead`/`finalFrom`/the returned `genreEnv.dropped` are therefore the NEWEST SOUND measured iteration, not simply the last one, when the last one instrument-stopped and an earlier one did not; `stop` itself never changes. The swap is never silent: the return also carries **`fellBack`** — `null` on every run where it never fired, else `{from, to, brokenStages}` naming which iteration was dropped, which now stands, and which of the dropped one's stages instrument-stopped — reported through `onPhase('author-fallback', …)` as it happens and printed at the terminal readout (`fellBackLines`, `src/authorreadout.js` — lifted out of `scripts/author-readout.mjs` by PANEL-BUILD.md P0; called from `src/authorrun.js`). **F179/F180 (history):** bare-agent 0.42.0's OpenAIProvider used to THROW mid-call on malformed tool-call JSON (a raw SyntaxError, after the billed round), crashing past this function; a 2026-09-15 stopgap caught it as a `provider-red`/`ask.providerError` casualty (booked `costUsd: null`) so a sound prior declaration was kept via the fallback above rather than discarded. **F179 (bare-agent 0.43.0, BA-27 — the real mechanism):** the throw is fixed upstream. `OpenAIProvider`/`OllamaProvider.generate` now return a malformed round PRICED (real `usage`, never `costUsd: null`) with `toolCalls: []` plus their own `malformedToolCall: {name, error}` marker, which `Loop.run` surfaces unchanged on its return. For the authoring declaration and confirm-turn calls, `askStructured` reads that marker and RETRIES through the existing malformed-emission retry ladder (`MAX_STRUCTURE_RETRIES`) instead of stopping the run — nothing is ever repaired, and a malformed FIRST call no longer needs an earlier sound declaration to fall back to. The generic provider-casualty fallback above (idle-timeout, etc.) still applies for every other admitted class. **F184 (fixed, worker path):** the scout and the worker path never joined that retry ladder (still true — retrying is arbiter-adjacent, out of scope), but the worker path's `ask()` (`src/planrun.js`) now at least READS the marker where it already sees it and emits a distinct spine record, `worker-malformed-tool-call` (`{phase, iteration, name, error}`), before returning `r` unexamined exactly as before — no retry, no change to strikes/ladder/attempt counting or verdict routing, purely the same visibility bar this section's authoring-path fix already set |
|
|
619
623
|
| everything above, composed | `authorCloseForJob({ verdictType, answers, repoPath, lang, generate, ceilingUsd?, ... })` → `{ ok, closeDecl, verdictType, refusal, cost, ... }` | refuses at the cheapest gate that can refuse: an interview refusal costs **zero**. THE GENRE REFUSAL LIVES HERE (not in the interview): a language the catalogue owns no data for, and a LOCKED KIND the model reached for, both come back as counted `request-red` demand. **`ceilingUsd` is ONE number handed to BOTH paid seams** (the survey's and the declaration loop's) — the advertised ceiling and the enforced ceiling are the same ceiling. It travels as an EXPLICIT `null` when unset, because unbounded is a stated choice and an absent field read as falsy is that state reached by accident — and anything that is neither a finite number nor `null` throws at the seam rather than becoming that state by typo |
|
|
620
|
-
| assembly | `assembleSpec(specDraft, { closeDecl, verdictType })` | folds the authored half into the OPERATOR's half. Budgets, the fence, cadence, escalation and the provider are never authored by anything here. The GOAL is passed through, not generated. **A draft that ALREADY carries the authored half is REFUSED — it THROWS, never merges and never overwrites.** `AUTHORED_SPEC_FIELDS` (`close`, `closeDecl`, `verdictType`) names the fields this fold WRITES, as data, so the refusal and the fold cannot disagree about which half of the spec is which; the rule is `job.js`'s one step earlier (two closes are two arbiters, and picking one silently is how a signed artefact stops meaning what the signer read). `scripts/run-author.mjs` asks the same question at **$0 BEFORE the scout**, so the answer never arrives after the model has been paid |
|
|
624
|
+
| assembly | `assembleSpec(specDraft, { closeDecl, verdictType })` | folds the authored half into the OPERATOR's half. Budgets, the fence, cadence, escalation and the provider are never authored by anything here. The GOAL is passed through, not generated. **A draft that ALREADY carries the authored half is REFUSED — it THROWS, never merges and never overwrites.** `AUTHORED_SPEC_FIELDS` (`close`, `closeDecl`, `verdictType`) names the fields this fold WRITES, as data, so the refusal and the fold cannot disagree about which half of the spec is which; the rule is `job.js`'s one step earlier (two closes are two arbiters, and picking one silently is how a signed artefact stops meaning what the signer read). `src/authorrun.js` (lifted out of `scripts/run-author.mjs` by PANEL-BUILD.md P0) asks the same question at **$0 BEFORE the scout**, so the answer never arrives after the model has been paid |
|
|
621
625
|
| the three gates | `prepareSigning({ spec, workdir, seedRef? })` → `{ ok, specHash, gates, work, guards, refusal }` | D9, and it NEVER signs |
|
|
622
626
|
|
|
623
627
|
**D5 — the mandatory guards are SHOWN and FIXED, and the battery keys off the VERDICT
|
|
@@ -858,13 +862,17 @@ goal has to state everything the close will judge, and **nothing derives one fro
|
|
|
858
862
|
or checks them against each other** — the only derivation runs close-stages → check menu, one
|
|
859
863
|
hop, one direction. So the sole defence against an unstated stage is that the person signing
|
|
860
864
|
reads both halves at once, and any surface you build around `prepareSigning` inherits that
|
|
861
|
-
obligation. Both reference runners now do it: `
|
|
865
|
+
obligation. Both reference runners now do it: `src/userrun.js --approve` (lifted out of
|
|
866
|
+
`scripts/run-u.mjs` by PANEL-BUILD.md P0) names every close
|
|
862
867
|
stage (name + kind) under the goal — for **BOTH spec forms**, through `closeStagesOf`, since
|
|
863
868
|
gating that block on `closeDecl` reached exactly one of the eleven shipped specs and left the
|
|
864
869
|
other ten (command `close[]`) showing the goal with no stages under it, which is the same
|
|
865
870
|
half-reading in the guard against it (F100); a command stage prints `[command]` and NOT the
|
|
866
|
-
catalogue kind name it does not carry — and `
|
|
867
|
-
|
|
871
|
+
catalogue kind name it does not carry — and `src/authorrun.js` (lifted out of
|
|
872
|
+
`scripts/run-author.mjs`) prints the goal above the
|
|
873
|
+
declaration via `declarationLines(spec)` in `src/authorreadout.js` (lifted out of
|
|
874
|
+
`scripts/author-readout.mjs` — no adapter script remains for that one, it moved whole),
|
|
875
|
+
rendered from the
|
|
868
876
|
**RESOLVED** spec — the bytes that get hashed, never the draft and never the authored half
|
|
869
877
|
alone. An absent goal renders as absent rather than as a bare label. This is a READING, not a
|
|
870
878
|
validator: nothing compares the goal to the declaration, because deriving one from the other
|
|
@@ -875,14 +883,25 @@ is exactly what F87 forbids.
|
|
|
875
883
|
`scripts/run-interview.mjs` existed the only way in was to hand-write an `answers.json` and a
|
|
876
884
|
spec draft — precisely the SWE tax this product refuses. That script is the half that ASKS
|
|
877
885
|
(`--verdict <class> --provider <anthropic-api|openai-api|gemini-api|clipipe-subscription>
|
|
878
|
-
--out <outdir> [--budget <usd>]`), and it is deliberately GLUE: **it
|
|
886
|
+
--out <outdir> [--budget <usd>] [--base-url <url>]`), and it is deliberately GLUE: **it
|
|
879
887
|
calls no provider at all**, so a whole interview costs $0 and no model ever sees it. `--provider`
|
|
880
888
|
is REQUIRED with NO default — bareloop is LLM-agnostic, and defaulting it would silently re-lock
|
|
881
889
|
every interview onto one vendor; missing or empty dies loud, at the door, naming the same menu
|
|
882
890
|
`src/job.js`'s own validator admits. It is written into the draft's `provider` field, so
|
|
883
|
-
`scripts/run-author.mjs` needs no flag of its own — it resolves whatever the interview wrote
|
|
891
|
+
`src/authorrun.js` (lifted out of `scripts/run-author.mjs` by PANEL-BUILD.md P0) needs no flag of its own — it resolves whatever the interview wrote
|
|
884
892
|
down.
|
|
885
893
|
|
|
894
|
+
`--base-url` is OPTIONAL with NO default (PRD item 33 close-out — L17 named the provider but
|
|
895
|
+
never admitted the endpoint): a provider name's table entry (`src/providers.js`) can be reached
|
|
896
|
+
through an OpenAI-compatible gateway other than its own default host — DeepSeek, today's one
|
|
897
|
+
secondary, is reached as `--provider openai-api --base-url https://api.deepseek.com/v1`. Absent,
|
|
898
|
+
the draft carries NO `baseUrl` field at all (never `null`, never `''`) and every provider
|
|
899
|
+
constructor defaults its own endpoint. When given, it lands in the draft's `baseUrl` field
|
|
900
|
+
beside `provider` and passes through the SAME `validateJob` pass every other field takes below,
|
|
901
|
+
at $0: `http://` to a public host, an embedded `user:pass@`, or an unparseable string reds there,
|
|
902
|
+
before anything is written. The key still follows the PROVIDER, not the endpoint — a DeepSeek
|
|
903
|
+
draft's key still loads from `OPENAI_API_KEY`, and the hand-off says so.
|
|
904
|
+
|
|
886
905
|
**Source and Destination are the interview's own FIRST TWO QUESTIONS, and they REPLACE
|
|
887
906
|
`--patient`** (PRD item 33 M3, ruling 2, `docs/product/ITEM33-BUILD.md`). Source — "a local
|
|
888
907
|
folder, subfolder or file, or one URL the machine can reach" — is asked first and checked
|
|
@@ -910,8 +929,10 @@ piece 4 step S6 (D5 = A), the FORM CONTINUES — the class's own questions, job
|
|
|
910
929
|
wall are all still asked, and the draft is still written, with no `writeScope` field
|
|
911
930
|
(`PLAIN_FOLDER_DEFERRED_FIELDS`, `src/authorjob.js` — Destination there is an output directory,
|
|
912
931
|
never a fence). The honest named stop (bareloop's check catalogue is code-genre only today,
|
|
913
|
-
ruling 7 → M4) no longer lands HERE — it moves to `run-author.mjs`,
|
|
914
|
-
|
|
932
|
+
ruling 7 → M4) no longer lands HERE — it moves to `run-author.mjs`, which now gives it
|
|
933
|
+
IMMEDIATELY, at $0 (D5 amended 2026-09-21, F191: no scout and no confirm turn either — a plain
|
|
934
|
+
folder has no code language, so the confirm turn's own code-derived protections cannot be
|
|
935
|
+
computed for one; see the run-author.mjs section below). Everything load-bearing in it is
|
|
915
936
|
borrowed rather than respelled —
|
|
916
937
|
the QUESTIONS are the library's frozen sets (`questionsFor` / `requiredAnswersFor`), printed as
|
|
917
938
|
handed over and never re-worded, re-ordered or re-numbered by the script; the REFUSALS are
|
|
@@ -946,7 +967,9 @@ spends it.
|
|
|
946
967
|
DIFFERENT PROCESS under its own ceiling; the interview prints its exact command line (with
|
|
947
968
|
`--budget` propagated only when one was given, and a note when the CHOSEN provider's own key —
|
|
948
969
|
`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`, resolved through the same provider table
|
|
949
|
-
`run-author.mjs` and `run-u.mjs` share — is unset in this shell
|
|
970
|
+
`run-author.mjs` and `run-u.mjs` share — is unset in this shell, OR set but MALFORMED: F181 — a
|
|
971
|
+
value carrying a line break/control character/stray whitespace (a two-line secret-store entry,
|
|
972
|
+
e.g.) reads a distinct reason via `apiKeyProblem` (`src/providers.js`), never the value)
|
|
950
973
|
before asking `Run it now? [y/N]`, so declining still leaves a command to paste and two files
|
|
951
974
|
already on disk. Only an explicit yes spends — the answer that costs nothing is the one you get
|
|
952
975
|
by saying nothing, the same lean the pause's doors take. On a yes it releases stdin before
|
|
@@ -976,7 +999,10 @@ into a success one process up.
|
|
|
976
999
|
- `run-author.mjs` — **1** a refusal or a failed gate (this now includes the confirm turn's own
|
|
977
1000
|
`confirm-abandoned`/`confirm-restart` stops — input ended, or the person chose to start over,
|
|
978
1001
|
both counted through the same generic "not-authored" branch every other authoring refusal
|
|
979
|
-
uses
|
|
1002
|
+
uses, and `source-deps-missing` — the prepared copy's `package.json` lists dependencies with
|
|
1003
|
+
no `node_modules`, refused at $0 before the scout and before the provider/key are even
|
|
1004
|
+
checked, naming the exact install command); **2** operator/config; **3** a leak; **4** a
|
|
1005
|
+
CRASH inside the paid span. **3
|
|
980
1006
|
deliberately OVERRIDES a 4**: a secret sitting in a written file is the harder line of the
|
|
981
1007
|
two, and the crash keeps both of its own louder channels — the whole error on stderr and its
|
|
982
1008
|
own `author-crash` spine record. **This script is now interactive** (step S4): the confirm
|
|
@@ -984,12 +1010,15 @@ into a success one process up.
|
|
|
984
1010
|
who wrote what and lets the person confirm the plan, fix it (up to 2 rounds), type the goal
|
|
985
1011
|
themselves, or start over — everything else (the survey, the declaration loop, D9's gates)
|
|
986
1012
|
stays unattended. `rl.close()` runs in a `finally` around the whole paid span. **A plain-folder
|
|
987
|
-
Source (step S6, D5
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1013
|
+
Source (step S6, D5 amended 2026-09-21, F191) gets NO scout AND NO confirm turn.** D5
|
|
1014
|
+
originally ran the confirm turn over a $0, no-git directory listing of the frozen tree first,
|
|
1015
|
+
THEN gave the honest "no checks yet" stop (`request-red`/`non-code-source`) once a plan was
|
|
1016
|
+
confirmed. Live run `mu4hc7sp` crashed inside that confirm turn instead: `confirmProtections`
|
|
1017
|
+
calls `classGuards` (`src/authoring.js`), which is keyed by CODE LANGUAGE and throws on a
|
|
1018
|
+
plain folder's `lang: 'none-detected'` — a plain folder has no language, so the confirm turn
|
|
1019
|
+
cannot honestly show real, code-derived protections for one, and now never runs one. The stop
|
|
1020
|
+
fires immediately after `author-start`, at $0, no scout, no confirm turn, no model call — the
|
|
1021
|
+
spine is exactly `author-start` → `author-end{outcome:'not-authored', stop:'non-code-source'}`.
|
|
993
1022
|
|
|
994
1023
|
**The protections a person is shown in the confirm turn come from CODE, never the model**
|
|
995
1024
|
(fix #1, live run mu0voeo4, 2026-09-14 — a run once showed a model-invented "behavior-
|
|
@@ -1001,10 +1030,33 @@ compose, each through a frozen plain-English line in `GUARD_DESCRIPTIONS` — be
|
|
|
1001
1030
|
`CONFIRM_MENU` — plus the write fence when one is set; a guard with no line there throws
|
|
1002
1031
|
rather than showing a blank description. The model instead returns `notChecked`: anything
|
|
1003
1032
|
the person asked for that no listed check can verify, in the person's own words, never
|
|
1004
|
-
silently dropped. It is shown under its own heading in the menu (`
|
|
1033
|
+
silently dropped. It is shown under its own heading in the menu (`src/authorrun.js`, lifted
|
|
1034
|
+
out of `scripts/run-author.mjs` by PANEL-BUILD.md P0)
|
|
1005
1035
|
and threaded through to `accepted.notChecked` and `confirmedBlock`'s own "THE PERSON ASKED
|
|
1006
1036
|
FOR THESE, BUT NOTHING CHECKS THEM" section for the composer.
|
|
1007
1037
|
|
|
1038
|
+
**The model drafting `notChecked` is now shown the SAME code-derived protections list before
|
|
1039
|
+
it drafts** (F183, ruling A, 2026-09-16 — live runs `mu2bjmed` and `mu2qmept` both had the
|
|
1040
|
+
model falsely claim an already-enforced guard or the write fence was missing, because it was
|
|
1041
|
+
never told what was actually enforced). `confirmProtections`'s output is computed once, before
|
|
1042
|
+
the paid round loop, and threaded into `confirmPrompt`'s model-facing prompt as an "ALREADY
|
|
1043
|
+
COVERED" block; `CONFIRM_SYSTEM` orders the model to check that list before naming anything in
|
|
1044
|
+
`notChecked`. The model still authors `notChecked` freely — this never makes the list
|
|
1045
|
+
deterministic, and it is still never a hand-typed second spelling of the guards. Not yet
|
|
1046
|
+
proven live.
|
|
1047
|
+
|
|
1048
|
+
**`accepted.openQuestions` is the model's OWN honest `questions`, from the plan being
|
|
1049
|
+
ACCEPTED** (F175, fixed 2026-09-14) — never a prior, superseded round's, and never limited to
|
|
1050
|
+
a person's typed "fix" text as it used to be. On the round-2 "fix" terminal path (D3: there is
|
|
1051
|
+
no 3rd call) the model's own questions come first, then the person's still-pending fix text.
|
|
1052
|
+
Shown to the composer by `confirmedBlock` ("state these in your notes, never decide them
|
|
1053
|
+
silently") and at the terminal signing readout (`openQuestionLines`, `src/authorreadout.js` —
|
|
1054
|
+
lifted out of `scripts/author-readout.mjs` by PANEL-BUILD.md P0; no adapter script remains
|
|
1055
|
+
for that one, it moved whole).
|
|
1056
|
+
**Not fully closed:** the confirm menu still lets a person pick "Confirm" without being forced
|
|
1057
|
+
to resolve a raised question first — only the silent-drop-from-the-signed-record half of F175
|
|
1058
|
+
is fixed; the menu-bypass half stays open.
|
|
1059
|
+
|
|
1008
1060
|
**A crash inside the paid span leaves a BODY.** The fallible span — from just after
|
|
1009
1061
|
`author-start` to the end of the main flow — sits in ONE try/catch that RETRIES NOTHING and
|
|
1010
1062
|
SWALLOWS NOTHING. The operator's copy goes first and verbatim (it must not depend on the two
|
|
@@ -1111,7 +1163,8 @@ races), never to be folded into worker/interpreter reds.
|
|
|
1111
1163
|
### `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`
|
|
1112
1164
|
|
|
1113
1165
|
**`closeTimeoutMs` (PRD item 27/M3) is now OPTIONAL for every real caller.** Omit it (both
|
|
1114
|
-
`src/cli.js`'s bundle runner and `scripts/run-u.mjs`
|
|
1166
|
+
`src/cli.js`'s bundle runner and `src/userrun.js` — lifted out of `scripts/run-u.mjs` by
|
|
1167
|
+
PANEL-BUILD.md P0 — do) and the run AUTOSETS the per-stage
|
|
1115
1168
|
close ceiling from a $0 seed timing pass, or reads the job spec's own signed override —
|
|
1116
1169
|
"guesstimate + loud sign + customer override", the same shape rates-passthrough already
|
|
1117
1170
|
uses (F113). Passing an explicit value here still wins outright over both (kept for
|
|
@@ -1120,7 +1173,8 @@ backward compatibility and test control) — see `resolveCloseTimeoutMs` further
|
|
|
1120
1173
|
**`closeDir` (PRD item 27/M3 Part B, optional but effectively required for any
|
|
1121
1174
|
hand-authored close that reads its own books)** — the close's own directory (pristine
|
|
1122
1175
|
copies, calibration thresholds, logs), outside the patient tree. `src/cli.js` sets it to
|
|
1123
|
-
`<bundleDir>/runs/<runid>/close/`; `scripts/run-u.mjs`
|
|
1176
|
+
`<bundleDir>/runs/<runid>/close/`; `src/userrun.js` (lifted out of `scripts/run-u.mjs` by
|
|
1177
|
+
PANEL-BUILD.md P0) sets it to its existing `spineDir`. A
|
|
1124
1178
|
close script whose source mentions `BARELOOP_CLOSE_DIR` and gets none here refuses the run
|
|
1125
1179
|
at $0 (`close-dir-required`, before any provider call) — see `checkCloseDirRequired` below.
|
|
1126
1180
|
|
|
@@ -1819,7 +1873,8 @@ that moved (`door-accept-red`) and a missing registry all come back as named red
|
|
|
1819
1873
|
a person) before it is honoured: hamr's ruling that an accept is not a rubber stamp, and the
|
|
1820
1874
|
answer to a tree that can move in the 60 days a door keeps. A red REFUSES the accept with the
|
|
1821
1875
|
stage named and records **nothing**. A pass records the disposition and, over a held judged
|
|
1822
|
-
green, RELEASES the credit through module 6's `applyDoorDecision`. `
|
|
1876
|
+
green, RELEASES the credit through module 6's `applyDoorDecision`. `src/userrun.js` (lifted
|
|
1877
|
+
out of `scripts/run-u.mjs` by PANEL-BUILD.md P0) resolves
|
|
1823
1878
|
the re-run's ceiling itself via `resolveCloseTimeoutMs` before calling this, and refuses outright
|
|
1824
1879
|
(F137) as a named `close-timing-red` door stop — `answerReviewDoor` never called, nothing
|
|
1825
1880
|
recorded — if that timing pass itself times out, rather than falling through to `runClose`'s
|
|
@@ -2185,7 +2240,8 @@ it came back for (the declared-fold precedent, `priorSpentUsd`'s exactly). Pass
|
|
|
2185
2240
|
`resumeBranch`; `runReuse` does it for you. `null` — a leg killed before it ever branched — is
|
|
2186
2241
|
the cold path, and correct, because a leg that never branched has no work to strand.
|
|
2187
2242
|
|
|
2188
|
-
`
|
|
2243
|
+
`bareloop run-u --resume <runid|path>` (`src/userrun.js`, lifted out of `scripts/run-u.mjs`
|
|
2244
|
+
by PANEL-BUILD.md P0 — the script is now a thin adapter over it) is the operator-side consumer: it skips the patient
|
|
2189
2245
|
reset (`resumeTreeGate` instead — a dirty tree is what a resume expects; only a moved HEAD
|
|
2190
2246
|
stops it), folds the halted run's money and wall in, returns to the dead leg's own work branch,
|
|
2191
2247
|
re-enters at the checkpoint, and arms the outside watchdog on the REMAINING wall. The top-up itself stays a spec edit the human signs.
|
|
@@ -2438,6 +2494,23 @@ objects will not be caught by this. `scripts/behaviour-readout.mjs <gate-audit.j
|
|
|
2438
2494
|
prints the block for one run or every run_id found in the file, skipping malformed lines with
|
|
2439
2495
|
a count rather than throwing.
|
|
2440
2496
|
|
|
2497
|
+
**F186 (2026-09-15) — the "run's own gate-audit JSONL" claim above was FALSE for a while, at the
|
|
2498
|
+
file-hygiene layer, never inside `runBehaviour` itself.** The authoring scout's gate audit
|
|
2499
|
+
(`src/authorscout.js`'s `defaultSurveyor`) writes directly into the patient tree at its root
|
|
2500
|
+
(`<workdir>/gate-audit.jsonl`), and the tree's own `.gitignore` denies `*.jsonl`, so
|
|
2501
|
+
`src/userrun.js`'s cold reset (`git clean -fd`, no `-x`, via `coldReset` — lifted with
|
|
2502
|
+
`moveStaleGateAudit` out of `scripts/u-patient.mjs` into `src/u-patient.js` by PANEL-BUILD.md
|
|
2503
|
+
P0) never removed it — the file
|
|
2504
|
+
accreted rows from every authoring scout AND every worker run that had ever touched the tree,
|
|
2505
|
+
and `run-u`'s end-of-run rename claimed the WHOLE accreted file as "this run's own audit"
|
|
2506
|
+
with no filtering. The fix is at the source, never a `run_id` filter bolted onto `runBehaviour`
|
|
2507
|
+
(a single run legitimately spans several run_ids): `src/authorrun.js` (lifted out of
|
|
2508
|
+
`scripts/run-author.mjs`) archives its own
|
|
2509
|
+
audit out of the tree the moment authoring ends; `src/userrun.js` moves any stale audit
|
|
2510
|
+
aside right after `coldReset`, before its own Gate ever opens (never on a resume, where the
|
|
2511
|
+
tree's audit is that same run's own prior leg). With both halves in place, the file this
|
|
2512
|
+
section describes really is scoped to the one run reading it, restoring the claim above.
|
|
2513
|
+
|
|
2441
2514
|
```js
|
|
2442
2515
|
import { runBehaviour, formatBehaviour } from 'bareloop';
|
|
2443
2516
|
const events = fs.readFileSync(auditFile, 'utf8').trim().split('\n').map(JSON.parse);
|
|
@@ -2456,12 +2529,21 @@ is `Math.round(bytesWithheld / 4)`, a bytes→tokens ESTIMATE named accordingly
|
|
|
2456
2529
|
it as measured. **Emitted only when the shim is armed** (any `readShim` arm but the off one) —
|
|
2457
2530
|
an unarmed run emits no record at all, never a fabricated zero. It carries no cost fields, so it
|
|
2458
2531
|
is not a spend record and no spend-slicing instrument needs to account for it.
|
|
2459
|
-
`scripts/run-u.mjs` prints it as a `MEMORY-CACHE` line right after `BEHAVIOUR` when armed
|
|
2532
|
+
`src/userrun.js` (lifted out of `scripts/run-u.mjs` by PANEL-BUILD.md P0) prints it as a `MEMORY-CACHE` line right after `BEHAVIOUR` when armed
|
|
2460
2533
|
(`no record` if the run ended before the summary could fire); `scripts/behaviour-readout.mjs`
|
|
2461
2534
|
prints the same line when handed the run's spine file as its optional third positional.
|
|
2462
2535
|
|
|
2463
2536
|
|
|
2464
|
-
### `replayRun(spineEvents, auditEvents?, { runId? })` / `formatReplay(summary)` — `src/replay.js`
|
|
2537
|
+
### `replayRun(spineEvents, auditEvents?, { runId? })` / `formatReplay(summary)` / `summarizeForAllLine(summary)` / `formatAllLines(entries)` — `src/replay.js`
|
|
2538
|
+
|
|
2539
|
+
`summarizeForAllLine(summary)` reduces one `replayRun` summary to one `--all`/directory-
|
|
2540
|
+
listing row's raw field strings (`id`/`job`/`shape`/`class`/`model`/`outcome`/`spend`/
|
|
2541
|
+
`wall`/`steps`/`reason`); `formatAllLines(entries)` renders every row `listSpines` (below)
|
|
2542
|
+
built for one directory as a fixed-column aligned table, with `<name> not-a-spine` lines
|
|
2543
|
+
interleaved at their original position for any file that didn't look like a spine. Both
|
|
2544
|
+
are PANEL-BUILD.md P0 additions to the package root: they already existed inside
|
|
2545
|
+
`src/replay.js` (built for `scripts/run-replay.mjs`'s own `--all` mode) but were not
|
|
2546
|
+
re-exported until this rung, alongside `src/replayio.js` below.
|
|
2465
2547
|
|
|
2466
2548
|
PRD build-list item 6: today no failed run can be reconstructed in under five minutes — every
|
|
2467
2549
|
byte is on disk (the spine JSONL + its gate-audit sidecar) but only hand-slicing JSONL reads it.
|
|
@@ -2711,6 +2793,45 @@ const audit = fs.readFileSync(auditFile, 'utf8').trim().split('\n').map(JSON.par
|
|
|
2711
2793
|
console.log(formatReplay(replayRun(spine, audit, { runId: 'mszcthk1' })));
|
|
2712
2794
|
```
|
|
2713
2795
|
|
|
2796
|
+
### `replayOne(spinePath, opts?)` / `listSpines(dir)` / `readHistoryLog(file)` — `src/replayio.js`
|
|
2797
|
+
|
|
2798
|
+
PANEL-BUILD.md P0, the rung's last task: the IO layer `replayRun`/`formatReplay` above
|
|
2799
|
+
never had — resolving a spine's gate-audit sibling on disk and reading it, and listing
|
|
2800
|
+
which `.jsonl` files in a directory are spines at all. This is the module `bareloop
|
|
2801
|
+
replay` and `bareloop history` are both thin callers of (previously duplicated ad hoc: a
|
|
2802
|
+
`readFileSync` inline in `src/cli.js`'s own `doHistory`, and a second copy of this exact
|
|
2803
|
+
IO logic living only in `scripts/run-replay.mjs` with no library form and no `bareloop`
|
|
2804
|
+
command over it — the gap PANEL-BUILD.md's own measured table named). Every function here
|
|
2805
|
+
is pure IO + content detection; the actual reconstruction is `replayRun`'s alone (above) —
|
|
2806
|
+
nothing here recomputes a field `replayRun`/`runBehaviour` already derive.
|
|
2807
|
+
|
|
2808
|
+
`src/replayio.js` also holds lower-level plumbing (`parseJsonl`, `isSidecarByName`,
|
|
2809
|
+
`looksLikeSpine`, `resolveSiblings`) that the three functions below compose internally —
|
|
2810
|
+
a raw JSONL parser, a filename predicate, a content predicate, and a sibling-path
|
|
2811
|
+
resolver. These are deliberately **not** re-exported from the package root: they are
|
|
2812
|
+
implementation steps, not their own adopter-facing promise, and each is exercised (and
|
|
2813
|
+
importable) directly from `src/replayio.js` by this repo's own tests.
|
|
2814
|
+
|
|
2815
|
+
- **`replayOne(spinePath, { preParsedSpine?, skipAudit? })`** → `ReturnType<replayRun>`.
|
|
2816
|
+
Reads the spine (+ sidecar unless `skipAudit`) and hands both straight to `replayRun`.
|
|
2817
|
+
`skipAudit` (carried over from the script this replaces, PR #23 review item 5): a
|
|
2818
|
+
directory listing never opens a run's sidecar at all, since `summarizeForAllLine` reads
|
|
2819
|
+
none of the audit-sourced fields — reading every run's sidecar in a `--all` listing was
|
|
2820
|
+
pure unused I/O, multiplied by every spine found.
|
|
2821
|
+
- **`listSpines(dir)`** → `Array<{kind:'spine', row: ReturnType<summarizeForAllLine>} |
|
|
2822
|
+
{kind:'not-a-spine', name}>`, one entry per `.jsonl` file found directly in `dir` (not
|
|
2823
|
+
recursive) that is not a sidecar by name. Feeds `formatAllLines` directly.
|
|
2824
|
+
- **`readHistoryLog(file)`** → `{rows, skipped}`. The one reader for a bundle's
|
|
2825
|
+
`history.jsonl` (`src/bundle.js`'s `appendHistory`); replaces `bareloop history`'s former
|
|
2826
|
+
hand-rolled `readFileSync`/split.
|
|
2827
|
+
|
|
2828
|
+
```js
|
|
2829
|
+
import { listSpines, replayOne } from 'bareloop';
|
|
2830
|
+
import { formatAllLines, formatReplay } from 'bareloop';
|
|
2831
|
+
console.log(formatAllLines(listSpines('/path/to/patient-repo')));
|
|
2832
|
+
console.log(formatReplay(replayOne('/path/to/patient-repo/u-mszcthk1.jsonl')));
|
|
2833
|
+
```
|
|
2834
|
+
|
|
2714
2835
|
### `PROMPT_REGISTERS` / `isPromptFile(path)` — `src/promptregisters.js`, and the prompt-commit rule
|
|
2715
2836
|
|
|
2716
2837
|
PRD build-list item 5 (TODO #8), Q9 answered (hamr, 2026-08-25): "a check". A commit that
|
|
@@ -2876,7 +2997,7 @@ defaulted knob. Arbiter constants, hamr 2026-09-07 verbatim: **floor = 120,000 m
|
|
|
2876
2997
|
| `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 |
|
|
2877
2998
|
| `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` |
|
|
2878
2999
|
| `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) |
|
|
2879
|
-
| `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 |
|
|
3000
|
+
| `resolveCloseTimeoutMs({job, stages, cwd, redact})` | → `{ closeTimeoutMs, source, timing, timedOut }` | the ONE resolver `src/planrun.js` and `src/userrun.js` (lifted out of `scripts/run-u.mjs` by PANEL-BUILD.md P0) 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 |
|
|
2880
3001
|
| `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 |
|
|
2881
3002
|
|
|
2882
3003
|
**`scripts/sign-close.mjs`** — `node scripts/sign-close.mjs <jobs/x.json> [--write]` or
|
|
@@ -2925,7 +3046,7 @@ hash and needs re-approval, exactly like any other semantic edit.
|
|
|
2925
3046
|
| `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` |
|
|
2926
3047
|
| `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 |
|
|
2927
3048
|
| `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 |
|
|
2928
|
-
| `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`
|
|
3049
|
+
| `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). `src/userrun.js`'s (lifted out of `scripts/run-u.mjs` by PANEL-BUILD.md P0) 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 |
|
|
2929
3050
|
| `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 |
|
|
2930
3051
|
| `bundle-missing` | `readBundle` | `dir` is not a directory at all |
|
|
2931
3052
|
| `manifest-invalid` / `spec-invalid` | `readBundle` | `manifest.json`/`spec.json` could not be read or parsed |
|
|
@@ -2944,7 +3065,7 @@ between a swapped script and a fake green, so `bareloop run` calls it as literal
|
|
|
2944
3065
|
before the envelope check, before the key check, before the worktree, before `runJob` is
|
|
2945
3066
|
ever reached.
|
|
2946
3067
|
|
|
2947
|
-
#### The CLI — `bareloop export | run | history`, `bin/bareloop.mjs`
|
|
3068
|
+
#### The CLI — `bareloop export | run | history | replay | run-u | interview | author`, `bin/bareloop.mjs`
|
|
2948
3069
|
|
|
2949
3070
|
`bin/bareloop.mjs` is a ~10-line adapter: it supplies the real `deps` (real `env`/`stdout`/
|
|
2950
3071
|
`stderr`/`cwd`/`stdin`) and turns the returned number into `process.exitCode` — never
|
|
@@ -2959,7 +3080,12 @@ same seam `tests/planrun.test.js` uses. `deps` also accepts `env`, `stdout`, `st
|
|
|
2959
3080
|
|
|
2960
3081
|
Bare `bareloop` (no sub-command) prints a numbered menu — `1 export 2 run 3 history q
|
|
2961
3082
|
quit` — and asks each sub-command's arguments one line at a time over the SAME code paths
|
|
2962
|
-
below.
|
|
3083
|
+
below. `run-u`, `interview` and `author` (below) are not on this menu (PANEL-BUILD.md P0
|
|
3084
|
+
tasks 2/4-4/4): each one's own flag grammar is too wide for a line-at-a-time wizard, and the
|
|
3085
|
+
menu's job is to ask the SAME questions the sub-command below already answers, never to
|
|
3086
|
+
invent a new interview — this is doubly true for `interview`/`author`, which already ARE
|
|
3087
|
+
interviews of their own. All three are dispatched by name only: `bareloop run-u <flags…>`,
|
|
3088
|
+
`bareloop interview <flags…>`, `bareloop author <flags…>`.
|
|
2963
3089
|
|
|
2964
3090
|
- **`bareloop export <jobs/x.json> --registry <dir> --out <dir>`** → resolves the spec's
|
|
2965
3091
|
close-script paths against the spec file's own directory (never the process cwd), calls
|
|
@@ -3001,7 +3127,7 @@ below.
|
|
|
3001
3127
|
readShim: 'cap', scout: true })`, spine written to `<bundleDir>/runs/<runid>/spine.jsonl`.
|
|
3002
3128
|
A thrown `runJob` crash is caught and reported, exit `1`.
|
|
3003
3129
|
8. The worktree's `gate-audit.jsonl` is moved to `<bundleDir>/runs/<runid>/gate-audit.jsonl`
|
|
3004
|
-
(same relocation `scripts/run-u.mjs
|
|
3130
|
+
(same relocation `src/userrun.js`, lifted out of `scripts/run-u.mjs`, does for in-repo runs). `spentUsd`/`spendComplete`
|
|
3005
3131
|
are read off this run's own `job-end` spine event — never fabricated as `0` when unknown.
|
|
3006
3132
|
9. `appendHistory` — one `history.jsonl` line: `{ runid, at, outcome, spentUsd,
|
|
3007
3133
|
spendComplete, budgetUsd, maxWallMs, worktree, branch, bundleHash, approveHash }` (the
|
|
@@ -3035,9 +3161,92 @@ below.
|
|
|
3035
3161
|
when none matches — which, cross-machine, is the common case, since the historic mint's
|
|
3036
3162
|
real close-script path and the importer's own bundle path essentially never coincide.
|
|
3037
3163
|
|
|
3038
|
-
- **`bareloop history <bundleDir>`** → prints every `history.jsonl`
|
|
3039
|
-
|
|
3040
|
-
|
|
3164
|
+
- **`bareloop history <bundleDir>`** → prints every `history.jsonl` row (via
|
|
3165
|
+
`readHistoryLog` — `src/replayio.js`, PANEL-BUILD.md P0's last task; a malformed row is
|
|
3166
|
+
counted and skipped, never reprinted corrupt), re-serialized with `JSON.stringify` (byte-
|
|
3167
|
+
identical to the original line for every well-formed row, since `appendHistory` itself
|
|
3168
|
+
writes `JSON.stringify(row)` — `src/bundle.js`), then every bridge's `listingRow(b)` from
|
|
3169
|
+
`<bundleDir>/bridges`. Always exit `0`; "(no runs yet)" when `history.jsonl` doesn't exist.
|
|
3170
|
+
|
|
3171
|
+
- **`bareloop replay <spine.jsonl>`** / **`bareloop replay --all <dir>`** (PANEL-BUILD.md
|
|
3172
|
+
P0, last of the rung's four tasks) → the read side for the panel's History/Run/Audit/Job
|
|
3173
|
+
tabs: point it at one run's spine (+ its `-gate-audit.jsonl` sidecar, resolved
|
|
3174
|
+
automatically) and get `formatReplay`'s one-page report; point `--all` at a directory and
|
|
3175
|
+
get every spine found directly in it as one aligned table row each
|
|
3176
|
+
(`formatAllLines`/`summarizeForAllLine`). Read-only, $0, mints no verdict, writes nothing.
|
|
3177
|
+
Lifted verbatim out of `scripts/run-replay.mjs` into `src/replayio.js` (see the Public API
|
|
3178
|
+
entry below); that script is now this command's own thin adapter (`bareloop replay` is a
|
|
3179
|
+
fresh sub-command name — `bareloop`'s own numbered menu is unchanged, same reasoning as
|
|
3180
|
+
`run-u`/`interview`/`author` above). A `.jsonl` that carries no `job-start`/`run-start`
|
|
3181
|
+
record — content-detected, never by filename pattern (the real archive uses `u-<id>`,
|
|
3182
|
+
`battery-A1-<id>`, `reuse-<id>`, `job2-<id>`, and more, with no shared convention) — errs
|
|
3183
|
+
`does not look like a spine`, exit `1`, rather than silently printing an empty report; a
|
|
3184
|
+
directory of only such files under `--all` prints each as `<name> not-a-spine` instead of
|
|
3185
|
+
a blank table. Exit `0` on a normal report/listing (regardless of the RUN's own outcome —
|
|
3186
|
+
this command reports, it never grades), `1` on a missing file/directory or a non-spine
|
|
3187
|
+
file, `1` with a usage line on no args.
|
|
3188
|
+
|
|
3189
|
+
- **`bareloop run-u <flags…>`** (PANEL-BUILD.md P0 task 2/4) → the person-path run flow
|
|
3190
|
+
(the JOBS-table/`--spec` runner, resume, the review door — `docs/logs/FINDINGS.md`'s
|
|
3191
|
+
U-mode). `src/cli.js`'s `run-u` dispatch hands `rest` straight to
|
|
3192
|
+
**`src/userrun.js`'s own `main(argv, deps)`**, unparsed — that function is the flag
|
|
3193
|
+
grammar's ONE owner, and `scripts/run-u.mjs` calls the exact same function directly, so
|
|
3194
|
+
there is exactly one place this grammar is parsed, never two that can drift). Flags:
|
|
3195
|
+
`--job <key>` (one of the JOBS table's rows) **or** `--spec <path>` (an authored
|
|
3196
|
+
`resolved-spec.json`, F185) — exactly one, never both, never neither; `--resume
|
|
3197
|
+
<runid|path>`; `--door <runid|path>` with `--decide <rerun|accept|pause>` and (`rerun`
|
|
3198
|
+
only) `--text <…>`; `--review-door`; `--approve <specHash>`; `--registry <dir>` +
|
|
3199
|
+
`--workflow <name>`; `--model <sonnet|haiku>`; `--read-shim <off|cap|diff|all>`; `--scout
|
|
3200
|
+
<on|off>`. No key/no injected `deps.provider`: prints the preview (invocation hints, the
|
|
3201
|
+
right key name for the job's own provider) and exits `0`, spending nothing — the same
|
|
3202
|
+
preview `scripts/run-u.mjs --job <x>` prints with no `ANTHROPIC_API_KEY`/`OPENAI_API_KEY`
|
|
3203
|
+
set. `scripts/run-u.mjs` is unchanged in behaviour: it is the pre-existing thin adapter
|
|
3204
|
+
over this same `src/userrun.js:main`, not rewired to go through `src/cli.js`.
|
|
3205
|
+
|
|
3206
|
+
- **`bareloop interview <flags…>`** (PANEL-BUILD.md P0 task 3/4) → the close-authoring
|
|
3207
|
+
interview, at the terminal, one question at a time (D10; `docs/logs/FINDINGS.md`'s
|
|
3208
|
+
interview surface). `src/cli.js`'s `interview` dispatch hands `rest` straight to
|
|
3209
|
+
**`src/interviewrun.js`'s own `main(argv, deps)`**, unparsed — the same one-owner shape
|
|
3210
|
+
`run-u` uses, and `scripts/run-interview.mjs` calls the exact same function directly.
|
|
3211
|
+
Flags: `--verdict <green|soft-green>`, `--provider <anthropic-api|openai-api|gemini-api>`,
|
|
3212
|
+
`--out <outdir>` (all three required, no defaults — PRD item 34 L17: a defaulted provider
|
|
3213
|
+
would silently re-lock every interview onto one vendor) plus optional `--budget <usd>` (the
|
|
3214
|
+
AUTHORING ceiling, unbounded if omitted) and `--base-url <url>`. It never talks to a
|
|
3215
|
+
provider itself: Source and Destination are its own first two questions (proven
|
|
3216
|
+
mechanically, $0, via `prepareSource`/`proveDestination`), then the picked class's frozen
|
|
3217
|
+
question set (`questionsFor`/`requiredAnswersFor`, `src/authorjob.js`), then the operator's
|
|
3218
|
+
own job name/budget/wall. On success it writes `answers.json`/`specdraft.json` under `--out`
|
|
3219
|
+
and either offers to spawn `scripts/run-author.mjs` (only when a clean key is present and no
|
|
3220
|
+
install gap remains — default answer is no) or prints the exact command to run it later.
|
|
3221
|
+
Because this module reads a real TTY/piped stdin, `src/cli.js` hands it `stdin`/`stdout`/
|
|
3222
|
+
`stderr` (the raw streams, not the wrapped `out`/`err` line-functions every other command
|
|
3223
|
+
here uses) rather than re-deriving them.
|
|
3224
|
+
|
|
3225
|
+
- **`bareloop author <flags…>`** (PANEL-BUILD.md P0 task 4/4) → the authoring pipeline: a
|
|
3226
|
+
real scout over a real repository, a real model filling the close declaration, D9's three
|
|
3227
|
+
mechanical gates, stopping at `prepareSigning` (it never signs and never runs the job —
|
|
3228
|
+
signing and running are `run-u`'s territory). `src/cli.js`'s `author` dispatch hands `rest`
|
|
3229
|
+
straight to **`src/authorrun.js`'s own `main(argv, deps)`**, unparsed, the same one-owner
|
|
3230
|
+
shape; `scripts/run-author.mjs` calls the exact same function directly. Flags: `--source
|
|
3231
|
+
<tree>` (a PREPARED tree a source door already froze — an unprepared path dies loud, naming
|
|
3232
|
+
the exact command to prepare one), `--answers <answers.json>`, `--draft <specdraft.json>`,
|
|
3233
|
+
`--verdict <green|soft-green>`, `--out <outdir>` (all required) plus optional `--timeout
|
|
3234
|
+
<ms>` (default 300000, per close stage) and `--budget <usd>` (the AUTHORING ceiling —
|
|
3235
|
+
unbounded, and announced as such, if omitted). A REFUSAL IS A RESULT: every stop (an
|
|
3236
|
+
interview refusal, an unauthorable close, a stage that cannot run, nothing red at the seed)
|
|
3237
|
+
is written to `--out` and counted as admission demand on the module's own spine
|
|
3238
|
+
(`author-<runid>.jsonl`), never swallowed. Exit vocabulary: `0` prepared/signable, `1` a
|
|
3239
|
+
refusal or a failed gate, `2` operator/config (including a malformed or missing provider
|
|
3240
|
+
key), `3` a secret-shaped leak in what this run wrote, `4` an unhandled crash (`author-crash`
|
|
3241
|
+
+ `author-end{outcome:'crashed'}` on the spine — a spine that stops mid-sentence is
|
|
3242
|
+
indistinguishable from a run still in flight, F191). A real OS signal (SIGINT/SIGTERM/
|
|
3243
|
+
SIGHUP) during the paid span still writes `author-killed` + `author-end{outcome:'killed'}`
|
|
3244
|
+
and then RE-RAISES the signal itself (128+signo), never invents an exit code; SIGKILL is
|
|
3245
|
+
named but, being uncatchable, cannot be covered. The library function itself never calls
|
|
3246
|
+
`process.exit()` anywhere, on any path — every stop is a returned exit code (an internal
|
|
3247
|
+
`throw new ExitSignal(n)`, caught at the bottom of `main`), the same "a library function
|
|
3248
|
+
returns a code / throws" rule `src/userrun.js` already keeps. Like `interview`, `src/cli.js`
|
|
3249
|
+
hands it raw `stdin`/`stdout`/`stderr` (its one interactive seam is the confirm turn).
|
|
3041
3250
|
|
|
3042
3251
|
**Tighten-only budget/wall.** `--budget`/`--wall` on `bareloop run` may only lower the
|
|
3043
3252
|
bundle's own signed `budgetUsd`/`maxWallMs` — `checkEnvelope`'s `envelope-widen` red refuses
|
|
@@ -3202,6 +3411,37 @@ freeze loop never trusts a buffer read a moment earlier), `into-exists`,
|
|
|
3202
3411
|
refusal never throws, never overwrites a person's file, and never lands a destination inside
|
|
3203
3412
|
the run's own scratch tree.
|
|
3204
3413
|
|
|
3414
|
+
**`prepareSource` copies ONLY git-tracked files** (hamr's ruling, unchanged) — a JS/TS repo's
|
|
3415
|
+
prepared copy therefore never carries `node_modules`, and every close stage that needs a tool
|
|
3416
|
+
(`tsc`, a test runner) would instrument-stop with nothing telling the person why. bareloop
|
|
3417
|
+
NEVER runs an install itself — it only ever touches gated primitives, never a shell-out to
|
|
3418
|
+
`npm`/`pnpm`/`yarn`/`bun` on the person's behalf. `missingDependencies` is exported ($0,
|
|
3419
|
+
JS/TS only for now — other languages return `null`, M3b's job): it takes `(treeDir,
|
|
3420
|
+
sourceSubdir?)` and walks from `sourceSubdir` up to `treeDir` (the same nearest-manifest rule
|
|
3421
|
+
`detectLanguage` uses, done independently to avoid a `source.js` ⇄ `detectlang.js` import
|
|
3422
|
+
cycle), and when the nearest `package.json` names a non-empty `dependencies`/`devDependencies`
|
|
3423
|
+
with no sibling `node_modules`, it returns `{manager, command, reason}` — the exact install
|
|
3424
|
+
line (`npm ci`/`pnpm install --frozen-lockfile`/`yarn install --frozen-lockfile`/`bun install
|
|
3425
|
+
--frozen-lockfile` by lockfile, else `npm install`), `cd`-prefixed for a subfolder job. An
|
|
3426
|
+
invalid `package.json`, no manifest found, or `node_modules` already present all return `null`
|
|
3427
|
+
— "cannot tell" is never a fabricated gap. `run-interview.mjs` prints the gap and the exact
|
|
3428
|
+
command right after `prepareSource` succeeds, and now PAUSES there (F182 fix, 2026-09-15):
|
|
3429
|
+
"Press Enter once it has finished to check again, or type skip to carry on without it" —
|
|
3430
|
+
Enter re-runs `missingDependencies` on the spot ("packages found — carrying on." or "still
|
|
3431
|
+
missing (…) — try again, or type skip to carry on without it.", looping), `skip` (or end of
|
|
3432
|
+
input, treated the same) carries on unresolved. It re-checks once more at the hand-off: a
|
|
3433
|
+
still-open gap suppresses the "Run it now?" offer entirely (the same shape the unkeyed-shell
|
|
3434
|
+
gate already uses) rather than spending on a run that would only instrument-stop — this is
|
|
3435
|
+
what the pause makes reachable: before the fix the interview fell straight through to the
|
|
3436
|
+
class questions and only ever re-checked once, too late for the offer to ever fire.
|
|
3437
|
+
`run-author.mjs` refuses
|
|
3438
|
+
`source-deps-missing` (a `request-red`, routed through the same `refusalEvents()` channel
|
|
3439
|
+
`language-unsupported` uses) at $0, before the scout and before the provider/key are even
|
|
3440
|
+
read. Once the person installs packages into the copy themselves, `prepareSource` has already
|
|
3441
|
+
hidden `node_modules` from `changedSet` via the copy's own PRIVATE `.git/info/exclude` (F177),
|
|
3442
|
+
so those installed files never read as the worker's own writes — the source repo's tracked
|
|
3443
|
+
`.gitignore` is never touched.
|
|
3444
|
+
|
|
3205
3445
|
**Destination is a DIRECTORY, never a filename** (D3 rework, hamr's ruling, 2026-09-12,
|
|
3206
3446
|
condensed in `docs/product/ITEM33-BUILD.md`). It may already exist and need not be empty; it
|
|
3207
3447
|
may sit inside the source itself (the same-dir case: "i can limit your actions to a certain
|
|
@@ -3229,7 +3469,8 @@ on a green then that would be the place agent allowed to do changes") — it nam
|
|
|
3229
3469
|
FENCE inside the copied repo, the signed `writeScope` field's job (`src/job.js:363`), never a
|
|
3230
3470
|
filesystem drop-off point. It is recorded as declared, never proven by `proveDestination` and
|
|
3231
3471
|
never handed to `copyOut` (`frontDoorFromManifest` returns `null` for a repo manifest).
|
|
3232
|
-
Wiring it into `writeScope` landed at M3, ruling 2 — `
|
|
3472
|
+
Wiring it into `writeScope` landed at M3, ruling 2 — `src/interviewrun.js` (lifted out of
|
|
3473
|
+
`scripts/run-interview.mjs` by PANEL-BUILD.md P0) parses its own
|
|
3233
3474
|
Destination answer straight into `draft.writeScope` for a repo Source; this door itself still
|
|
3234
3475
|
only records what it was told, unvalidated, exactly as before.
|
|
3235
3476
|
|
|
@@ -3252,7 +3493,7 @@ run — the same way `bareloop run <bundle> --repo <path>` varies the repo insta
|
|
|
3252
3493
|
touching the bundle's own signature. So `job.js`/`jobSpecHash` carry nothing about it: the
|
|
3253
3494
|
run's own instance lives entirely in the manifest `prepareSource` writes.
|
|
3254
3495
|
|
|
3255
|
-
`scripts/run-u.mjs` reads that manifest via `readSourceManifest(dirname(workdir))` (`workdir`
|
|
3496
|
+
`src/userrun.js` (lifted out of `scripts/run-u.mjs` by PANEL-BUILD.md P0) reads that manifest via `readSourceManifest(dirname(workdir))` (`workdir`
|
|
3256
3497
|
IS `<into>/tree`, so its parent is `<into>`) and reduces it with `frontDoorFromManifest`. No
|
|
3257
3498
|
manifest → no front-door behaviour at all — every existing repo-patient job is untouched. A
|
|
3258
3499
|
manifest that exists but cannot be read or parsed is a named stop, never a silent skip. When
|
|
@@ -3269,6 +3510,23 @@ Not wired yet, named rather than silently absent: `bareloop run` (the exported b
|
|
|
3269
3510
|
33/M5. The intake form that lets a person fill in Source/Destination through the authoring
|
|
3270
3511
|
interview (rather than the CLI above) is item 33/M3, not yet built.
|
|
3271
3512
|
|
|
3513
|
+
**Running an authored spec needs no developer hand-step any more (F185, fixed 2026-09-16).**
|
|
3514
|
+
`scripts/run-u.mjs --spec <path to resolved-spec.json>` is an alternative to `--job <key>`
|
|
3515
|
+
(both, or neither, is a loud $0 refusal): it `validateJob`'s the spec on disk explicitly and
|
|
3516
|
+
early, then finds the ONE `source-*/` sibling directory beside it that carries a
|
|
3517
|
+
`source.json` (`run-interview.mjs` prepares the source door and invokes `run-author.mjs`
|
|
3518
|
+
against the same `--out`, so `resolved-spec.json` and `source-<runid>/{source.json,tree/}`
|
|
3519
|
+
are always siblings) and reads the workdir (`<that dir>/tree`) and seed off that SAME
|
|
3520
|
+
manifest `readSourceManifest` already serves elsewhere — never typed, guessed, or defaulted.
|
|
3521
|
+
No sibling, more than one, a missing seed, or a gone tree each refuse by name. Everything
|
|
3522
|
+
past that resolution — the `--approve <jobSpecHash>` signature, budget/wall ceilings,
|
|
3523
|
+
coldReset, the close-first precheck — is untouched: `--spec` only NAMES the job, exactly as
|
|
3524
|
+
a `--job` row does. `src/authorrun.js`'s (lifted out of `scripts/run-author.mjs` by
|
|
3525
|
+
PANEL-BUILD.md P0) "SIGNING PREPARED — NOT SIGNED" screen now
|
|
3526
|
+
prints the ready-to-paste `--spec` command (real path, real `--approve <hash>`, the
|
|
3527
|
+
provider's real env-key per F187) — a person finishing the interview alone can reach a
|
|
3528
|
+
running job with no `jobs/` edit and no JOBS-table row.
|
|
3529
|
+
|
|
3272
3530
|
## Architecture
|
|
3273
3531
|
|
|
3274
3532
|
Three layers. An **outer shell** (dumb, permanent): per-run budget cap via bareguard,
|
|
@@ -3337,6 +3595,18 @@ under it — this is a passthrough, not a jail.
|
|
|
3337
3595
|
denies the gate audit path and every `ARBITER_BOOK_STORES` path under the workdir
|
|
3338
3596
|
(`src/authorscout.js:295`); a plan step's read scope is the workdir alone
|
|
3339
3597
|
(`src/planrun.js:1894`).
|
|
3598
|
+
- **A signed fence can never name `.git` or `node_modules` (F178/F177).** `scopeContained`
|
|
3599
|
+
(`src/validate.js`, design law #1's own function — `validateJob`'s `writeScope` and
|
|
3600
|
+
`validatePlan`'s step `scope`/exit `target`/`path` all go through it) refuses any fence/scope
|
|
3601
|
+
whose normalized prefix has a whole path segment equal to `.git` or `node_modules` — not a
|
|
3602
|
+
look-alike like `.github/**`, `my.git/**`, or `node_modules_util/**`. That alone cannot catch
|
|
3603
|
+
a NESTED `node_modules` reachable through an otherwise-legal fence (a monorepo package scope,
|
|
3604
|
+
or one a worker creates fresh during the run) — bareguard's `fs.deny` is exact
|
|
3605
|
+
prefix-containment only, no glob, so it cannot express "at any depth" even if it were used
|
|
3606
|
+
here. **`fs.deny` is deliberately NOT used for either book**: it blocks ALL fs actions,
|
|
3607
|
+
reads included, and hamr's ruling blocks WRITING only — reading stays allowed. The runtime
|
|
3608
|
+
belt is `tools.denyArgPatterns` (`FORBIDDEN_WRITE_SEGMENT_PATTERN`, `src/planrun.js`): keyed
|
|
3609
|
+
to `write`/`edit` ONLY, matched against the serialized action's `path` field, whole-segment.
|
|
3340
3610
|
|
|
3341
3611
|
**What it does not guarantee:** no network egress control and no container/VM isolation —
|
|
3342
3612
|
this repo has neither built nor documented either. `CLOSE_ENV_DENY`'s deny rule matches
|