bareloop 0.8.0 → 0.9.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.
@@ -98,6 +98,7 @@ the step plan at run time (gated by `validatePlan`); the human signs only:
98
98
  | `goal` | non-empty text | what the agent plans against |
99
99
  | `verdictType` | `green` \| `soft-green` \| `hitl` | declared radio, never inferred (`VERDICT_TYPES`, frozen). v1 ADMITS only `green`; declaring `soft-green`/`hitl` reds `request-red` with the type as a structured `verb` field (declared-but-locked — the tool-menu pattern). Every `request-red` also carries `lib` — the territory the demand lands against, stamped at the emit site (`verdictType` → `bareloop`, a locked tool verb → `bare-agent`): the ledger keys and its `suggestedAsk` seed on it, so a bareloop-catalogue refusal never files as an upstream ask |
100
100
  | `close` | **an ORDERED LIST of named stages** `[{ name, cmd, expect, judged?, gapKeep?, offer?, needs? }, ...]` (PRD v1.28), or a close object (table below) **only** for the declared-but-locked verdict classes | the destination, the only thing hand-authored; the check menu DERIVES from it (below). The plan flow executes a staged close directly and adapts a bare `predicate` object into a one-stage list; a `gold`/`rubric`/`hitl` object close validates (the declared-but-locked verdict classes still parse) but the plan flow refuses it at runtime as `close-unsupported` — it names no command to run |
101
+ | `closeDecl` | **the AUTHORED close** `{ genre: "TYPES", lang: "js"\|"python", stages: [{ name, kind, params }], notes? }` | the ALTERNATIVE to `close`, and the point of the close-authoring rung: the user answers an interview and an LLM composes a DECLARATION over kinds whose implementations we own — never a script, never a shell fragment, never a new kind. **`close` and `closeDecl` are alternatives**: declaring both reds `close-duplicated` (two closes are two arbiters). It is HARD-class by construction (`CLASS_BY_CLOSE.declared`), so a locked verdict on one reds `close-hierarchy` as well as `request-red`. The declaration is validated by `validateCloseDecl`; the TREE-GROUNDED half of that gate (the path rule, and the scoped-job derivation that arms the F84 one-population law) is DEFERRED at spec-validation time — a job spec is validated with no repository in hand — and the runner re-runs it GROUNDED against the real seed before any stage and before any token. It stores the counting RULE and never a number (D12): there is no seed field, and `baseline: "seed"` is measured at each run's own HEAD |
101
102
  | `checks` | **RETIRED** (PRD v1.28/v1.32) | hand-authored checks are gone, not merely discouraged: declaring `checks` reds `checks-derived` by name. The check menu is DERIVED from the close's own stages instead — see **Staged close** below. The hazard this removes is measured, not theoretical: job #5's three hand-written checks were re-implementations of three stages the close already ran, and a hand-carved copy can drift LENIENT (the worker passes the operator's ruler and fails the real inspection) |
102
103
  | `tools` | optional unique subset of `TOOL_MENU` (14 verbs, below) | the CEILING every plan step's grant must fit inside (omitting it means the full menu — and the hash is taken over that RESOLVED form, so a `TOOL_MENU` widening flips an omitted-`tools` spec's hash and forces a re-sign; see `jobSpecHash`, MED-1); `run` is `LOCKED_TOOLS` and reds `request-red` — locked-but-listed, and the red IS the admission evidence the ledger tallies (a typo stays `invalid-value`). A ceiling of write-class and store-class verbs ONLY reds `invalid-value`: the scout surveys read-only, so it would be handed an empty menu and survey blind |
103
104
 
@@ -306,9 +307,12 @@ on refusals). It binds WRITES, not observations: on the exit side it fires only
306
307
  the narrow one does), and `artifact-written`/`json-valid` paths carry no step-scope red at
307
308
  all — the evaluator asks nothing about who wrote the file, so naming a prior step's artifact
308
309
  is legal and satisfiable. `job-invalid` — a plan validated against a
