bareloop 0.23.0 → 0.25.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 +218 -0
- package/bareloop.context.md +233 -19
- package/package.json +1 -1
- package/src/authorflow.js +17 -7
- package/src/authoring.js +32 -0
- package/src/authorjob.js +108 -36
- package/src/calibrate.js +33 -12
- package/src/cardauthor.js +23 -16
- package/src/cli.js +36 -5
- package/src/declaredclose.js +17 -10
- package/src/index.js +17 -5
- package/src/job.js +58 -3
- package/src/judged.js +132 -14
- package/src/kinds.js +84 -13
- package/src/planrun.js +14 -2
- package/src/providers.js +143 -18
- package/src/run.js +9 -4
- package/src/source.js +849 -0
- package/src/validate.js +19 -0
- package/types/authoring.d.ts +30 -0
- package/types/authorjob.d.ts +10 -6
- package/types/calibrate.d.ts +9 -2
- package/types/cardauthor.d.ts +13 -11
- package/types/cli.d.ts +2 -1
- package/types/declaredclose.d.ts +2 -0
- package/types/index.d.ts +2 -1
- package/types/job.d.ts +22 -1
- package/types/judged.d.ts +123 -14
- package/types/kinds.d.ts +66 -3
- package/types/planrun.d.ts +10 -2
- package/types/providers.d.ts +73 -11
- package/types/run.d.ts +9 -3
- package/types/source.d.ts +206 -0
- package/types/validate.d.ts +7 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,224 @@ All notable changes to bareloop are documented here. Format:
|
|
|
5
5
|
[SemVer](https://semver.org/spec/v2.0.0.html). Pre-1.0: **minor** = a ladder rung or
|
|
6
6
|
feature lands, **patch** = docs, fixes, scaffolding.
|
|
7
7
|
|
|
8
|
+
## [0.25.0] — 2026-09-12
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **The source front door** (PRD item 33/M2) — a plain folder, a plain file, or one
|
|
13
|
+
`http(s)` URL now runs without a `--patient` git checkout: `prepareSource`
|
|
14
|
+
(`src/source.js`) freezes it into a hidden, scratch git tree bareloop owns (the person
|
|
15
|
+
never sees the repo), `proveDestination` proves a drop-off file is landable at $0 before
|
|
16
|
+
any token spends, and `copyOut` delivers the run's own output there once, on a minted
|
|
17
|
+
green, never overwriting. New CLI `scripts/prep-source.mjs`. `scripts/run-u.mjs` wires
|
|
18
|
+
both: a manifest-declared destination refuses before any token, and copies out on a
|
|
19
|
+
green (a refused copy-out never changes the verdict). The destination is a PER-RUN value
|
|
20
|
+
recorded in the manifest (`<into>/source.json`), never a signed job-spec field — a job
|
|
21
|
+
spec is a repeatable shape, signed once; nothing was added to `job.js`/`jobSpecHash`.
|
|
22
|
+
Exported: `prepareSource`, `proveDestination`, `copyOut`, `readSourceManifest`,
|
|
23
|
+
`frontDoorFromManifest`. `git()` (`src/kinds.js`) is now exported for reuse by this
|
|
24
|
+
module rather than re-spawned a second way.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **Source front door (PRD item 33/M2 review):** `output` is now validated the ONE way
|
|
29
|
+
everywhere it is read or written (new `output-invalid` refusal) — a bare
|
|
30
|
+
`resolve(tree, output)` previously let a shape like `output/../../x` escape the frozen
|
|
31
|
+
tree; a hand-edited manifest carrying such a value is now treated as no front door at
|
|
32
|
+
all by `frontDoorFromManifest`, never handed to `copyOut` unvalidated. `prepareSource`
|
|
33
|
+
now proves the destination (`proveDestination`) BEFORE creating `into`, so a refused
|
|
34
|
+
destination leaves nothing on disk. `copyOut` takes an optional `into` (the run's
|
|
35
|
+
scratch root); `scripts/run-u.mjs`'s two call sites now pass `dirname(wd)` instead of
|
|
36
|
+
`wd`, so a destination inside the scratch area but outside the frozen tree is caught as
|
|
37
|
+
`destination-contained` instead of slipping through.
|
|
38
|
+
- **Source front door hard-line defect (PRD item 33/M2, live-proven):** every frozen
|
|
39
|
+
file's content — a folder, a single file, or a fetched URL body — is now scanned for a
|
|
40
|
+
known secret shape (the same `scanSecrets` inventory, `src/validate.js`) BEFORE
|
|
41
|
+
anything is written under `into`; a hit refuses `source-carries-secret`, naming the
|
|
42
|
+
file(s) and pattern name(s) only, never the matched text, and leaves nothing on disk.
|
|
43
|
+
Previously the door scanned only the URL string, so a plain folder or file carrying a
|
|
44
|
+
real API key (e.g. an `.env`) was frozen into the tree and committed to the hidden git
|
|
45
|
+
seed untouched.
|
|
46
|
+
- **Source front door, M2b review fixes 2–8 (PRD item 33):** `.env`/`.env.*` refused by
|
|
47
|
+
NAME whatever it contains (`source-env-file`), belt-and-braces beside the content scan;
|
|
48
|
+
a per-file `MAX_BUFFER` ceiling for folder and single-file sources (`source-file-
|
|
49
|
+
oversize`; repo sources exempt); the delivered file now carries the date
|
|
50
|
+
(`profile-2026-09-12.md`, `-2`/`-3` the same day, cap 99) — new exports
|
|
51
|
+
`datedDestination`/`pickDelivery`, used by both `proveDestination` and `copyOut` so the
|
|
52
|
+
two can never disagree; `copyOut` returns `{bytes, sha256, path}` and `run-u.mjs`
|
|
53
|
+
spine-records the real landed path (`destination-written {path, declared, bytes,
|
|
54
|
+
sha256}`); a URL's redirects are followed but never invisible — the final URL rides in
|
|
55
|
+
the manifest (`finalUrl`) and is printed by `prep-source`, and is secret-scanned like
|
|
56
|
+
the typed one; a nested `.git` anywhere below the root refuses `source-nested-repo`,
|
|
57
|
+
`git add` runs `-f` so a source `.gitignore` cannot drop a file from the seed, and the
|
|
58
|
+
seed is read back with `git ls-tree` and diffed against the manifest's file list
|
|
59
|
+
(`source-seed-incomplete` on a shortfall); a git repo root is now an allowed source
|
|
60
|
+
(`kind: 'repo'`) — copied with its history, working files at the tree root, seed on top
|
|
61
|
+
of existing history; a `.git` FILE (linked worktree/submodule) refuses
|
|
62
|
+
`source-is-linked-worktree`; `prep-source` prints a folder-blast-radius note (mirrored
|
|
63
|
+
in `bareloop.context.md`).
|
|
64
|
+
- **Repo-source hard-line defect (live-proven during the M2b 7 review):** a copied
|
|
65
|
+
`.git/hooks` carried the SOURCE repo's own `pre-commit`/`commit-msg`/`post-commit`
|
|
66
|
+
scripts, and `git commit` ran them — arbitrary code from the source repo, executed
|
|
67
|
+
inside bareloop's own process, before a single token spent. Fixed: the copied hooks
|
|
68
|
+
directory is stripped after the `.git` copy, and every git call the door makes pins
|
|
69
|
+
`core.hooksPath` to a path that is never created, so a `core.hooksPath` set in the
|
|
70
|
+
copied `.git/config` cannot reopen the hole either.
|
|
71
|
+
|
|
72
|
+
- **Source front door D1/D2/D3 rework (PRD item 33, hamr's rulings, 2026-09-12):**
|
|
73
|
+
- **D1 — repo sources copy only what git tracks** (hamr, verbatim: "copy only what git
|
|
74
|
+
tracks"; closes F164/F165): repo sources are enumerated with `git ls-files --stage` in
|
|
75
|
+
the source, never a filesystem walk; each tracked path's working-tree content is
|
|
76
|
+
frozen, and nothing untracked or gitignored is ever a candidate (npm
|
|
77
|
+
`node_modules/.bin/*` symlinks, this repo's own `.claude/`, a gitignored `.env`,
|
|
78
|
+
none of it reaches the tree or the seed). `git add -A` drops `-f` for repo sources
|
|
79
|
+
(kept for plain folder/file/URL sources). A gitlink (a real submodule) refuses
|
|
80
|
+
`source-nested-repo`; a tracked symlink refuses `source-symlink` only when it
|
|
81
|
+
resolves outside the source root — one staying inside is copied verbatim as a link.
|
|
82
|
+
- **D2 — secrets scanned inside binary content too** (closes the F166 residual): every
|
|
83
|
+
frozen file's content is scanned for secrets, binary files included, decoded `latin1`
|
|
84
|
+
(byte-preserving) instead of `utf8` — the same `SECRET_PATTERNS` inventory, no second
|
|
85
|
+
pattern list.
|
|
86
|
+
- **D3 — destination is a DIRECTORY, never a filename** (hamr's ruling, 2026-09-12): it
|
|
87
|
+
may already exist, need not be empty, and may sit inside the source itself
|
|
88
|
+
(`destination-in-source` is gone). A job may produce more than one file; `copyOut`
|
|
89
|
+
delivers every non-empty file under `output/` (excluding `output/.gitkeep`), each
|
|
90
|
+
under its own dated name, and returns `{files: [{path, bytes, sha256}]}` instead of a
|
|
91
|
+
single object. The single-file `output` manifest field and `output-invalid` validator
|
|
92
|
+
are removed — the escape vector they guarded is now closed structurally, since
|
|
93
|
+
`copyOut` reads `tree/output/` itself rather than a manifest-declared path. A repo
|
|
94
|
+
source's `destination` is recorded as the declared write fence and never proven or
|
|
95
|
+
copied into (`frontDoorFromManifest` returns `null` for a repo manifest) — wiring it
|
|
96
|
+
into the signed `writeScope` field is left to a later milestone. New refusal codes:
|
|
97
|
+
`destination-not-directory`, `destination-not-writable`, `destination-invalid`
|
|
98
|
+
(repo-destination type guard); retired: `output-invalid`,
|
|
99
|
+
`destination-output-required`, `destination-in-source`. `scripts/prep-source.mjs`
|
|
100
|
+
drops `--output`; `scripts/run-u.mjs`'s copy-out call site emits one
|
|
101
|
+
`destination-written` per delivered file.
|
|
102
|
+
- **D4 — measured a real 15 MB repo** (`~/PycharmProjects/adaptlearn`, the repo that
|
|
103
|
+
broke F164): 1296 files, 3.16 MB copied, 0.89s elapsed, ~84 MB maxRSS, no gitignored
|
|
104
|
+
or untracked content reached the tree or the seed. No size cap added.
|
|
105
|
+
|
|
106
|
+
## [0.24.0] — 2026-09-10
|
|
107
|
+
|
|
108
|
+
### Added
|
|
109
|
+
|
|
110
|
+
- **`gemini-api` on the provider menu, ADMITTED-PENDING-PROBE** (PRD item 31.3;
|
|
111
|
+
hamr, 2026-09-09: *"anthropic, openai, gemini drop ollama for now"*). Reads
|
|
112
|
+
`GEMINI_API_KEY` and maps two genuinely different tiers — `sonnet` →
|
|
113
|
+
`gemini-2.5-pro`, `haiku` → `gemini-2.5-flash`. It has **zero runs**: the
|
|
114
|
+
probe rule (no endpoint in the menu without its own clean paid probe) is not
|
|
115
|
+
waived for it. `PROBE_STATUS` in `src/providers.js` records that debt per
|
|
116
|
+
provider and `probeWarningLines` renders the loud `⚠ UNPROVEN PROVIDER`
|
|
117
|
+
marker `scripts/run-u.mjs` prints at launch, right where money is about to be
|
|
118
|
+
spent. The table is read to WARN, never to REFUSE — refusing a provider hamr
|
|
119
|
+
admitted would be a second, invisible ruling.
|
|
120
|
+
- **Per-provider `endpointKey`** — each table entry declares the CONSTRUCTOR
|
|
121
|
+
option name it reads an endpoint from, instead of a hardcoded `baseUrl`.
|
|
122
|
+
bare-agent's providers do not agree on one spelling (`baseUrl` for
|
|
123
|
+
anthropic/openai/gemini, `url` for Ollama) and none of them validate unknown
|
|
124
|
+
option names, so a mis-spelled endpoint is silently DROPPED, not rejected:
|
|
125
|
+
measured live, `new OllamaProvider({baseUrl: X}).url` stays
|
|
126
|
+
`http://localhost:11434` with no error. That is the F149 class (DeepSeek
|
|
127
|
+
silently ignoring `max_completion_tokens`: asked 64 output tokens, returned
|
|
128
|
+
665). A job spec still carries ONE field name, `baseUrl`; the translation to
|
|
129
|
+
each constructor's own spelling happens once, in `makeProvider`.
|
|
130
|
+
|
|
131
|
+
### Changed
|
|
132
|
+
|
|
133
|
+
- **The judge is no longer a Claude judge** (PRD item 32; hamr, 2026-09-10:
|
|
134
|
+
*"what i care about is that judge becomes llm agnostic and not set to one
|
|
135
|
+
model or provider"*). `JUDGE_MODEL` is no longer read by any grading path.
|
|
136
|
+
The judge identity is RESOLVED per job by the new exported `resolveJudge`:
|
|
137
|
+
the spec's signed `judge: {provider, model}` if it names one, else **the
|
|
138
|
+
job's own worker provider and model** — so a DeepSeek or Gemini job judges on
|
|
139
|
+
its own provider and needs no second account. There is deliberately no
|
|
140
|
+
library fall-back: a run that cannot name its judge has not wired one, and
|
|
141
|
+
the judged stage instrument-STOPS rather than grading under an identity
|
|
142
|
+
nobody chose. `JUDGE_MODEL` stays exported as the pre-item-32 pin so an
|
|
143
|
+
adopter's import survives. The judge is now built through the provider
|
|
144
|
+
factory like every other provider — no `new AnthropicProvider` survives for
|
|
145
|
+
a judge in `scripts/run-u.mjs`, `scripts/run-author.mjs` or `src/cli.js` —
|
|
146
|
+
and its key follows the resolved judge provider's own `envKey`, with
|
|
147
|
+
`JUDGE_API_KEY` as a role-named override in front.
|
|
148
|
+
|
|
149
|
+
**BREAKING for direct library callers.** `runCalibration`,
|
|
150
|
+
`foldJudgedArtifacts` and `prepareSigning`/`authorCloseForJob` now require
|
|
151
|
+
the resolved `judgeModel` and THROW without it; `runJob`/`runPlan` take a
|
|
152
|
+
`judgeModel` beside `judgeProvider`, and a judged stage stops as a wiring gap
|
|
153
|
+
when it is absent. Adding a default would be the silent fall-back this change
|
|
154
|
+
exists to remove.
|
|
155
|
+
|
|
156
|
+
**No threshold moved.** The judge still renders no verdict — it locates facts
|
|
157
|
+
and a deterministic `decide()` renders; unsure is RED; `CALIBRATION_SIZE` is
|
|
158
|
+
10 with a 10-of-10 floor and no partial credit. Un-pinning a provider is not
|
|
159
|
+
a licence to move a bar. Self-grading (a worker's own family reading its
|
|
160
|
+
work) is licensed by MEASUREMENT: the calibration gate is exactly the
|
|
161
|
+
instrument that catches a judge which cannot see failures, and its frozen set
|
|
162
|
+
contains cases that MUST fail.
|
|
163
|
+
- **A new optional signed spec field, `judge: {provider, model}`** — both
|
|
164
|
+
halves required together, `provider` from the `PROVIDERS` menu, unknown
|
|
165
|
+
nested keys red. It is part of `jobSpecHash`, so changing the judge kills the
|
|
166
|
+
signature and forces recalibration — and the recalibration guard now fires
|
|
167
|
+
across a PROVIDER change, not just a model bump. The drafting agent cannot
|
|
168
|
+
express it: naming your own examiner is arbiter territory.
|
|
169
|
+
- **The judge's API key is demanded only when a judge is CALLED** (PRD item
|
|
170
|
+
31.4). `scripts/run-u.mjs` hard-exited on a missing `ANTHROPIC_API_KEY` for
|
|
171
|
+
every job, so a `verdictType: 'green'` job with a DeepSeek or Gemini worker
|
|
172
|
+
and a mechanical close could not start without an Anthropic account it would
|
|
173
|
+
never spend a token against. Now the WORKER's key comes from the provider
|
|
174
|
+
table's own `envKey` and is always required; the JUDGE's key follows the
|
|
175
|
+
RESOLVED judge provider's own `envKey` (item 32.1 — the judge is no longer
|
|
176
|
+
pinned to Anthropic), with `JUDGE_API_KEY` as a role-named override in
|
|
177
|
+
front, and is required only when the close actually judges. When it does
|
|
178
|
+
refuse it refuses at $0, before the worker spends anything, naming the
|
|
179
|
+
judged-stage count and the resolved judge model — a judged run that
|
|
180
|
+
discovered this at the close would have paid in full for a verdict it
|
|
181
|
+
cannot render. A green `anthropic-api` job behaves byte-identically to
|
|
182
|
+
before. `bareloop run` (the bundle runner) is unchanged: `ANTHROPIC_API_KEY`
|
|
183
|
+
-only by a deliberate, documented contract.
|
|
184
|
+
- **One reading of "does this close judge?"** — `judgedStages()`/`closeJudges()`
|
|
185
|
+
are exported from `src/kinds.js` and the six open-coded copies of that
|
|
186
|
+
predicate now share them. The runner's own comment warned against "a second
|
|
187
|
+
reading of the declaration that can drift from the one the runner actually
|
|
188
|
+
executes"; 31.4 needed a seventh, to decide a REFUSAL, which is that warning
|
|
189
|
+
coming true.
|
|
190
|
+
|
|
191
|
+
- **`hitl` leaves the authoring menu; its code stays** (PRD item 31.1; hamr:
|
|
192
|
+
*"retire hitl from list, keep its code"*). Three distinct class lists replace
|
|
193
|
+
two: `LOCKED_CLASSES` (no guard battery exists — empty today),
|
|
194
|
+
`UNLISTED_CLASSES` (`['hitl']` — built, validates and RUNS, simply never
|
|
195
|
+
offered) and `MENU_CLASSES` (live minus unlisted). Only the AUTHORING surface
|
|
196
|
+
reads them, which is what keeps hitl's code alive and under test; locking the
|
|
197
|
+
class instead would have taken `validateCloseDecl`, the guard battery and two
|
|
198
|
+
whole test files with it. `scripts/run-interview.mjs` no longer advertises
|
|
199
|
+
`--verdict hitl` in a usage line the library then refuses.
|
|
200
|
+
- **Green-only prose corrected** (PRD item 31.2). `soft-green` was never
|
|
201
|
+
locked; the source comment claiming *"v1 STILL ADMITS ONLY green"* was false
|
|
202
|
+
and had already sent one build in the wrong direction, and
|
|
203
|
+
`bareloop.context.md` — the file that SHIPS — said all three classes were
|
|
204
|
+
admitted. Fixed, and pinned by tests, because prose drifts when nothing
|
|
205
|
+
executes it.
|
|
206
|
+
|
|
207
|
+
### Security
|
|
208
|
+
|
|
209
|
+
- **`SECRET_PATTERNS` learns the Gemini/Google API key shape** (PRD item
|
|
210
|
+
31.3, F160). `gemini-api` was admitted to real runs (this file, above)
|
|
211
|
+
without its own key shape joining the ONE secret-shape inventory
|
|
212
|
+
(`src/validate.js`) that drives `scanSecrets`, `redactSecrets` and
|
|
213
|
+
`sweepSecretLiterals` — a `GEMINI_API_KEY`-shaped literal (`AIza` + 35
|
|
214
|
+
chars) reaching a spine, a close's output, or a signed doc would have gone
|
|
215
|
+
undetected and unredacted. Fixed with a left-bounded pattern in the same
|
|
216
|
+
style as the existing shapes; monotonic (detection only added, nothing
|
|
217
|
+
loosened).
|
|
218
|
+
|
|
219
|
+
### Notes
|
|
220
|
+
|
|
221
|
+
- Ollama is deliberately NOT admitted. It takes no key and bills nothing, so
|
|
222
|
+
every round would price at $0 through machinery that treats $0 as a real
|
|
223
|
+
price — the honesty violation the `?? 0` rule exists to stop. That question
|
|
224
|
+
gets answered before a menu entry, not after.
|
|
225
|
+
|
|
8
226
|
## [0.23.0] — 2026-09-09
|
|
9
227
|
|
|
10
228
|
### Added
|
package/bareloop.context.md
CHANGED
|
@@ -79,8 +79,28 @@ inherited rule carries the green that minted it and the contrast that attributed
|
|
|
79
79
|
an unknown provider name THROWS there, it never falls back to a default. Per-model
|
|
80
80
|
request-key gating lives in that table: `deepseek-chat` sets bare-agent's
|
|
81
81
|
`legacyMaxTokens`, because DeepSeek silently ignores `max_completion_tokens` and an
|
|
82
|
-
output cap that does not bind is a money hazard.
|
|
83
|
-
|
|
82
|
+
output cap that does not bind is a money hazard. `provider: 'gemini-api'` (PRD item 31.3)
|
|
83
|
+
takes the same `Loop` path, reads `GEMINI_API_KEY`, and maps two real tiers
|
|
84
|
+
(`sonnet` → `gemini-2.5-pro`, `haiku` → `gemini-2.5-flash`). It is
|
|
85
|
+
**ADMITTED-PENDING-PROBE**: it has ZERO runs, and the probe rule is not waived for it —
|
|
86
|
+
`PROBE_STATUS` (`src/providers.js`) is the machine-readable form of that debt, and
|
|
87
|
+
`probeWarningLines(provider)` renders the loud launch-time marker `scripts/run-u.mjs`
|
|
88
|
+
prints. That table is read to WARN, never to REFUSE. Each entry also declares its own
|
|
89
|
+
`endpointKey` — the CONSTRUCTOR option name it reads an endpoint from — because
|
|
90
|
+
bare-agent's providers do not agree on one spelling (`baseUrl` for anthropic/openai/
|
|
91
|
+
gemini, `url` for Ollama) and none of them validate unknown option names, so a
|
|
92
|
+
mis-spelled endpoint is silently DROPPED rather than rejected. Your job spec keeps one
|
|
93
|
+
field name, `baseUrl`; the translation happens once, inside `makeProvider`. Ollama is
|
|
94
|
+
deliberately NOT admitted: it takes no key and bills nothing, so every round would price
|
|
95
|
+
at $0 through machinery that treats $0 as a real price. The JUDGE IS NO LONGER PINNED
|
|
96
|
+
(PRD item 32): `resolveJudge` (exported) resolves it per job — the spec's signed
|
|
97
|
+
`judge: {provider, model}` if it names one, else **this job's own worker provider and
|
|
98
|
+
model**, so a DeepSeek or Gemini job judges on its own provider and needs no second
|
|
99
|
+
account. There is no library fall-back: a run that cannot name its judge has not wired
|
|
100
|
+
one, and the judged stage instrument-STOPS rather than grading under an identity nobody
|
|
101
|
+
chose. `JUDGE_MODEL` stays exported as the pre-item-32 pin — no grading path reads it.
|
|
102
|
+
The judge's key follows the resolved judge provider's own `envKey`, with `JUDGE_API_KEY`
|
|
103
|
+
as a role-named override in front of it. The bundle runner
|
|
84
104
|
(`bareloop run`) is `ANTHROPIC_API_KEY`-only and REFUSES at $0, naming the key it would
|
|
85
105
|
have needed, rather than constructing another provider with the wrong key.
|
|
86
106
|
- **Reuse — where a workflow comes from:** a plain `runJob` always drafts cold. Passing
|
|
@@ -143,13 +163,14 @@ minting claim, or the shell-owned retry cap — all unknown-field reds.
|
|
|
143
163
|
|---|---|---|
|
|
144
164
|
| `job` | kebab-case slug | |
|
|
145
165
|
| `description` | non-empty string | |
|
|
146
|
-
| `provider` | `anthropic-api` \| `openai-api` \| `clipipe-subscription` | menu (`PROVIDERS`); part of the lineage key by definition. Only `anthropic-api` is guaranteed (F48); `openai-api` (PRD item 28) goes through the provider factory (`src/providers.js`), reads `OPENAI_API_KEY`, and admits only `deepseek-chat` today — see "Worker surface" above for its `baseUrl` field and per-model gating; `clipipe-subscription` drives the worker natively and needs `opts.nativeProvider` |
|
|
166
|
+
| `provider` | `anthropic-api` \| `openai-api` \| `gemini-api` \| `clipipe-subscription` | menu (`PROVIDERS`); part of the lineage key by definition. Only `anthropic-api` is guaranteed (F48); `openai-api` (PRD item 28) goes through the provider factory (`src/providers.js`), reads `OPENAI_API_KEY`, and admits only `deepseek-chat` today — see "Worker surface" above for its `baseUrl` field and per-model gating; `gemini-api` (PRD item 31.3) reads `GEMINI_API_KEY` and is ADMITTED-PENDING-PROBE — zero runs, and every launch says so (`PROBE_STATUS`/`probeWarningLines`); `clipipe-subscription` drives the worker natively and needs `opts.nativeProvider` |
|
|
147
167
|
| `conditions` | `{ providerPath?, closeVerbosity?, taskFraming?, scaffold? }` | declared keys only, string values — the environment label (consumed by the N3 lineage key; recorded on spines from run one) |
|
|
148
168
|
| `cadence` | `{ unit: hour\|day\|week, every: 1..30 }` | validated now, consumed at N5 (Scheduler) |
|
|
149
169
|
| `budgetUsd` | `0 < n <= shell cap` | ceiling chain: workflow ≤ job ≤ shell — each layer may tighten, never exceed |
|
|
150
170
|
| `maxWallMs` | optional integer ms `>= MIN_WALL_MS` (one close timeout) | the run's wall clock. **NO DEFAULT, by ruling** — absent means time-unbounded *by explicit operator choice*, never by fallback (F45: a defaulted cap is a silent second ceiling). Enforcement is a BETWEEN-ROUND deadline, so the honest worst case is `maxWallMs + closeStages × closeTimeoutMs` — every stage of a staged close gets the FULL timeout — and all three numbers are reported (`loop.stop()` cannot cut an in-flight call — F61 measured 500ms→4,018ms). The `MIN_WALL_MS` floor is a ONE-stage, un-autoset-default number, so a spec with many stages OR a raised/autoset `closeTimeoutMs` can validate while its overshoot dwarfs its cap: `runPlan` adds a RUN-START check (PRD item 27/M3) that refuses `wall-under-close-timeout` at $0 once the EFFECTIVE per-stage ceiling is known (autoset or signed), rather than a silent clamp. Operator-only, tighten-only; adding or changing it changes the spec hash |
|
|
151
171
|
| `closeTimeoutMs` | optional integer ms `>= CLOSE_TIMEOUT_FLOOR_MS` (120,000 — hamr's arbiter floor, 2026-09-07) | PRD item 27/M3's SIGNED override for the close's own per-stage wall-clock cap. Absent (the normal case): the run autosets it from a $0 seed timing pass — `max(FLOOR, K × slowest measured stage)`, `K = 5` (hamr, 2026-09-07: "2 and 5 are fine"). Present: the operator's own number wins outright and may legally sit ABOVE *or below* the autoset estimate (the rates-passthrough shape, F113 — never tighten-only). Arbiter territory exactly like `budgetUsd`/`maxWallMs`/`close[].sha256`: the authoring pipeline's `assembleSpec` refuses a draft carrying it |
|
|
152
|
-
| `model` | optional non-empty string, exact provider model id (e.g. `"claude-sonnet-5"`) | the WORKER's model, build-list #3. Absent means today's runner default; present it wins over a `--model` flag outright, and a flag naming a different id is refused (`resolveWorkerModel`, `src/job.js`) rather than silently overridden. Part of the signed hash like every other field. Worker only — the
|
|
172
|
+
| `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's model comes from the `judge` row below, which DEFAULTS to this same worker model |
|
|
173
|
+
| `judge` | optional `{provider, model}`, both non-empty; `provider` from the `PROVIDERS` menu | the signed JUDGE override (PRD item 32). Absent, the judge resolves to this job's own `provider`/`model` (`resolveJudge`) — which is what lets a non-Anthropic job judge without a second account. Present, it pins a judge deliberately different from the worker (an independent reader). Both halves are required together: a judge with a provider and no model is an identity nobody signed. Part of the signed hash, so changing it kills the signature and forces recalibration. Unknown nested keys red — no smuggling level exists in a signed spec. The drafting agent cannot express it: naming your own examiner is arbiter territory |
|
|
153
174
|
| `writeScope` | array of contained globs | the operator's outer fence; the plan's own scopes must fit inside it, same containment code |
|
|
154
175
|
| `steps` | RETIRED | operator-authored `steps[]` was deleted (PRD v1.32); a spec carrying it reds `shape-retired:steps` by name rather than half-running |
|
|
155
176
|
| `escalation` | `{ mode: "decision-ready" }` | the pain channel is not optional |
|
|
@@ -160,7 +181,7 @@ the step plan at run time (gated by `validatePlan`); the human signs only:
|
|
|
160
181
|
| field | shape | notes |
|
|
161
182
|
|---|---|---|
|
|
162
183
|
| `goal` | non-empty text | what the agent plans against |
|
|
163
|
-
| `verdictType` | `green` \| `soft-green` \| `hitl` | declared radio, never inferred (`VERDICT_TYPES`, frozen). **All three
|
|
184
|
+
| `verdictType` | `green` \| `soft-green` \| `hitl` | declared radio, never inferred (`VERDICT_TYPES`, frozen). **All three still VALIDATE and RUN** — `LOCKED_VERDICTS` is empty, so nothing reds `request-red` on the class itself, and an existing spec of any class executes. **But AUTHORING offers two: `green` and `soft-green` (`MENU_CLASSES`).** `soft-green` has its judged floor (`judged-floor`, signed rubric card + calibration gate) and is the forward path for a job needing judgement, paired with the end-of-run review door; `hitl` is RETIRED and UNLISTED (`UNLISTED_CLASSES`, PRD item 31.1) — the class moved to fwdloop, so `runInterview` refuses a hitl pick as counted demand and no new hitl job can be drafted, while its battery, its runtime and an already-signed hitl spec are untouched and still work exactly as documented here. The three lists are DIFFERENT facts: `LOCKED_CLASSES` (no battery — cannot validate or run; empty today), `UNLISTED_CLASSES` (built and runnable, never offered — `['hitl']`), `MENU_CLASSES` (what authoring offers). Every `request-red` (e.g. for a locked TOOL verb, not a verdict class) 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 |
|
|
164
185
|
| `close` | **an ORDERED LIST of named stages** `[{ name, cmd, expect, judged?, gapKeep?, offer?, needs?, direction? }, ...]` (PRD v1.28), or a close object (table below) | 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 still VALIDATES (`CLOSE_TYPES`) but the plan flow refuses all three at runtime as `close-unsupported` — an object close names no command to run. The modern route to a judged or human-decided close is a staged `closeDecl` (`judged-floor` / `human-confirms` stage kinds), not this object form |
|
|
165
186
|
| `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 |
|
|
166
187
|
| `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) |
|
|
@@ -509,7 +530,7 @@ Escalations are decision-ready (category, options, spend); cap-halt is its own
|
|
|
509
530
|
category, never merged with "wrong". A thrown middle is relayed by its `category`
|
|
510
531
|
property (`cap-halt`, `gate-red`, …); an unnamed throw is `interpreter-red`. Close output
|
|
511
532
|
is scrubbed at capture (an injected `redact`, wired to bareguard by `runPlan` with the
|
|
512
|
-
validators' full secret-shape inventory — Bearer/sk-/ghp_/github_pat_/AKIA/xox) so a
|
|
533
|
+
validators' full secret-shape inventory — Bearer/sk-/ghp_/github_pat_/AKIA/xox/AIza) so a
|
|
513
534
|
secret a checked command echoes never enters the append-only spine or a worker prompt —
|
|
514
535
|
a benign gap is byte-identical (secrets hard line; design law #7 intact).
|
|
515
536
|
|
|
@@ -571,7 +592,7 @@ a step, or test it without a provider.
|
|
|
571
592
|
|
|
572
593
|
| step | call | what it is |
|
|
573
594
|
|---|---|---|
|
|
574
|
-
| 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)`), all three
|
|
595
|
+
| 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)`), all three sets still EXIST, but the interview ADMITS only what is on the menu: nothing is locked today (`LOCKED_CLASSES` is empty) and `hitl` is UNLISTED (PRD item 31.1), so `runInterview({verdictType:'hitl'})` returns a refusal rather than `ok:true` — ask `MENU_CLASSES` which classes can be authored, never this sentence. The interview asks NOTHING about a genre and NOTHING about the repository. **Ask the library for the set and its numbers — never hardcode a count or a slot number**: the green set has lost 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 RETIRED, UNLISTED `hitl` set (still readable via `questionsFor`, never authorable) 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 likewise those five plus two more — Q6 the signed rubric card (what separates a pass from a fail) and Q7 the frozen calibration set (one example to pass, one to fail) — the judged floor's two required inputs. `verdictType` and `repoPath` are STRUCTURED input, never parsed out of prose. Answers are scrubbed at INGEST |
|
|
575
596
|
| 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 |
|
|
576
597
|
| 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 |
|
|
577
598
|
| 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 |
|
|
@@ -617,10 +638,13 @@ so it takes no `cmd`, no `timeoutMs` and no `env`; it is `offer: false` BY LAW
|
|
|
617
638
|
(`NEVER_OFFERED_KINDS` — a paid judge is never an in-run ruler); and it SKIPS the seed-verdict
|
|
618
639
|
read (ruling 8) as a recorded `skipped` row.
|
|
619
640
|
|
|
620
|
-
**Wiring a judged close, and what it costs.** The judge is
|
|
621
|
-
is never a step knob
|
|
622
|
-
`
|
|
623
|
-
|
|
641
|
+
**Wiring a judged close, and what it costs.** The judge is RESOLVED per job (`resolveJudge`,
|
|
642
|
+
PRD item 32) and is never a step knob or agent-selectable — naming your own examiner is arbiter
|
|
643
|
+
territory, and `judge` is a field the human SIGNS into the hash. The runner supplies BOTH halves,
|
|
644
|
+
`runJob({ judgeProvider, judgeModel })` — its own seam, not `provider`/`providerFor` — and they
|
|
645
|
+
travel together: a judged stage missing EITHER instrument-STOPS as a wiring gap (two distinct
|
|
646
|
+
messages, identity checked first) rather than grading on whatever binding was at hand or under a
|
|
647
|
+
name nobody wrote. Each locate call is metered per call and emitted
|
|
624
648
|
as a distinct `judge-round` spine record, which `runJob`'s ONE ledger accounts exactly like a
|
|
625
649
|
`worker-round` (`ACCOUNTED_ROUND_TYPES`, and `readResume`'s fold reads that same list, so a
|
|
626
650
|
resumed leg cannot silently widen a signed budget by the close's own spend). A budget funds the
|
|
@@ -690,8 +714,9 @@ null rather than an exact-looking floor.
|
|
|
690
714
|
carrying a `judged-floor` stage is refused when no set is stored, when no `judgeLoop` seam is
|
|
691
715
|
wired (a wiring gap, never a silent pass), when the gate is a casualty, and when anything short
|
|
692
716
|
of all-of-them grades. It runs **after** gates 1–3, so no $0 refusal is ever paid for. The
|
|
693
|
-
signing record keeps WHAT was certified — `cardHash`, `casesHash`, `setHash` (which covers
|
|
694
|
-
|
|
717
|
+
signing record keeps WHAT was certified — `cardHash`, `casesHash`, `setHash` (which covers the
|
|
718
|
+
resolved judge identity), the graded rows and the battery — beside `judgeModel` itself.
|
|
719
|
+
`runCalibration` takes `judgeModel` as a REQUIRED argument and throws without one.
|
|
695
720
|
|
|
696
721
|
**D9.3 for a judged-ONLY close** (ruling 3, hamr: *"fix it now, we are delivering softgreen"*).
|
|
697
722
|
A judged stage skips the seed read (ruling 8), so a close whose only work stage is judged has no
|
|
@@ -705,10 +730,12 @@ bump forces a full recalibration"* used to be an operator-side rule with nothing
|
|
|
705
730
|
which makes it prose (F45); it now has both halves of a detector. The tier the signer's set was
|
|
706
731
|
graded by is stored beside the cases as **`calibration.judgeModel`** — `CALIBRATION_FIELDS`
|
|
707
732
|
enumerates a stored set as exactly `cases` and `judgeModel` and nothing else — written by
|
|
708
|
-
`foldJudgedArtifacts` from the
|
|
709
|
-
Because it is INSIDE the spec, `jobSpecHash` covers it by construction:
|
|
710
|
-
every signed judged spec hashes differently, the
|
|
711
|
-
|
|
733
|
+
`foldJudgedArtifacts` from the RESOLVED judge identity the caller hands in (required; it throws
|
|
734
|
+
without one). Because it is INSIDE the spec, `jobSpecHash` covers it by construction: change the
|
|
735
|
+
judge — its model OR its provider — and every signed judged spec hashes differently, the
|
|
736
|
+
signature dies, and the re-sign is what re-runs the calibration gate. `runJudgedFloor` refuses to
|
|
737
|
+
grade when the stored identity is not the one about to grade, naming both, and it does not
|
|
738
|
+
degrade in either direction. **`validateCloseDecl` REQUIRES it** on any stored set (`missing-required`
|
|
712
739
|
on `…calibration.judgeModel`) — a set nobody can attribute to a tier is a floor nobody can tell
|
|
713
740
|
apart from a bumped one. **`declaredStages` stamps it onto the judged stage** as
|
|
714
741
|
`calibrationJudgeModel`, from the SIGNED bytes and through the one seam that already turns a
|
|
@@ -2880,6 +2907,183 @@ provider but `anthropic-api`, spine bundling for `formatReplay` on the minting r
|
|
|
2880
2907
|
`runid` is noted), and tarball packing — a bundle is a plain directory; use `npm pack` on it
|
|
2881
2908
|
yourself if you want a tarball.
|
|
2882
2909
|
|
|
2910
|
+
### The source front door — a plain folder/file/URL, no `--patient` (PRD item 33/M2, `src/source.js`)
|
|
2911
|
+
|
|
2912
|
+
A repo job still gives `runJob` a `--patient` checkout it owns. A NON-code job (a resume, a
|
|
2913
|
+
CSV, a page) has no repo to point at — this door is what lets it run anyway, WITHOUT the
|
|
2914
|
+
person ever seeing a git repo (H1's fix): the source is frozen into a hidden, scratch git
|
|
2915
|
+
tree bareloop creates and owns, before any token spends, so the run and its close judge the
|
|
2916
|
+
SAME bytes.
|
|
2917
|
+
|
|
2918
|
+
```js
|
|
2919
|
+
import { prepareSource, proveDestination, copyOut, readSourceManifest, frontDoorFromManifest } from 'bareloop';
|
|
2920
|
+
|
|
2921
|
+
const prepared = await prepareSource({
|
|
2922
|
+
source: '/home/me/resume.md', // an http(s) URL, an existing file, or an existing plain folder/repo
|
|
2923
|
+
into: '/home/me/.bareloop-jobs/j1', // must NOT already exist — fresh, never reused
|
|
2924
|
+
destination: '/home/me/profile-drafts', // optional — a DIRECTORY (D3), need not exist or be empty
|
|
2925
|
+
});
|
|
2926
|
+
// prepared.tree === '/home/me/.bareloop-jobs/j1/tree' — hand THIS to runJob as `workdir`
|
|
2927
|
+
// prepared.manifest.seed — the commit `seedAtHead` will read at run start
|
|
2928
|
+
```
|
|
2929
|
+
|
|
2930
|
+
Or the CLI: `node scripts/prep-source.mjs --source <path-or-url> --into <dir> [--destination
|
|
2931
|
+
<absolute-directory>]` — prints the tree path, the seed, and the exact next command.
|
|
2932
|
+
|
|
2933
|
+
> **If your source is a folder, read this first (hamr's ruling):** make a new folder, put
|
|
2934
|
+
> only the file(s) this job needs in it, point bareloop at that — never your original
|
|
2935
|
+
> folder. Everything in the folder you name is frozen, read, and paid for. `prep-source`
|
|
2936
|
+
> prints the same line on every folder prep, and the panel (N6) shows it beside the Source
|
|
2937
|
+
> field.
|
|
2938
|
+
|
|
2939
|
+
**Layout.** `<into>/tree/input/…` is the frozen copy; `<into>/tree/output/` is empty (a
|
|
2940
|
+
`.gitkeep` seeds it) and is where the run writes; `<into>/source.json` is the manifest,
|
|
2941
|
+
OUTSIDE the tree — no worker verb can ever read or edit it. One hidden commit, authored
|
|
2942
|
+
`bareloop <bareloop@localhost>` with `commit.gpgsign` pinned off — never the operator's
|
|
2943
|
+
identity, never global git config (this repo's own tests run with an empty `HOME`).
|
|
2944
|
+
|
|
2945
|
+
**What is admitted today (hamr: "start with already supported files").** A URL is fetched
|
|
2946
|
+
ONCE with plain `fetch` — no credentials, no cookies, no custom headers — under
|
|
2947
|
+
`PROVIDER_TIMEOUT_MS` and capped at `MAX_BUFFER`, both reused from elsewhere in this library,
|
|
2948
|
+
never a second number. Content-type must be `text/*`, `application/json`, or
|
|
2949
|
+
`application/xml`. A local file or folder must be plain text (a NUL byte in the first 8KB of
|
|
2950
|
+
any file refuses the whole prep, naming every offending file) and must contain no symlinks
|
|
2951
|
+
(named, never followed). Each file has its OWN `MAX_BUFFER` ceiling (`source-file-oversize`)
|
|
2952
|
+
— there is no folder TOTAL cap, hamr's call. A file named `.env` or `.env.*` is refused by
|
|
2953
|
+
NAME whatever it contains (`source-env-file`), belt-and-braces beside the content scan below.
|
|
2954
|
+
A `.git` found anywhere BELOW the root is a nested repo and refuses (`source-nested-repo`):
|
|
2955
|
+
its objects are a second history nobody declared, and the seed could not hold them honestly.
|
|
2956
|
+
PDF and Word input have no reader here (hole H7, PRD item 33) — logged, not built.
|
|
2957
|
+
|
|
2958
|
+
**A git repo IS an allowed source** (hamr: yes — the PR-review job). It is COPIED with its
|
|
2959
|
+
history, never used in place (the patients-are-copies rule): the working files land at the
|
|
2960
|
+
TREE ROOT rather than under `input/`, `.git` is copied verbatim, `output/` is added, and the
|
|
2961
|
+
seed commit goes on TOP of the existing history — so `manifest.kind === 'repo'` and the
|
|
2962
|
+
worker gets a real repo to review. Its guards (the input stays untouched) arrive with M4. A
|
|
2963
|
+
`.git` that is a FILE, not a directory — a linked worktree or a submodule — refuses
|
|
2964
|
+
`source-is-linked-worktree`: its real git directory lives elsewhere, and a copy would
|
|
2965
|
+
silently commit into the original. Text-only and the per-file ceiling do not apply to a repo
|
|
2966
|
+
source (a real repo legitimately carries binaries).
|
|
2967
|
+
|
|
2968
|
+
**A repo source copies only what git tracks** (D1 rework, hamr's ruling verbatim: "copy only
|
|
2969
|
+
what git tracks" — closes F164/F165). Files are enumerated with `git ls-files --stage` in the
|
|
2970
|
+
SOURCE, never a filesystem walk: each tracked path's WORKING-TREE content is frozen (so
|
|
2971
|
+
uncommitted edits to a tracked file come along), and nothing untracked or gitignored is ever a
|
|
2972
|
+
candidate — real npm `node_modules/.bin/*` symlinks, this repo's own `.claude/`, a gitignored
|
|
2973
|
+
`.env`, none of it is read or written. `git add -A` drops `-f` for a repo source (kept for a
|
|
2974
|
+
plain folder/file/URL, where a `.gitignore` has no honest meaning yet). A gitlink entry (a real
|
|
2975
|
+
submodule) refuses `source-nested-repo`; a tracked symlink refuses `source-symlink` only when
|
|
2976
|
+
it resolves OUTSIDE the source root — one that stays inside is legal tracked content, copied
|
|
2977
|
+
verbatim as a link, never dereferenced. Measured live against a real 15 MB repo carrying
|
|
2978
|
+
`node_modules` (`~/PycharmProjects/adaptlearn`, the repo that broke F164): 1296 files, 3.16 MB,
|
|
2979
|
+
0.89s elapsed, ~84 MB maxRSS — no gitignored or untracked content reached the tree or the seed.
|
|
2980
|
+
|
|
2981
|
+
**Every frozen file's content is secret-scanned, binary files included** (D2, closes the F166
|
|
2982
|
+
residual). A binary file (a NUL byte in its first 8KB) used to be hashed but never scanned —
|
|
2983
|
+
now it runs through the SAME `SECRET_PATTERNS` inventory, decoded `latin1` (byte-preserving)
|
|
2984
|
+
instead of `utf8`, so an ASCII key embedded in a compiled artifact is still found. This only
|
|
2985
|
+
matters where binaries are admitted at all (a repo source today; a plain folder/file still
|
|
2986
|
+
refuses binary content outright via `source-not-text`, unchanged).
|
|
2987
|
+
|
|
2988
|
+
**A URL's redirects are followed but never invisible.** The URL the body actually came from
|
|
2989
|
+
rides into the manifest as `finalUrl` and is printed by `prep-source` whenever it differs
|
|
2990
|
+
from what was typed, so a link that quietly lands on a login or error page is something a
|
|
2991
|
+
person can see before a token spends. The final URL is secret-scanned the same way the typed
|
|
2992
|
+
one is.
|
|
2993
|
+
|
|
2994
|
+
**The seed holds every frozen file, and that is CHECKED, not hoped.** `git add` runs with
|
|
2995
|
+
`-f`, so a `.gitignore` inside the source cannot drop files from the seed (a file present on
|
|
2996
|
+
disk but absent from the seed would read to a close as the worker having written it); then
|
|
2997
|
+
the seed is read back with `git ls-tree -r --name-only HEAD` and diffed against the manifest's
|
|
2998
|
+
own file list by name. A shortfall refuses `source-seed-incomplete`.
|
|
2999
|
+
|
|
3000
|
+
**Every frozen file's content is scanned for a known secret shape before anything is
|
|
3001
|
+
written under `into`** (a folder, a single file, or a fetched URL body — the SAME inventory
|
|
3002
|
+
`scanSecrets` reads, `src/validate.js`; live-proven fix — the door used to scan only the URL
|
|
3003
|
+
string, so a plain `.env` with a real API key was frozen into the tree and committed to the
|
|
3004
|
+
hidden git seed untouched). A hit refuses `source-carries-secret`, naming the file path(s)
|
|
3005
|
+
and the pattern name(s) only — the matched text itself never appears in the refusal, the
|
|
3006
|
+
manifest, or any file, and nothing is written to disk (`into` does not exist afterward). This
|
|
3007
|
+
is a shape-based check, not a filename denylist: a secret whose shape is not in the inventory
|
|
3008
|
+
(a plain database password in a `.env`, say) still passes through — a named, accepted
|
|
3009
|
+
residual, not this fix's job to close.
|
|
3010
|
+
|
|
3011
|
+
**Every refusal is a named `{stop, code}`, never a throw and never silent:** `source-
|
|
3012
|
+
unreadable`, `source-symlink`, `source-env-file`, `source-file-oversize`,
|
|
3013
|
+
`source-nested-repo`, `source-is-linked-worktree`, `source-seed-incomplete`, `source-git-failed`, `source-not-text`, `source-fetch-failed`,
|
|
3014
|
+
`source-fetch-timeout`, `source-fetch-oversize`, `source-carries-secret`, `into-exists`,
|
|
3015
|
+
`destination-invalid`, `destination-not-absolute`, `destination-not-directory`,
|
|
3016
|
+
`destination-not-writable`, `destination-contained`, `destination-parent-missing`,
|
|
3017
|
+
`destination-parent-unwritable`, `destination-exists`, `destination-output-missing`,
|
|
3018
|
+
`destination-write-failed`, `source-manifest-unreadable`, `source-manifest-invalid`. A
|
|
3019
|
+
refusal never throws, never overwrites a person's file, and never lands a destination inside
|
|
3020
|
+
the run's own scratch tree.
|
|
3021
|
+
|
|
3022
|
+
**Destination is a DIRECTORY, never a filename** (D3 rework, hamr's ruling, 2026-09-12,
|
|
3023
|
+
condensed in `docs/product/ITEM33-BUILD.md`). It may already exist and need not be empty; it
|
|
3024
|
+
may sit inside the source itself (the same-dir case: "i can limit your actions to a certain
|
|
3025
|
+
fix in a certain dir and your output will also be there" — `destination-in-source` is gone). A
|
|
3026
|
+
job may produce MORE THAN ONE file (his example: a flight search producing one sheet for
|
|
3027
|
+
"SFO→LAX red-eye" and another for "SFO under $700"); the agent names them, and there is no
|
|
3028
|
+
`output` field in the manifest any more — `copyOut` always reads `tree/output/` itself, never
|
|
3029
|
+
a manifest-declared path, which closes the old escape vector (`output/../../x`) structurally
|
|
3030
|
+
rather than by validation.
|
|
3031
|
+
|
|
3032
|
+
**Destination is proven at prepare time, not just in-run** (hamr's ruling: source AND
|
|
3033
|
+
destination are proven at job start, $0): when `destination` is given, `prepareSource` calls
|
|
3034
|
+
`proveDestination` BEFORE creating `into` — a refused destination leaves nothing on disk.
|
|
3035
|
+
`proveDestination` only proves the directory is USABLE (exists as a directory and is writable,
|
|
3036
|
+
or its parent exists and is writable so it can be created) — it cannot foresee individual
|
|
3037
|
+
delivered FILE names, since those are not known until the run actually produces them.
|
|
3038
|
+
`copyOut` takes an optional `into` (the run's scratch root, `<into>`, of which `tree` is the
|
|
3039
|
+
`tree/` subdirectory; defaults to `tree` for direct callers with no `into`) so a destination
|
|
3040
|
+
sitting inside the scratch area but outside the frozen tree — e.g. `<into>/profile.md` — is
|
|
3041
|
+
also caught as `destination-contained`, not just one strictly inside `tree`. `run-u.mjs`'s two
|
|
3042
|
+
call sites pass `dirname(wd)` (`wd` IS `<into>/tree`) as `into` for exactly this reason.
|
|
3043
|
+
|
|
3044
|
+
**A `kind: 'repo'` source's destination is a DIFFERENT thing** (hamr: "destination if for code
|
|
3045
|
+
on a green then that would be the place agent allowed to do changes") — it names the WRITE
|
|
3046
|
+
FENCE inside the copied repo, the signed `writeScope` field's job (`src/job.js:363`), never a
|
|
3047
|
+
filesystem drop-off point. It is recorded as declared, never proven by `proveDestination` and
|
|
3048
|
+
never handed to `copyOut` (`frontDoorFromManifest` returns `null` for a repo manifest).
|
|
3049
|
+
Wiring it into `writeScope` is M3/M4's job, not this door's.
|
|
3050
|
+
|
|
3051
|
+
**Every non-empty file under `output/` is delivered, each under its OWN dated name, and
|
|
3052
|
+
nothing is ever overwritten.** A file named `profile.md` LANDS as `profile-2026-09-12.md`
|
|
3053
|
+
inside the destination directory; a second delivery of the same name the same day lands as
|
|
3054
|
+
`profile-2026-09-12-2.md`, then `-3` (the work-branch collision rule), up to `-99` before
|
|
3055
|
+
`destination-exists`. An empty file is silently skipped (not a delivered result); the batch
|
|
3056
|
+
refuses `destination-output-missing` only when NOTHING non-empty exists under `output/` at
|
|
3057
|
+
all (excluding the `output/.gitkeep` seed placeholder). `datedDestination(destination, now,
|
|
3058
|
+
n)` and `pickDelivery(destination, now?)` are exported and unchanged from M2b — `copyOut`
|
|
3059
|
+
calls them per file now, at delivery time, rather than once against a single declared path.
|
|
3060
|
+
`copyOut` returns `{files: [{path, bytes, sha256}]}` and `run-u.mjs` spine-records ONE
|
|
3061
|
+
`destination-written` per delivered file — the declared directory is not the path anything
|
|
3062
|
+
wrote.
|
|
3063
|
+
|
|
3064
|
+
**The destination is a PER-RUN value, never a signed job-spec field.** A job spec is a
|
|
3065
|
+
repeatable SHAPE (goal, checks, judge rules), signed once; source and destination vary per
|
|
3066
|
+
run — the same way `bareloop run <bundle> --repo <path>` varies the repo instance without
|
|
3067
|
+
touching the bundle's own signature. So `job.js`/`jobSpecHash` carry nothing about it: the
|
|
3068
|
+
run's own instance lives entirely in the manifest `prepareSource` writes.
|
|
3069
|
+
|
|
3070
|
+
`scripts/run-u.mjs` reads that manifest via `readSourceManifest(dirname(workdir))` (`workdir`
|
|
3071
|
+
IS `<into>/tree`, so its parent is `<into>`) and reduces it with `frontDoorFromManifest`. No
|
|
3072
|
+
manifest → no front-door behaviour at all — every existing repo-patient job is untouched. A
|
|
3073
|
+
manifest that exists but cannot be read or parsed is a named stop, never a silent skip. When
|
|
3074
|
+
a manifest declares a destination, `proveDestination` runs BEFORE any token spends (a job
|
|
3075
|
+
that cannot land its output should never buy a worker turn first), and `copyOut` runs once,
|
|
3076
|
+
on a minted `green` — the ONE outcome string a graded close ever mints; `soft-green` jobs
|
|
3077
|
+
mint the same string, since the class lives on `spec.verdictType`, never on `runJob`'s
|
|
3078
|
+
return value. A refused copy-out is spine-recorded (`destination-refused {code, detail}`)
|
|
3079
|
+
beside the green it could not deliver and NEVER changes that verdict — a delivery failure is
|
|
3080
|
+
not a grading failure.
|
|
3081
|
+
|
|
3082
|
+
Not wired yet, named rather than silently absent: `bareloop run` (the exported bundle CLI,
|
|
3083
|
+
`src/cli.js`) still requires `--repo`; a bundled front-door job is parked past PRD item
|
|
3084
|
+
33/M5. The intake form that lets a person fill in Source/Destination through the authoring
|
|
3085
|
+
interview (rather than the CLI above) is item 33/M3, not yet built.
|
|
3086
|
+
|
|
2883
3087
|
## Architecture
|
|
2884
3088
|
|
|
2885
3089
|
Three layers. An **outer shell** (dumb, permanent): per-run budget cap via bareguard,
|
|
@@ -2977,8 +3181,18 @@ blast radius.
|
|
|
2977
3181
|
|
|
2978
3182
|
## Gotchas
|
|
2979
3183
|
|
|
2980
|
-
*
|
|
2981
|
-
|
|
3184
|
+
*Recorded here as they're found (repo-side friction goes to `docs/logs/FINDINGS.md`).*
|
|
3185
|
+
|
|
3186
|
+
- **`non-green-verdict` is a misleading refusal verb, and it is kept on purpose.** When
|
|
3187
|
+
`runInterview` refuses a job for having no git repository (`repoPath` absent), the refusal
|
|
3188
|
+
carries `verb: 'non-green-verdict'` and `path: 'repoPath'`. The name is wrong: the refusal
|
|
3189
|
+
is about the GENRE — this authoring flow drafts code-genre closes against a git seed, and
|
|
3190
|
+
that is true of BOTH classes it offers — not about the verdict class, and it fires for a
|
|
3191
|
+
`soft-green` pick exactly as it does for `green`. **Read `path`, never `verb`, to know what
|
|
3192
|
+
was refused.** The verb is kept because it is the demand ledger's key for this refusal:
|
|
3193
|
+
renaming it would split one running count into two that read as different demands. Named
|
|
3194
|
+
rather than fixed (PRD item 31.2); say so if you would rather have the accurate name and
|
|
3195
|
+
accept the discontinuity.
|
|
2982
3196
|
|
|
2983
3197
|
## Constraints
|
|
2984
3198
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bareloop",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.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",
|