bareloop 0.10.0 → 0.11.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.
@@ -15,6 +15,20 @@
15
15
  > file ships with the package and is the complete adopter contract; the README is only
16
16
  > the pitch.
17
17
 
18
+ > **DIRECTION CHANGE — 2026-08-17 (design decided, NOT built; nothing below has moved yet).**
19
+ > **`hitl` is RETIRED as a verdict class.** The verdict classes become **`green` +
20
+ > `soft-green`**, and the hitl pause machinery is **RE-HOMED, not deleted**: it becomes the
21
+ > **review door** at the end of EVERY run (green and soft-green alike), where the person
22
+ > `accept`s, `rerun`s (their text is the gap, funded as a FRESH engagement with its own money
23
+ > and time) or `pause`s — and **the door never changes the loop's own verdict**. `soft-green`
24
+ > gets a `judged-floor` stage behind a signed rubric card and a signed calibration set.
25
+ > **Everything documented in this file describes the code as it actually is on this branch**,
26
+ > where `hitl` IS admitted, `human-confirms` IS live, and the three `hitl-*` terminals exist —
27
+ > integrate against what is written here, and expect the class-level rename when the next rung
28
+ > lands. Records: PRD Addendum v1.71 and
29
+ > `docs/02-features/2026-08-17-softgreen-review-door-design.md` (repo-only, not shipped);
30
+ > evidence F102–F105.
31
+
18
32
  ## What this is
19
33
 
20
34
  **"Automate this job — I don't know the best workflow."** bareloop runs tasks that are
@@ -96,7 +110,7 @@ the step plan at run time (gated by `validatePlan`); the human signs only:
96
110
  | field | shape | notes |
97
111
  |---|---|---|
98
112
  | `goal` | non-empty text | what the agent plans against |
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 |
113
+ | `verdictType` | `green` \| `soft-green` \| `hitl` | declared radio, never inferred (`VERDICT_TYPES`, frozen). **`green` and `hitl` are ADMITTED** (N4 slice 1) — *and `hitl` is RETIRED as a class by the 2026-08-17 design (see the direction note at the top): it still works exactly as documented here on this branch, and the forward path for a job needing judgement is `soft-green` plus the end-of-run review door*; `soft-green` is still declared-but-locked and reds `request-red` with the type as a structured `verb` field (the tool-menu pattern) until its judged floor exists. 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
114
  | `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
115
  | `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 |
