Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,30 @@ feature lands, **patch** = docs, fixes, scaffolding.

## [Unreleased]

## [0.5.1] — 2026-07-24

### Added
- **Layer R wired into the plan-v1 flow** (F50). The within-run ratchet (`layerRoot`) now
engages in `runPlan` — one root per EXECUTE step (red-set = the exit evaluator's own gap)
AND in the outer close-fix loop (red-set = the close's own `gapKeep`) — not only on the
legacy `steps[]` path. So a plan-shape job can emit `root-injected` and the pre-registered
ON-vs-OFF default-flip read becomes possible on the accepted surface. The write-tee is wired
so same-path target rewrites are visible to the detector. Still OFF by default (`layerRoot:
false`, F41). Excluded on native/clipipe (no `onToolResult` seam — F48 fallback surface).

### Fixed
- **F49 (security-hardening): the agent-authored `artifact-written` regex can no longer hang
the exit evaluator.** `plan.js` rejects nested-quantifier ReDoS patterns (`(a+)+`, `(\d*)*`,
`(x+){1,}`, and redundant-wrapper forms like `((a+))+` / `(?:(a+))*` / `(((a+)))+`) at the
validation gate, before any tokens burn. The wrapper class was a review-caught false negative
in the first (flat-only) scan — each measured to hang `RegExp.test` >8s on ~29 chars — closed
by propagating an inner repeat up through the enclosing group; the change is MONOTONIC (it only
ever adds rejections, the fail-safe direction, and cannot introduce a new false negative). LOW
self-DoS, no arbiter compromise; input-bounding was rejected as theater (a 33-char body hangs)
and a JS regex timeout as disproportionate (worker/RE2 dep).
- **F50: `runJob` no longer silently ignores `layerRoot` for plan-shape jobs** (was accepted
and dropped — an advertised no-op).

## [0.5.0] — 2026-07-23

### Added
Expand Down
23 changes: 14 additions & 9 deletions bareloop.context.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ middle writes; `validatePlan` gates it against the SIGNED job spec before tokens
| `steps[].tools` | non-empty unique subset of the SPEC ceiling | a verb beyond the ceiling reds `verb-escape` with the verb as structured data (overreach, distinct from the operator-side `request-red`); `run` is `verb-escape` at every layer |
| `steps[].rounds` | int 1..shell cap (default 40) | the step's per-attempt tool-round bound (the Gate's `maxTurns` natively) |
| `steps[].target` | path inside the fence | v1.18 deliverable; REQUIRED on write-granted steps |
| `steps[].exit` | 1..2 items (`MAX_EXITS_PER_STEP`), ALL must pass (AND-only, no OR/NOT) | closed menu (`EXIT_TYPES`): `artifact-written(path, pattern?)` · `tree-changed(scope)` · `json-valid(path)` · `check-passes(name)`. `check-passes` must name a SIGNED check (`check-unknown` red names the signed menu); on a write-granted step it must be paired with `tree-changed` (`exit-illegal` — the seed tree is green, a lone check would pass untouched, F17/F46). Exits verify FORM, not truth — progress gates; the operator's close stays the one arbiter |
| `steps[].exit` | 1..2 items (`MAX_EXITS_PER_STEP`), ALL must pass (AND-only, no OR/NOT) | closed menu (`EXIT_TYPES`): `artifact-written(path, pattern?)` · `tree-changed(scope)` · `json-valid(path)` · `check-passes(name)`. `check-passes` must name a SIGNED check (`check-unknown` red names the signed menu); on a write-granted step it must be paired with `tree-changed` (`exit-illegal` — the seed tree is green, a lone check would pass untouched, F17/F46). `artifact-written.pattern` must compile AND survive a ReDoS shape check — an unbounded quantifier over a group that itself repeats unboundedly (`(a+)+`, `(\d*)*`, even wrapped: `((a+))+`) is an `invalid-value` red (F49, catastrophic-backtracking footgun; rewrite without a repeated group inside a repeat). Exits verify FORM, not truth — progress gates; the operator's close stays the one arbiter |

Red vocabulary (all three validators): `parse-error`, `unknown-field`, `missing-required`,
`invalid-value`, `bounds`, `duplicate-id`, `close-type`, `close-hierarchy`,
Expand Down Expand Up @@ -376,14 +376,19 @@ through `Loop`'s `onToolResult`). So a repeat that never applied is told its anc
missed, and only content actually in the file is ever described as having landed. Spine event `root-injected` carries
`{stage, mode, streak, paths, redSetSize}` — counts and paths only, NEVER content (the
spine is append-only). State dies with the run: this is within-run scratch, not
across-run memory. `layerRoot: true` (on `interpret` and `runJob`) is the ON/experimental
arm; the default is OFF. Status (F41): the ratchet is MEASURED inert on every current
job — two frozen probes + an archive sweep read 0 fixated pairs in 14 (the F21 repetition
disease was a broken-loop symptom, since cured). Because ON has therefore never won its
own A/B, it ships **OFF by default (decided 2026-07-21)** — armed and correct, but not
default-enabled until a stuck job (Layer 2, or a manufactured-fixation probe) shows ON
beats OFF. Flip the default to `true` the day that evidence lands; a `root-injected` event
on your spine (when you pass `layerRoot: true`) is a signal worth reading, not noise.
across-run memory. `layerRoot: true` is the ON/experimental arm; the default is OFF. It is
wired on BOTH surfaces (F50): the legacy `steps[]` path (`interpret`) and the accepted
plan-v1 flow (`runPlan`, one root per EXECUTE step plus one in the outer close-fix loop). On
the plan flow the per-step red-set is the exit evaluator's OWN gap (the whole normalized
complaint) rather than a single close's gapKeep, while the fix loop uses the close's own
gapKeep (raw close output); the write-tee makes same-path target rewrites visible to the
detector. It is NOT wired on the native/clipipe worker (no `onToolResult` seam — F48 fallback). Status (F41):
the ratchet is MEASURED inert on every current job — two frozen probes + an archive sweep
read 0 fixated pairs in 14 (the F21 repetition disease was a broken-loop symptom, since
cured). Because ON has therefore never won its own A/B, it ships **OFF by default (decided
2026-07-21)** — armed and correct, but not default-enabled until a stuck job shows ON beats
OFF. Flip the default to `true` the day that evidence lands; a `root-injected` event on your
spine (when you pass `layerRoot: true`) is a signal worth reading, not noise.