309
- missing or non-plan-shape job fails CLOSED). `stageClose(close)` is the ONE staging
310
- every check-menu consumer shares (array → itself; legacy object predicate → its
311
- one-stage list, named `close`; gold/rubric/hitl → null). The `secret-literal` sweep is
310
+ missing or non-plan-shape job fails CLOSED). `closeStagesOf(job)` is the ONE staging
311
+ every check-menu consumer shares — it reads whichever close field the spec carries:
312
+ `closeDecl` → its declared stages (each enriched with the arbiter's own `gapKeep`), else
313
+ `stageClose(close)` (array → itself; legacy object predicate → its one-stage list, named
314
+ `close`; gold/rubric/hitl → null). A consumer reading `close` alone would see a declared
315
+ job as closeless and offer the drafter an empty check menu. The `secret-literal` sweep is
312
316
  defense-in-depth against known token shapes — env-only loading remains the law, not the
313
317
  sweep.
314
318
 
@@ -449,7 +453,117 @@ retired-but-accepted, so a plan carries no iteration bound to check; `scopes` is
449
453
  `tree-changed` scope menu — the array of scope strings the drafter prompt listed, which
450
454
  `runJob`/`runPlan` build internally from the signed fence plus the real directories beneath
451
455
  it (a direct `validatePlan` caller may pass its own array); omitted, it derives from the
452
- signed `writeScope` alone, never a free-text fallback. Menus exported: `EXIT_TYPES`, `MAX_EXITS_PER_STEP`, `MAX_PLAN_STEPS`, `WRITE_VERBS` — plus `stageClose`.
456
+ signed `writeScope` alone, never a free-text fallback. Menus exported: `EXIT_TYPES`, `MAX_EXITS_PER_STEP`, `MAX_PLAN_STEPS`, `WRITE_VERBS` — plus `stageClose` and `closeStagesOf`.
457
+
458
+ ### Close authoring — the user declares what done means (`src/kinds.js`, `src/authoring.js`, `src/authorscout.js`, `src/authorflow.js`, `src/declaredclose.js`, `src/authorjob.js`)
459
+
460
+ The one layer where a human still wrote code. A close used to be a hand-written script
461
+ per patient; now the user PICKS A VERDICT CLASS, answers that class's questions, and an LLM
462
+ composes a DECLARATION over kinds whose implementations bareloop owns. **v1 admits exactly
463
+ ONE genre** (`TYPES`: a type checker stops complaining without breaking the tests) and
464
+ exactly one verdict class (`green`) — everything else refuses honestly, and every refusal
465
+ is COUNTED.
466
+
467
+ **The verdict class is the USER's answer (PRD v1.57 §1), and it DRIVES the authoring.**
468
+ `verdictType` is a declared radio the preflight validates, never inferred:
469
+ `VERDICT_CLASSES` = `green` | `soft-green` | `hitl`, with `LOCKED_CLASSES` =
470
+ `soft-green` | `hitl` declared-but-locked. Picking a locked one returns a counted
471
+ `request-red` refusal at ADMISSION — before that class's questions are ever asked. The
472
+ pick is also a PROMISE: every catalogue kind carries the `verdictClass` it can honestly
473
+ render, `closeCeiling(declaration)` reports the highest class a declaration demands, and a
474
+ declaration ABOVE the pick is a `class-ceiling` red naming the kind that raised it (inert
475
+ in v1 by construction — every live kind is mechanical).
476
+
477
+ **The pipeline, in call order.** Each piece is exported so an adopter can drive it, cache
478
+ a step, or test it without a provider.
479
+
480
+ | step | call | what it is |
481
+ |---|---|---|
482
+ | interview | `runInterview({ verdictType, answers, repoPath })` → `{ ok, answers, verdictType, refusal, reds }` | PURE — no model, no repo, no clock. THREE frozen question sets keyed by verdict class (`QUESTION_SETS`; `questionsFor(cls)` / `requiredAnswersFor(cls)`), and the interview asks NOTHING about a genre. The green set is six questions, answers keyed by number; the soft-green and hitl sets are named but LOCKED (`questions: null`, absent rather than empty) and their selection refuses before they run. `verdictType` and `repoPath` are STRUCTURED input, never parsed out of prose. Answers are scrubbed at INGEST |
483
+ | survey | `runAuthorScout({ workdir, provider, attempts? })` → `{ state, facts, reason, meta, raw, raws, calls }` | 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 |
484
+ | 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 |
485
+ | authoring | `authorClose({ workdir, seedRef, lang, verdictType, answers, scout, listing, generate })` | 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 |
486
+ | everything above, composed | `authorCloseForJob({ verdictType, answers, repoPath, lang, generate, ... })` → `{ 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 |
487
+ | 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 |
488
+ | the three gates | `prepareSigning({ spec, workdir, seedRef? })` → `{ ok, specHash, gates, work, guards, refusal }` | D9, and it NEVER signs |
489
+
490
+ **D5 — the mandatory guards are SHOWN and FIXED, and the battery keys off the VERDICT
491
+ CLASS** (PRD v1.57 §2 — what a battery is FOR is the class of dishonesty that class of
492
+ verdict admits). `CLASS_BATTERIES` is the attachment point (structure, un-removability,
493
+ which slot is the model's); the tool-specific CONTENTS — which suppression patterns, which
494
+ extensions — resolve at COMPOSITION from the language the declaration names. The green
495
+ battery injects `changed-from-seed`
496
+ and `no-suppressions` FULLY PARAMETERISED (`classGuards({ verdictType, lang })`, which
497
+ THROWS rather than ever hand back an empty battery for a known class and language); the
498
+ model fills exactly one
499
+ slot (the target prefixes) and cannot change, drop, rename, re-kind or NARROW them —
500
+ `validateDeclaration` reds `guard-weakened` on any of it, including on an added `scope` that
501
+ would shrink what the suppression scan covers. `genreEnv(lang, { sourcePrefixes })` is the
502
+ same idea for environment: `MYPYPATH` on a Python patient is a fact no user can supply and
503
+ no model found, it moves no seed number, and a declaration that authors it itself reds
504
+ `genre-owned-env`.
505
+
506
+ **The kind catalogue** (`KIND_CATALOGUE`, and it IS the whole vocabulary): `command-exit`,
507
+ `count-not-worse`, `pattern-absent-in-diff`, `files-changed` are live; `judged-floor` and
508
+ `human-confirms` are NAMED BUT LOCKED (and carry `verdictClass` `soft-green`/`hitl`), so
509
+ declaring one is a counted `locked-kind` red rather than an unknown-kind typo; `harness-loop` (TESTGEN) is ABSENT from v1 entirely. In
510
+ tool mode a locked kind is INEXPRESSIBLE — the schema carries one branch per live kind — so
511
+ that demand arrives through the interview layer instead (`refuseLockedKind(kind)`).
512
+
513
+ **The three gates, and the signature (D9).** Nothing LLM-judges a close.
514
+ 1. `validateCloseDecl(closeDecl, { verdictType, ... })` — schema, kinds, params, the F84
515
+ one-population law, F49's static nested-quantifier reject, the D5 guard equality, the
516
+ class ceiling, and the listing rule (a declared path SELECTS from the seed tree or it
517
+ does not exist). `verdictType` is REQUIRED and has no default: the guard battery hangs
518
+ off it, and a validator with no class in hand reds `class-absent` rather than reporting
519
+ a declaration it never checked D5 against.
520
+ 2. the CLOSE PRECHECK — every stage runs against the real patient. A stage that cannot run
521
+ is `broken-close`: a CASUALTY, never a red.
522
+ 3. the SEED-VERDICT READ — every stage, offered or not, at the seed. Which are RED (that is
523
+ the work) and which are GREEN (those are the guards) is handed back for the user to read.
524
+ **A close with no WORK stage red at seed is refused decision-ready** — it grades nothing,
525
+ and an instrument that scans nothing reads clean exactly like one that measures correctly.
526
+
527
+ Then the human signs `specHash`, unchanged: the approvals array and the human's word are
528
+ the arbiter relocating to the user, not disappearing. **Re-authoring is a spec edit (D6)** —
529
+ a different declaration is different bytes, a different resolved hash, and a new signature.
530
+ The guards are stored ENUMERATED and every short-form parser is expanded, so no
531
+ omittable-with-a-default field can change what runs without changing what was signed.
532
+
533
+ **Refusals are COUNTED (D13).** `refusalEvents(refusal)` returns the spine events: a
534
+ `job-red` carrying `{ code: 'request-red', verb, lib: 'bareloop' }` — the `lib` stamped at
535
+ the EMIT SITE, so `classifyIncidents` files it against bareloop's own catalogue and its
536
+ `suggestedAsk` reads `bareloop: …` rather than seeding an upstream ask — plus one
537
+ decision-ready `escalation` under `close-unauthorable`, which is in the ledger's excluded
538
+ set precisely because the demand is already counted once. A refusal that is a STOP rather
539
+ than demand (a broken instrument, a close with nothing to do) emits the escalation ALONE.
540
+ **Known limit, stated:** v1's derivation cannot tell `soft-green` from `hitl` and does not
541
+ try — both refuse under one verb (`non-green-verdict`), because naming which locked class a
542
+ job belongs to would be a guess about prose.
543
+
544
+ **At run time** the declared close is executed by the kind executor, never compiled down to
545
+ shell (that would turn owned kinds back into authored strings — the exact thing D3 makes
546
+ inexpressible). `runPlan` picks the executor from which field the signed spec carries and
547
+ everything downstream is the same code: first-red-wins, the `CLOSE_FAULTS` forbidden zone
548
+ (routed by a TYPED fault, so a timeout still offers "raise the close timeout"), the same
549
+ `close stage "<name>" failed:` gap header the trend reader parses, the same scrub at the
550
+ emission boundary, and the check menu still derived from stage names one hop. The seed is
551
+ READ at run start (`seedAtHead`) and recorded on a `close-decl` spine event together with
552
+ the grounded re-validation's result. Direct executor access: `runDeclaredStages(stages,
553
+ redact, { cwd, seedRef, timeoutMs })` returns `runClose`'s verdict shape **plus one added
554
+ field, `notes`** — the DIAGNOSTIC channel, per stage and on the summary, ABSENT when empty
555
+ (never `''`). M1's kinds announce some things whichever way a stage lands (a declared env var
556
+ that was dropped, the scope filter's own arithmetic), and a RED carries those out inside its
557
+ `gap` while a GREEN and an instrument stop had nowhere to put them — checking the GREENS is
558
+ doctrine, and an audit trail that only survives a red cannot do it. It is deliberately NOT
559
+ `gap`: every consumer guards with `if (gap)`, so a green carrying one would read as
560
+ revision-worthy and a stop carrying one would look like a verdict it never rendered. **Nothing
561
+ routes on it, nothing bounds on it, and no verdict moves because of it** — it rides the records
562
+ that already spread the whole verdict (`close-precheck`, `outer-close`, ralph's
563
+ `close-verdict`). `runDeclaredClose`
564
+ and `seedRead` are the kind executor's own two entries (`runDeclaredClose` is named apart
565
+ from ralph's shipped `runClose` because one runs a DECLARATION and the other an argv);
566
+ `closeStagesOf(job)` is the ONE staging every close consumer reads, widened to both fields.
453
567
 
454
568
  ### `snapshotScope(dir, scope)` / `evalExits(exits, { dir, snapshot?, runCheck? })` — `src/exits.js`
455
569
 
@@ -504,10 +618,10 @@ Reserved spine vocabulary (V7, machinery-free until job #1 surfaces one):
504
618
  `coordination-red` — a failure between units (scope contention, step order, store
505
619
  races), never to be folded into worker/interpreter reds.
506
620
 
507
- ### `runJob(spec, { approvals, workdir, provider, nativeProvider?, providerFor?, emit, capRuns?, strikeLimit?, shellCapUsd?, closeTimeoutMs?, layerRoot?, bridge?, priorSpentUsd?, priorSpendComplete?, priorWallMs?, resumeSeed?, resumeGrades?, resumeReplans? })` → outcome — `src/run.js`
621
+ ### `runJob(spec, { approvals, workdir, provider, nativeProvider?, providerFor?, emit, capRuns?, strikeLimit?, shellCapUsd?, closeTimeoutMs?, layerRoot?, bridge?, priorSpentUsd?, priorSpendComplete?, priorWallMs?, resumeSeed?, resumeGrades?, resumeReplans?, resumeBranch? })` → outcome — `src/run.js`
508
622
 
509
- The last six are the RESUME fold and are documented under *Resuming a killed run* below; they
510
- default to `0` / `true` / `0` / `null` / `[]` / `null`, so a fresh run passes none of them.
623
+ The last seven are the RESUME fold and are documented under *Resuming a killed run* below; they
624
+ default to `0` / `true` / `0` / `null` / `[]` / `null` / `null`, so a fresh run passes none of them.
511
625
  Three of them are folds of a bound the operator SIGNED (money, wall, replans) and one is a
512
626
  readout seed (grades) — the distinction matters and is spelled out there.
513
627
 
@@ -519,8 +633,11 @@ known-answer round-trip before tokens: `smoke-red` — a silent degradation thro
519
633
  `min(job budget − spent, shell cap)`.
520
634
 
521
635
  Outcomes: `green | already-green | escalated | unapproved-spec | job-red | smoke-red |
522
- plan-red | check-red | close-red | close-unsupported | recipe-stale | pricing-red |
523
- provider-red | interpreter-red | cap-halt | wall-halt | step-stalled | step-red:<id>`. `provider-red` is a
636
+ plan-red | check-red | close-red | close-unsupported | recipe-stale | branch-red | pricing-red |
637
+ provider-red | interpreter-red | cap-halt | wall-halt | step-stalled | step-red:<id>`.
638
+ `branch-red` is the WORK BRANCH refusing (below): the patient is not a git checkout, its
639
+ branch namespace has no free name, or a resume's recorded branch is gone. Zero tokens, and
640
+ never a fallback to working on the branch the run was handed. `provider-red` is a
524
641
  transport throw or a worker round the API cut off mid-generation (`truncated:max_tokens`,
525
642
  BA-6 — before which it laundered into a clean finish, F25): no verdict exists and the failed
526
643
  round's spend is only partly known (F6). `cap-halt` is the wallet; `wall-halt` is the clock
@@ -568,7 +685,9 @@ their own ledger): **close precheck** (`already-green` is a
568
685
  DISTINCT zero-token outcome; a forbidden-zone verdict escalates before spend) → **check-menu