102
116
  | `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) |
@@ -194,7 +208,9 @@ declared-but-locked verdict classes** (a close is data, never code; verdict-clas
194
208
  is a named red `close-hierarchy`). The go-forward shape is the staged list above; a
195
209
  `predicate` object is legal shorthand for a one-stage list and the plan flow adapts it, but
196
210
  `gold`/`rubric`/`hitl` validate and then refuse at runtime (`close-unsupported`) since v1
197
- admits `verdictType: green` only:
211
+ admits `verdictType: green` and `hitl` only — and N4's hitl is a STAGE
212
+ (`human-confirms` inside a `closeDecl`), never a close TYPE, so the `hitl` OBJECT form below
213
+ stays refused at runtime exactly as it was:
198
214
 
199
215
  | type | fields (exact — extras red) | legal class |
200
216
  |---|---|---|
@@ -475,7 +491,9 @@ is COUNTED.
475
491
  **The verdict class is the USER's answer (PRD v1.57 §1), and it DRIVES the authoring.**
476
492
  `verdictType` is a declared radio the preflight validates, never inferred:
477
493
  `VERDICT_CLASSES` = `green` | `soft-green` | `hitl`, with `LOCKED_CLASSES` =
478
- `soft-green` | `hitl` declared-but-locked. Picking a locked one returns a counted
494
+ `soft-green` alone (N4 slice 1 admitted `hitl`; `soft-green` waits for its judged floor —
495
+ **which is now the forward path: `hitl` is retired as a class by the 2026-08-17 design and its
496
+ pause becomes the end-of-run review door, unbuilt as of this branch**). Picking a locked one returns a counted
479
497
  `request-red` refusal at ADMISSION — before that class's questions are ever asked. The
480
498
  pick is also a PROMISE: every catalogue kind carries the `verdictClass` it can honestly
481
499
  render, `closeCeiling(declaration)` reports the highest class a declaration demands, and a
@@ -487,7 +505,7 @@ a step, or test it without a provider.
487
505
 
488
506
  | step | call | what it is |
489
507
  |---|---|---|
490
- | 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 |
508
+ | 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 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 two slots since it was frozen (D13's genre confirm, then the repo question, dropped 2026-08-15 because `repoPath` is mandatory structured input and asking for it again invites a second, drifting answer for a fact the machine already holds), and it renumbers CONTIGUOUSLY from 1 each time rather than leaving a gap. Today the green set is five questions, answers keyed by number; the HITL set is those five byte for byte plus one more (what the signer is deciding when they look at the result — `human-confirms`' `ask`), which is the spread's next number and moves with a green-side deletion; the soft-green set is named but LOCKED (`questions: null`, absent rather than empty) and its selection refuses before it runs. `verdictType` and `repoPath` are STRUCTURED input, never parsed out of prose. Answers are scrubbed at INGEST |
491
509
  | 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 |
492
510
  | 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 |
493
511
  | 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 |
@@ -512,11 +530,33 @@ no model found, it moves no seed number, and a declaration that authors it itsel
512
530
  `genre-owned-env`.
513
531
 
514
532
  **The kind catalogue** (`KIND_CATALOGUE`, and it IS the whole vocabulary): `command-exit`,
515
- `count-not-worse`, `pattern-absent-in-diff`, `files-changed` are live; `judged-floor` and
516
- `human-confirms` are NAMED BUT LOCKED (and carry `verdictClass` `soft-green`/`hitl`), so
517
- declaring one is a counted `locked-kind` red rather than an unknown-kind typo; `harness-loop` (TESTGEN) is ABSENT from v1 entirely. In
518
- tool mode a locked kind is INEXPRESSIBLE — the schema carries one branch per live kind — so
519
- that demand arrives through the interview layer instead (`refuseLockedKind(kind)`).
533
+ `count-not-worse`, `pattern-absent-in-diff`, `files-changed` and `human-confirms` are live;
534
+ `judged-floor` is NAMED BUT LOCKED (`verdictClass: 'soft-green'`), so declaring it is a counted
535
+ `locked-kind` red rather than an unknown-kind typo; `harness-loop` (TESTGEN) is ABSENT from v1
536
+ entirely. In tool mode a locked kind is INEXPRESSIBLE — the schema carries one branch per live
537
+ kind — so that demand arrives through the interview layer instead (`refuseLockedKind(kind)`).
538
+
539
+ **`human-confirms` is the one kind that measures NOTHING** (N4 slice 1). Its whole parameter
540
+ surface is `ask` — the plain question the signer answers — so it cannot spawn, cannot be
541
+ env-capable, and cannot be handed a second job. Four laws ride on it, all checked rather than
542
+ assumed: it is `offer: false` BY LAW (a declaration that offers one reds `human-stage-offered`,
543
+ and `declaredStages` stamps the law onto the stage the runner sees, so `checkMenu` can never
544
+ hand the agent `check-passes(<a person>)`); it must be the LAST stage
545
+ (`human-stage-not-last` — first-red-wins would otherwise silently delete every mechanical stage
546
+ behind it and empty the evidence package the person is owed); it is at-most-once (one signer,
547
+ one judge); and it SKIPS the seed-verdict read (ruling 8), with the skip recorded as a row
548
+ carrying `verdict: 'skipped'` rather than taken in silence.
549
+
550
+ **A human stage does not RUN — it PAUSES.** `StageResult.verdict` gains `pause` (and `skipped`),
551
+ `runDeclaredClose` reports `pause`/`pausedAt`, and the arbiter bridge translates it to the
552
+ distinct `HUMAN_PAUSE` verdict — never `satisfied`, never `needs_revision`, and never a
553
+ `CLOSE_FAULTS` word (a fault says the close could not render a verdict; a pause says it is not
554
+ FINISHED rendering one). With the signer's ruling in hand (`ctx.humanRuling`, carried by the
555
+ runner and never authored) the same stage renders what the person said: `accept` is a green a
556
+ PERSON judged, `rerun` is a red whose gap is their own words. `normalizeHumanRuling` is the
557
+ library half of the gate — `null` is the pause path, a fourth door is refused, and a `rerun`
558
+ with empty or whitespace-only text is refused by name, because an empty gap re-runs the worker
559
+ as though nothing had been said.
520
560
 
521
561
  **A red `count-not-worse` gap carries the LINES it counted, not only how many (F98).** Each
522
562
  term's **KEPT** matched lines nest under that term's own breakdown row — the row that already
@@ -590,6 +630,81 @@ alone. An absent goal renders as absent rather than as a bare label. This is a R
590
630
  validator: nothing compares the goal to the declaration, because deriving one from the other
591
631
  is exactly what F87 forbids.
592
632
 
633
+ **The path an adopter actually drives is TWO scripts, and the first one spends nothing.**
634
+ `runInterview` is a pure function over answers with no prompt loop in it, so until
635
+ `scripts/run-interview.mjs` existed the only way in was to hand-write an `answers.json` and a
636
+ spec draft — precisely the SWE tax this product refuses. That script is the half that ASKS
637
+ (`--patient <repoPath> --verdict <class> --out <outdir> [--budget <usd>] [--lang js]`, default
638
+ `js`), and it is deliberately GLUE: **it calls no provider at all**, so a whole interview costs
639
+ $0 and no model ever sees it. Everything load-bearing in it is borrowed rather than respelled —
640
+ the QUESTIONS are the library's frozen sets (`questionsFor` / `requiredAnswersFor`), printed as
641
+ handed over and never re-worded, re-ordered or re-numbered by the script; the REFUSALS are
642
+ `runInterview`'s; the SCRUB is `redactSecrets` at capture AND again at the library's own ingest,
643
+ so no keystroke reaches stdout or disk by a route that skips the one inventory; and the spec
644
+ draft is checked by the SAME `validateJob` that will judge it after the paid call, with the
645
+ authored half's expected reds filtered off `AUTHORED_SPEC_FIELDS` by name. A typo'd slug or an
646
+ under-floor wall therefore costs $0 rather than a scout plus a model call.
647
+
648
+ A LOCKED class refuses BEFORE a single question is asked: the script hands `runInterview` an
649
+ empty answer set, prints the counted `request-red` verbatim, and stops — nothing is asked and
650
+ nothing is written, because asking a class's questions for a job nothing here can close is an
651
+ interview at the wrong price. On success it writes exactly TWO files into `--out`:
652
+ `answers.json`, which is the LIBRARY's redacted reading of the answers (`iv.answers`) and never
653
+ the raw keystrokes; and `specdraft.json`, which is the OPERATOR's half ONLY — no `close`, no
654
+ `closeDecl`, no `verdictType`, because those three ARE `AUTHORED_SPEC_FIELDS` and `assembleSpec`
655
+ refuses a draft carrying any of them rather than merging over it. `tools` is deliberately
656
+ OMITTED from the draft too (MED-1: an omitted menu hashes as the concrete current `TOOL_MENU`,
657
+ which pins WHICH menu was signed; naming today's list would freeze it into the operator's half).
658
+ An interview that never finishes writes NOTHING: a half-collected answer set that looks finished
659
+ is the failure nobody sees.
660
+
661
+ **Two ceilings, two names, never pooled on screen or anywhere else.** `--budget` is the
662
+ AUTHORING ceiling — what the authoring call may spend — parsed by the same `parseCeiling` rule
663
+ `run-author.mjs` parses it with: absent is UNBOUNDED and ANNOUNCED before anything spends,
664
+ malformed is an ERROR rather than a silent fallback, and zero or negative is rejected on the
665
+ same rule. `budgetUsd` is the JOB's budget, asked separately and signed into the spec. The
666
+ interview stores neither: the ceiling is handed straight to the child, which is the process that
667
+ spends it.
668
+
669
+ **It ends by OFFERING the paid step, and the offer's default is NO.** `run-author.mjs` is a
670
+ DIFFERENT PROCESS under its own ceiling; the interview prints its exact command line (with
671
+ `--budget` propagated only when one was given, and a note when `ANTHROPIC_API_KEY` is unset)
672
+ before asking `Run it now? [y/N]`, so declining still leaves a command to paste and two files
673
+ already on disk. Only an explicit yes spends — the answer that costs nothing is the one you get
674
+ by saying nothing, the same lean the pause's doors take. On a yes it releases stdin before
675
+ spawning (two readers on one terminal is a keystroke landing in whichever happens to be
676
+ listening) and then propagates the CHILD's exit status rather than flattening a refusal there
677
+ into a success one process up.
678
+
679
+ **Both scripts speak exit codes, and an adopter wrapping them in CI reads them:**
680
+
681
+ - `run-interview.mjs` — **0** the interview finished (and, when it spawned the paid step, that
682
+ step's own status is what you get, never a flattened 0); **1** a REFUSAL — a locked class, the
683
+ library's own reds over the answers, or a spec draft that does not validate; **2**
684
+ operator/config — usage, a missing or non-existent `--patient`, a present-but-empty `--lang`,
685
+ a malformed `--budget`, stdin ending mid-interview, or a child that could not be started;
686
+ **3** a LEAK that `scanSecrets` found in a file it had just written (count and path only —
687
+ echoing the matched string is the same leak, one hop on).
688
+ - `run-author.mjs` — **1** a refusal or a failed gate; **2** operator/config; **3** a leak;
689
+ **4** a CRASH inside the paid span. **3 deliberately OVERRIDES a 4**: a secret sitting in a
690
+ written file is the harder line of the two, and the crash keeps both of its own louder
691
+ channels — the whole error on stderr and its own `author-crash` spine record.
692
+
693
+ **A crash inside the paid span leaves a BODY.** The fallible span — from just after
694
+ `author-start` to the end of the main flow — sits in ONE try/catch that RETRIES NOTHING and
695
+ SWALLOWS NOTHING. The operator's copy goes first and verbatim (it must not depend on the two
696
+ writes below it succeeding), then the spine gets `author-crash` via `crashRecord`: `name`,
697
+ `message` and `code` through `redactSecrets`, the STACK through `scrubRaw` — the ONE persist
698
+ boundary, the same `SECRET_PATTERNS` inventory, bounded with the bound announcing its own size —
699
+ plus `author-end {outcome: 'crashed'}`, because `author-end` is the one record that says a run
700
+ stopped AT ALL and a crash omitting it leaves a spine that reads as still-running. The spine
701
+ write is itself guarded and its failure said out loud (F70: a crash handler that crashes
702
+ destroys the report it was built to make). Nothing ABOVE the try is inside it, deliberately —
703
+ the argv and config `die()` paths run before the spine file exists, so they stop loud on stderr
704
+ and 2 instead. **Latent, not live:** nothing reads the author spine today, and the nearest
705
+ reader (`classifyIncidents`, `src/ledger.js`) keys on types neither record uses, so both fall
706
+ through every branch UNCOUNTED — the fail-safe direction, uncounted rather than miscounted.
707
+
593
708
  **Refusals are COUNTED (D13).** `refusalEvents(refusal)` returns the spine events: a
594
709
  `job-red` carrying `{ code: 'request-red', verb, lib: 'bareloop' }` — the `lib` stamped at
595
710
  the EMIT SITE, so `classifyIncidents` files it against bareloop's own catalogue and its
@@ -694,7 +809,35 @@ known-answer round-trip before tokens: `smoke-red` — a silent degradation thro
694
809
 
695
810
  Outcomes: `green | already-green | escalated | unapproved-spec | job-red | smoke-red |