### `runJob(spec, { approvals, workdir, provider, nativeProvider?, emit, target?, capRuns?, shellCapUsd?, closeTimeoutMs?, execCmd?, layerRoot? })` → outcome — `src/run.js`

Expand Down
30 changes: 21 additions & 9 deletions docs/01-product/LAYERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,15 +145,27 @@ default-enabled (`layerRoot: false`; pass `true` for the ON/experimental arm). I
read (repetition drop, ON vs OFF) DEFERS to the first run whose spine records
`root-injected`.

> **⚠ Layer 2 TODO — decide the Layer R default.** Layer 2's narrow micro-wheel steps are
> the expected pressure point that finally produces natural fixation (a stuck run). The
> day a Layer 2 job records `root-injected`, run the pre-registered ON-vs-OFF acceptance
> read on it. **That result decides whether the `layerRoot` default flips to `true`
> (ON helps → keep it on) or stays `false` (no lift → keep it off).** Until then the
> default is provisional, not settled. (A cheaper alternative that does NOT need Layer 2:
> a manufactured-fixation probe — force a real worker to repeat and measure whether the
> note breaks the loop; caveat F41 — strong models resist fixating, so the probe may
> struggle to produce its own precondition honestly.)
**Wired into the plan-v1 flow (2026-07-23, F50).** Until then the ratchet was wired only
into the legacy `steps[]` path (`interpret.js`); the accepted plan flow silently ignored
`layerRoot`, so it could never emit `root-injected`. Now `runPlan` engages one root per
EXECUTE step (each micro-wheel is the Layer-1 atom; red-set = the exit evaluator's own gap,
`gapKeep '\S'`, the whole normalized complaint — since a check's `^`-anchored gapKeep does
not survive the exit wrapper `check "x" red: …`) AND one in the outer close-fix loop (red-set
= the close's own `gapKeep`, the raw close output where the anchor works). The write-tee is
wired so same-path target rewrites are visible to the detector (the cumulative audit dedups
by path and cannot see them alone). Excluded on native/clipipe (no `onToolResult` seam; F48
fallback surface, not the experiment surface).

> **⚠ Layer 2/3 TODO — decide the Layer R default.** The wiring now EXISTS on the accepted
> surface (F50); the experiment is possible but not yet run. Layer 2's narrow micro-wheel
> steps are the expected pressure point that finally produces natural fixation (a stuck
> run). The day a real plan-flow job records `root-injected`, run the pre-registered
> ON-vs-OFF acceptance read on it. **That result decides whether the `layerRoot` default
> flips to `true` (ON helps → keep it on) or stays `false` (no lift → keep it off).** Until
> then the default is provisional, not settled. (A cheaper alternative that does NOT need a
> natural stuck run: a manufactured-fixation probe — force a real worker to repeat and
> measure whether the note breaks the loop; caveat F41 — strong models resist fixating, so
> the probe may struggle to produce its own precondition honestly.)

### Layer 2 — micro-wheels (the road)
The workflow becomes a **sequence of small wheels**, each with one goal and only the verbs
Expand Down
71 changes: 71 additions & 0 deletions docs/01-product/PRD.md
Original file line number Diff line number Diff line change
Expand Up @@ -1313,3 +1313,74 @@ plan (the plan-as-executed spine already holds the checkpoint; not yet wired), a
**separate clipipe-subscription battery** to validate the native surface (module 4d) on its
own baseline. Process note (F47): run 1 fired without the frozen pre-fire health probe (4
casualties), and a single-message liveness probe is not a sustained-load throughput check.

## Addendum v1.23 — 2026-07-23 (the two workflow shapes: plan-v1 is the target for ALL verdicts; legacy `steps[]` sunsets at the verdict-classes rung, not at Layer 2 landing — F49/F50, hamr)

**Corrects v1.22.** v1.22 recorded "`steps[]` and config-v1 sunset on landing." That was
premature for `steps[]` (config-v1 is genuinely dead — F22). Layer 2 landed for the **green
verdict only**: plan-v1 v1 admits `green` and LOCKS `soft-green`/`hitl` (`LOCKED_VERDICTS` —
declaring one is a `request-red`). So the legacy `steps[]` path could NOT retire at landing —
it uniquely hosts the two locked verdicts, and one of them (`hitl`) is the only *working*
human-verdict path (the draft-PR flow, `run.js` `openDraftPr`).

**The mapping today (honest, no papering over):**

| verdict | shape it runs on | status |
|---|---|---|
| `green` | **plan-v1** (`planrun.js`, agent-authored plan) | shipped v0.5.0, ACCEPTED (F47) |
| `soft-green` (rubric) | legacy `steps[]` (`interpret.js`, operator-authored) | locked in plan-v1 |
| `hitl` (draft-PR) | legacy `steps[]` (`interpret.js`) | locked in plan-v1 |

**Is the split principled? No — it is a build-order artifact, not a design.** There is no
reason `soft-green`/`hitl` "belong" on the old shape; plan-v1 is the go-forward home for ALL
three verdicts. The new way is better on the product's own thesis — **the agent authors the
workflow** (validator-gated before tokens, one wallet, in-run self-checks F46/F47, and now
Layer R F50). Legacy is operator-HARDCODED steps: a human writes the workflow, the exact
thing bareloop exists to not require ("automate this — I don't know the best workflow"). If
you already know the steps you do not need the emergence; that is relayfact's job, not
bareloop's (§8). **Nothing about legacy is better for the product's goals.** The one real
reason it stays is pragmatic sequencing: its hitl middle is working code today, and hitl is
not needed until the non-code-jobs goal (§8 later-goal) — keeping it is cheaper than
rebuilding hitl before it is wanted, NOT a reason to preserve the split.

**Sunset criteria (the honest gate — supersedes v1.22's "on landing"):** legacy `steps[]`
retires when plan-v1 **admits AND implements** both locked verdicts:
1. **`hitl` in the plan flow** — the draft-PR / human-verdict path ported onto `planrun.js`
(the door to non-code jobs: resume/LinkedIn, §8 later-goal).
2. **`soft-green` in the plan flow** — a rubric close, WITH its RSI caveat first: a rubric
close is self-consistency in disguise and needs a judged-floor analog before it can gate
anything (RSI-LEARNINGS / v1.21).

That is the **verdict-classes rung**, sequenced after Layer 3 (the soft-green/hitl ladder
follows the deterministic-close infra, §8/§10). Retirement is a **rewrite-deletion** (the
`run.js` legacy for-loop, `interpret.js`'s legacy dispatch, `job.js`'s `steps[]` validation,
`interpret.test.js`) done in one deliberate gated pass — never a copy (graduation is a
rewrite). It is a **product-shape decision parked for hamr's explicit go**, not shipped
unilaterally.

**This session (F49/F50), landing in v0.5.1.** Layer R was wired onto plan-v1 — one root per
EXECUTE step (red-set = the exit evaluator's own gap) and one in the outer close-fix loop
(red-set = the close's own `gapKeep`) — moving the **last capability that was stranded on
legacy** onto the go-forward surface (so it does not retire with legacy). A plan-flow job can
now emit `root-injected`, making the pre-registered ON-vs-OFF default-flip read (v1.20)
possible on the accepted surface; default stays OFF until that evidence lands (F41). F49
hardened the agent-authored `artifact-written` exit regex — a nested-quantifier ReDoS reject
at the validation gate (LOW self-DoS, no arbiter compromise; input-bounding refuted as
theater by a 33-char-body hang), extended after a follow-up review that caught a false
negative in the flat-only scan (redundant-wrapper forms like `((a+))+` slipped through and
still hang >8s) — closed monotonically by propagating an inner repeat up through the
wrapping group, the fail-safe direction that only ever adds rejections. After v0.5.1, the ONLY legacy-unique surface left is the two
locked verdicts above — so the sunset gate is exactly (1)+(2).

**Parked (medium review #2, 2026-07-24): the Layer R tee wiring is duplicated across
`interpret.js` (legacy) and `planrun.js` (plan-v1)** — `fileHash` / `teeingTranslator` /
the deny-discard `policy` wrapper / `onToolOutcome` (the Finding 6/7 settle logic), ~60
lines, currently faithful (no live defect). NOT extracted into a shared helper, deliberately:
legacy is a scheduled **rewrite-deletion** at the verdict-classes rung (above), so one of the
two copies is *deleted*, not refactored — extracting shared arbiter-adjacent wiring out of code
slated for deletion is negative-value, and reconciling the two shapes (interpret gates on
`mode === 'tools'`; planrun is tool-only) risks reintroducing the blind-instrument class the tee
guards against (a future write-class verb bypassing the tee). Accepted cost: a **drift window**
until legacy retires — any Finding-6/7 change or new write-class verb must be applied to BOTH
files. The clean resolution is the legacy deletion itself; revisit only if legacy outlives
Layer 3 or the tee logic starts changing often.
Loading