bareloop 0.16.0 → 0.17.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 CHANGED
@@ -7,6 +7,20 @@ feature lands, **patch** = docs, fixes, scaffolding.
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.17.0] — 2026-08-31
11
+
12
+ ### Added
13
+
14
+ - **Publish workflow gates on the types being usable BY AN ADOPTER, not just internally.** `npm run typecheck` (`tsc --noEmit`) checks the *source*; it cannot see the generated `.d.ts` as an adopter resolves it from inside `node_modules`, which is the one thing consumers actually get. The publish workflow now packs the tarball, installs it into a clean consumer project, and compiles a quickstart against it, so a release whose published types are broken cannot reach the registry. The consumer pins `@types/node` to the major this package builds against instead of floating to the newest, so a stricter DefinitelyTyped release cannot turn the publish gate red for reasons unrelated to the commit being published. Verified locally: the quickstart compiles green against a packed tarball, and a deliberately broken dereference fails it. CI only — no runtime or published-artifact change.
15
+
16
+ - **Worker model name in the signed spec (build-list #3)**: `job-v1` gains an optional
17
+ `model` field — a non-empty string naming the exact worker provider model id. Absent,
18
+ behaviour is unchanged (the runner's own default); present, it is part of the signed
19
+ spec hash and `scripts/run-u.mjs` runs the job on that model. A `--model <tier>` flag
20
+ naming a different model is refused (`resolveWorkerModel`, `src/job.js`) rather than
21
+ silently overridden — the spec always wins. Worker only; the judge model stays
22
+ library-pinned pending recalibration.
23
+
10
24
  ## [0.16.0] — 2026-08-30
11
25
 
12
26
  ### Added
@@ -52,6 +52,35 @@ inherited rule carries the green that minted it and the contrast that attributed
52
52
  - **Is not:** a general agent, a swarm, or an orchestrator framework. One-off or small
53
53
  jobs are out of scope — that's a CLI session, not a bareloop job.
54
54
 
55
+ ## Which mode / components do I need?
56
+
57
+ - **Verdict class — how "done" gets decided:** `green` (a mechanical close: exit code =
58
+ truth, `src/kinds.js` `LIVE_KINDS`) is the default and the only class with a live-proven
59
+ paid run. `soft-green` (a judged floor behind a signed calibration gate, plus a review
60
+ door) is fully admitted (`VERDICT_TYPES`, `src/job.js:106`) but has never rendered a live
61
+ verdict — see `## Public API` → "THE JUDGED FLOOR" and "THE REVIEW DOOR" below. `hitl` is
62
+ LEGACY: still admitted (removal is a future breaking change) but retired as a design
63
+ (PRD Addendum v1.71); its pause machinery lives on as the review door, not as a class to
64
+ pick for new jobs.
65
+ - **Worker surface — how the agent runs:** `provider: 'anthropic-api'` (the default, the
66
+ only one with an operational guarantee, F48) drives `bare-agent`'s `Loop` and takes the
67
+ shell-owned `provider` binding. `provider: 'clipipe-subscription'` (`src/job.js:135`)
68
+ drives the worker natively over the Claude CLI's own tool channel and requires
69
+ `opts.nativeProvider` in `runJob`/`runPlan` — its `costUsd` axis is notional, never pools
70
+ with `anthropic-api` on cost, and budgets don't transfer between them.
71
+ `job.provider === 'clipipe-subscription'` with no `nativeProvider` supplied is
72
+ `interpreter-red`, never a silent fall-back to the metered API (`src/planrun.js:1073`).
73
+ - **Reuse — where a workflow comes from:** a plain `runJob` always drafts cold. Passing
74
+ `bridge` starts from one standalone bridge file (`src/reuse.js`'s envelope, `## The reuse
75
+ ENVELOPE and runReuse` below). The CLI's `--registry <dir>` / library-level `registryDir`
76
+ (`src/reuse.js`, `scripts/run-u.mjs`) instead reads/writes dated rows in an
77
+ operator-supplied directory of plain JSON files — no database, no default location; a
78
+ named-but-missing registry is a red, never conjured (`## The reuse registry (Layer 3)`
79
+ below).
80
+ - **Read shim:** `readShim` defaults to `false` on both `runJob` (`src/run.js:205`) and
81
+ `runPlan` (`src/planrun.js:808`) — the OFF arm wraps nothing at all. Opt in with `true`,
82
+ `'cap'`, or `'diff'` (`src/readshim.js`) only once the read-hygiene tradeoff is wanted.
83
+
55
84
  ## Minimal usage
56
85
 
57
86
  ```js
@@ -106,6 +135,7 @@ minting claim, or the shell-owned retry cap — all unknown-field reds.
106
135
  | `cadence` | `{ unit: hour\|day\|week, every: 1..30 }` | validated now, consumed at N5 (Scheduler) |
107
136
  | `budgetUsd` | `0 < n <= shell cap` | ceiling chain: workflow ≤ job ≤ shell — each layer may tighten, never exceed |
108
137
  | `maxWallMs` | optional integer ms `>= MIN_WALL_MS` (one close timeout) | the run's wall clock. **NO DEFAULT, by ruling** — absent means time-unbounded *by explicit operator choice*, never by fallback (F45: a defaulted cap is a silent second ceiling). Enforcement is a BETWEEN-ROUND deadline, so the honest worst case is `maxWallMs + closeStages × closeTimeoutMs` — every stage of a staged close gets the FULL timeout — and all three numbers are reported (`loop.stop()` cannot cut an in-flight call — F61 measured 500ms→4,018ms). The `MIN_WALL_MS` floor is a ONE-stage number, so a spec with many stages can validate while its overshoot dwarfs its cap: the clock reports that honestly rather than the floor pretending to prevent it (stage-aware floor parked, PRD v1.39). Operator-only, tighten-only; adding or changing it changes the spec hash |
138
+ | `model` | optional non-empty string, exact provider model id (e.g. `"claude-sonnet-5"`) | the WORKER's model, build-list #3. Absent means today's runner default; present it wins over a `--model` flag outright, and a flag naming a different id is refused (`resolveWorkerModel`, `src/job.js`) rather than silently overridden. Part of the signed hash like every other field. Worker only — the judge model stays library-pinned (`JUDGE_MODEL`) pending recalibration |
109
139
  | `writeScope` | array of contained globs | the operator's outer fence; the plan's own scopes must fit inside it, same containment code |
110
140
  | `steps` | RETIRED | operator-authored `steps[]` was deleted (PRD v1.32); a spec carrying it reds `shape-retired:steps` by name rather than half-running |
111
141
  | `escalation` | `{ mode: "decision-ready" }` | the pain channel is not optional |
@@ -2480,6 +2510,73 @@ JSONL spine (single source for every UI), litectx store per job, per-run ledger.
2480
2510
  the bare suite: bareagent, bareguard, litectx, barebrowse, baremobile — the full surface
2481
2511
  is disclosed to the authoring agent; only admitted verbs are callable per job.
2482
2512
 
2513
+ ## Extension contract
2514
+
2515
+ hamr's law, verbatim: **the agent may author anything whose only verbs are gated
2516
+ primitives; it may never author the arbiter — the close, the budget, the fence, the
2517
+ merge.** What an adopter can plug into `runJob` (`src/run.js:205`):
2518
+
2519
+ - **Providers.** `provider` (shell-owned LLM binding for the `Loop`/API path),
2520
+ `nativeProvider` (a factory required for a `clipipe-subscription` job),
2521
+ `providerFor(tier)` (per-step model-tier provider, forwarded to the plan flow), and
2522
+ `judgeProvider` (SOFTGREEN's own seam — the provider a judged close stage runs its
2523
+ locate-only call through; distinct from `provider`/`providerFor` because the judge tier
2524
+ is fixed, never the worker's). All four are documented at `src/run.js:91-101`.
2525
+ - **Close stage kinds.** The catalogue is fixed and enumerated (`LIVE_KINDS`,
2526
+ `src/kinds.js:142`: `command-exit`, `count-not-worse`, `pattern-absent-in-diff`,
2527
+ `files-changed`, `human-confirms`, `judged-floor`). An adopter's declaration SELECTS
2528
+ from this menu; it never authors a new kind — a stage naming an unimplemented or unknown
2529
+ kind is a hard runtime stop (`src/kinds.js:1885-1888`), not a way to run adopter code
2530
+ inside the close.
2531
+ - **`emit` — the spine sink.** `makeSpine(file)` (`src/spine.js:21`) returns an
2532
+ `emit(type, data)` that appends one JSONL line per event; any function with that
2533
+ signature can stand in as a custom sink, but it is write-only — nothing in bareloop reads
2534
+ the spine back, so a custom sink cannot feed anything into a run's own decisions.
2535
+ - **`rates` is NOT an extension point.** A rates passthrough was designed but never built;
2536
+ pricing is guesstimate-plus-loud-`estimated`-flag by ruling, and rate tables are the
2537
+ customer's own responsibility, not a bareloop seam to wire up.
2538
+
2539
+ Everything else in `runJob`'s options — budgets, caps, the close, the fence, merge/publish
2540
+ — is arbiter territory and is never adopter-suppliable as behavior, only as signed,
2541
+ schema-validated data.
2542
+
2543
+ ## Threat model summary
2544
+
2545
+ **Local-trust model, NOT a sandbox.** The worker runs with the OS user's own permissions.
2546
+ Both the close (`src/ralph.js:222`) and a declared close stage's environment
2547
+ (`src/kinds.js:351`) start from a full copy of `process.env` and spawn child processes
2548
+ under it — this is a passthrough, not a jail.
2549
+
2550
+ **What the arbiter guarantees:**
2551
+ - **Cap-not-estimate money and time.** `budgetUsd` is validated `0 < budget <= shellCapUsd`
2552
+ with the check's own name, `cap-not-estimate` (`src/job.js:227`); spend is never a
2553
+ projection.
2554
+ - **A signed spec hash refuses drift.** `jobSpecHash`/`checkApproval` (`src/job.js:703,
2555
+ 723`) mean an edited spec is unapproved by construction; `runJob` red-lines
2556
+ `'unapproved-spec'` before any provider call if no approval record matches the exact spec
2557
+ version (`src/run.js:289-290`).
2558
+ - **Secrets never enter the tree, the spine, the ledger, or a close's child process.**
2559
+ `redactSecrets` (`src/validate.js:130`) scrubs at every emission boundary in the
2560
+ authoring path; `CLOSE_ENV_DENY` (`src/ralph.js:136-153`) strips provider keys and
2561
+ known credential-shaped env-var names from the copy of `process.env` handed to a close
2562
+ child, by name/prefix/suffix-shape — a strip of a COPY, so the host process (and
2563
+ bare-agent, which reads the real key every round) keeps its own credentials.
2564
+ - **The worker is fenced off the arbiter's own books.** The authoring scout's read scope
2565
+ denies the gate audit path and every `ARBITER_BOOK_STORES` path under the workdir
2566
+ (`src/authorscout.js:295`); a plan step's read scope is the workdir alone
2567
+ (`src/planrun.js:1894`).
2568
+
2569
+ **What it does not guarantee:** no network egress control and no container/VM isolation —
2570
+ this repo has neither built nor documented either. `CLOSE_ENV_DENY`'s deny rule matches
2571
+ only credential-shaped names (`src/ralph.js:136-153`); any env var that doesn't match that
2572
+ shape passes through to the close's child process untouched. `SSH_AUTH_SOCK` is one of them
2573
+ and stays on purpose — stripping it would break git-over-SSH inside a close — but the deny
2574
+ list itself carries no comment saying so; this paragraph is the record.
2575
+
2576
+ **What an adopter must do:** keys load from the environment only (`.env.example` below);
2577
+ the patient is always a COPY of the target repo, never the original — the copy is the
2578
+ blast radius.
2579
+
2483
2580
  ## What's NOT in bareloop, and why
2484
2581
 
2485
2582
  - **No agent-authored arbiters.** Closes, budgets, caps, merge/publish decisions live
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bareloop",
3
- "version": "0.16.0",
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.",
3
+ "version": "0.17.0",
4
+ "description": "Workflows that earn their own design, with receipts \u2014 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",
7
7
  "types": "./types/index.d.ts",
package/src/job.js CHANGED
@@ -140,7 +140,7 @@ export const CONDITION_KEYS = Object.freeze(['providerPath', 'closeVerbosity', '
140
140
  // `steps` stays in the field list ONLY so a retired spec reds by name
141
141
  // (`shape-retired`) instead of falling through to a generic unknown-field —
142
142
  // the operator gets told what happened, not just that something is wrong.
143
- const JOB_FIELDS = ['schema', 'job', 'description', 'provider', 'conditions', 'cadence', 'budgetUsd', 'maxWallMs', 'writeScope', 'steps', 'escalation', 'goal', 'verdictType', 'close', 'closeDecl', 'checks', 'tools'];
143
+ const JOB_FIELDS = ['schema', 'job', 'description', 'provider', 'conditions', 'cadence', 'budgetUsd', 'maxWallMs', 'model', 'writeScope', 'steps', 'escalation', 'goal', 'verdictType', 'close', 'closeDecl', 'checks', 'tools'];
144
144
  /** the four-field plan shape's core (decision 5) — presence of any of these
145
145
  * declares the shape; `tools` (the ceiling) rides the shape but alone does not
146
146
  * declare it, so a legacy spec carrying it gets a pointed red, not a conflict */
@@ -240,6 +240,17 @@ export function validateJob(input, { shellCapUsd = 2 } = {}) {
240
240
  `integer milliseconds >= ${MIN_WALL_MS} (one close timeout). Enforcement is a between-round deadline, measured as maxWallMs + closeStages x closeTimeoutMs (design addendum 1: loop.stop() cannot cut an in-flight call; W5: every close stage runs under the full timeout), so a budget under one close cannot fund its own close and the advertised number would be more wrong than right`);
241
241
  }
242
242
 
243
+ // MODEL — the worker's provider model id (build-list #3, hamr's GO
244
+ // 2026-08-30). OPTIONAL, exactly like budgetUsd/maxWallMs before it: absent
245
+ // means today's behaviour (the runner's own default), present it is part
246
+ // of the signed hash — jobSpecHash canonizes the whole resolved spec
247
+ // (resolveSpec), so a spec that names a model is an operator signing off
248
+ // on THAT model, not merely on the goal/close it runs against. The judge
249
+ // model stays library-pinned pending recalibration — out of scope here.
250
+ if (spec.model !== undefined && !isNonEmptyString(spec.model)) {
251
+ red('invalid-value', 'model', 'non-empty string — exact provider model id (e.g. "claude-sonnet-5")');
252
+ }
253
+
243
254
  // 3. the outer write fence — operator law (interview decision #4), same
244
255
  // containment rules as the workflow layer THROUGH THE SAME CODE (F9: two
245
256
  // transforms would let a fence validate green and mean something else).
@@ -732,3 +743,30 @@ export function checkApproval(job, approvals) {
732
743
  const h = createHash('sha256').update(c).digest('hex');
733
744
  return approvals.some((a) => isObj(a) && /** @type {Record<string, unknown>} */ (a).specHash === h);
734
745
  }
746
+
747
+ /**
748
+ * Pure decision: which worker model id a run actually uses (build-list #3,
749
+ * hamr's GO 2026-08-30). The signed spec's `model`, if present, is what an
750
+ * operator SIGNED — it wins outright. A `--model` runner flag naming a
751
+ * DIFFERENT id is refused rather than silently overridden (spec wins, a
752
+ * mismatched flag is a mistake worth stopping on, never a silent swap); the
753
+ * same id from both is not a conflict. Extracted from scripts/run-u.mjs so
754
+ * the decision is testable without running the script or touching a provider.
755
+ * @param {{ specModel?: string, flagModel?: string, defaultModel: string }} args
756
+ * `specModel` — `job.model` if the signed spec carries one, else undefined.
757
+ * `flagModel` — the model id an explicit `--model <tier>` resolved to, else
758
+ * undefined (a flag that was never passed, not merely a default tier name).
759
+ * `defaultModel` — the runner's own default when neither spec nor flag says.
760
+ * @returns {{ model: string, source: 'spec'|'flag'|'default' }}
761
+ * @throws {Error} when the spec names a model and the flag names a different one
762
+ */
763
+ export function resolveWorkerModel({ specModel, flagModel, defaultModel }) {
764
+ if (specModel !== undefined) {
765
+ if (flagModel !== undefined && flagModel !== specModel) {
766
+ throw new Error(`spec names model "${specModel}" but --model resolved to "${flagModel}" — the signed spec wins; a mismatched --model flag is refused, never silently overridden. Drop --model or point it at the same model.`);
767
+ }
768
+ return { model: specModel, source: 'spec' };
769
+ }
770
+ if (flagModel !== undefined) return { model: flagModel, source: 'flag' };
771
+ return { model: defaultModel, source: 'default' };
772
+ }
package/types/job.d.ts CHANGED
@@ -82,6 +82,30 @@ export function jobSpecHash(job: object): string;
82
82
  * @returns {boolean}
83
83
  */
84
84
  export function checkApproval(job: object, approvals: unknown): boolean;
85
+ /**
86
+ * Pure decision: which worker model id a run actually uses (build-list #3,
87
+ * hamr's GO 2026-08-30). The signed spec's `model`, if present, is what an
88
+ * operator SIGNED — it wins outright. A `--model` runner flag naming a
89
+ * DIFFERENT id is refused rather than silently overridden (spec wins, a
90
+ * mismatched flag is a mistake worth stopping on, never a silent swap); the
91
+ * same id from both is not a conflict. Extracted from scripts/run-u.mjs so
92
+ * the decision is testable without running the script or touching a provider.
93
+ * @param {{ specModel?: string, flagModel?: string, defaultModel: string }} args
94
+ * `specModel` — `job.model` if the signed spec carries one, else undefined.
95
+ * `flagModel` — the model id an explicit `--model <tier>` resolved to, else
96
+ * undefined (a flag that was never passed, not merely a default tier name).
97
+ * `defaultModel` — the runner's own default when neither spec nor flag says.
98
+ * @returns {{ model: string, source: 'spec'|'flag'|'default' }}
99
+ * @throws {Error} when the spec names a model and the flag names a different one
100
+ */
101
+ export function resolveWorkerModel({ specModel, flagModel, defaultModel }: {
102
+ specModel?: string;
103
+ flagModel?: string;
104
+ defaultModel: string;
105
+ }): {
106
+ model: string;
107
+ source: "spec" | "flag" | "default";
108
+ };
85
109
  export const CLOSE_TYPES: readonly string[];
86
110
  /** the hierarchy: which verdict classes a close type may claim (PRD §7). Read by
87
111
  * the verdictType check — a declared verdict may not demand a class its close