696
811
  plan-red | check-red | close-red | close-unsupported | recipe-stale | branch-red | pricing-red |
697
- provider-red | interpreter-red | cap-halt | wall-halt | step-stalled | step-red:<id>`.
812
+ provider-red | interpreter-red | cap-halt | wall-halt | step-stalled | hitl-pause |
813
+ hitl-decision-red | step-red:<id>`.
814
+
815
+ **The two hitl terminals (N4 slice 1, doors re-cut 2026-08-18)** are the class's whole surface
816
+ at this layer, and each is a CLEAN exit (`spendComplete` stays true — only the two casualties floor). `hitl-pause` is a
817
+ decision-ready CHECKPOINT: the close reached a stage no machine can render, so the run stops
818
+ holding everything it has done, emits the evidence package (every mechanical stage's result, the
819
+ close's own `ask`, and the changed paths — bounded, trim announced), and the clock stops with
820
+ it. It is on `CHECKPOINT_OUTCOMES` (the canonical `resumableOutcomes` list, exported so an
821
+ exported bundle inherits one spelling), and `checkpointAgeGate` refuses one older than
822
+ `PAUSE_TTL_MS` (60 days) by NAMING the age and the TTL. The signer's own `pause` door mints
823
+ that SAME terminal, with `humanDecision: 'pause'` and an explicit `gap: null` on the record: no
824
+ fix loop, no worker round, nothing spent, the rerun allowance untouched, and the checkpoint left
825
+ exactly as it was. `hitl-decision-red` refuses a decision the run cannot act on — a word that is
826
+ not one of the three doors, a `rerun` with empty text, or a decision handed to a close with no
827
+ human stage — before anything is spent. Neither demotes a bridge, and both are excluded from the
828
+ ledger's escalation counting.
829
+
830
+ There was a third terminal, `hitl-cancel`, for a `cancel` door that no longer exists (hamr,
831
+ 2026-08-17: *"pause can resume — that would be more honest"*). Nothing mints it and no constant
832
+ exports it; the ledger still RECOGNISES the bare string so a spine written before the change
833
+ reads as governance rather than as a counted capability gap. An unresumed pause expiring under
834
+ the TTL is what cancel used to be.
835
+
836
+ Pass the signer's answer as `humanRuling: { decision, text? }` (`accept | rerun | pause`, the
837
+ enumerated `HUMAN_DECISIONS` set); it is spent once, at
838
+ the close readings up to the moment the fix loop opens, so the human's words become `post.gap`
839
+ through the SAME seam (same bound, same scrub) and the next machine-clean tree pauses for a
840
+ SECOND review rather than converting one sentence forever.
698
841
  `branch-red` is the WORK BRANCH refusing (below): the patient is not a git checkout, its
