Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
2081c0c
feat(runner): run a delegated role through a command-line harness, co…
REPPL Sep 30, 2026
45377da
refactor(runner): drop an unused helper and cut a validator's detail …
REPPL Sep 30, 2026
ae0cdb0
merge: bring main into runner2
REPPL Sep 30, 2026
0e328d9
fix(runner): admit a runner's model by the allowlist alone, after mai…
REPPL Sep 30, 2026
6ce51f2
feat(build): start a routed role through its command-line runner from…
REPPL Sep 30, 2026
45cdd1a
feat(build): route a role through its runner from build and implement…
REPPL Sep 30, 2026
1419266
refactor(build): unexport the loop's advance now that the step drives
REPPL Sep 30, 2026
294f940
docs(specs): record the runner's loop wiring and capture its owed liv…
REPPL Sep 30, 2026
386b1de
fix(runner): refuse a harness binary inside the run's checkout, not o…
REPPL Sep 30, 2026
70d4949
fix(runner): bound the harness-reported model and refuse a harness ot…
REPPL Sep 30, 2026
6a005a2
chore: resolve iss-2609301557141123 and iss-2609301557191251 — harnes…
REPPL Sep 30, 2026
978b849
Merge origin/main into feat/runner-loop
REPPL Sep 30, 2026
ef28cee
fix(runner): admit a harness only the administrator group can write
REPPL Sep 30, 2026
c35d281
docs(brief): the build chapter says where the record names a runner
REPPL Sep 30, 2026
1d6b26e
docs(build): only the claude runner grants the role's tools unasked
REPPL Sep 30, 2026
10079f0
feat(runner): close the runner's spec and ship itd-2609201916056194
REPPL Sep 30, 2026
6c6f97c
fix(runner): pass the delimiter and hermetic-git detectors
REPPL Sep 30, 2026
fe297bf
fix(runner): admit only darwin's admin group, never gid 0
REPPL Sep 30, 2026
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
56 changes: 48 additions & 8 deletions .abcd/development/brief/04-surfaces/34-build.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,15 +206,19 @@ repository abcd manages has one, so a run is managed-only by construction. Each
run directory is created one level at a time and proved real, the state file is
replaced atomically inside an `os.Root`, and the reader decodes strictly,
refusing an unknown field, a schema version it does not know, or a file stored
under a run id it does not name. The state is schema version 7. Version 7
under a run id it does not name. The state is schema version 8. Version 8
added the runner's record (itd-2609201916056194): the run's `fallbacks`, one
receipt per role a routed runner did not run, and the `route` a verified receipt
or a validator's recorded return names when a runner ran its agent. Version 7
added the landing (a lane's `landing`, the implementers' `receipts` it verified
with the model each runner reported, and the captures its receipts declared
fixed, `resolves`) and the run's captured `transcripts`. Version 6
added the fix-round cap (ruling DR1): the pace's `fix_rounds` and a lane's
`hand_back`. Version 5 added the validate stage's record (a lane's
`validation`). Each earlier version is the next one's strict subset, read as a
run that predates the addition (a version-5 run runs on the bundled cap) and
written back at version 7 by its next mutation; an earlier version carrying what
run that predates the addition (a version-5 run runs on the bundled cap, a
version-7 run is one the host ran every agent of) and
written back at version 8 by its next mutation; an earlier version carrying what
only a later one writes is refused. Version 4
renamed the lane's stage (BU1, iss-2609291313276243): a lane's and a record
line's `step` became `stage`, so "step" names only the spec's steps (`spec_step`,
Expand Down Expand Up @@ -289,9 +293,41 @@ reported complete and closes no window.
A stage whose body this build does not carry is refused naming the stage, the
lane and the spec piece that delivers it, and the run is unchanged, ready to
resume in a build that carries it. This build carries every stage of the
sequence. The process driver (piece 3) is the same loop
called by a process instead of a host, starting the named agent through the
runner and handing its receipt back.
sequence.

**The runner** (piece 3, the process driver's loop half, and itd-2609201916056194).
A role's route is `roles.<role>.runner` in the layered configuration, the
repository's or the machine's: `host`, the default, or a runner the machine
enables under `runner.<name>` (`claude`, `opencode`), with an optional model
route admitted against its provider's allowlist. The build verb reads the
configuration before it creates a run, and the step verb before each stage; a
fault, a model route off the allowlist included, is refused at the `runner`
stage before anything is created or launched, and its diagnostics (a role no
agent answers to) go to stderr. When a stage hands the lane to a role that is
routed to a runner, the step verb starts the runner itself, outside the run's
lock, in the lane's worktree, with the brief and the receipt path the host would
be handed, the claude runner with the role's tools granted without a prompt and
opencode under the permissions its own configuration sets; the runner's
transcript lands in abcd's history store, keyed on the repository's root
commit, and its receipt is handed back through the same receipt verb and
verified by the stage's own verifier, so a verified one completes the stage in
the same call. The verified receipt, or the validator's recorded return, names
the route that ran it (asked, ran, the model the runner reported), which is the
only field a runner-run review's record differs in from a host-run one's; the
record's receipt or verdict line names the runner that ran it. A runner that is absent, refuses, fails, runs past
its time, answers unparsably or writes a receipt the verifier refuses leaves the
lane awaiting: the call records one fallback receipt (the role, the runner asked
for, the reason and the route that runs it) in the state and the record, and
hands the host the await as the host-driven step does, naming the fallback. A
role left unset is the host's, and the call is the host-driven step byte for
byte. A step that re-tells an await starts nothing. The claude runner runs in
print mode with the bare flag, so the repository's hooks, plugins and
configured servers do not run; opencode runs in run mode with `--pure`. An
interrupt or a termination kills the runner's process group. The status and
record verbs count the fallbacks per runner and per role. A host session
always drives this: the no-host path, where a host-routed role goes to the
machine's `runner.fallback_host`, is the process driver's reversal of the
host-delegated boundary, and waits on the ADR decision 6 of the intent owes.

## The lane

Expand Down Expand Up @@ -488,8 +524,10 @@ until the last step.
verb reads a run's state back as its record: every lane with its spec step,
branch and heads, the implementers' receipts the loop verified with the model
each runner reported (as reported; the binary cannot verify it), every verdict
the loop recorded, round by round, the captures the lane fixed, its pull request
and what its landing did, the run's pending steps, the transcripts captured and
the loop recorded, round by round, the route that ran a receipt's or a return's
agent when a runner ran it, the captures the lane fixed, its pull request
and what its landing did, the run's pending steps, every fallback with the
count per runner and per role, the transcripts captured and
the record's lines, in text and JSON. On a complete run, the record verb
captures each transcript it is named into the history store as the history
verb's capture of one path does, one capture per path, and records it in the
Expand Down Expand Up @@ -519,6 +557,8 @@ the remedy as fields.
- The shared run state and the claim the peers check reads:
[`27-implement.md`](27-implement.md).
- The pick: itd-2609211116005482 and its design record, spc-2609212015048113.
- The runner a routed role goes through: itd-2609201916056194 and its design
record, spc-2609221533057881 (`internal/core/runner`).
- The plugin surface: `commands/build.md`.

<!-- surface-appendix:begin — generated from the command tree by `go generate ./internal/surface/cli`; never edit by hand -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,11 @@ _None open._

## Audit Notes

_Empty. Populated by intent-auditor when intent moves to shipped/._
- 2026-09-30 — Closed on the fake-harness tests under the person's ruling RN1 (2026-09-30: "(a) SAME RULE, FINISH ON FAKES: the runner may close on the fake-harness tests with the three live checks (iss-2609301519558538) recorded as owed."). The acceptance criteria are proven against fake harnesses on a pinned PATH (the opencode review-route criterion structurally, and the no-host fallback path in the core only, since its surface waits on the ADR decision 6 owes); no real claude or opencode binary has run a role. The three live checks are owed and not yet run: iss-2609301519558538 tracks them and stays open.

<!-- abcd-review: OWED receipt=rcp-e5eee57c3e2a -->
Fidelity review OWED (receipt rcp-e5eee57c3e2a).
<!-- abcd-review-end receipt=rcp-e5eee57c3e2a -->

## Grounds

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,36 @@ The runner interface is the same shape the validator stage already defines for r
| 6 bare flag | scope 2 |
| 7 receipts differ only in route | scope 1, 2 |
| 8 security review | scope 6 |

## Progress

The spec stays open: the live proof is owed (iss-2609301519558538) and no
ruling yet lets the intent close on fake harnesses.

- **Landed: scopes 1 to 5 in the core** (`internal/core/runner`): the runner
interface and the one dispatcher, the claude adapter (print mode, `--bare`,
stream-json, `--permission-mode dontAsk`, the role's tools allowed) and the
opencode adapter (run mode, JSON events, `--pure`), the route through the
layered resolver, the fallback with one receipt writer and `Tally`, and the
allowlist admitted at the read and again before a launch; a model a
provider lists is admitted by its allowlist alone and refused only by a
configured `oracle.denylist` entry (adr-2609300107513982).
- **Landed: the loop wiring** (spc-2609202134338445 piece 3): `implement step`
drives through `loop.Drive`, which starts a routed role through the
dispatcher with the brief and receipt path the host would get, validates
its answer with the stage's own receipt verifier, stores its transcript in
abcd's history store, and stamps the verified receipt or recorded return
with the route that ran it (criteria 1 and 7, structurally); an unset role
leaves the step and the state byte-identical (criterion 2); a fallback is
recorded in the state's `fallbacks` and the record (criterion 3) and counted
per runner and per role by `implement status` and `implement record`
(criterion 4); `build` and `step` refuse a runner configuration fault, a
model off its allowlist included, before anything is created or launched
(criterion 5). The bare flag is asserted in the launch (criterion 6).
- **Not built: the no-host path at the surface.** `runner.fallback_host`
works in the core, but a surface that runs without a host session is the
process driver's reversal of the host-delegated boundary, which waits on
itd-2609201916151817's decision-6 ADR.
- **Owed: the live proof** of criterion 7 and the phase-1 unverified points
(iss-2609301519558538), and **criterion 8**, the security review, which
the lane's report lists point by point for the reviewer.
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,15 @@ spec stays open until the last lane closes it.
every commit on the lane's branch past its base, a passing definition of
done's output and the report, each inside the lane's directory, or a refusal
naming every gap.
- **Seam left, not built: piece 3**, the process driver. It waits on the runner
intent (itd-2609201916056194) and calls the same `Advance` and `Receipt`.
- **Landed in part (lane runner2): piece 3**, the process driver. `implement
step` drives through `loop.Drive`: when a stage hands the lane to a role
that `roles.<role>.runner` routes to a command-line runner
(itd-2609201916056194), the loop starts the agent itself through the runner
and hands its receipt back through `Receipt`, and the run record names the
runner that ran it (criterion 9); a role left on the host is handed to the
host exactly as before. Not built: the loop driving itself with no host
session, which is decision 6's reversal of the host-delegated boundary and
waits on the ADR that decision owes.
- **Landed (lane fidelityOnce): piece 8**, the validators with the itd-58
verdict invariant. The validate stage hands the lane's head to a fresh
ruthless reviewer and a fresh security reviewer, one at a time, and on the
Expand Down Expand Up @@ -214,5 +221,5 @@ spec stays open until the last lane closes it.
receipt carries `handback: {kind, reason, home}`, which the loop reads at the
receipt, before the validators, discarding the lane's worktree and branch
and ending the lane handed back.
- **Remaining: 11** (`--auto-plan` with its ADR), and piece 3 (the process
driver, on the runner). `--auto-plan` is not a flag yet.
- **Remaining: 11** (`--auto-plan` with its ADR), and piece 3's no-host
driving, behind the same ADR. `--auto-plan` is not a flag yet.
2 changes: 2 additions & 0 deletions .abcd/work/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2623,3 +2623,5 @@ together (the script's header says why there is no escape hatch).
- 2026-09-30 — Correcting three points of the entry above after its review (lane fix-drainOwnRule of autonomous run A). The drain-rule offer of `ahoy install` is asked only of a person at a terminal, the itd-131 precedent the git identity question set, rather than behind a named opt-in flag: off a terminal neither its category question nor the offer is asked, so a piped answer stream keeps the order it had before the offer existed and a scripted yes never writes the record, and the run reports `drain_rule.offered` under `optional_skipped` naming the terminal as the way to be asked. The terminal gate was chosen over a `--drain-rule` flag because the record decides what an unattended agent may do, which a scripted answer is not a person's yes to, and a flag would hide the offer from the person at a terminal it is for. A checkout holding no release tag (a shallow clone fetches none) marks the anchor unknown rather than reading every deferral as lapsed: every record carrying a deferral is handed back as `deferred`, naming the missing tags and `git fetch --tags`, which keeps the rest of the dry run readable where refusing the whole plan would not. The rule's reader refuses, as malformed, a record that states any frontmatter key twice (not only a `drain_` key) and one whose frontmatter `id` disagrees with its file name, and reads each record through the capped trust-boundary reader, so a record that is a symlink or past the size cap refuses; every refusal of the rule exits 2 on the dry run as on the bare verb.
- 2026-09-30 — Six entries above appear twice, verbatim: the five dated 2026-09-29 from "Two itd-111 follow-ups from its fidelity audit" to "Ruling J13", and the 2026-09-30 entry beginning "The 2026-09-29 itd111Follow entry above". Two histories carried them in opposite order relative to the 2026-09-30 BU1/BT1 entry (main below them, the implement-loop lanes above them), so joining them in integration 24b-3 kept main's order and repeated the six after BU1 in the lanes' order, the one merge result the append-only gate admits (every parent's lines kept in their order, DA002; no line beyond what the merge base held plus what each side added, DA003). Each pair is one decision recorded once: the first copy is the record, and the second repeats it (recorded by the integration lane of autonomous run A).
- 2026-09-30 — An older site interface-string file keeps building: `abcd site setup` and `abcd site build` add to `site-src/ui.json` each label the allowlist declares and the file does not carry, with abcd's default words, name each on stderr, and change nothing else in it (the product thinker's ruling TG1 of 2026-09-30, relayed verbatim: "(b) ABCD ADDS THE MISSING LABELS: on the next site setup or site build, abcd adds only the missing required labels (with the default words); the project's own wording elsewhere in ui.json is never changed. No failure, no manual step; both intents stay impact: additive."). It is the one exception to "a file the repository owns once it exists is kept", recorded as adr-2609301720596683, which refines adr-47 and leaves decision 2's closed allowlist untouched: a blank declared label and an unknown key are still refused, and the site gate's own render never completes the file. itd-2609212103568351 and itd-2609212103572513 keep `impact: additive` (lane tgLabels of autonomous run A).
- 2026-09-30 — A command-line runner admits a harness reached through a directory, or a binary, that is group-writable only when the group is the system administrator group (gid 0 anywhere, gid 80 `admin` on darwin) and other cannot write it; other-writable stays refused whatever the group, and every other group stays refused (lane runner2Land of autonomous run A, `internal/core/runner/proc.go` `adminGroupWritableOnly`). Reason: the runner2 re-verification (reverify-runner2) found that on a Homebrew Mac `/opt/homebrew/bin` is `drwxrwsr-x` group admin, so a harness installed there was refused with the `chmod go-w` message; members of the administrator group can already act as root, so that write grants them nothing new, and asking a person to strip Homebrew's own directory mode would break Homebrew.
- 2026-09-30 — Narrowing the administrator-group exception in the entry above (lane fix-runnerAdmin of autonomous run A, `internal/core/runner/proc.go` `adminGroupWritableOnly`): a command-line runner admits a harness binary, or a directory it is reached through, that is group-writable (never other-writable) only on darwin and only when the group is gid 80 `admin`; gid 0 is refused on every OS, and gid 80 is refused off darwin. Reason: the entry above granted gid 0 on the premise that members of the administrator group can already act as root, which holds for darwin's admin group (its members may sudo by default) but not for Linux's gid 0 root group nor darwin's gid 0 wheel, whose membership does not by itself let someone act as root (the runner2Land review note). The Homebrew `/opt/homebrew/bin` case the exception exists for is group admin, so it stays admitted.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
schema_version: 1
id: "iss-2609301519558538"
slug: "deferred-owed-to-a-person-the-live-proof-of-the-command-line"
severity: "minor"
category: "future-work-seed"
source: "agent-finding"
found_during: "autonomous run A resumed 2026-09-25"
origin: researcher-authored
production_mode: hand-written
found_at: "internal/core/runner"
remedy: "Waits on a ruling: either the product thinker rules the runner may close on fakes with the live proof recorded as owed (as ruling L does for the three paid adapters), and spec close runs with Delivers, or a person runs checks (1) to (3) with their own credential and records each outcome on this record before the close."
---

Deferred, owed to a person: the live proof of the command-line runner (itd-2609201916056194). Phase 1 and 2 prove every criterion on fake harness binaries only; no ruling admits a live check of the runner (ruling L names the site setup, the API adapter and the decision adapter; H8 lists the three paid-service checks and itd-6's RepoPrompt run), so the intent cannot close. What a person must run: (1) the intent's first proof, criterion 7 live: a ruthless review of a real lane routed roles.ruthless-reviewer.runner=opencode, from a lane the host drives, its return recorded beside a host-run review's and differing only in the route; (2) the claude runner under --bare with --permission-mode dontAsk and --allowedTools, confirming a nested claude does not refuse under an inherited CLAUDECODE, and noting that the headless page (read 2026-09-30) says bare mode never reads OAuth or the keychain, so the claude runner needs ANTHROPIC_API_KEY, a paid API key rather than the person's subscription; (3) opencode run --format json --pure: the event shape (the CLI page does not document it; the adapter assumes step_start/text/step_finish with a sessionID and a part) and whether run mode prompts for a permission, which the timeout bounds and records as a fallback.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
schema_version: 1
id: "iss-2609301557141123"
slug: "the-claude-runner-takes-the-model-its-harness-s-init-event"
severity: "minor"
category: "bug"
source: "impl-review"
found_during: "autonomous run 2026-09-23"
origin: researcher-authored
production_mode: hand-written
found_at: "internal/core/runner/claude.go"
remedy: "Bound and shape the reported model as sessionRe bounds a session id (a plain id of at most 128 characters, a bracketed context suffix admitted), and refuse any other as an unparsable answer, so the route falls back and the fallback is recorded rather than the model written into the state."
resolution: "The init event's model is held to modelRe (bounded, plain, a bracketed suffix admitted); any other is an unparsable answer, so the route falls back and is recorded and the model never reaches state.json."
impact: fix
resolved_by:
commit: "70d49494f"
---

The claude runner takes the model its harness's init event reports unbounded and unshaped into the answer (internal/core/runner/claude.go parseClaude), and the loop writes it into state.json through the route record; a model string over 4 MiB pushes state.json past its read bound, so every later read refuses and the run is bricked, and control bytes travel into the record verbatim.

## Grounds

- pursued: a harness reporting a 5 MiB model or one carrying control bytes falls back with an unparsable reason and the run's state stays readable with no receipt carrying it; a real id such as claude-opus-4-6[1m] refused, or a state.json holding the odd model, would show it wrong
Loading
Loading