569
686
  preflight** (`check-menu` names the derived menu, then every OFFERED stage's chain runs once
570
687
  at $0 — an unrunnable stage is a `check-red` stop before tokens, not a fault mid-plan) →
571
- **SCOUT** (read-only by construction: neither the
688
+ **THE WORK BRANCH** (below — created before the first paid call, `branch-red` if it cannot
689
+ be; on a RESUME it runs FIRST, ahead of the precheck and the preflight, because the recorded
690
+ branch IS the tree those instruments must measure) → **SCOUT** (read-only by construction: neither the
572
691
  write-class nor the store-class verbs are in its menu; hard-bounded rounds) → **PLAN** (the decompose call —
573
692
  the planner never sees the repo, only the scout blob; drafted against a schema
574
693
  description with check NAMES only; `validatePlan` gates it, one redraft with the reds
@@ -586,6 +705,56 @@ plan-red | check-red | close-red | wall-halt`. Worker prompts hold the v1.12 §5
586
705
  (mutation-proven): the absolute repo root, the step's action/target, prior artifacts,
587
706
  the gap — NEVER the budget, the close command, a check's command, or the arbiter's books.
588
707
 
708
+ **THE WORK BRANCH — a hard rule, and a REQUIREMENT on the patient (PRD v1.57 §3).** hamr's
709
+ ruling, verbatim: *"The agent creates a NEW BRANCH before it touches any code — a HARD RULE,
710
+ no exceptions. Not a default, not a preference: no job edits the branch it was handed, and
711
+ none edits `main`."* So **the patient must be a git repository with at least one commit** —
712
+ that is new, and it is the honest consequence of the rule rather than an option. And the
713
+ patient itself is **ALWAYS a separate COPY of the repo being treated, never the original**
714
+ (hamr, 2026-08-09): the branch bounds where writes land, but workers and close stages
715
+ still mutate the patient tree (branches per try, caches, worktrees) — the copy is the
716
+ blast radius; the original never carries it. What the run does, before its first paid
717
+ call:
718
+
719
+ - **The name is DERIVED from the SIGNED spec**, never model-authored (a branch name is
720
+ arbiter bookkeeping): `bareloop-` + the spec's own `job` slug — `bareloop-typecheck-fix`.
721
+ `workBranchName(spec)` is exported so a runner can show it at its approval gate. The goal
722
+ prose is deliberately NOT an input: it is edited between runs without changing what the
723
+ job is, and the branch name would move with it.
724
+ - **A name already taken is SUFFIXED** `-2`, `-3`… — a second run of the same spec is a
725
+ second blast radius and never reuses the first one's branch.
726
+ - **A RESUME returns to ITS OWN branch.** Pass `resumeBranch` (read off the dead spine by
727
+ `readResume` as `restart.branch`); without it the deterministic name would collide with the
728
+ killed leg's own branch and mint a `-2` beside the work the resume exists to keep. The
729
+ recorded branch is the ONLY branch a resume can land on — it is validated and checked out,
730
+ never re-derived from the spec and never suffixed. A recorded branch that no longer exists
731
+ is a `branch-red` STOP, never a fresh start.
732
+ - **On a RESUME this step runs FIRST**, ahead of the close precheck and the check preflight
733
+ (cold, it runs after them — see the already-green bullet). Both are $0 readings OF A TREE,
734
+ and on a resume the tree they must read is the recorded branch's: standing on whatever ref
735
+ the operator handed back, the precheck could read `already-green` off a tree that is not
736
+ this run's and every `baseline: "seed"` stage would baseline against it. Nothing is minted
737
+ by moving it — the resume arm creates nothing — and a resume whose branch is gone or foreign
738
+ now stops `branch-red` before the arbiter grades anything at all.
739
+ - **`work-branch { branch, created, resumed, from, base, repo, collided? }`** lands on the
740
+ spine at creation — the run's books say where its work went.
741
+ - **Every failure is `branch-red`**, escalated decision-ready with zero spend. There is no
742
+ fallback to the handed branch anywhere; and `mkWorker`, the ONE seam that grants
743
+ write-class verbs, refuses to build a write-capable worker with no branch prepared, so the
744
+ ordering is a convenience and the guard is the rule.
745
+ - **A COLD run's `already-green` tree leaves no branch behind** — the precheck returns above
746
+ this step, and a run with no work has no blast radius to bound. The clause is about
747
+ MINTING: a RESUME that reads `already-green` off its own branch (which it is standing on by
748
+ then) leaves that branch and the paid work on it exactly where they were.
749
+
750
+ **What it does NOT do.** bareloop never commits, so the branch bounds where work LANDS: the
751
+ tree stands on a branch nobody handed the run, and the handed ref is never moved or written
752
+ through. It is not containment — v1.41's local-trust model is unchanged, and worker/close
753
+ children still run as the operator's OS user with network egress. A workdir that is a
754
+ SUBDIRECTORY of a repository gets its branch in the enclosing repository (git resolves
755
+ upwards, exactly as every other git read in the flow does); the `work-branch` record's `repo`
756
+ field names which repository that was, rather than leaving it to be discovered.
757
+
589
758
  **The strike ladder — how a STEP ends (F77–F79).** A plan step is **not** bounded by a
590
759
  number of iterations. It runs until it stops making PROGRESS: a *strike* is a red iteration
591
760
  that repeats an already-seen gap or wrote nothing, and `strikeLimit` strikes (shell option on
@@ -713,7 +882,7 @@ is read, so a resume of a resume inherits the previous leg's chain and not the o
713
882
  (`createClock`, `src/clock.js`) from the signed `maxWallMs` and emits `wall-clock` with the
714
883
  requested AND enforced numbers up front, plus the `closeStages` count that explains the gap
715
884
  between them (`enforcedMs = maxWallMs + closeStages × closeTimeoutMs`, one expression — the
716
- stage count comes from `stageClose`, the same staging the runner executes). Enforcement is a
885
+ stage count comes from `closeStagesOf`, the same staging the runner executes). Enforcement is a
717
886
  **between-round deadline** — the only seam that exists, since `loop.stop()` cannot cut an
718
887
  in-flight call (F61: fired at 500ms,
719
888
  returned at 4,018ms) — so an attempt that crosses the deadline mid-flight emits `wall-bounded`,
@@ -1054,8 +1223,8 @@ the secrets scan.
1054
1223
  A reuse run is up to `tries + 1` full jobs long, so a kill mid-run is a real event and it
1055
1224
  must not cost the whole envelope. **`readResume(events, {deathAt?, direct?, resumableOutcomes?})`**
1056
1225
  reads the dead run's own spine back into the state a resume continues from: `{ started,
1057
- approvalHash, completed[], tried[], restart (with its step-level `seed` and its close
1058
- `grades`), r1Missing, spentUsd,
1226
+ approvalHash, completed[], tried[], restart (with its step-level `seed`, its close
1227
+ `grades`, and its work `branch`), r1Missing, spentUsd,
1059
1228
  spendComplete, carrySpentUsd, carrySpendComplete, ended, greened, … }`. Hand that object to
1060
1229
  **`runReuse({…, resume})`** and the run picks up where it stopped.
1061
1230
 
@@ -1079,10 +1248,17 @@ semantics are byte-unchanged (PRD v1.46 §3):
1079
1248
  work on disk and the plan on the spine. A green or any red stays non-resumable under every
1080
1249
  setting — a verdict already rendered is never re-bought.
1081
1250
 
1251
+ `restart.branch` is the WORK BRANCH the dead leg was working on, READ off its `work-branch`
1252
+ record and never re-derived: the name is deterministic from the signed spec, so a restart that
1253
+ recomputed it would collide with its own predecessor and be handed a fresh `-2` beside the work
1254
+ it came back for (the declared-fold precedent, `priorSpentUsd`'s exactly). Pass it on as
1255
+ `resumeBranch`; `runReuse` does it for you. `null` — a leg killed before it ever branched — is
1256
+ the cold path, and correct, because a leg that never branched has no work to strand.
1257
+
1082
1258
  `scripts/run-u.mjs --resume <runid|path>` is the operator-side consumer: it skips the patient
1083
1259
  reset (`resumeTreeGate` instead — a dirty tree is what a resume expects; only a moved HEAD
1084
- stops it), folds the halted run's money and wall in, re-enters at the checkpoint, and arms the
1085
- outside watchdog on the REMAINING wall. The top-up itself stays a spec edit the human signs.
1260
+ stops it), folds the halted run's money and wall in, returns to the dead leg's own work branch,
1261
+ 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.
1086
1262
  **A zero wall remainder REFUSES the launch** (exit 2, before the key and before the patient):
1087
1263
  W-2 says a run out of time keeps the grade it has and stops, so a resume with no time to start
1088
1264
  anything in is a decision, not an unbounded launch — and launching it used to hand the outside
@@ -1221,12 +1397,17 @@ whichever territory was seen first), `broken-close` (consumer-attributed),
1221
1397
  `verb` field, prose-quoted verb as legacy fallback; both classes take their `lib` from the
1222
1398
  red's own stamp, so neither seeds an upstream ask for a bareloop-catalogue refusal),
1223
1399
  `retention-red`, `config-red`
1224
- (drafting friction — attributed to bareloop's own schema/prompt). Deliberate exclusions
1400
+ (drafting friction — attributed to bareloop's own schema/prompt). Close-authoring
1401
+ refusals ride the SAME `request-red` channel with `lib: 'bareloop'` stamped at the emit
1402
+ site (`refusalEvents`), so genre/verdict/locked-kind demand is counted without ever
1403
+ seeding an upstream ask. Deliberate exclusions
1225
1404
  (`EXCLUDED_ESCALATIONS`, a runtime set — anything outside classified ∪ excluded is
1226
1405
  counted as unmapped, never dropped): `cap-halt`/`wall-halt` (budget stories, money and
1227
1406
  time), `step-stalled` (the stall fuse firing is our governance, not an observed provider
1228
1407
  failure), `step-variance` (a planning story), `gate-red`/`smoke-red` (governance working
1229
- as intended / already counted), `hitl-close`/`close-unsupported` (by design),
1408
+ as intended / already counted), `hitl-close`/`close-unsupported`/`close-unauthorable` (by design — and the last one
1409
+ is excluded for a SECOND reason: its demand is already counted once as the `request-red`
1410
+ the same refusal emits, so counting the escalation too would double every refusal),
1230
1411
  `close-timeout`/`close-killed`/`close-crashed` (the arbiter's own named terminals, F17);
1231
1412
  `close-verdict`/`artifact-red` stay worker stories, `pr-red` operator environment.
1232
1413
  `suggestedAsk` on every row is a template seed for an upstream ask — filing stays human;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bareloop",
3
- "version": "0.8.0",
3
+ "version": "0.9.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",