bareloop 0.25.1 → 0.27.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 +525 -0
- package/bareloop.context.md +384 -84
- package/package.json +4 -3
- package/src/authorflow.js +947 -104
- package/src/authoring.js +33 -15
- package/src/authorjob.js +161 -5
- package/src/authorscout.js +16 -11
- package/src/cardauthor.js +28 -18
- package/src/cli.js +10 -1
- package/src/detectlang.js +200 -0
- package/src/index.js +26 -30
- package/src/job.js +45 -28
- package/src/planrun.js +81 -2
- package/src/promptregisters.js +14 -0
- package/src/providers.js +51 -3
- package/src/source.js +290 -33
- package/src/text.js +45 -0
- package/src/validate.js +25 -1
- package/types/authorflow.d.ts +476 -105
- package/types/authoring.d.ts +23 -8
- package/types/authorjob.d.ts +22 -4
- package/types/cardauthor.d.ts +7 -3
- package/types/detectlang.d.ts +38 -0
- package/types/index.d.ts +16 -16
- package/types/job.d.ts +24 -2
- package/types/planrun.d.ts +41 -1
- package/types/providers.d.ts +37 -2
- package/types/source.d.ts +87 -0
- package/types/text.d.ts +35 -0
- package/types/validate.d.ts +22 -0
package/bareloop.context.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
> close's MECHANICAL stages only — never a judged floor, never a person. Shipped in v0.12.0
|
|
16
16
|
> (2026-08-23); see CHANGELOG `## [0.12.0]` onward for the softgreen rung and the fixes/rungs
|
|
17
17
|
> that followed through v0.15.0. **The plan shape is the ONLY shape**: the legacy
|
|
18
|
-
> operator-authored `steps[]` path, config-v1 and
|
|
18
|
+
> operator-authored `steps[]` path, config-v1 and its draft-PR step were deleted
|
|
19
19
|
> 2026-07-26 (PRD v1.32), so `interpret`, `validateConfig` and `extractRules` are gone from
|
|
20
20
|
> the public surface — a breaking change. Runs on two worker surfaces — the API `Loop` and,
|
|
21
21
|
> for `clipipe-subscription`, the CLI's native tool channel (module 4d). API sections fill in
|
|
@@ -23,18 +23,6 @@
|
|
|
23
23
|
> refusals, the constraints — is settled for good. Per LIBRARY_CONVENTIONS §3 this file ships
|
|
24
24
|
> with the package and is the complete adopter contract; the README is only the pitch.
|
|
25
25
|
|
|
26
|
-
> **LEGACY — `hitl` (retired by design, PRD Addendum v1.71, 2026-08-17).** `hitl` remains
|
|
27
|
-
> ADMITTED in code today — `VERDICT_TYPES` still lists it, the `human-confirms` stage kind is
|
|
28
|
-
> still live (never offered, at-most-once, must be last), and the two `hitl-*` terminals
|
|
29
|
-
> (`hitl-pause`, `hitl-decision-red`) still exist and still work exactly as documented
|
|
30
|
-
> wherever this file describes them below. It is retired as a *design direction*, not deleted
|
|
31
|
-
> as *code*: `LOCKED_CLASSES` is empty, so nothing refuses a `hitl` job today, but new
|
|
32
|
-
> integrations should target `green` / `soft-green` plus the review door instead — that pair
|
|
33
|
-
> is the forward path for anything needing a person's judgement, and `hitl`'s removal is a
|
|
34
|
-
> future breaking change, not yet scheduled. Records: PRD Addendum v1.71,
|
|
35
|
-
> `docs/product/2026-08-17-softgreen-review-door-design.md` (repo-only, not shipped);
|
|
36
|
-
> evidence F102–F105.
|
|
37
|
-
|
|
38
26
|
## What this is
|
|
39
27
|
|
|
40
28
|
**"Automate this job — I don't know the best workflow."** bareloop runs tasks that are
|
|
@@ -58,10 +46,8 @@ inherited rule carries the green that minted it and the contrast that attributed
|
|
|
58
46
|
truth, `src/kinds.js` `LIVE_KINDS`) is the default and the only class with a live-proven
|
|
59
47
|
paid run. `soft-green` (a judged floor behind a signed calibration gate, plus a review
|
|
60
48
|
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. `
|
|
62
|
-
|
|
63
|
-
(PRD Addendum v1.71); its pause machinery lives on as the review door, not as a class to
|
|
64
|
-
pick for new jobs.
|
|
49
|
+
verdict — see `## Public API` → "THE JUDGED FLOOR" and "THE REVIEW DOOR" below. `green` and
|
|
50
|
+
`soft-green` are the only classes on offer for new jobs.
|
|
65
51
|
- **Worker surface — how the agent runs:** `provider: 'anthropic-api'` (the default, the
|
|
66
52
|
only one with an operational guarantee, F48) drives `bare-agent`'s `Loop` and takes the
|
|
67
53
|
shell-owned `provider` binding. `provider: 'clipipe-subscription'` (`src/job.js:135`)
|
|
@@ -73,11 +59,13 @@ inherited rule carries the green that minted it and the contrast that attributed
|
|
|
73
59
|
`provider: 'openai-api'` (PRD item 28) takes the ordinary `Loop` path against an
|
|
74
60
|
OpenAI-shaped endpoint, reads `OPENAI_API_KEY`, and accepts an optional job-level
|
|
75
61
|
`baseUrl` (https, or http on loopback only; credentials in the URL are a red). Its one
|
|
76
|
-
admitted model is `deepseek-
|
|
77
|
-
|
|
62
|
+
admitted model is `deepseek-flash`, which earned the slot with a real paid green under
|
|
63
|
+
its retired predecessor `deepseek-chat` (F171: no longer served as of 2026-09-13,
|
|
64
|
+
swapped to `deepseek-flash`) — no endpoint or model enters the menu without its own
|
|
65
|
+
clean paid probe. The provider table
|
|
78
66
|
lives in `src/providers.js` (`resolveProvider`/`makeProvider`/`buildRunnerProviders`);
|
|
79
67
|
an unknown provider name THROWS there, it never falls back to a default. Per-model
|
|
80
|
-
request-key gating lives in that table: `deepseek-
|
|
68
|
+
request-key gating lives in that table: `deepseek-flash` sets bare-agent's
|
|
81
69
|
`legacyMaxTokens`, because DeepSeek silently ignores `max_completion_tokens` and an
|
|
82
70
|
output cap that does not bind is a money hazard. `provider: 'gemini-api'` (PRD item 31.3)
|
|
83
71
|
takes the same `Loop` path, reads `GEMINI_API_KEY`, and maps two real tiers
|
|
@@ -163,7 +151,7 @@ minting claim, or the shell-owned retry cap — all unknown-field reds.
|
|
|
163
151
|
|---|---|---|
|
|
164
152
|
| `job` | kebab-case slug | |
|
|
165
153
|
| `description` | non-empty string | |
|
|
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-
|
|
154
|
+
| `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-flash` today (F171: swapped from the retired `deepseek-chat`) — 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` |
|
|
167
155
|
| `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) |
|
|
168
156
|
| `cadence` | `{ unit: hour\|day\|week, every: 1..30 }` | validated now, consumed at N5 (Scheduler) |
|
|
169
157
|
| `budgetUsd` | `0 < n <= shell cap` | ceiling chain: workflow ≤ job ≤ shell — each layer may tighten, never exceed |
|
|
@@ -175,14 +163,20 @@ minting claim, or the shell-owned retry cap — all unknown-field reds.
|
|
|
175
163
|
| `steps` | RETIRED | operator-authored `steps[]` was deleted (PRD v1.32); a spec carrying it reds `shape-retired:steps` by name rather than half-running |
|
|
176
164
|
| `escalation` | `{ mode: "decision-ready" }` | the pain channel is not optional |
|
|
177
165
|
|
|
166
|
+
`resolveWorkerModel` (`src/job.js`, referenced above) is exported (item 34 L20):
|
|
167
|
+
|
|
168
|
+
```js
|
|
169
|
+
import { resolveWorkerModel } from 'bareloop';
|
|
170
|
+
```
|
|
171
|
+
|
|
178
172
|
**The plan shape — the only shape** (Layer 2, design record 2026-07-21). The AGENT authors
|
|
179
173
|
the step plan at run time (gated by `validatePlan`); the human signs only:
|
|
180
174
|
|
|
181
175
|
| field | shape | notes |
|
|
182
176
|
|---|---|---|
|
|
183
177
|
| `goal` | non-empty text | what the agent plans against |
|
|
184
|
-
| `verdictType` | `green` \| `soft-green`
|
|
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;
|
|
178
|
+
| `verdictType` | `green` \| `soft-green` | declared radio, never inferred (`VERDICT_TYPES`, frozen). **AUTHORING offers both (`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. 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 |
|
|
179
|
+
| `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; an object close names no command to run, so the plan flow refuses one at runtime as `close-unsupported`. The modern route to a judged or human-decided close is a staged `closeDecl` (`judged-floor` / `human-confirms` stage kinds), not this object form |
|
|
186
180
|
| `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 |
|
|
187
181
|
| `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) |
|
|
188
182
|
| `tools` | optional unique subset of `TOOL_MENU` (14 verbs, below) | the CEILING every plan step's grant must fit inside (omitting it means the full menu — and the hash is taken over that RESOLVED form, so a `TOOL_MENU` widening flips an omitted-`tools` spec's hash and forces a re-sign; see `jobSpecHash`, MED-1); `run` is `LOCKED_TOOLS` and reds `request-red` — locked-but-listed, and the red IS the admission evidence the ledger tallies (a typo stays `invalid-value`). A ceiling of write-class and store-class verbs ONLY reds `invalid-value`: the scout surveys read-only, so it would be handed an empty menu and survey blind |
|
|
@@ -278,19 +272,17 @@ a few explicit requirements rather than prose. Preflight already names every sta
|
|
|
278
272
|
**Close types and the hierarchy — the OBJECT form, which the plan flow only partially
|
|
279
273
|
supports** (a close is data, never code; verdict-class laundering is a named red
|
|
280
274
|
`close-hierarchy`). The go-forward shape is the staged list above; a `predicate` object is
|
|
281
|
-
legal shorthand for a one-stage list and the plan flow adapts it, but `gold`/`rubric
|
|
275
|
+
legal shorthand for a one-stage list and the plan flow adapts it, but `gold`/`rubric`
|
|
282
276
|
validate and then refuse at runtime (`close-unsupported`) regardless of verdict class — the
|
|
283
|
-
plan flow has never executed an object close beyond `predicate`. `
|
|
284
|
-
(`
|
|
285
|
-
|
|
286
|
-
refused at runtime exactly as they were:
|
|
277
|
+
plan flow has never executed an object close beyond `predicate`. `soft-green`'s judged floor
|
|
278
|
+
is a STAGE (`judged-floor` inside a `closeDecl`) — never a close TYPE — so both OBJECT forms
|
|
279
|
+
below stay refused at runtime exactly as they were:
|
|
287
280
|
|
|
288
281
|
| type | fields (exact — extras red) | legal class |
|
|
289
282
|
|---|---|---|
|
|
290
283
|
| `predicate` | `cmd`, `expect` (int exit code), `judged?`, `gapKeep?` | `hard` |
|
|
291
284
|
| `gold` | `expected`, `compare: exact\|json-equal` | `hard` |
|
|
292
285
|
| `rubric` | `criteria` | `soft` only — can never mint automatically |
|
|
293
|
-
| `hitl` | `prompt` | `hitl` — a human IS the close |
|
|
294
286
|
|
|
295
287
|
**`judged` — the judgment-rendered signal (optional, `predicate` only; PRD v1.11 / F17).**
|
|
296
288
|
`{ pattern: string, min: int >= 1 }` — a regex with **exactly one** capture group (zero and
|
|
@@ -318,7 +310,7 @@ exactly the state a fix job starts from — falls under the floor and is escalat
|
|
|
318
310
|
instrument crash at precheck, before the worker it hired ever runs. `# tests (\d+)` and
|
|
319
311
|
`^ℹ tests (\d+)$` hold on a red tree; `(\d+) passed` does not (F40).
|
|
320
312
|
|
|
321
|
-
Omitting it is legal (a linter
|
|
313
|
+
Omitting it is legal (a linter has nothing to count) and stamps
|
|
322
314
|
`unaudited: true` on the verdict plus a `close-unaudited` spine event: **the blind spot is
|
|
323
315
|
named, never assumed away.** The agent-drafted workflow config cannot express `judged` —
|
|
324
316
|
it is the arbiter's own honesty check (unknown-field red, enforced per section at every
|
|
@@ -408,7 +400,7 @@ missing or non-plan-shape job fails CLOSED). `closeStagesOf(job)` is the ONE sta
|
|
|
408
400
|
every check-menu consumer shares — it reads whichever close field the spec carries:
|
|
409
401
|
`closeDecl` → its declared stages (each enriched with the arbiter's own `gapKeep`), else
|
|
410
402
|
`stageClose(close)` (array → itself; legacy object predicate → its one-stage list, named
|
|
411
|
-
`close`; gold/rubric
|
|
403
|
+
`close`; gold/rubric → null). A consumer reading `close` alone would see a declared
|
|
412
404
|
job as closeless and offer the drafter an empty check menu. The `secret-literal` sweep is
|
|
413
405
|
defense-in-depth against known token shapes — env-only loading remains the law, not the
|
|
414
406
|
sweep.
|
|
@@ -442,8 +434,8 @@ per-stage graded numbers) on the close-fix loop. A governor may supply an option
|
|
|
442
434
|
there is no planner at the close); it may never override the category or the outcome. Both
|
|
443
435
|
modules are INTERNAL (not on the public surface), so a direct `ralph` caller uses
|
|
444
436
|
`capRuns` — the strike rule reaches you through `runJob`/`runPlan`'s `strikeLimit`. `close` is an argv
|
|
445
|
-
whose exit code is truth (`runClose`
|
|
446
|
-
|
|
437
|
+
whose exit code is truth (`runClose` and `runStages` are also exported — **async since 0.6:
|
|
438
|
+
both return Promises**; the child is awaited instead of spawnSync so a running close
|
|
447
439
|
no longer freezes the host event loop (F68); every close semantic — timeout signal, output
|
|
448
440
|
bounds, gap shape, exit bands — is byte-identical); the red gap text feeds the next
|
|
449
441
|
iteration, tail-biased when bounded (400 head + 1500 tail — the assertion diff lives at
|
|
@@ -547,6 +539,9 @@ generic unknown-field. Menus exported: `CLOSE_TYPES`, `CLASS_BY_CLOSE`, `GOLD_CO
|
|
|
547
539
|
`CADENCE_UNITS`, `PROVIDERS`, `CONDITION_KEYS`, `TOOL_MENU`, `LOCKED_TOOLS`, `STORE_VERBS`,
|
|
548
540
|
`VERDICT_TYPES`, `LOCKED_VERDICTS` — plus `checkMenu` itself.
|
|
549
541
|
|
|
542
|
+
`globToPrefix(scope)` and `scanSecrets(text)` are exported too — the write-scope prefix
|
|
543
|
+
mapping and the one secret-shape sweep `validateJob` itself is built from (`src/validate.js`).
|
|
544
|
+
|
|
550
545
|
### `validatePlan(input, { job, maxStepRounds?, scopes? })` → `{ ok, reds, plan }` — `src/plan.js`
|
|
551
546
|
|
|
552
547
|
`validateJob`'s SIBLING in the two-document split, never a third validator over it (the
|
|
@@ -566,6 +561,33 @@ signed `writeScope` alone, never a free-text fallback. Menus exported: `EXIT_TYP
|
|
|
566
561
|
|
|
567
562
|
### Close authoring — the user declares what done means (`src/kinds.js`, `src/authoring.js`, `src/authorscout.js`, `src/authorflow.js`, `src/declaredclose.js`, `src/authorjob.js`)
|
|
568
563
|
|
|
564
|
+
Every function and constant this section discusses by name is exported at the package root
|
|
565
|
+
(item 34 L20 — the export list equals what this document names):
|
|
566
|
+
|
|
567
|
+
```js
|
|
568
|
+
import {
|
|
569
|
+
authorClose, authorCloseForJob, AUTHORED_SPEC_FIELDS, AUTHOR_SCOUT_VERBS, assembleSpec,
|
|
570
|
+
buildSeedListing, CALIBRATION_SIZE, CASUALTY_AXES, CLASS_BATTERIES, classGuards,
|
|
571
|
+
closeCeiling, decide, declaredStages, DOOR_OPEN_OUTCOMES, doorOpens, expectedOf,
|
|
572
|
+
foldJudgedArtifacts, genreEnv, genreInstruments, HUMAN_DECISIONS, HUMAN_PAUSE,
|
|
573
|
+
INJECTION_LOCATE_BATTERY, JUDGE_ATTEMPTS, JUDGED_MARKER, JUDGE_RULE_IDS, JUDGE_RULES,
|
|
574
|
+
KIND_CATALOGUE, LIVE_KINDS, LOCKED_CLASSES, LOCKED_KINDS, MAX_JUDGED_PATHS, MAX_REVISIONS,
|
|
575
|
+
mechanicalStages, NEVER_OFFERED_KINDS, normalizeHumanRuling, prepareSigning,
|
|
576
|
+
proposeJudgedArtifacts, questionsFor, RAW_PERSIST_MAX, readShimArm, READ_SHIM_CAP,
|
|
577
|
+
READ_SHIM_DIFF_STRATEGY, READ_SHIM_STRATEGY, refusalEvents, refuseLockedKind,
|
|
578
|
+
requiredAnswersFor, REVIEW_DOOR_CLASSES, runAuthorScout, runCalibration, runClose,
|
|
579
|
+
runDeclaredClose, runDeclaredStages, runInterview, runLocate, runStage, SCOUT_ATTEMPTS,
|
|
580
|
+
SCOUT_RETRY_CAUSES, scrubRaw, seedAtHead, SEED_EXEMPT_KINDS, seedRead, signJudgedArtifacts,
|
|
581
|
+
STOP_FAULTS, validateCalibrationSet, validateCloseDecl, validateDeclaration,
|
|
582
|
+
ACCOUNTED_ROUND_TYPES,
|
|
583
|
+
} from 'bareloop';
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
`JUDGED_MARKER` (`src/kinds.js`) is the one identity every close script in this repo imports
|
|
587
|
+
(and every close script an exported bundle ships, since `exportBundle` rewrites a close
|
|
588
|
+
script's `from '../src/kinds.js'` to `from 'bareloop'` at export time) — it is a required
|
|
589
|
+
root export even though this doc had never named it before item 34 L20.
|
|
590
|
+
|
|
569
591
|
The one layer where a human still wrote code. A close used to be a hand-written script
|
|
570
592
|
per patient; now the user PICKS A VERDICT CLASS, answers that class's questions, and an LLM
|
|
571
593
|
composes a DECLARATION over kinds whose implementations bareloop owns. **v1 admits exactly
|
|
@@ -574,11 +596,10 @@ everything else refuses honestly, and every refusal is COUNTED.
|
|
|
574
596
|
|
|
575
597
|
**The verdict class is the USER's answer (PRD v1.57 §1), and it DRIVES the authoring.**
|
|
576
598
|
`verdictType` is a declared radio the preflight validates, never inferred:
|
|
577
|
-
`
|
|
599
|
+
`MENU_CLASSES` = `green` | `soft-green`, with `LOCKED_CLASSES` = **empty** —
|
|
578
600
|
`soft-green` shipped its judged floor in v0.12.0 (2026-08-23) and is fully admitted
|
|
579
|
-
alongside `green
|
|
580
|
-
|
|
581
|
-
review door (`accept`/`rerun`/`pause`) that re-homes hitl's pause machinery is shipped and
|
|
601
|
+
alongside `green`. The
|
|
602
|
+
review door (`accept`/`rerun`/`pause`) is shipped and
|
|
582
603
|
live — always on for `soft-green`, opt-in for `green` via `reviewDoor: true` /
|
|
583
604
|
`--review-door`. A future LOCKED_CLASSES entry would still return a counted `request-red`
|
|
584
605
|
refusal at ADMISSION — before that class's questions are ever asked. The
|
|
@@ -592,10 +613,10 @@ a step, or test it without a provider.
|
|
|
592
613
|
|
|
593
614
|
| step | call | what it is |
|
|
594
615
|
|---|---|---|
|
|
595
|
-
| interview | `runInterview({ verdictType, answers, repoPath })` → `{ ok, answers, verdictType, refusal, reds }` | PURE — no model, no repo, no clock.
|
|
616
|
+
| interview | `runInterview({ verdictType, answers, repoPath })` → `{ ok, answers, verdictType, refusal, reds }` | PURE — no model, no repo, no clock. The interview ADMITS only what is on the menu (`MENU_CLASSES`): nothing is locked today (`LOCKED_CLASSES` is empty), and any pick off the menu returns a refusal rather than `ok:true` — ask `MENU_CLASSES` which classes can be authored, never this sentence. The interview asks NOTHING about a genre and NOTHING about the repository. **Ask the library for the set and its numbers — never hardcode a count or a slot number**: the green set has lost several slots since it was frozen (D13's genre confirm; the repo question, dropped 2026-08-15 because `repoPath` is mandatory structured input; and, at PRD item 33 M3 piece 3, the old "which files change/read" question, replaced by Source and Destination — MECHANICAL fields, proven against the machine, never a numbered answer), and it renumbers CONTIGUOUSLY from 1 each time rather than leaving a gap. Today the green set is THREE questions (Goal, Success, Guardrails); the `soft-green` set is those three plus one more — Judge Examples (one example to pass, one to fail, and why) — the judged floor's one required input, compiled into the signed rubric card and the frozen calibration set. `verdictType` and `repoPath` are STRUCTURED input, never parsed out of prose. Answers are scrubbed at INGEST |
|
|
596
617
|
| 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 |
|
|
597
618
|
| 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 |
|
|
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 |
|
|
619
|
+
| authoring | `authorClose({ workdir, seedRef, lang, verdictType, answers, scout, listing, generate, ceilingUsd? })` | the grounded loop: author → validate → run EVERY stage at the seed → feed the MEASURED results back → revise, bounded at `MAX_REVISIONS` (2) — a passed `maxRevisions` is TIGHTEN-ONLY, so a caller may LOWER that ceiling and never buy more revise rounds than the constant (floor **0**: one authoring call with no revise round is a legal ask, and unlimited revising would launder thrash as adaptation) — early-stop on an unchanged declaration. The declaration is emitted through a SCHEMA-FORCED TOOL CALL (`declare_close`), never parsed out of prose; the feedback is EXECUTION OUTPUT only — no model ever reviews another model's close. The return carries **`raws`**: what every model call actually SAID, the scout's attempts absorbed together with the declaration's, in the cost book's own order and under the same labels it meters. Each is `{ label, attempt, bytes, trimmed, text, cause, reason }` through the ONE persist helper (`scrubRaw` — redacted over the ONE `SECRET_PATTERNS` inventory, since a raw is the record most likely in this system to carry a live credential; bounded at `RAW_PERSIST_MAX` (8000) with the trim ANNOUNCING its own full size, and the cut walked back off a continuation byte so a multi-byte character is never split). `raws` is present on EVERY path **including the `$0` preflight refusals** — the path that spends nothing more is exactly the one whose evidence used to vanish with the process. `iterations` records what each turn MEANT (declaration, validation, seed read); `raws` records what it SAID, and a malformation is only ever visible in the second. **`ceilingUsd` is the same operator ceiling the survey took** — the scout's spend is ABSORBED into this loop's cost book before its first call is weighed, so spend already incurred folds in and re-entering cannot silently widen it. It is checked immediately before EVERY paid call (the author call, each revise, and each malformed-emission retry). **A MALFORMED ceiling is an ERROR, not a silent unbounded run** (v1.64 §3): the seam every ceiling read goes through (`capStop`, `src/text.js`) THROWS on a non-null non-finite value — `'2.50'`, `NaN`, `Infinity`, `true`, `{}` — before the first paid call, and the throw propagates uncaught rather than being caught into an ABSENT survey, because a caught one would hand you the very silent-unbounded run it exists to prevent. `null`/omitted is the STATED operator choice for unbounded; `0` and negatives are finite, legal, and cap-halt immediately. The advertised ceiling and the enforced ceiling can therefore never be different numbers. A `cap-halt` here can arrive with `ok:true` — a close that was already validated and measured survives the money stop, exactly as a late provider casualty does. **F176 (fixed):** "kept the last accepted revision" is not the same claim as "kept the best revision" — a later revise round can VALIDATE fine yet MEASURE worse (every stage instrument-stops rather than reading a real red/green verdict). `declaration`/`seedRead`/`finalFrom`/the returned `genreEnv.dropped` are therefore the NEWEST SOUND measured iteration, not simply the last one, when the last one instrument-stopped and an earlier one did not; `stop` itself never changes. The swap is never silent: the return also carries **`fellBack`** — `null` on every run where it never fired, else `{from, to, brokenStages}` naming which iteration was dropped, which now stands, and which of the dropped one's stages instrument-stopped — reported through `onPhase('author-fallback', …)` as it happens and printed at the terminal readout (`fellBackLines`, `scripts/author-readout.mjs`). **F179/F180 (history):** bare-agent 0.42.0's OpenAIProvider used to THROW mid-call on malformed tool-call JSON (a raw SyntaxError, after the billed round), crashing past this function; a 2026-09-15 stopgap caught it as a `provider-red`/`ask.providerError` casualty (booked `costUsd: null`) so a sound prior declaration was kept via the fallback above rather than discarded. **F179 (bare-agent 0.43.0, BA-27 — the real mechanism):** the throw is fixed upstream. `OpenAIProvider`/`OllamaProvider.generate` now return a malformed round PRICED (real `usage`, never `costUsd: null`) with `toolCalls: []` plus their own `malformedToolCall: {name, error}` marker, which `Loop.run` surfaces unchanged on its return. For the authoring declaration and confirm-turn calls, `askStructured` reads that marker and RETRIES through the existing malformed-emission retry ladder (`MAX_STRUCTURE_RETRIES`) instead of stopping the run — nothing is ever repaired, and a malformed FIRST call no longer needs an earlier sound declaration to fall back to. The generic provider-casualty fallback above (idle-timeout, etc.) still applies for every other admitted class. **F184 (fixed, worker path):** the scout and the worker path never joined that retry ladder (still true — retrying is arbiter-adjacent, out of scope), but the worker path's `ask()` (`src/planrun.js`) now at least READS the marker where it already sees it and emits a distinct spine record, `worker-malformed-tool-call` (`{phase, iteration, name, error}`), before returning `r` unexamined exactly as before — no retry, no change to strikes/ladder/attempt counting or verdict routing, purely the same visibility bar this section's authoring-path fix already set |
|
|
599
620
|
| everything above, composed | `authorCloseForJob({ verdictType, answers, repoPath, lang, generate, ceilingUsd?, ... })` → `{ ok, closeDecl, verdictType, refusal, cost, ... }` | refuses at the cheapest gate that can refuse: an interview refusal costs **zero**. THE GENRE REFUSAL LIVES HERE (not in the interview): a language the catalogue owns no data for, and a LOCKED KIND the model reached for, both come back as counted `request-red` demand. **`ceilingUsd` is ONE number handed to BOTH paid seams** (the survey's and the declaration loop's) — the advertised ceiling and the enforced ceiling are the same ceiling. It travels as an EXPLICIT `null` when unset, because unbounded is a stated choice and an absent field read as falsy is that state reached by accident — and anything that is neither a finite number nor `null` throws at the seam rather than becoming that state by typo |
|
|
600
621
|
| assembly | `assembleSpec(specDraft, { closeDecl, verdictType })` | folds the authored half into the OPERATOR's half. Budgets, the fence, cadence, escalation and the provider are never authored by anything here. The GOAL is passed through, not generated. **A draft that ALREADY carries the authored half is REFUSED — it THROWS, never merges and never overwrites.** `AUTHORED_SPEC_FIELDS` (`close`, `closeDecl`, `verdictType`) names the fields this fold WRITES, as data, so the refusal and the fold cannot disagree about which half of the spec is which; the rule is `job.js`'s one step earlier (two closes are two arbiters, and picking one silently is how a signed artefact stops meaning what the signer read). `scripts/run-author.mjs` asks the same question at **$0 BEFORE the scout**, so the answer never arrives after the model has been paid |
|
|
601
622
|
| the three gates | `prepareSigning({ spec, workdir, seedRef? })` → `{ ok, specHash, gates, work, guards, refusal }` | D9, and it NEVER signs |
|
|
@@ -652,8 +673,10 @@ attempt PLUS its close. Unpriced is never free: a null cost is a `pricing-red` s
|
|
|
652
673
|
never retried, and the ladder is otherwise ONE retry (`JUDGE_ATTEMPTS`) — after that a broken
|
|
653
674
|
judge is an instrument stop, never a red about the tree.
|
|
654
675
|
|
|
655
|
-
**The two SIGNED artifacts of a judged close** (softgreen module 4, design §4.3/§4.4).
|
|
656
|
-
|
|
676
|
+
**The two SIGNED artifacts of a judged close** (softgreen module 4, design §4.3/§4.4). The
|
|
677
|
+
softgreen interview's Judge Examples answer (PRD item 33 M3 piece 3 — one example to pass, one
|
|
678
|
+
to fail, and why; formerly two questions, Q6+Q7) compiles into the RUBRIC CARD and the FROZEN
|
|
679
|
+
CALIBRATION SET, on
|
|
657
680
|
the D5 shown-and-fixed path: `proposeJudgedArtifacts({answers, generate, book})` buys ONE
|
|
658
681
|
schema-forced proposal (tool `propose_rubric`, over the same bounded malformed-emission ladder
|
|
659
682
|
the declaration uses — a parse failure is an `artifact-red` retried under the cap, a proposal
|
|
@@ -852,9 +875,58 @@ is exactly what F87 forbids.
|
|
|
852
875
|
`runInterview` is a pure function over answers with no prompt loop in it, so until
|
|
853
876
|
`scripts/run-interview.mjs` existed the only way in was to hand-write an `answers.json` and a
|
|
854
877
|
spec draft — precisely the SWE tax this product refuses. That script is the half that ASKS
|
|
855
|
-
(`--
|
|
856
|
-
`
|
|
857
|
-
$0 and no model ever sees it.
|
|
878
|
+
(`--verdict <class> --provider <anthropic-api|openai-api|gemini-api|clipipe-subscription>
|
|
879
|
+
--out <outdir> [--budget <usd>] [--base-url <url>]`), and it is deliberately GLUE: **it
|
|
880
|
+
calls no provider at all**, so a whole interview costs $0 and no model ever sees it. `--provider`
|
|
881
|
+
is REQUIRED with NO default — bareloop is LLM-agnostic, and defaulting it would silently re-lock
|
|
882
|
+
every interview onto one vendor; missing or empty dies loud, at the door, naming the same menu
|
|
883
|
+
`src/job.js`'s own validator admits. It is written into the draft's `provider` field, so
|
|
884
|
+
`scripts/run-author.mjs` needs no flag of its own — it resolves whatever the interview wrote
|
|
885
|
+
down.
|
|
886
|
+
|
|
887
|
+
`--base-url` is OPTIONAL with NO default (PRD item 33 close-out — L17 named the provider but
|
|
888
|
+
never admitted the endpoint): a provider name's table entry (`src/providers.js`) can be reached
|
|
889
|
+
through an OpenAI-compatible gateway other than its own default host — DeepSeek, today's one
|
|
890
|
+
secondary, is reached as `--provider openai-api --base-url https://api.deepseek.com/v1`. Absent,
|
|
891
|
+
the draft carries NO `baseUrl` field at all (never `null`, never `''`) and every provider
|
|
892
|
+
constructor defaults its own endpoint. When given, it lands in the draft's `baseUrl` field
|
|
893
|
+
beside `provider` and passes through the SAME `validateJob` pass every other field takes below,
|
|
894
|
+
at $0: `http://` to a public host, an embedded `user:pass@`, or an unparseable string reds there,
|
|
895
|
+
before anything is written. The key still follows the PROVIDER, not the endpoint — a DeepSeek
|
|
896
|
+
draft's key still loads from `OPENAI_API_KEY`, and the hand-off says so.
|
|
897
|
+
|
|
898
|
+
**Source and Destination are the interview's own FIRST TWO QUESTIONS, and they REPLACE
|
|
899
|
+
`--patient`** (PRD item 33 M3, ruling 2, `docs/product/ITEM33-BUILD.md`). Source — "a local
|
|
900
|
+
folder, subfolder or file, or one URL the machine can reach" — is asked first and checked
|
|
901
|
+
against the machine ($0) the moment it is typed; language is then detected from it the same
|
|
902
|
+
way it always was (below), never asked. Destination is asked second, and its SHAPE depends on
|
|
903
|
+
what Source turned out to be (the SAME rule `prepareSource` itself uses to route this,
|
|
904
|
+
`looksLikeRepoSource`, `src/source.js` — never a second, hand-typed copy of it): for a REPO
|
|
905
|
+
source (a local directory whose nearest ancestor, walking up, carries a `.git` directory —
|
|
906
|
+
Source itself or one it sits inside), Destination IS the write fence — the
|
|
907
|
+
question this interview used to ask separately, later, as "The FENCE"; that later question is
|
|
908
|
+
GONE, and `draft.writeScope` is parsed straight out of the Destination answer. For every other
|
|
909
|
+
kind (a plain folder, a single file, or a URL), Destination is an absolute DIRECTORY, re-asked
|
|
910
|
+
in a loop (via `proveDestination`) until it is one the machine can actually write into. Either
|
|
911
|
+
way, the moment both are answered the script calls `prepareSource({source, into, destination})`
|
|
912
|
+
itself — the SAME $0 freeze `scripts/prep-source.mjs` performs, under a fresh `into` scratch
|
|
913
|
+
root nested inside `--out` — and everything downstream (the class's own questions, the job
|
|
914
|
+
spec's `description`, and the `--source` handed to `run-author.mjs`) reads the PREPARED COPY,
|
|
915
|
+
never the original Source again (patients are copies, always). A Source one level inside a
|
|
916
|
+
repo — a subfolder with no `.git` of its own — IS a repo source, routed to the whole repo's
|
|
917
|
+
tracked files by the nearest-ancestor walk `nearestGitAncestor` shares with `prepareSource`
|
|
918
|
+
(ruling 2 addendum, 2026-09-13; see the repo-source section below for the full rule and the
|
|
919
|
+
untracked-subfolder refusal). A
|
|
920
|
+
non-repo source gets Source and Destination proven and frozen and then, since PRD item 33 M3
|
|
921
|
+
piece 4 step S6 (D5 = A), the FORM CONTINUES — the class's own questions, job name, budget and
|
|
922
|
+
wall are all still asked, and the draft is still written, with no `writeScope` field
|
|
923
|
+
(`PLAIN_FOLDER_DEFERRED_FIELDS`, `src/authorjob.js` — Destination there is an output directory,
|
|
924
|
+
never a fence). The honest named stop (bareloop's check catalogue is code-genre only today,
|
|
925
|
+
ruling 7 → M4) no longer lands HERE — it moves to `run-author.mjs`, which now gives it
|
|
926
|
+
IMMEDIATELY, at $0 (D5 amended 2026-09-21, F191: no scout and no confirm turn either — a plain
|
|
927
|
+
folder has no code language, so the confirm turn's own code-derived protections cannot be
|
|
928
|
+
computed for one; see the run-author.mjs section below). Everything load-bearing in it is
|
|
929
|
+
borrowed rather than respelled —
|
|
858
930
|
the QUESTIONS are the library's frozen sets (`questionsFor` / `requiredAnswersFor`), printed as
|
|
859
931
|
handed over and never re-worded, re-ordered or re-numbered by the script; the REFUSALS are
|
|
860
932
|
`runInterview`'s; the SCRUB is `redactSecrets` at capture AND again at the library's own ingest,
|
|
@@ -886,7 +958,11 @@ spends it.
|
|
|
886
958
|
|
|
887
959
|
**It ends by OFFERING the paid step, and the offer's default is NO.** `run-author.mjs` is a
|
|
888
960
|
DIFFERENT PROCESS under its own ceiling; the interview prints its exact command line (with
|
|
889
|
-
`--budget` propagated only when one was given, and a note when
|
|
961
|
+
`--budget` propagated only when one was given, and a note when the CHOSEN provider's own key —
|
|
962
|
+
`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`, resolved through the same provider table
|
|
963
|
+
`run-author.mjs` and `run-u.mjs` share — is unset in this shell, OR set but MALFORMED: F181 — a
|
|
964
|
+
value carrying a line break/control character/stray whitespace (a two-line secret-store entry,
|
|
965
|
+
e.g.) reads a distinct reason via `apiKeyProblem` (`src/providers.js`), never the value)
|
|
890
966
|
before asking `Run it now? [y/N]`, so declining still leaves a command to paste and two files
|
|
891
967
|
already on disk. Only an explicit yes spends — the answer that costs nothing is the one you get
|
|
892
968
|
by saying nothing, the same lean the pause's doors take. On a yes it releases stdin before
|
|
@@ -897,16 +973,79 @@ into a success one process up.
|
|
|
897
973
|
**Both scripts speak exit codes, and an adopter wrapping them in CI reads them:**
|
|
898
974
|
|
|
899
975
|
- `run-interview.mjs` — **0** the interview finished (and, when it spawned the paid step, that
|
|
900
|
-
step's own status is what you get, never a flattened 0); **1** a REFUSAL — a locked class,
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
**
|
|
908
|
-
|
|
909
|
-
|
|
976
|
+
step's own status is what you get, never a flattened 0); **1** a REFUSAL — a locked class, a
|
|
977
|
+
language-unsupported Source, a non-repo Source's honest M4 stop, the library's own reds over
|
|
978
|
+
the answers, a Source/Destination `prepareSource`/`proveDestination` refusal, or a spec draft
|
|
979
|
+
that does not validate; **2** operator/config — usage, a present `--patient` or `--lang` (both
|
|
980
|
+
removed), a nonexistent Source, a malformed `--budget`, stdin ending mid-interview, or a child
|
|
981
|
+
that could not be started; **3** a LEAK that `scanSecrets` found in a file it had just written
|
|
982
|
+
(count and path only — echoing the matched string is the same leak, one hop on). An
|
|
983
|
+
**ambiguous-language Source no longer dies here** (PRD item 33 M3 piece 4, step S5): this
|
|
984
|
+
script stays PROVIDER-FREE (D1), so it says which two manifests it saw and continues the form
|
|
985
|
+
with a placeholder `LANG` — the confirm turn (`run-author.mjs`, below) asks the person which
|
|
986
|
+
one interactively, before the scout (D7), and that pick — not this placeholder — is what lands
|
|
987
|
+
in `closeDecl.lang`. The interview also asks **no goal question** any more (ruling 5's 2026-
|
|
988
|
+
09-13 addendum, D2 = option B): the confirm turn drafts the signed goal sentence from the
|
|
989
|
+
Goal/Success answers and the person confirms or fixes it there; `specdraft.json` carries no
|
|
990
|
+
`goal` field at all (`CONFIRM_AUTHORED_FIELDS`, `src/authorjob.js` — the `$0` validator pass
|
|
991
|
+
filters its reds the same way it already filters `AUTHORED_SPEC_FIELDS`'s).
|
|
992
|
+
- `run-author.mjs` — **1** a refusal or a failed gate (this now includes the confirm turn's own
|
|
993
|
+
`confirm-abandoned`/`confirm-restart` stops — input ended, or the person chose to start over,
|
|
994
|
+
both counted through the same generic "not-authored" branch every other authoring refusal
|
|
995
|
+
uses, and `source-deps-missing` — the prepared copy's `package.json` lists dependencies with
|
|
996
|
+
no `node_modules`, refused at $0 before the scout and before the provider/key are even
|
|
997
|
+
checked, naming the exact install command); **2** operator/config; **3** a leak; **4** a
|
|
998
|
+
CRASH inside the paid span. **3
|
|
999
|
+
deliberately OVERRIDES a 4**: a secret sitting in a written file is the harder line of the
|
|
1000
|
+
two, and the crash keeps both of its own louder channels — the whole error on stderr and its
|
|
1001
|
+
own `author-crash` spine record. **This script is now interactive** (step S4): the confirm
|
|
1002
|
+
turn (`runConfirmTurn`, `src/authorflow.js`) asks, over a readline `ask` seam bound to stdin,
|
|
1003
|
+
who wrote what and lets the person confirm the plan, fix it (up to 2 rounds), type the goal
|
|
1004
|
+
themselves, or start over — everything else (the survey, the declaration loop, D9's gates)
|
|
1005
|
+
stays unattended. `rl.close()` runs in a `finally` around the whole paid span. **A plain-folder
|
|
1006
|
+
Source (step S6, D5 amended 2026-09-21, F191) gets NO scout AND NO confirm turn.** D5
|
|
1007
|
+
originally ran the confirm turn over a $0, no-git directory listing of the frozen tree first,
|
|
1008
|
+
THEN gave the honest "no checks yet" stop (`request-red`/`non-code-source`) once a plan was
|
|
1009
|
+
confirmed. Live run `mu4hc7sp` crashed inside that confirm turn instead: `confirmProtections`
|
|
1010
|
+
calls `classGuards` (`src/authoring.js`), which is keyed by CODE LANGUAGE and throws on a
|
|
1011
|
+
plain folder's `lang: 'none-detected'` — a plain folder has no language, so the confirm turn
|
|
1012
|
+
cannot honestly show real, code-derived protections for one, and now never runs one. The stop
|
|
1013
|
+
fires immediately after `author-start`, at $0, no scout, no confirm turn, no model call — the
|
|
1014
|
+
spine is exactly `author-start` → `author-end{outcome:'not-authored', stop:'non-code-source'}`.
|
|
1015
|
+
|
|
1016
|
+
**The protections a person is shown in the confirm turn come from CODE, never the model**
|
|
1017
|
+
(fix #1, live run mu0voeo4, 2026-09-14 — a run once showed a model-invented "behavior-
|
|
1018
|
+
preservation guard" no kind in the catalogue can check). The model is no longer even asked
|
|
1019
|
+
for `protections`: the confirm tool's schema dropped the field. What the menu displays and
|
|
1020
|
+
what lands in `accepted.protections`/`confirmedBlock` is rendered by `confirmProtections`
|
|
1021
|
+
(`src/authorflow.js`) from the SAME guards `classGuards` (`src/authoring.js`) will actually
|
|
1022
|
+
compose, each through a frozen plain-English line in `GUARD_DESCRIPTIONS` — beside
|
|
1023
|
+
`CONFIRM_MENU` — plus the write fence when one is set; a guard with no line there throws
|
|
1024
|
+
rather than showing a blank description. The model instead returns `notChecked`: anything
|
|
1025
|
+
the person asked for that no listed check can verify, in the person's own words, never
|
|
1026
|
+
silently dropped. It is shown under its own heading in the menu (`scripts/run-author.mjs`)
|
|
1027
|
+
and threaded through to `accepted.notChecked` and `confirmedBlock`'s own "THE PERSON ASKED
|
|
1028
|
+
FOR THESE, BUT NOTHING CHECKS THEM" section for the composer.
|
|
1029
|
+
|
|
1030
|
+
**The model drafting `notChecked` is now shown the SAME code-derived protections list before
|
|
1031
|
+
it drafts** (F183, ruling A, 2026-09-16 — live runs `mu2bjmed` and `mu2qmept` both had the
|
|
1032
|
+
model falsely claim an already-enforced guard or the write fence was missing, because it was
|
|
1033
|
+
never told what was actually enforced). `confirmProtections`'s output is computed once, before
|
|
1034
|
+
the paid round loop, and threaded into `confirmPrompt`'s model-facing prompt as an "ALREADY
|
|
1035
|
+
COVERED" block; `CONFIRM_SYSTEM` orders the model to check that list before naming anything in
|
|
1036
|
+
`notChecked`. The model still authors `notChecked` freely — this never makes the list
|
|
1037
|
+
deterministic, and it is still never a hand-typed second spelling of the guards. Not yet
|
|
1038
|
+
proven live.
|
|
1039
|
+
|
|
1040
|
+
**`accepted.openQuestions` is the model's OWN honest `questions`, from the plan being
|
|
1041
|
+
ACCEPTED** (F175, fixed 2026-09-14) — never a prior, superseded round's, and never limited to
|
|
1042
|
+
a person's typed "fix" text as it used to be. On the round-2 "fix" terminal path (D3: there is
|
|
1043
|
+
no 3rd call) the model's own questions come first, then the person's still-pending fix text.
|
|
1044
|
+
Shown to the composer by `confirmedBlock` ("state these in your notes, never decide them
|
|
1045
|
+
silently") and at the terminal signing readout (`openQuestionLines`, `scripts/author-readout.mjs`).
|
|
1046
|
+
**Not fully closed:** the confirm menu still lets a person pick "Confirm" without being forced
|
|
1047
|
+
to resolve a raised question first — only the silent-drop-from-the-signed-record half of F175
|
|
1048
|
+
is fixed; the menu-bypass half stays open.
|
|
910
1049
|
|
|
911
1050
|
**A crash inside the paid span leaves a BODY.** The fallible span — from just after
|
|
912
1051
|
`author-start` to the end of the main flow — sits in ONE try/catch that RETRIES NOTHING and
|
|
@@ -930,9 +1069,9 @@ the EMIT SITE, so `classifyIncidents` files it against bareloop's own catalogue
|
|
|
930
1069
|
decision-ready `escalation` under `close-unauthorable`, which is in the ledger's excluded
|
|
931
1070
|
set precisely because the demand is already counted once. A refusal that is a STOP rather
|
|
932
1071
|
than demand (a broken instrument, a close with nothing to do) emits the escalation ALONE.
|
|
933
|
-
**Known limit, stated:** v1's derivation cannot tell `soft-green` from
|
|
934
|
-
try — both refuse under one verb (`non-green-verdict`), because naming
|
|
935
|
-
job belongs to would be a guess about prose.
|
|
1072
|
+
**Known limit, stated:** v1's derivation cannot tell `soft-green` from an off-menu verdict
|
|
1073
|
+
class and does not try — both refuse under one verb (`non-green-verdict`), because naming
|
|
1074
|
+
which locked class a job belongs to would be a guess about prose.
|
|
936
1075
|
|
|
937
1076
|
**At run time** the declared close is executed by the kind executor, never compiled down to
|
|
938
1077
|
shell (that would turn owned kinds back into authored strings — the exact thing D3 makes
|
|
@@ -1047,8 +1186,8 @@ known-answer round-trip before tokens: `smoke-red` — a silent degradation thro
|
|
|
1047
1186
|
|
|
1048
1187
|
Outcomes: `green | already-green | escalated | unapproved-spec | job-red | smoke-red |
|
|
1049
1188
|
plan-red | check-red | close-red | close-unsupported | recipe-stale | branch-red | pricing-red |
|
|
1050
|
-
provider-red | interpreter-red | cap-halt | wall-halt | step-stalled |
|
|
1051
|
-
|
|
1189
|
+
provider-red | interpreter-red | cap-halt | wall-halt | step-stalled |
|
|
1190
|
+
step-red:<id> | runner-drained`.
|
|
1052
1191
|
|
|
1053
1192
|
**`runner-drained` (F140/PRD item 28(c))** is a distinct class from every outcome above: it is
|
|
1054
1193
|
NOT `runJob`'s own return value, it is a `beforeExit` backstop's terminal, emitted from outside
|
|
@@ -1125,8 +1264,9 @@ knob, not a product default — the spec names no scout, so the signed hash is u
|
|
|
1125
1264
|
`--read-shim`: an unrecognised value exits 2 at argv, and every re-invocation the runner prints
|
|
1126
1265
|
carries `--scout off` when set, so a resume never silently drops the arm.
|
|
1127
1266
|
|
|
1128
|
-
**The two
|
|
1129
|
-
at this layer, and each is a CLEAN exit (`spendComplete` stays true —
|
|
1267
|
+
**The review door's two checkpoint terminals (N4 slice 1, doors re-cut 2026-08-18)** are the
|
|
1268
|
+
class's whole surface at this layer, and each is a CLEAN exit (`spendComplete` stays true —
|
|
1269
|
+
only the two casualties floor). The pause terminal is a
|
|
1130
1270
|
decision-ready CHECKPOINT: the close reached a stage no machine can render, so the run stops
|
|
1131
1271
|
holding everything it has done, emits the evidence package (every mechanical stage's result, the
|
|
1132
1272
|
close's own `ask`, and the changed paths — bounded, trim announced), and the clock stops with
|
|
@@ -1135,12 +1275,12 @@ exported bundle inherits one spelling), and `checkpointAgeGate` refuses one olde
|
|
|
1135
1275
|
`PAUSE_TTL_MS` (60 days) by NAMING the age and the TTL. The signer's own `pause` door mints
|
|
1136
1276
|
that SAME terminal, with `humanDecision: 'pause'` and an explicit `gap: null` on the record: no
|
|
1137
1277
|
fix loop, no worker round, nothing spent, the rerun allowance untouched, and the checkpoint left
|
|
1138
|
-
exactly as it was.
|
|
1139
|
-
not one of the three doors, a `rerun` with empty text, or a decision handed to a
|
|
1140
|
-
|
|
1141
|
-
ledger's escalation counting.
|
|
1278
|
+
exactly as it was. The decision-refusal terminal refuses a decision the run cannot act on — a
|
|
1279
|
+
word that is not one of the three doors, a `rerun` with empty text, or a decision handed to a
|
|
1280
|
+
close with no matching stage — before anything is spent. Neither demotes a bridge, and both are
|
|
1281
|
+
excluded from the ledger's escalation counting.
|
|
1142
1282
|
|
|
1143
|
-
There was a third terminal,
|
|
1283
|
+
There was a third terminal, for a `cancel` door that no longer exists (hamr,
|
|
1144
1284
|
2026-08-17: *"pause can resume — that would be more honest"*). Nothing mints it and no constant
|
|
1145
1285
|
exports it; the ledger still RECOGNISES the bare string so a spine written before the change
|
|
1146
1286
|
reads as governance rather than as a counted capability gap. An unresumed pause expiring under
|
|
@@ -1164,8 +1304,8 @@ the byte-identical question again. The two markers are deliberately not one: the
|
|
|
1164
1304
|
"spends" the ruling for THIS leg's close readings the moment it opens, while whether the person
|
|
1165
1305
|
must be asked AGAIN depends on work actually having been bought.
|
|
1166
1306
|
|
|
1167
|
-
A leg handed BOTH a fresh `humanRuling` and a `heldRuling` is refused (
|
|
1168
|
-
naming the held decision) before anything costs anything: two answers to one question is
|
|
1307
|
+
A leg handed BOTH a fresh `humanRuling` and a `heldRuling` is refused (the decision-refusal
|
|
1308
|
+
terminal, naming the held decision) before anything costs anything: two answers to one question is
|
|
1169
1309
|
ambiguity, not a merge. `resolveHumanRuling(fresh, held)` is the exported seam that decides this
|
|
1170
1310
|
(and whether the leg is a fresh engagement, below), so a runner cannot admit what a run refuses.
|
|
1171
1311
|
|
|
@@ -1294,7 +1434,7 @@ never replans) — plus at most ONE arbiter-granted extra on a second variance s
|
|
|
1294
1434
|
REAL close and governed by the close TREND rule below (`capRuns`, default 3, survives only as
|
|
1295
1435
|
its blind fallback). `plan-executed` (the plan-as-executed record, design law #2) lands on the
|
|
1296
1436
|
spine on every path that executed steps. Additional outcomes: `already-green |
|
|
1297
|
-
plan-red | check-red | close-red | wall-halt
|
|
1437
|
+
plan-red | check-red | close-red | wall-halt`.
|
|
1298
1438
|
A `human-confirms` stage is caught at BOTH close seams — the precheck (reaching a person there
|
|
1299
1439
|
means the machine half already passes on the untouched tree, so pausing costs $0 where drafting
|
|
1300
1440
|
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
|
|
@@ -1674,12 +1814,18 @@ about its task, beside the absolute-path law it is the twin of — both are fenc
|
|
|
1674
1814
|
cannot infer. It renders for EVERY worker on every grant (a worker granted only `write` gets
|
|
1675
1815
|
no component strategy paragraph at all and must still be told). The fence is unchanged; what
|
|
1676
1816
|
changed is that the rule is stated instead of learned one refusal at a time, which cost a real
|
|
1677
|
-
run the rounds of a bounded attempt. N2 bounds (honest): `gold`/`rubric
|
|
1678
|
-
closes refuse `close-unsupported` —
|
|
1679
|
-
never a close TYPE, so there is one live expression of
|
|
1817
|
+
run the rounds of a bounded attempt. N2 bounds (honest): `gold`/`rubric` OBJECT
|
|
1818
|
+
closes refuse `close-unsupported` — a `human-confirms` STAGE lives inside a `closeDecl`,
|
|
1819
|
+
never as a close TYPE, so there is one live expression of it rather than two.
|
|
1680
1820
|
|
|
1681
1821
|
### THE REVIEW DOOR — the three doors at the END of a run (softgreen module 8, PRD v1.71 §3)
|
|
1682
1822
|
|
|
1823
|
+
`answerReviewDoor` (`src/reviewdoor.js`) is exported (item 34 L20):
|
|
1824
|
+
|
|
1825
|
+
```js
|
|
1826
|
+
import { answerReviewDoor } from 'bareloop';
|
|
1827
|
+
```
|
|
1828
|
+
|
|
1683
1829
|
The pause machinery re-homed: the same doors, the same evidence package and the same 60-day TTL,
|
|
1684
1830
|
one level OUT — from a stage inside the close to the door at the end of a run.
|
|
1685
1831
|
|
|
@@ -1742,6 +1888,23 @@ chain's ceiling); the WALL does not (F103).
|
|
|
1742
1888
|
|
|
1743
1889
|
### The reuse registry (Layer 3) — `src/bridges.js`, `src/selection.js`
|
|
1744
1890
|
|
|
1891
|
+
Every name below is exported from the package root:
|
|
1892
|
+
|
|
1893
|
+
```js
|
|
1894
|
+
import {
|
|
1895
|
+
BRIDGE_SCHEMA, QUARANTINED_VERDICTS, quarantinesCredit, deriveStatus, validateBridge,
|
|
1896
|
+
mintBridge, appendGreen, appendRed, newestEligibleVersion, reuseEligibility, recordDoor,
|
|
1897
|
+
applyDoorDecision, listingRow, loadGate, loadBridge, loadRegistry, saveBridge, makeRegistry,
|
|
1898
|
+
registryExists, renderListing, selectionPrompt,
|
|
1899
|
+
} from 'bareloop';
|
|
1900
|
+
```
|
|
1901
|
+
|
|
1902
|
+
`BRIDGE_SCHEMA` is the literal schema tag (`'bridge-v1'`) every stored entry carries and
|
|
1903
|
+
`validateBridge(input)` checks against — never throws on JSON text or plain parsed data,
|
|
1904
|
+
the same ingest contract `validateJob`/`validatePlan` hold; every failure is a named red,
|
|
1905
|
+
never a throw. `registryExists(dir)` is the one predicate `runReuse` and the CLI both ask
|
|
1906
|
+
before treating a path as a real registry, rather than conjuring one from a typo.
|
|
1907
|
+
|
|
1745
1908
|
A **bridge** is the plan a green actually executed, kept so the next run of the same SHAPE
|
|
1746
1909
|
starts from it instead of cold. Storage is a directory of plain JSON files at an
|
|
1747
1910
|
**operator-supplied path** — no database, no default location (a missing registry reds
|
|
@@ -1778,7 +1941,7 @@ it is simply worth nothing yet. A held green does not count in `deriveStatus` (a
|
|
|
1778
1941
|
only greens are held derives `null` and renders `HELD`, never `NO-GREEN`), and
|
|
1779
1942
|
`reuseEligibility(bridge)` / `newestEligibleVersion(bridge)` refuse it for reuse **with a
|
|
1780
1943
|
stated reason** — the same visible-skip discipline an unreadable registry file gets.
|
|
1781
|
-
`green`
|
|
1944
|
+
`green` is untouched, byte for byte: the key is ABSENT, because the hold is about
|
|
1782
1945
|
the young JUDGE, not about every class that is not green. `QUARANTINED_VERDICTS` is the one
|
|
1783
1946
|
list; `quarantinesCredit(verdictType)` is the one predicate. The class travels on the green
|
|
1784
1947
|
record as `verdictType` — an unrecognised value REDS rather than falling through to unheld.
|
|
@@ -1855,7 +2018,9 @@ draft anywhere in the prompt.
|
|
|
1855
2018
|
|
|
1856
2019
|
**`renderListing(registry)` / `selectionPrompt(listing, ask)` — `src/selection.js`.** Pure
|
|
1857
2020
|
text: they read no file, call no model and decide nothing. `renderListing` takes
|
|
1858
|
-
`loadRegistry`'s result (or a bare array)
|
|
2021
|
+
`loadRegistry`'s result (or a bare array), maps each entry through the exported
|
|
2022
|
+
`listingRow(bridge)` (the same per-entry summary — name, goal, status, counts, cost band —
|
|
2023
|
+
`renderListing` renders), and prints one compact block per bridge — name,
|
|
1859
2024
|
the goal sentence it greened, status, greens/reds with the last outcome, and the cost/time
|
|
1860
2025
|
BAND of its greens. An unknown cost or duration renders `UNKNOWN` and a partial aggregate
|
|
1861
2026
|
says how many it skipped (never a `$0.00` that reads as exact); entries that could not be
|
|
@@ -1868,6 +2033,15 @@ or `runReuse` below, which is the shipped one.
|
|
|
1868
2033
|
|
|
1869
2034
|
### The reuse ENVELOPE and `runReuse` (Layer 3, D7) — `src/reuse.js`
|
|
1870
2035
|
|
|
2036
|
+
Exported from the package root:
|
|
2037
|
+
|
|
2038
|
+
```js
|
|
2039
|
+
import {
|
|
2040
|
+
validateEnvelope, resolveTrySpec, resolveReuse, reuseSpecHash, selectBridge, runReuse,
|
|
2041
|
+
REUSE_GRADED_RED, checkpointAgeGate, PAUSE_TTL_MS, writeRunGreenRow,
|
|
2042
|
+
} from 'bareloop';
|
|
2043
|
+
```
|
|
2044
|
+
|
|
1871
2045
|
`runReuse` is `runJob` under an operator-signed **envelope**: try a stored workflow, then
|
|
1872
2046
|
another, then draft cold — hamr's *"$5 and 30 mins x2 then start anew"*. It composes; it
|
|
1873
2047
|
decides nothing the arbiter owns.
|
|
@@ -1987,6 +2161,12 @@ the secrets scan.
|
|
|
1987
2161
|
|
|
1988
2162
|
#### Resuming a killed run (module C)
|
|
1989
2163
|
|
|
2164
|
+
Exported from the package root: `readResume` and `resumeTreeGate` (both below).
|
|
2165
|
+
|
|
2166
|
+
```js
|
|
2167
|
+
import { readResume, resumeTreeGate } from 'bareloop';
|
|
2168
|
+
```
|
|
2169
|
+
|
|
1990
2170
|
A reuse run is up to `tries + 1` full jobs long, so a kill mid-run is a real event and it
|
|
1991
2171
|
must not cost the whole envelope. **`readResume(events, {deathAt?, direct?, resumableOutcomes?})`**
|
|
1992
2172
|
reads the dead run's own spine back into the state a resume continues from: `{ started,
|
|
@@ -2015,7 +2195,8 @@ semantics are byte-unchanged (PRD v1.46 §3):
|
|
|
2015
2195
|
plan is on the spine with nothing decided against them. **The set is the CALLER's** — the
|
|
2016
2196
|
parameter stays a parameter, and its empty default is what the reuse loop's own graded-row
|
|
2017
2197
|
semantics depend on — but the canonical ANSWER is exported as **`CHECKPOINT_OUTCOMES`**
|
|
2018
|
-
(`['cap-halt', 'wall-halt', 'step-stalled',
|
|
2198
|
+
(`['cap-halt', 'wall-halt', 'step-stalled', ...]`, plus the pause checkpoint's own outcome),
|
|
2199
|
+
and the reference runner
|
|
2019
2200
|
CONSUMES it rather than keeping a copy. The first two are governance halts — an operator-owned allowance ran
|
|
2020
2201
|
out. `step-stalled` joined them 2026-08-13 on hamr's ruling (PRD v1.64 §1): it is the one
|
|
2021
2202
|
terminal whose OWN escalation already offers *"retry the run"*, nothing about the work on
|
|
@@ -2057,7 +2238,7 @@ watchdog `--wall-ms 0`, which it defaulted to `null` and armed no deadline at al
|
|
|
2057
2238
|
still advertising one. The refusal names the lever: raise `maxWallMs`, which moves the spec hash
|
|
2058
2239
|
and is re-signed.
|
|
2059
2240
|
|
|
2060
|
-
**A
|
|
2241
|
+
**A pause checkpoint is answered on the same command line** (N4, 2026-08-12 §5.2 — the
|
|
2061
2242
|
terminal is the v1 surface; the panel is N6's). The reference runner takes
|
|
2062
2243
|
`--decide accept|rerun|pause` with `--text` for the rerun door, gated on the SAME
|
|
2063
2244
|
`--approve <specHash>` signature the run itself is signed with (ruling 4: the spec hash is the
|
|
@@ -2216,6 +2397,13 @@ write site — the spine is append-only, so a scan after the bytes land is too l
|
|
|
2216
2397
|
|
|
2217
2398
|
### `updateLedger({ ledgerFile, spineFiles })` → `{ appended, fold }` — `src/ledger.js`
|
|
2218
2399
|
|
|
2400
|
+
`rateProvenance`, `spendProvenance` and `VOUCHED_RATE_SOURCES` are exported alongside it
|
|
2401
|
+
(item 34 L20):
|
|
2402
|
+
|
|
2403
|
+
```js
|
|
2404
|
+
import { rateProvenance, spendProvenance, VOUCHED_RATE_SOURCES } from 'bareloop';
|
|
2405
|
+
```
|
|
2406
|
+
|
|
2219
2407
|
The upstream ledger: spines fold into ONE append-only incident JSONL both the consumer
|
|
2220
2408
|
(workflow health) and the maintainer (upstream asks) read. Spines stay ground truth —
|
|
2221
2409
|
the ledger is derived and reconstructible (delete it, re-run the collector: same fold).
|
|
@@ -2243,15 +2431,22 @@ time), `step-stalled` (the stall fuse firing is our governance, not an observed
|
|
|
2243
2431
|
failure), `step-variance` (a planning story), `gate-red`/`smoke-red` (governance working
|
|
2244
2432
|
as intended / already counted — and `gate-red` is no longer minted anywhere in this library,
|
|
2245
2433
|
kept listed because this set is EXECUTABLE: dropping a name would not delete the category, it
|
|
2246
|
-
would re-file any future emission of it as a counted capability gap),
|
|
2434
|
+
would re-file any future emission of it as a counted capability gap), the legacy human-close terminal/`close-unsupported`/`close-unauthorable` (by design — and the last one
|
|
2247
2435
|
is excluded for a SECOND reason: its demand is already counted once as the `request-red`
|
|
2248
2436
|
the same refusal emits, so counting the escalation too would double every refusal),
|
|
2249
2437
|
`close-timeout`/`close-killed`/`close-crashed` (the arbiter's own named terminals, F17);
|
|
2250
2438
|
`close-verdict`/`artifact-red` stay worker stories, `pr-red` operator environment.
|
|
2251
2439
|
`suggestedAsk` on every row is a template seed for an upstream ask — filing stays human;
|
|
2252
2440
|
status rows (`open → filed → fixed → consumed`) are human-appended, and the fold shows
|
|
2253
|
-
the latest per key. Pure pieces exported for custom folds:
|
|
2254
|
-
|
|
2441
|
+
the latest per key. Pure pieces exported for custom folds:
|
|
2442
|
+
|
|
2443
|
+
```js
|
|
2444
|
+
import { classifyIncidents, foldLedger, ledgerDeltas, LEDGER_CLASSES } from 'bareloop';
|
|
2445
|
+
```
|
|
2446
|
+
|
|
2447
|
+
`classifyIncidents(events, {spine?})` classifies raw events; `foldLedger(rows)` derives the
|
|
2448
|
+
per-key fold; `ledgerDeltas(fold, occurrences)` reads what changed since the last pass;
|
|
2449
|
+
`LEDGER_CLASSES` is the frozen menu above. Riding with them,
|
|
2255
2450
|
and deliberately NOT a ledger class: `rateProvenance(record)` → `vouched|guessed|unpriced|unknown`
|
|
2256
2451
|
and `spendProvenance(events)` → per-provenance `{rounds, usd, unpricedRounds}` buckets answer
|
|
2257
2452
|
"how much of this run's spend was priced by a rate nobody vouched for" (BA-21 — see *Every cost
|
|
@@ -2285,6 +2480,20 @@ objects will not be caught by this. `scripts/behaviour-readout.mjs <gate-audit.j
|
|
|
2285
2480
|
prints the block for one run or every run_id found in the file, skipping malformed lines with
|
|
2286
2481
|
a count rather than throwing.
|
|
2287
2482
|
|
|
2483
|
+
**F186 (2026-09-15) — the "run's own gate-audit JSONL" claim above was FALSE for a while, at the
|
|
2484
|
+
file-hygiene layer, never inside `runBehaviour` itself.** The authoring scout's gate audit
|
|
2485
|
+
(`src/authorscout.js`'s `defaultSurveyor`) writes directly into the patient tree at its root
|
|
2486
|
+
(`<workdir>/gate-audit.jsonl`), and the tree's own `.gitignore` denies `*.jsonl`, so
|
|
2487
|
+
`scripts/run-u.mjs`'s cold reset (`git clean -fd`, no `-x`) never removed it — the file
|
|
2488
|
+
accreted rows from every authoring scout AND every worker run that had ever touched the tree,
|
|
2489
|
+
and `run-u.mjs`'s end-of-run rename claimed the WHOLE accreted file as "this run's own audit"
|
|
2490
|
+
with no filtering. The fix is at the source, never a `run_id` filter bolted onto `runBehaviour`
|
|
2491
|
+
(a single run legitimately spans several run_ids): `scripts/run-author.mjs` archives its own
|
|
2492
|
+
audit out of the tree the moment authoring ends; `scripts/run-u.mjs` moves any stale audit
|
|
2493
|
+
aside right after `coldReset`, before its own Gate ever opens (never on a resume, where the
|
|
2494
|
+
tree's audit is that same run's own prior leg). With both halves in place, the file this
|
|
2495
|
+
section describes really is scoped to the one run reading it, restoring the claim above.
|
|
2496
|
+
|
|
2288
2497
|
```js
|
|
2289
2498
|
import { runBehaviour, formatBehaviour } from 'bareloop';
|
|
2290
2499
|
const events = fs.readFileSync(auditFile, 'utf8').trim().split('\n').map(JSON.parse);
|
|
@@ -2622,6 +2831,18 @@ not a rule violation, never a red.
|
|
|
2622
2831
|
|
|
2623
2832
|
### Bundles — export a job, run it elsewhere (`src/bundle.js`, `src/cli.js`, `bin/bareloop.mjs`)
|
|
2624
2833
|
|
|
2834
|
+
Every function this section discusses by name is exported at the package root
|
|
2835
|
+
(item 34 L20):
|
|
2836
|
+
|
|
2837
|
+
```js
|
|
2838
|
+
import {
|
|
2839
|
+
absolutePathLiteralsOf, appendHistory, bless, bundleHash, checkBundleDeps,
|
|
2840
|
+
checkCloseAbsolutePaths, checkCloseByteSignature, checkEnvelope, checkStageByteSignature,
|
|
2841
|
+
exportBundle, hashCloseScriptBytes, readBundle, readCloseScripts, resolveBundleSpec,
|
|
2842
|
+
signCloseScripts, verifyBlessing,
|
|
2843
|
+
} from 'bareloop';
|
|
2844
|
+
```
|
|
2845
|
+
|
|
2625
2846
|
`docs/product/EXPORT-BUILD.md` (frozen build spec, 2026-09-05). A **bundle** is a plain
|
|
2626
2847
|
directory that ships a proven job — spec, close scripts, and registry history — so it can
|
|
2627
2848
|
run on a different machine without this repo. `npm pack` is its shipping form (there is no
|
|
@@ -2675,6 +2896,15 @@ scope, on both halves, exactly as before. The shape test itself
|
|
|
2675
2896
|
demand so the demand and the detectors can never drift apart about which stages are in
|
|
2676
2897
|
scope.
|
|
2677
2898
|
|
|
2899
|
+
**Documented limit (PRD item 34 L3):** `npx` is one of the recognized interpreters, and the
|
|
2900
|
+
path-shaped test only asks for a `/` or a known script extension — so a close command like
|
|
2901
|
+
`npx @scope/pkg` reads its scoped package name (`@scope/pkg`) as the addressable token,
|
|
2902
|
+
because it contains a `/`. That token is not a real file on disk, so `signCloseScripts`
|
|
2903
|
+
finds nothing to hash and such a close can never be signed. This is KEPT as a documented
|
|
2904
|
+
limit, not fixed: it fails safe (a close that cannot be signed cannot run unsigned either)
|
|
2905
|
+
and no shipped job uses an `npx @scope/pkg`-shaped close command. Spell such a close as a
|
|
2906
|
+
script path (`node ./close/run-pkg.mjs`) or an unscoped command instead.
|
|
2907
|
+
|
|
2678
2908
|
| function | args → returns | notes |
|
|
2679
2909
|
|---|---|---|
|
|
2680
2910
|
| `absolutePathLiteralsOf(source)` | close script text → `string[]` | the scan itself (moved here from `src/bundle.js`, which now imports it) |
|
|
@@ -2963,7 +3193,12 @@ worker gets a real repo to review. Its guards (the input stays untouched) arrive
|
|
|
2963
3193
|
`.git` that is a FILE, not a directory — a linked worktree or a submodule — refuses
|
|
2964
3194
|
`source-is-linked-worktree`: its real git directory lives elsewhere, and a copy would
|
|
2965
3195
|
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).
|
|
3196
|
+
source (a real repo legitimately carries binaries). A Source that is a SUBFOLDER inside a repo
|
|
3197
|
+
is a repo job too (ruling 2 addendum, 2026-09-13, option A) — the whole repo's tracked files
|
|
3198
|
+
freeze, not just the subfolder. When that subfolder is itself gitignored or simply untracked,
|
|
3199
|
+
git tracks nothing under it at all, so the freeze would otherwise silently succeed holding
|
|
3200
|
+
none of the person's own files — refused instead (ruling A, 2026-09-13) as
|
|
3201
|
+
`source-untracked-in-repo`: commit the files, or point Source at a folder outside the repo.
|
|
2967
3202
|
|
|
2968
3203
|
**A repo source copies only what git tracks** (D1 rework, hamr's ruling verbatim: "copy only
|
|
2969
3204
|
what git tracks" — closes F164/F165). Files are enumerated with `git ls-files --stage` in the
|
|
@@ -3010,8 +3245,12 @@ residual, not this fix's job to close.
|
|
|
3010
3245
|
|
|
3011
3246
|
**Every refusal is a named `{stop, code}`, never a throw and never silent:** `source-
|
|
3012
3247
|
unreadable`, `source-symlink`, `source-env-file`, `source-file-oversize`,
|
|
3013
|
-
`source-nested-repo`, `source-is-linked-worktree`, `source-
|
|
3014
|
-
`source-
|
|
3248
|
+
`source-nested-repo`, `source-is-linked-worktree`, `source-untracked-in-repo`,
|
|
3249
|
+
`source-seed-incomplete`, `source-git-failed`, `source-not-text`, `source-fetch-failed`,
|
|
3250
|
+
`source-fetch-timeout`, `source-fetch-oversize`, `source-carries-secret`,
|
|
3251
|
+
`source-changed-after-scan` (a file's bytes changed on disk between the scan and the freeze
|
|
3252
|
+
— item 34 L1's TOCTOU close, re-verified by hash rather than cached, so a repo source's
|
|
3253
|
+
freeze loop never trusts a buffer read a moment earlier), `into-exists`,
|
|
3015
3254
|
`destination-invalid`, `destination-not-absolute`, `destination-not-directory`,
|
|
3016
3255
|
`destination-not-writable`, `destination-contained`, `destination-parent-missing`,
|
|
3017
3256
|
`destination-parent-unwritable`, `destination-exists`, `destination-output-missing`,
|
|
@@ -3019,6 +3258,37 @@ unreadable`, `source-symlink`, `source-env-file`, `source-file-oversize`,
|
|
|
3019
3258
|
refusal never throws, never overwrites a person's file, and never lands a destination inside
|
|
3020
3259
|
the run's own scratch tree.
|
|
3021
3260
|
|
|
3261
|
+
**`prepareSource` copies ONLY git-tracked files** (hamr's ruling, unchanged) — a JS/TS repo's
|
|
3262
|
+
prepared copy therefore never carries `node_modules`, and every close stage that needs a tool
|
|
3263
|
+
(`tsc`, a test runner) would instrument-stop with nothing telling the person why. bareloop
|
|
3264
|
+
NEVER runs an install itself — it only ever touches gated primitives, never a shell-out to
|
|
3265
|
+
`npm`/`pnpm`/`yarn`/`bun` on the person's behalf. `missingDependencies` is exported ($0,
|
|
3266
|
+
JS/TS only for now — other languages return `null`, M3b's job): it takes `(treeDir,
|
|
3267
|
+
sourceSubdir?)` and walks from `sourceSubdir` up to `treeDir` (the same nearest-manifest rule
|
|
3268
|
+
`detectLanguage` uses, done independently to avoid a `source.js` ⇄ `detectlang.js` import
|
|
3269
|
+
cycle), and when the nearest `package.json` names a non-empty `dependencies`/`devDependencies`
|
|
3270
|
+
with no sibling `node_modules`, it returns `{manager, command, reason}` — the exact install
|
|
3271
|
+
line (`npm ci`/`pnpm install --frozen-lockfile`/`yarn install --frozen-lockfile`/`bun install
|
|
3272
|
+
--frozen-lockfile` by lockfile, else `npm install`), `cd`-prefixed for a subfolder job. An
|
|
3273
|
+
invalid `package.json`, no manifest found, or `node_modules` already present all return `null`
|
|
3274
|
+
— "cannot tell" is never a fabricated gap. `run-interview.mjs` prints the gap and the exact
|
|
3275
|
+
command right after `prepareSource` succeeds, and now PAUSES there (F182 fix, 2026-09-15):
|
|
3276
|
+
"Press Enter once it has finished to check again, or type skip to carry on without it" —
|
|
3277
|
+
Enter re-runs `missingDependencies` on the spot ("packages found — carrying on." or "still
|
|
3278
|
+
missing (…) — try again, or type skip to carry on without it.", looping), `skip` (or end of
|
|
3279
|
+
input, treated the same) carries on unresolved. It re-checks once more at the hand-off: a
|
|
3280
|
+
still-open gap suppresses the "Run it now?" offer entirely (the same shape the unkeyed-shell
|
|
3281
|
+
gate already uses) rather than spending on a run that would only instrument-stop — this is
|
|
3282
|
+
what the pause makes reachable: before the fix the interview fell straight through to the
|
|
3283
|
+
class questions and only ever re-checked once, too late for the offer to ever fire.
|
|
3284
|
+
`run-author.mjs` refuses
|
|
3285
|
+
`source-deps-missing` (a `request-red`, routed through the same `refusalEvents()` channel
|
|
3286
|
+
`language-unsupported` uses) at $0, before the scout and before the provider/key are even
|
|
3287
|
+
read. Once the person installs packages into the copy themselves, `prepareSource` has already
|
|
3288
|
+
hidden `node_modules` from `changedSet` via the copy's own PRIVATE `.git/info/exclude` (F177),
|
|
3289
|
+
so those installed files never read as the worker's own writes — the source repo's tracked
|
|
3290
|
+
`.gitignore` is never touched.
|
|
3291
|
+
|
|
3022
3292
|
**Destination is a DIRECTORY, never a filename** (D3 rework, hamr's ruling, 2026-09-12,
|
|
3023
3293
|
condensed in `docs/product/ITEM33-BUILD.md`). It may already exist and need not be empty; it
|
|
3024
3294
|
may sit inside the source itself (the same-dir case: "i can limit your actions to a certain
|
|
@@ -3046,7 +3316,9 @@ on a green then that would be the place agent allowed to do changes") — it nam
|
|
|
3046
3316
|
FENCE inside the copied repo, the signed `writeScope` field's job (`src/job.js:363`), never a
|
|
3047
3317
|
filesystem drop-off point. It is recorded as declared, never proven by `proveDestination` and
|
|
3048
3318
|
never handed to `copyOut` (`frontDoorFromManifest` returns `null` for a repo manifest).
|
|
3049
|
-
Wiring it into `writeScope`
|
|
3319
|
+
Wiring it into `writeScope` landed at M3, ruling 2 — `scripts/run-interview.mjs` parses its own
|
|
3320
|
+
Destination answer straight into `draft.writeScope` for a repo Source; this door itself still
|
|
3321
|
+
only records what it was told, unvalidated, exactly as before.
|
|
3050
3322
|
|
|
3051
3323
|
**Every non-empty file under `output/` is delivered, each under its OWN dated name, and
|
|
3052
3324
|
nothing is ever overwritten.** A file named `profile.md` LANDS as `profile-2026-09-12.md`
|
|
@@ -3084,6 +3356,22 @@ Not wired yet, named rather than silently absent: `bareloop run` (the exported b
|
|
|
3084
3356
|
33/M5. The intake form that lets a person fill in Source/Destination through the authoring
|
|
3085
3357
|
interview (rather than the CLI above) is item 33/M3, not yet built.
|
|
3086
3358
|
|
|
3359
|
+
**Running an authored spec needs no developer hand-step any more (F185, fixed 2026-09-16).**
|
|
3360
|
+
`scripts/run-u.mjs --spec <path to resolved-spec.json>` is an alternative to `--job <key>`
|
|
3361
|
+
(both, or neither, is a loud $0 refusal): it `validateJob`'s the spec on disk explicitly and
|
|
3362
|
+
early, then finds the ONE `source-*/` sibling directory beside it that carries a
|
|
3363
|
+
`source.json` (`run-interview.mjs` prepares the source door and invokes `run-author.mjs`
|
|
3364
|
+
against the same `--out`, so `resolved-spec.json` and `source-<runid>/{source.json,tree/}`
|
|
3365
|
+
are always siblings) and reads the workdir (`<that dir>/tree`) and seed off that SAME
|
|
3366
|
+
manifest `readSourceManifest` already serves elsewhere — never typed, guessed, or defaulted.
|
|
3367
|
+
No sibling, more than one, a missing seed, or a gone tree each refuse by name. Everything
|
|
3368
|
+
past that resolution — the `--approve <jobSpecHash>` signature, budget/wall ceilings,
|
|
3369
|
+
coldReset, the close-first precheck — is untouched: `--spec` only NAMES the job, exactly as
|
|
3370
|
+
a `--job` row does. `scripts/run-author.mjs`'s "SIGNING PREPARED — NOT SIGNED" screen now
|
|
3371
|
+
prints the ready-to-paste `--spec` command (real path, real `--approve <hash>`, the
|
|
3372
|
+
provider's real env-key per F187) — a person finishing the interview alone can reach a
|
|
3373
|
+
running job with no `jobs/` edit and no JOBS-table row.
|
|
3374
|
+
|
|
3087
3375
|
## Architecture
|
|
3088
3376
|
|
|
3089
3377
|
Three layers. An **outer shell** (dumb, permanent): per-run budget cap via bareguard,
|
|
@@ -3152,6 +3440,18 @@ under it — this is a passthrough, not a jail.
|
|
|
3152
3440
|
denies the gate audit path and every `ARBITER_BOOK_STORES` path under the workdir
|
|
3153
3441
|
(`src/authorscout.js:295`); a plan step's read scope is the workdir alone
|
|
3154
3442
|
(`src/planrun.js:1894`).
|
|
3443
|
+
- **A signed fence can never name `.git` or `node_modules` (F178/F177).** `scopeContained`
|
|
3444
|
+
(`src/validate.js`, design law #1's own function — `validateJob`'s `writeScope` and
|
|
3445
|
+
`validatePlan`'s step `scope`/exit `target`/`path` all go through it) refuses any fence/scope
|
|
3446
|
+
whose normalized prefix has a whole path segment equal to `.git` or `node_modules` — not a
|
|
3447
|
+
look-alike like `.github/**`, `my.git/**`, or `node_modules_util/**`. That alone cannot catch
|
|
3448
|
+
a NESTED `node_modules` reachable through an otherwise-legal fence (a monorepo package scope,
|
|
3449
|
+
or one a worker creates fresh during the run) — bareguard's `fs.deny` is exact
|
|
3450
|
+
prefix-containment only, no glob, so it cannot express "at any depth" even if it were used
|
|
3451
|
+
here. **`fs.deny` is deliberately NOT used for either book**: it blocks ALL fs actions,
|
|
3452
|
+
reads included, and hamr's ruling blocks WRITING only — reading stays allowed. The runtime
|
|
3453
|
+
belt is `tools.denyArgPatterns` (`FORBIDDEN_WRITE_SEGMENT_PATTERN`, `src/planrun.js`): keyed
|
|
3454
|
+
to `write`/`edit` ONLY, matched against the serialized action's `path` field, whole-segment.
|
|
3155
3455
|
|
|
3156
3456
|
**What it does not guarantee:** no network egress control and no container/VM isolation —
|
|
3157
3457
|
this repo has neither built nor documented either. `CLOSE_ENV_DENY`'s deny rule matches
|