699
842
  branch namespace has no free name, or a resume's recorded branch is gone. Zero tokens, and
700
843
  never a fallback to working on the branch the run was handed. `provider-red` is a
@@ -789,7 +932,10 @@ never replans) — plus at most ONE arbiter-granted extra on a second variance s
789
932
  REAL close and governed by the close TREND rule below (`capRuns`, default 3, survives only as
790
933
  its blind fallback). `plan-executed` (the plan-as-executed record, design law #2) lands on the
791
934
  spine on every path that executed steps. Additional outcomes: `already-green |
792
- plan-red | check-red | close-red | wall-halt`. Worker prompts hold the v1.12 §5 contract
935
+ plan-red | check-red | close-red | wall-halt | hitl-pause | hitl-decision-red`.
936
+ A `human-confirms` stage is caught at BOTH close seams — the precheck (reaching a person there
937
+ means the machine half already passes on the untouched tree, so pausing costs $0 where drafting
938
+ a plan first would spend a budget to arrive at the same question) and the post-steps close. Worker prompts hold the v1.12 §5 contract
793
939
  (mutation-proven): the absolute repo root, the step's action/target, prior artifacts,
794
940
  the gap — NEVER the budget, the close command, a check's command, or the arbiter's books.
795
941
 
@@ -1119,8 +1265,9 @@ about its task, beside the absolute-path law it is the twin of — both are fenc
1119
1265
  cannot infer. It renders for EVERY worker on every grant (a worker granted only `write` gets
1120
1266
  no component strategy paragraph at all and must still be told). The fence is unchanged; what
1121
1267
  changed is that the rule is stated instead of learned one refusal at a time, which cost a real
1122
- run the rounds of a bounded attempt. N2 bounds (honest): `gold`/`rubric` closes refuse
1123
- `close-unsupported` (execution lands at N4).
1268
+ run the rounds of a bounded attempt. N2 bounds (honest): `gold`/`rubric`/`hitl` OBJECT
1269
+ closes refuse `close-unsupported` — N4's hitl is a `human-confirms` STAGE inside a `closeDecl`,
1270
+ never a close TYPE, so there is one live expression of hitl rather than two.
1124
1271
 
1125
1272
  ### The reuse registry (Layer 3) — `src/bridges.js`, `src/selection.js`
1126
1273
 
@@ -1342,9 +1489,11 @@ semantics are byte-unchanged (PRD v1.46 §3):
1342
1489
  - **`resumableOutcomes: [...]`** reclassifies a landed `job-end` whose outcome is a
1343
1490
  CHECKPOINT rather than a verdict as a restart rather than a completed row. "A landed
1344
1491
  job-end is complete" is true of a VERDICT; a checkpoint means the work is on disk and the
1345
- plan is on the spine with nothing decided against them. **The set is the CALLER's**, and
1346
- the reference runner's is `['cap-halt', 'wall-halt', 'step-stalled']` (`run-u`'s
1347
- `RESUMABLE_HALTS`). The first two are governance halts — an operator-owned allowance ran
1492
+ plan is on the spine with nothing decided against them. **The set is the CALLER's** — the
1493
+ parameter stays a parameter, and its empty default is what the reuse loop's own graded-row
1494
+ semantics depend on — but the canonical ANSWER is exported as **`CHECKPOINT_OUTCOMES`**
1495
+ (`['cap-halt', 'wall-halt', 'step-stalled', 'hitl-pause']`), and the reference runner
1496
+ CONSUMES it rather than keeping a copy. The first two are governance halts — an operator-owned allowance ran
1348
1497
  out. `step-stalled` joined them 2026-08-13 on hamr's ruling (PRD v1.64 §1): it is the one
1349
1498
  terminal whose OWN escalation already offers *"retry the run"*, nothing about the work on
1350
1499
  disk is wrong, and the shape that named the fix is a stall that trips with time left and
@@ -1385,6 +1534,20 @@ watchdog `--wall-ms 0`, which it defaulted to `null` and armed no deadline at al
1385
1534
  still advertising one. The refusal names the lever: raise `maxWallMs`, which moves the spec hash
1386
1535
  and is re-signed.
1387
1536
 
1537
+ **A `hitl-pause` checkpoint is answered on the same command line** (N4, 2026-08-12 §5.2 — the
1538
+ terminal is the v1 surface; the panel is N6's). The reference runner takes
1539
+ `--decide accept|rerun|pause` with `--text` for the rerun door, gated on the SAME
1540
+ `--approve <specHash>` signature the run itself is signed with (ruling 4: the spec hash is the
1541
+ only signer proof this repo has, and no second identity mechanism was invented). The decision
1542
+ SEMANTICS are the library's — an adopter's runner should call `normalizeHumanRuling` and print
1543
+ its refusal rather than re-spell "three doors, no fourth" or "a rerun needs text", and hand the
1544
+ result to `runJob` as `humanRuling`. Two questions belong to the RUNNER because only it can ask
1545
+ them: a decision with no run to answer, and a decision aimed at a checkpoint that is not a pause
1546
+ (an `accept` there would mint a green over evidence nobody was shown). Before any of it, the
1547
+ paused spine goes through `checkpointAgeGate`, and the run is not resumed without an explicit
1548
+ decision: the lean-rerun rule (2026-08-12 §4) governs which door a prompt LEADS with, never an
1549
+ action taken for the operator.
1550
+
1388
1551
  - **Completed tries are not re-run.** They come back as rows (marked `inherited`) and
1389
1552
  their bridges stay excluded from every later selection. A try whose `job-end` landed —
1390
1553
  the close judged it — counts as completed even if the kill beat its `try-end`; paying
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bareloop",
3
- "version": "0.10.0",
3
+ "version": "0.11.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",
@@ -23,8 +23,8 @@
23
23
  "prepublishOnly": "npm run build:types"
24
24
  },
25
25
  "dependencies": {
26
- "bare-agent": "^0.35.0",
27
- "bareguard": "^0.12.0",
26
+ "bare-agent": "^0.36.0",
27
+ "bareguard": "^0.13.0",
28
28
  "litectx": "^0.32.0"
29
29
  },
30
30
  "devDependencies": {
package/src/authorflow.js CHANGED
@@ -163,21 +163,63 @@ export const STRUCTURE_INSTRUCTION_TEXT = 'Your reply could not be read as one J
163
163
  * cannot emit anything else (D2/D3), so a genre it has never seen is a
164
164
  * composition problem, not a catalogue problem.
165
165
  *
166
- * The GREEN set is questions 1–6 of the frozen TYPES set (prereg §5) BYTE FOR
167
- * BYTE. What is gone is the seventh slot — D13's genre CONFIRM ("this looks like
168
- * a type-fixing job, correct?") — superseded in full: the interview never asks
169
- * about a genre again. The refusal it carried did not disappear; it MOVED to the
170
- * composer, on the same counted `request-red` path (see `authorCloseForJob`).
171
- * The remaining six are already genre-neutral, which is why generalising them
172
- * required deleting a slot rather than rewording one.
166
+ * The GREEN set descends from questions 1–6 of the frozen TYPES set (prereg §5).
167
+ * TWO slots have been deleted since, both because the interview was ASKING FOR
168
+ * SOMETHING IT ALREADY HAD or already knew:
169
+ *
170
+ * - D13's genre CONFIRM ("this looks like a type-fixing job, correct?") —
171
+ * superseded in full: the interview never asks about a genre again, and the
172
+ * refusal it carried MOVED to the composer, on the same counted
173
+ * `request-red` path (see `authorCloseForJob`);
174
+ * - "Is there a code repo I can look at? Where?" — DROPPED on hamr's ruling
175
+ * after he drove the terminal interview himself (2026-08-15, verbatim: *"drop
176
+ * Q6"*). The repository is a MANDATORY, STRUCTURED input already —
177
+ * `runInterview`'s own `repoPath`, which the runner takes from `--patient` and
178
+ * refuses to start without. Asking a person to re-type in prose a path the
179
+ * machine is holding in a variable is the SWE tax this product refuses, and
180
+ * it invites a second, drifting answer for the same fact (hamr's own answer
181
+ * on the live run was *"Yes — the patient itself."*). Nothing downstream lost
182
+ * anything: the scout reads `repoPath`, never the answer.
183
+ *
184
+ * Question 2 was WIDENED in the same ruling: it now asks for the read-or-draw-from
185
+ * files as well as the ones that change. The two halves belong in one answer
186
+ * because they are one decision for the person answering — what the work touches,
187
+ * and what it touches only to look at.
188
+ *
189
+ * The five that remain are genre-neutral, which is why every generalisation so far
190
+ * has deleted a slot rather than reworded one. They are NUMBERED CONTIGUOUSLY from
191
+ * 1: the number a person sees is the key their answer is filed under, and a gap
192
+ * would be a hole in a form nobody can explain.
173
193
  */
174
194
  export const GREEN_QUESTIONS = Object.freeze({
175
195
  1: 'What do you want done?',
176
- 2: 'Which files or folders should change?',
196
+ 2: 'Which files or folders should change? And which files should the work read or draw from (they stay untouched)?',
177
197
  3: 'Is there anything that must not change?',
178
198
  4: 'How do you check today whether it\'s working?',
179
199
  5: 'What would make you say this came back worse than before?',
180
- 6: 'Is there a code repo I can look at? Where?',
200
+ });
201
+
202
+ /**
203
+ * THE HITL SET (N4 slice 1) — the green questions BYTE FOR BYTE, plus one.
204
+ *
205
+ * The five are unchanged deliberately rather than economically: the
206
+ * mechanical-first composition law (2026-08-07) says every hitl close is
207
+ * *deterministic stages first, judge minimal, human last*, so a hitl job needs
208
+ * exactly the same mechanical facts a green one does — what changes, what must
209
+ * not, how it is checked today, what "worse" looks like. Rewording them would
210
+ * fork one interview into two that drift.
211
+ *
212
+ * The last one is the only thing the green set cannot supply, and it is not a
213
+ * flourish: `human-confirms` requires an `ask` — the question the signer answers
214
+ * at the end of the run — and nothing else in the interview names it. Without it
215
+ * the composer would have to invent what a person is deciding, which is the one
216
+ * thing a human stage must never have done for it. It is the SPREAD's next number
217
+ * rather than a fixed one, so a green-side deletion renumbers it instead of
218
+ * leaving a gap (it was 7 while the green set held six, and is 6 now it holds five).
219
+ */
220
+ export const HITL_QUESTIONS = Object.freeze({
221
+ ...GREEN_QUESTIONS,
222
+ 6: 'When you look at the finished result yourself, what are you deciding?',
181
223
  });
182
224
 
183
225
  /**
@@ -195,7 +237,11 @@ export const QUESTION_SETS = Object.freeze({
195
237
  required: Object.freeze(Object.keys(GREEN_QUESTIONS).map(Number).sort((a, b) => a - b)),
196
238
  }),
197
239
  'soft-green': Object.freeze({ locked: true, questions: null, required: null }),
198
- hitl: Object.freeze({ locked: true, questions: null, required: null }),
240
+ hitl: Object.freeze({
241
+ locked: false,
242
+ questions: HITL_QUESTIONS,
243
+ required: Object.freeze(Object.keys(HITL_QUESTIONS).map(Number).sort((a, b) => a - b)),
244
+ }),
199
245
  });
200
246
 
201
247
  /**
@@ -214,7 +260,15 @@ export const CLASS_STATEMENTS = Object.freeze({
214
260
  + 'measurement of this repository. If part of what they asked for cannot be measured that way, leave it out and '
215
261
  + 'say so in your notes rather than approximating it with a stage that means something else.',
216
262
  'soft-green': null,
217
- hitl: null,
263
+ // N4 slice 1. The mechanical-first composition law is stated as an ORDER
264
+ // because first-red-wins makes the order the mechanism: the cheap stages shield
265
+ // the expensive one, and the person is never the first filter.
266
+ hitl: 'The person declared this job HITL: a PERSON renders the final verdict, because their judgement is what '
267
+ + '"done" means here. Compose the mechanical part first — everything about this repository a command can decide '
268
+ + 'is still a mechanical stage, exactly as it would be for a green job — and put ONE human-confirms stage LAST, '
269
+ + 'carrying the question they answer when they look at the result. Never make the person the first filter: a '
270
+ + 'stage a command can render is never handed to them, and they are asked only once, at the end, about the part '
271
+ + 'no command can hold.',
218
272
  });
219
273
 
220
274
  /** @param {string} verdictType @returns {{locked: boolean, questions: any, required: any}} */
@@ -354,6 +408,12 @@ export const PARAM_SCHEMAS = Object.freeze({
354
408
  extensions: { type: 'array', minItems: 1, items: { type: 'string' } },
355
409
  allowPrefixes: { type: 'array', minItems: 1, items: { type: 'string' } },
356
410
  requireNonEmpty: { const: true },
411
+ ask: {
412
+ type: 'string',
413
+ minLength: 1,
414
+ description: 'the plain question the person answers while looking at the finished result. They see it verbatim, '
415
+ + 'so write what they are DECIDING — never a bare "approve?".',
416
+ },
357
417
  });
358
418
 
359
419
  /**
@@ -912,8 +972,19 @@ export function resolveSourcePrefixes({ workdir, sourcePaths, seedFiles }) {
912
972
  * run could not be read afterwards. A cost entry with no raw beside it is that
913
973
  * run again.
914
974
  */
915
- /** @param {{ceilingUsd?: number|null}} [o] the OPERATOR's ceiling; `null`/absent is UNBOUNDED */
916
- export function makeCostBook({ ceilingUsd = null } = {}) {
975
+ /**
976
+ * @param {{ceilingUsd?: number|null,
977
+ * onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number}) => void}} [o]
978
+ * `ceilingUsd` is the OPERATOR's ceiling; `null`/absent is UNBOUNDED.
979
+ * `onCall` is a REPORTING seam fired as each metered call lands — the shell's
980
+ * only chance to know what has been spent while the run is still alive. It is
981
+ * never consulted and never governs: `capStop` below is still the one
982
+ * predicate, still asked between calls, and still reads the same `entries`.
983
+ * `absorb` deliberately does NOT fire it: those calls were metered (and
984
+ * reported) by whoever made them, and reporting them twice would double the
985
+ * scout's spend in every reader downstream.
986
+ */
987
+ export function makeCostBook({ ceilingUsd = null, onCall = () => {} } = {}) {
917
988
  /** @type {{label: string, costUsd: number|null, unpricedRounds: number}[]} */
918
989
  const entries = [];
919
990
  /** @type {ReturnType<typeof scrubRaw>[]} */
@@ -921,8 +992,10 @@ export function makeCostBook({ ceilingUsd = null } = {}) {
921
992
  return {
922
993
  /** @param {string} label @param {any} r a bare-agent run result @param {number} [attempt] */
923
994
  add(label, r, attempt = 1) {
924
- entries.push({ label, ...priceOf(r) });
995
+ const call = { label, ...priceOf(r) };
996
+ entries.push(call);
925
997
  raws.push(scrubRaw({ label, attempt, text: r?.text, reason: r?.error ? String(r.error) : null }));
998
+ onCall({ ...call });
926
999
  return entries.at(-1);
927
1000
  },
928
1001
  /** @param {{label: string, costUsd: number|null, unpricedRounds: number}[]} calls
@@ -1085,6 +1158,8 @@ async function askDeclaration({ messages, generate, mode, retries, label, book,
1085
1158
  * listing?: {stop: string|null, files: string[]|null, block: string|null, meta: any}|null,
1086
1159
  * generate: Function, seedReadFn?: Function, closeCtx?: any,
1087
1160
  * ceilingUsd?: number|null,
1161
+ * onPhase?: (phase: string, data?: any) => void,
1162
+ * onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number}) => void,
1088
1163
  * maxRevisions?: number, structureRetries?: number,
1089
1164
  * structuredMode?: 'tool'|'text', catalogue?: Record<string, any>}} o
1090
1165
  */
@@ -1094,6 +1169,11 @@ export async function authorClose({
1094
1169
  scout, listing = null,
1095
1170
  generate, seedReadFn = runSeedReadStages, closeCtx = {},
1096
1171
  ceilingUsd = null,
1172
+ // The two REPORTING seams, defaulted to nothing so every existing caller is
1173
+ // byte-identical. Neither is consulted: the revise ladder, the ceiling and the
1174
+ // seed read all run exactly as they did — the shell is merely told, while the
1175
+ // run is still alive, which of the up-to-15 silent minutes it is inside.
1176
+ onPhase = () => {}, onCall = () => {},
1097
1177
  maxRevisions = MAX_REVISIONS,
1098
1178
  structureRetries = MAX_STRUCTURE_RETRIES,
1099
1179
  structuredMode = 'tool',
@@ -1118,7 +1198,7 @@ export async function authorClose({
1118
1198
  // no default for exactly this reason, and `shellCapUsd` left off judged against
1119
1199
  // a number nobody chose). An unbounded authoring run is legal; it is just never
1120
1200
  // an accident, which is why the driver PRINTS it.
1121
- const book = makeCostBook({ ceilingUsd });
1201
+ const book = makeCostBook({ ceilingUsd, onCall });
1122
1202
  // The scout's calls AND its raws, together. The raws matter most on the path
1123
1203
  // that spends nothing more: run mslhn707 refused at the $0 preflight below,
1124
1204
  // and the survey text that would have said WHY died with the process.
@@ -1306,6 +1386,11 @@ export async function authorClose({
1306
1386
  try {
1307
1387
  for (let i = 0; i <= revisionCap; i += 1) {
1308
1388
  const label = i === 0 ? 'author' : `revise-${i}`;
1389
+ // WHICH call, and how many the ladder may make. `of` is the ENFORCED cap
1390
+ // (`revisionCap`, already clamped tighten-only) rather than the constant —
1391
+ // a readout quoting a number the loop does not obey is a second
1392
+ // instrument, and this file's own history is what that costs.
1393
+ onPhase('author-call', { call: label, i, of: revisionCap });
1309
1394
  const ask = await askDeclaration({ messages, generate, mode: structuredMode, retries: retryCap, label, book, catalogue });
1310
1395
  messages = ask.convo;
1311
1396
 
@@ -1392,7 +1477,20 @@ export async function authorClose({
1392
1477
  // `gapKeep` defaults to EMPTY rather than being left undefined: M1
1393
1478
  // prefixes every gap line with it verbatim, so an unset one prints the
1394
1479
  // word "undefined" on every failure line the model is asked to read
1395
- const rows = await seedReadFn(injected.declaration, { gapKeep: '', ...closeCtx, workdir, seedRef, seedTrees });
1480
+ // THE LONG ONE. Every stage of the declaration spawns a real toolchain
1481
+ // against the patient, so this single await is where a run spends
1482
+ // minutes saying nothing — the per-stage line below is the whole point,
1483
+ // and the block boundary is what makes it readable.
1484
+ const stageCount = injected.declaration?.stages?.length ?? null;
1485
+ onPhase('seed-read', { call: label, stages: stageCount });
1486
+ // `onStage` sits AFTER the `closeCtx` spread beside `workdir`/`seedRef`:
1487
+ // it is the runner's own reporting channel, not a knob a caller's close
1488
+ // context may quietly redirect.
1489
+ const rows = await seedReadFn(injected.declaration, {
1490
+ gapKeep: '', ...closeCtx, workdir, seedRef, seedTrees,
1491
+ onStage: (/** @type {any} */ row) => onPhase('stage', { call: label, ...row }),
1492
+ });
1493
+ onPhase('seed-read-done', { call: label, stages: stageCount });
1396
1494
  iter.seedRead = rows;
1397
1495
  accepted = injected.declaration;
1398
1496
  acceptedSeedRead = rows;