Skip to content

senior-dev: a coding agent built into codeaf takes a whole task, in the folder itself - #1488

Open
ZeroPoint95 wants to merge 165 commits into
devfrom
zeropoint95/senior-dev-harness
Open

ZeroPoint95 wants to merge 165 commits into
devfrom
zeropoint95/senior-dev-harness

Conversation

@ZeroPoint95

@ZeroPoint95 ZeroPoint95 commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Draft: not ready to merge. The test of the chat's routing against a real model has not been run yet.

How /senior-dev works

  • It ships inside codeaf. senior-dev (formerly swe-pro-go) is compiled into the codeaf binary, so there is nothing to install and no separate key to set. That is why the diff is so large: about 69k of the ~99k added lines are internal/seniordev, and about 37k are tests. Its solution process is unchanged; codeaf hosts it and meters it.
  • The person or the chat can start it.
    • The person types /senior-dev <brief>.
    • The chat proposes a task with via: "senior-dev". The chat prefers senior-dev for complex, multi-part coding work, like an issue in a mature codebase, and nearly always uses it when asked by name.
    • Either way it goes through the usual approval card. From a shell, codeaf senior-dev <brief> runs it without the TUI.
  • It is a task in the conversation's task sidebar. It appears there with a [senior-dev] badge ([sd] when narrow), beside ordinary /tasks.
    • The row shows its current step and spend.
    • Opening it shows the actions senior-dev takes, grouped by the step of its process each serves: SETUP, SPEC, EXPLORE, PIN, CHECKLIST, IMPLEMENT, SUBMIT, VERIFY and FINISH. It does not show a chat with a model.
    • ctrl+y shows the raw model calls.
  • It runs once, from a single brief. One brief goes in and it works alone to the end: done, not finished, or stopped.
    • It reads no messages while it runs and asks nothing; the person can watch it and stop it.
    • A run is never resumed. Instead, its ending goes to the chat, which acts on it: checks the work, fixes a small gap on the branch, or hands it back to senior-dev with a sharper brief. It sends it back at most twice on its own, and never after a dollar or time limit without the person's word.
    • Every attempt on the same work stays on one branch.
  • It works in git and non-git projects, in the folder itself. No copy is made.
    • In a git repo: codeaf switches the checkout to a new branch, task/<title>-<id>. The person's own branch never moves. At the end, the work is committed on that branch and the branch stays checked out. A run that changed nothing switches back and deletes its branch. A checkout with uncommitted changes is refused before the card.
    • In a plain folder: it works in place and commits nothing. senior-dev itself uses git only if it is there, so a folder without git history can never end a run with "not a git repository".
    • Either way: one run per folder, and while it runs, codeaf's own tools and tasks won't write there. The home folder is never used.
  • Money and time are codeaf's. senior-dev holds no key: every model call goes through a loopback model API in codeaf.
    • Every call is priced into the conversation's total, the spend limit, the machine ledger, /cost and home.
    • The run's dollar and time ceilings come from the conversation.
    • Its wall time runs from the hand-off to the moment its process exits.

Added

  • The /senior-dev command, the via field on propose_task, and codeaf senior-dev in the shell.
  • internal/seniordev (the program), internal/delegate (the protocol a built-in program uses to talk to codeaf; spec in docs/design/delegate/PROTOCOL.md), and internal/run/delegateworker.go (the host).
  • Program badges: any program's tasks wear its name ([senior-dev]) wherever a task is named. Ordinary tasks wear none.
  • The actions view of a program's task page, and ctrl+y for the raw calls.
  • Manual pages internal/manual/chat/senior-dev.md and delegates.md.
  • scripts/clean-run.sh (the make clean-run target), which opens bin/codeaf on a fresh state root.

Changed

  • The chat's prompt tells it to hand complex, multi-part coding work to a built-in program. If the person named senior-dev and the proposal leaves out via, it is sent back once.
  • When the person asked for a program, the trivial-ask floor no longer refuses the proposal. A commit, undo or revert still stays in the chat.
  • While a program holds a folder, the chat's write, edit and file-generating tools refuse paths inside it, and so does every ordinary task road. bash is not fenced.
  • SIZE-BUDGET rises from 54,600,000 to 57,400,000, the size the engine adds; PERF.md shows the figures.
  • The prompt-size caps after merging dev santos/dev2 lands on dev: the worker harness as default, runs as work, skills from other harnesses, the chat as manager #1410 are 57,124 bytes (full) and 49,590 (lean): dev's 56,146 and 48,814 plus this branch's 978 and 776.
  • senior-dev's ending informs codeaf, not the person. Every run's end wakes the conversation with a verdict (passed, unverified, failed, limit, crashed), which run it was, its cost, and one next step, under a playbook page of its own (prompts/program-outcome.md). The chat checks, fixes or re-hands the work. A third automatic hand-off, or one after a limit, is refused in code. The landed card says ended and "senior-dev's ending went to the chat" instead of the status.
  • senior-dev's page names its task once. The head is one row: title, badge, a ▸ brief dropdown that shows the whole brief in grey (also ctrl+o), and the pinned facts. The trail crumb and the Reading: label are gone. Every action opens to its whole step on a click. The page's notes say the ending went to the chat, not the program's status.
  • Each change to the work wears git's +N,-M on senior-dev's page, the added lines in green and the removed in red. The file tools count them (write now counts its own, as edit and apply_patch did), and the protocol's step record carries them as added and removed.
  • The macOS unix socket limit is 103 bytes, not 104. A path of exactly 104 bytes passed the check, then failed at bind().

Fixed

  • Opening a task's room froze its side-list clock at the click. The row now hides its clock while the room is open.
  • On reopen, a run row codeaf had closed under could make the whole task list be set aside as corrupt. It is now kept.
  • A run codeaf closed under read running for ever. It now reads ended, with its time stopped.
  • Home counted a run's dollars twice; now it counts them once.
  • The run summary card, and questions asked of a run on its page, were model calls no spend record counted. They are counted now.
  • A senior-dev task's landed card (opened) and the chat's tasks tool show what the run cost. Its settled row now carries the run's total, as its index row already did. It's a display figure only, so nothing is charged twice.
  • A senior-dev proposal whose depends_on names unfinished work is refused before its card; before, it started at once and dropped the dependency. Any task may name a senior-dev run that ended done, and one still going is refused in its own sentence.
  • senior-dev tasks no longer offer a steer in the @ block or a retry on the task card. ContinueRun and the task row refuse to carry a senior-dev run on, in one sentence.
  • A senior-dev task opened from another window shows its actions view, read-only.
  • Opening another conversation's running work from the tasks list ("enter read it as it runs") was unreachable on a normal launch, for every task. The window couldn't read other conversations' presence through its engine connection. It now reads them from disk (session.ElsewhereOf). This also switches on the "another window is already in these files" warning for /task on a normal launch.
  • senior-dev ended at once with "workspace is not a git repository" whenever it was started without --in-place in a folder with no git history. It now chooses git only for a work tree with a commit, and its snapshot recorder otherwise.
  • A second senior-dev run in a folder the last one left on its branch stacked a new branch on it and called the first run's branch "your branch". A second run that changed nothing deleted its branch and switched back to the first's. It now carries on on the same branch, and its ending names the person's real branch.
  • Opening any conversation built an empty task graph, because endInterruptedProgramRun called graph() rather than tasker().

Known gaps (not in this change)

  • In the sessions list, pressing Enter on a running senior-dev task opens a nearly empty stored page that reads incomplete, instead of its actions view.
  • /stop on a page opened from another window does nothing and says nothing.
  • tasks.md quotes the stale-status line differently from the code (current status unavailable — showing the last known state).
  • One run per conversation, now that /task defaults to the run road (santos/dev2 lands on dev: the worker harness as default, runs as work, skills from other harnesses, the chat as manager #1410). A conversation drives one run, and senior-dev runs alone in its folder. So while senior-dev works, a /task or proposal in the same conversation is refused ("task N did not start: … senior-dev runs alone …"), where it used to run beside it on the old engine. Other conversations can still run tasks on other folders.

How it was checked

  • make pr-ready, run in its parts, on 2f2dc745d, and test-touched again after the gap fixes (rebased onto dev f101ad2e7):
    • test-quick, shard-test_test.sh, and test-touched all pass. test-touched covered internal/session (8 shards), internal/tui3 (8 shards), cmd/codeaf and every other package this branch touches.
    • make test-laws passes in all 28 packages.
    • Two parts fail on dev on this Mac too:
      • scripts/one-suite_test.sh, because this Mac has no flock;
      • TestTheWarmTranscriptEncodeCostsTheSameAtEightyTurnsAsAtEight.
    • It caught two failures, both fixed here:
      • Opening any conversation built a task graph, because endInterruptedProgramRun called graph() rather than tasker(). That was the first CI failure, in TestTheSessionAcceptanceDoesNotMintChecksFromProse.
      • The ssh policy test's control socket came to 104 bytes under macOS's $TMPDIR.
  • Merged with dev santos/dev2 lands on dev: the worker harness as default, runs as work, skills from other harnesses, the chat as manager #1410 (the worker harness as default, runs as work) at fff90333c:
    • The run start keeps dev's start lock and join-or-wait, and adds senior-dev's road (its folder, program and requested models; it never joins a run).
    • dev's set-aside ends an abandoned senior-dev run in its own words first.
    • A closed conversation leaves an ordinary run interrupted; a senior-dev run is ended and its folder finished on its own branch.
    • Commits carry dev's gitSignature, and the task page uses dev's trail and facts head with senior-dev's badge and pinned line.
    • After the merge, test-touched passes for session (8 shards), tui3 (8 shards) and every other touched package except three tests that also fail on a clean dev on this Mac: two cmd/codeaf /var vs /private/var path tests, and the provider allocation test.
    • On screen with the merged binary: a senior-dev run with its badge and actions page, a /task refused while it runs, the landed card at $0.05, and the stub's $0.1353 matching the conversation's $0.14.
  • On screen, in tmux with a stub model:
    • the landed card for a senior-dev run, opened, shows $0.05 for a run whose row records $0.0492. The stub charged $0.2583 across two conversations showing $0.14 + $0.12, so nothing is counted twice;
    • a second window's tasks list offers enter read it as it runs on another conversation's senior-dev run. It opens the actions view read-only, ctrl+y shows the raw calls and back, typing is refused in a sentence, there is no stop, and the final page shows once the run ends;
    • the card and badge, the [sd] row at 110 columns, the actions view and ctrl+y;
    • a repo run that cuts a branch and leaves it checked out, with main untouched;
    • the uncommitted-changes refusal, a plain folder, and a new folder on a fake home's Desktop;
    • stop, a busy folder, and a /task refused while senior-dev holds the folder;
    • the shell road, and a run that changed nothing.
  • Two independent reviews confirmed 24 findings: 7 about the actions view, routing and badge, and 17 about working in the folder. All are fixed, each with a test.
  • Not yet run: the real-model test of whether the chat proposes senior-dev by itself. The harness is ready, but it needs an OpenRouter key in the environment.

Checklist

  • A change entry: docs/changes/unreleased/1488-senior-dev.md.
  • The manual knows about it: internal/manual/chat/ is updated in this change.
  • No new line in .github/known-red.txt.
  • Only my own paths are staged; no git add -A.

🤖 Generated with Claude Code

ZeroPoint95 and others added 30 commits September 24, 2026 19:08
A draft for discussion: an outside program such as swe-pro becomes one more
worker kind behind the run supervisor rather than a slash command of its own.
Names the seams it rides, the contract it asks of a program, what swe-pro
would have to change, and the waves.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ands drawn beside it

Owner feedback 2026-09-21: the UX is /swe-pro <brief>, not /task via. The
row is generated from the manifest and rides the task door. The control-plane
change and the spend-record question are handed to the swe-pro side.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…s spend

Records what landed on the swe-pro side (f3b9716): no plane needed, a
cumulative `stage: spend` record the live limit reads, and the rule never to
sum message.updated costs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…oad time

Owner's call 2026-09-21: delegates are added at a person's discretion, so
the /<name> row cannot be a build-time list. The manifest ships its own
manual page, the corpus layers installed pages over the packed one, and a
page that does not spell its command refuses the manifest. codeaf-as-delegate
moves to later.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Owner's call 2026-09-21: one commit per delegated task, subject the task's
title, body the terminal record's claim and observation, then the ordinary
merge home. Eager commits stay on inside the copy.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Decisions up front in one table, short numbered sections, every "already
exists" seam in a table, no content removed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ext-landing delegates

Owner's call 2026-09-21: no per-program readers. stage, spend, step and
terminal on stdout, everything else ignored; swe-pro already conforms bar
two optional asks, and a delegate may land text instead of a tree.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…d and a step record

swe-pro 5793499 landed both optional asks, output-only. The reader needs no
compatibility spelling and the task page gets steps.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The interface a program meets to be a codeaf delegate, standalone: launch,
the four stdout records, the terminal, SIGTERM, tree or text, the manifest
and its manual page.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ch, and the run's delegate worker

Wave 1 of docs/design/delegate/DESIGN.md. internal/delegate is a leaf: a
manifest beside its manual page, loaded from ~/.codeaf/delegates with the
manual law checked at load; a reader over the four stdout records that
ignores everything else; and a launch under its own process group that
SIGTERMs on the context, waits a grace, then kills, keeping a terminal
written on the way out. internal/run seats it as one more worker kind:
stages feed the live step, step records enter the trajectory, spend reaches
the run's bank, and the terminal record is the ending. Proven against a
scripted program and a recorded swe-pro stream. No door reaches it yet.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…/delegate lists them; via on propose_task

Wave 2 of docs/design/delegate/DESIGN.md. The conversation's door: a
delegated run rides the run road whatever the belt switch says, the
program is seated on the root task, nothing joins it, and its commits are
squashed into one task commit that comes home the way every run's copy
does. The surface generates one command row per installed delegate at
launch, appended to the live table and never to the literal; /delegate
lists what is here, what is absent and what was not added. The model is
told the delegates this launch has, by name, and names one with `via`.
The manual gets its Delegates page and the probes that reach it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… over the packed corpus

Wave 3 of docs/design/delegate/DESIGN.md. A delegate ships its page beside
its manifest; the loader keeps the text and the conversation's manual tool
answers from the packed corpus with those pages layered under
`delegate-<name>`, so "what does /swe-pro do" is answered from swe-pro's
page and nowhere else. The overlay adds and never replaces. A ready-to-copy
swe-pro manifest and page sit under docs/delegates. Wave 4 needs no wire:
a hosted surface installs no rows and /delegate says whose delegates they
are. The design doc records the decisions taken while building.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…s row

startKnownTaskRun became a wrapper over startKnownTaskRunVia when the
delegate door landed, so the row's publisher is the via variant.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… the loop, and the laws hold

Wave 4 of docs/design/delegate/DESIGN.md. Delegate.List and Delegate.Start
ride wire version 18, so a --host surface generates its rows from the far
machine's registry and a delegate runs there; the surface asks for the
list beside the line at the launch and on a switch, and starts one in the
line. The two roads the door lengthened are split along their phases
(joinBeltRun, commitProposalToRun), the stop-law ledger names both
publishers, and the manual pages name no retired worker.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…dir it before installing one

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…its own repository

swe-pro-go renamed its binary and namespace to senior-dev at b43daaf: the
binary and cmd/senior-dev, the .senior-dev/ run folder, refs/senior-dev/*
and every SENIOR_DEV_* variable. Its command line, its four stdout records
and its terminal data spellings are unchanged, so the reader needs nothing
new; the manifest, the page, the fixture, the tests and both documents take
the new name, and the docs pin senior-dev at 6103488 with one line saying
what it used to be called.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a placeholder

fill substituted a brief into argv and then kept rewriting it in map order,
and checked the RESULT for unknown placeholders. So a review of a Go template
or a Helm chart ({{ .Name }}) refused the launch every time, and a brief that
said {{key}} or {{workspace}} could be rewritten after insertion, splicing the
person's API key into a command line. Each element is now substituted in one
pass by a strings.Replacer, which never rescans what it inserted, and the
unknown-placeholder check reads the manifest's own text. Reported by the pr-af
session with a reproduction; both faults are pinned by a test through Run that
fails on the old code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What was true: a delegate was an outside program installed by hand — a
manifest and a page in ~/.codeaf/delegates, a binary on PATH, `{{placeholders}}`
filled at launch with the person's key among them — listed by `/delegate` and
documented in the public docs/DELEGATE-PROTOCOL.md.

What is true now (the owner's direction of 2026-09-23): the programs codeaf
can hand a whole task to are compiled into it. internal/delegate is the v2
contract — a Delegate is a Go value in internal/delegate/builtin's list, with
its own commands and flags; it runs as a child of codeaf's own executable
(`codeaf <name> run --json --dir … -- <brief>`); it writes `hello`, `stage`,
`step` and `terminal` on stdout; and it reaches a model only through the model
API codeaf will serve each run, whose address and token are the only model
names in its environment. ChildEnv takes every provider key and redirection
out, so no key reaches a program or a command its model runs. A `hello` of
another protocol stops the run: the engine outlived a rebuild. The
conversation log record (delegate.Turn) is fixed here for the API to write and
the task page to read.

Gone: the manifest, its validator and fill, the loader and its folder,
`/delegate`, the manual overlay, docs/delegates and the public protocol page
(kept on the tag delegate-manifest-v1). The chat's `/<name>` rows, the door,
`propose_task`'s `via`, the run road and the squashed landing carry over.
"Delegate" is a working title, so every sentence a person or the model reads
names the program itself. docs/design/delegate/PROTOCOL.md is the internal page.

The build carries no program yet; senior-dev joins the list with its engine.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… lanes

Two pieces three lanes will build on at once, fixed here so none of them
invents its own: internal/provider/modelapi.ChatURL, the one place a program
in codeaf's tree gets its model route from (the funnel law lets only
internal/provider spell it), and delegate.ProgramFile, the record of which
program a run handed its task to and the stages its hello named, written by
the worker and read by the task page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What was true: a task a run handed to a program (senior-dev) read like any
other task. Its page carried the store row, the brief and the steps its
worker recorded, and nothing of the program: not its name, not the stages it
said it would move through, not the calls it made to a model. PlanTaskPage
never set the page's own Live, so the stage the worker publishes as the live
step was drawn nowhere on the page, and a row named no program at all.

What is true now: a row names the program its task was handed to and the
stage it is in (PlanTaskRow.Program, .Stage), read off the program record in
the task's own folder, or off the conversation's live run in the moment
before the program's hello. A page carries PlanProgram for a task whose
folder holds a program record or a conversation log: the name and stages,
the newest 200 calls in the order they started with every text cut to its
first line and the run's own copy taken out of paths, how many earlier calls
it leaves out, and how many calls reached a model. The page's Live is set.
The spend a page shows is still the store's spend rows, which is the figure
that moves while the run goes once the model API banks one row per call.
The run's ceiling has a field and no source yet: it is worked out when the
run starts and written down nowhere a page can read.

Every read is inside the page and row reads the surface already makes off
its loop, and the remote wire carries the new fields unchanged on the page's
own call (client_test.go proves the round trip).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What was true: a run handed to a program (senior-dev) opened on the page
every task opens on — a telemetry line, the brief, the steps the program
reported — with a note box and the sentence "the worker reads a note at its
next step", neither of which a program ever honoured. What the program was
doing, the calls it made to a model, was drawn nowhere; the rail row wore
only its clock, and a plan row drew the program's stage behind the shell's
`$` as if it were a command.

What is true now: a program's page is the conversation between the program
and the model that answered it, drawn from the page the surface already
holds (taskconversation.go). The brief opens it under the program's name;
each call is the program's side (a tool's result as `<tool>: <line>`, its own
words, or `summarized its history so far`) and the model's (its short name,
the first line of its answer, one dim row per tool it asked for behind that
tool's action mark); a refused call is codeaf's one line, a failed call the
model's; the call in flight is the last line, the running mark, the model
and its seconds, and it goes when the call returns. The line under the title
is pinned — stage (or the state word), spend so far, calls, age — so it
survives the page following its bottom edge; the head and the foot are now
counted by the frame, the window and the scroll alike. A program's page, and
its run's tab, have no box and take no note: the foot is the rule and the
keys, and typed letters are nothing. The rail row of a program's run says its
stage and its spend so far; a program's plan row draws its stage and never a
`$` command. Nothing drawn is zero or unknown, every row is fitted to its
width, every text is one clean row, and every mark comes through the glyph
door.

The manual's worker-harness page says what the page shows, and three probes
reach it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ot all of it

What was true: the new section on a program's task page said the whole of
every call is kept in the task's own record. The conversation log keeps each
message cut at a cap and the newest messages of a long call, so that claimed
more than the record holds.

What is true now: the section says the task's own record keeps more of every
call than the page draws, which is what the log does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…an show it

What was true: a program's task page had a place for the run's dollar ceiling
beside its spend, but nothing wrote the ceiling down where a page could read it
after the run started, so the page never drew it.

What is true now: delegate.ProgramRecord carries CeilingUSD, written with the
hello, and the page reads it into PlanProgram.CeilingUSD; zero still draws
nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dapted

senior-dev lived in its own repository (swe-pro-go at the tag codeaf-absorb,
6103488) and ran as its own binary with its own key. Its packages are now
under internal/seniordev at the same sub-paths, and the product half of its
command (the pipeline, the solo run, verification, the workspace recorders,
the engine adapters, the durable sessions and their tests) is the package
internal/seniordev/app.

This commit is the copy and only the copy, so the adaptation that follows can
be read as a diff: the module path is rewritten, `package main` is `package
app`, and every Go file carries a !windows constraint (the two stat files keep
their darwin and linux ones). Left out entirely: internal/afield (the control
plane), internal/attribution (OpenRouter's attribution headers), the
tree-sitter marker file, and the command's CLI glue (main, args, the parser,
cli_run, serve, cpbridge, trace) and their tests. It does not build on its
own yet; the next commit makes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n folder stays out of a worktree's git

Four of senior-dev's tests failed on macOS, all on the same fact: every
temporary folder there is /var/folders/…, a link to /private/var/folders/….
Three were tests comparing two spellings of one folder. The fourth was real:
the per-file commit measured the written file against git's resolved top
level, so in any workspace reached through a link the path walked out of the
repository, `git add` refused it, and every `wip(write)`/`wip(edit)` commit
stopped without a word. Both sides are now resolved before they are compared,
and the three tests compare resolved folders.

senior-dev also wrote its `.senior-dev/` exclude into <git-dir>/info/exclude.
In a linked worktree, which is what codeaf cuts for a task, git ignores that
folder in favour of the common dir's, so `.senior-dev/` stayed untracked and
a landing that stages the tree's status would have committed senior-dev's
database and spec into the person's branch. The exclude now goes where
`git rev-parse --git-path info/exclude` says git reads it.

Its commit identity came from the AgentField attribution package, which is
not carried; senior-dev's own commits now carry `senior-dev
<senior-dev@localhost>` as -c overrides (GIT_AUTHOR_*/GIT_COMMITTER_* still
win), and no AgentField trailer. Those commits never reach a person's branch
as they are: codeaf squashes a run into one commit under its own identity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rts only through the host

senior-dev was a binary of its own. It read OPENROUTER_API_KEY and
OPENROUTER_BASE_URL (or a config's apiKey/baseURL), called OpenRouter
directly with attribution headers and an optional provider-routing block,
printed every bus payload and a running `spend` on stdout as NDJSON, mirrored
itself onto an AgentField control plane, and ended with a terminal event its
CLI layer wrote.

Now it is the program internal/seniordev.Program: one command, `run`, whose
body takes a delegate.Host. Every model call goes to
modelapi.ChatURL(host.Models().BaseURL) and leaves by one door, the backend's
fetch, which puts the run's token on it over the one http.Client. No key or
base URL is read anywhere, and a senior-dev.json that sets a service's apiKey,
baseURL or any providerRouting block is refused by name; an ad-hoc `provider`
option is dropped. prompt_cache_key, x-session-affinity, reasoning,
usage.include and the tools are kept. The one "openrouter" spelling it needs
is modelsource.DefaultID, read through orclient.Service.

On stdout it writes only the protocol's records, through the host: a hello
naming its thirteen stages in the order a run reaches them (a law in
stages_test.go holds the list to the source), a stage per phase change, a step
per finished tool call, and exactly one terminal, written by the body so a
panic is an ending too. The terminal keeps the model's claim (its submission
reason) and what senior-dev itself observed of the project's build and tests
apart, in words a person reads. Bus payloads and spend stay in the process;
each stage's data goes to stderr, which codeaf keeps beside the task.

A stop is the truth, not a crash: SIGTERM ends the run's context, it starts
nothing new, ships what it has, and says `stopped before it finished` (or
budget-exhausted past a ceiling, or pass for a frozen candidate). A check cut
by the stop is recorded as incomplete rather than failed, so it neither fails
a submitted candidate nor restores an unsubmitted tree. Every file carries a
!windows constraint, and a test holds the tree to it. Left behind with the
command line: --format, --tui, serve, the control plane and the stderr trace.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The build's list of carried programs was empty on every platform, so there
was no /senior-dev row and `codeaf senior-dev` was an unknown command.

Now the unix list carries internal/seniordev.Program: `/senior-dev <brief>` in
the chat and `codeaf senior-dev <brief>` at a shell, `run` its one command,
landing a tree. Windows still carries nothing.

In the same change, the chat's manual gains internal/manual/chat/senior-dev.md,
written from what the code does: what it does and when to use it, how to write
the brief, what it cannot do (it cannot ask you anything, has no step cap,
reaches a model only through codeaf, needs git unless --in-place, is absent on
Windows), where its work lands (one squashed commit, the claim and what it
observed kept apart, .senior-dev/ never lands), what a run costs (every call
through codeaf, the dollar ceiling refuses the call that would cross it, its
landing reserve), its flags, and how a run ends. Six questions in a person's
words hold it reachable; two older questions that the first draft crowded out
("what flags does codeaf do take", "is my task stuck while it waits for its
test suite") were fixed in the page, not the test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Was: internal/provider/modelapi held only the route (ChatURL). A program
codeaf carries had an address and a token in its environment and nothing
answering at them, so no call it made could reach a model.

Now: modelapi.Open serves one run's OpenAI-style chat-completions API on
127.0.0.1, opened by one minted token and closed with the run, so the
token dies with it. Every call:

- is refused 401 without the token, and 402 in OpenRouter's error shape
  before it is made once the run's metered spend has reached its ceiling,
  written down as a refused turn and counted (RefusedAtCeiling);
- is decoded whole into the funnel's types — messages of every role with
  assistant tool calls and tool results, tools and the program's own
  tool_choice, max_tokens/max_completion_tokens, temperature,
  response_format, the reasoning depth (as a configured effort), the
  prompt_cache_key (or the x-session-affinity header) as the cache key,
  and working handed back on assistant messages — while the program's
  provider-routing fields are dropped for codeaf's router to decide;
- goes out through the completer the run hands it, armed with
  provider.WithBilling and WithReconcile so every billed answer and every
  late receipt is metered once, as it happens, to Config.Bank;
- is answered in OpenRouter's own shape: one chat.completion, or an event
  stream ending in a usage chunk carrying cost and cached tokens and then
  [DONE], with `: keepalive` comments every 15 s while the funnel thinks
  (whitespace on a whole body), and model failures as the router's error
  envelope with a status that means the same thing;
- is written to the task's conversation log twice under one number — at
  its start and at its end — with only what the thread had not sent
  before (Restarted when the program rewrote its history), per thread.

Which model answers is one rule, Resolve: the model the program asked for
when one of this person's services can take a call on it, the run's work
seat when none can (Served names it), and never a refusal only because
this machine does not know the id; a call the funnel itself cannot serve
(no key, a withdrawn model) goes out once more on the seat.

Two small seams in internal/session carry it: ServesModel exposes the
account pool's own "can a service answer this model" test (and
RunSpec.Serves hands it to a run, read live), so the API and the pool
cannot disagree; and WithOwnCacheLineage lets a marked call keep the
program's own prompt_cache_key instead of the conversation's stamp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y call

Was: the delegate worker started the program with an empty model API, so
nothing it asked a model could be answered; it banked the dollars the
program reported about itself in v1 `spend` records, trusted the
terminal's cost_usd when it was higher, and wrote one spend row for the
whole run at the end. A program refused at the ceiling ended as whatever
it said it was, `crashed` included.

Now: before the program starts, the worker opens the run's model API
(modelapi.Open) over the conversation's own completer, with the run's
dollar ceiling, the services the conversation can reach and the run's
work seat (WorkSeat, the crew's own seatModel answer), and hands the child
its address and token through delegate.ChildEnv; it closes the API the
moment the program exits. Every metered call reaches three books as it
happens: the run's live bank (bankSpend), one spend row per call under
`delegate/<name>`, and this machine's spending ledger, one row per call,
written here and nowhere else — the conversation folds the run's total
without a ledger row of its own. Report.USD is the API's metered total;
the terminal's own cost is never banked and there is no end-of-run row.
On the child's hello the program record (delegate.WriteProgram) is
written beside the conversation log in the task's own folder.

A program the API refused at the ceiling is reported in the ceiling's
words — "<name> reached the run's dollar ceiling of $X" — whatever it said
it was, and the run ends on its cost limit.

The v1 `spend` record is gone from the protocol: RecordSpend, Sink.Spend
and Reading.SpendUSD are removed, and a spend line a program still writes
is one more line the reader ignores and counts.

The tests run the worker against real children: this test binary started
as the program (delegate_child_test.go), asking the real API over a real
socket, and a shell script that curls it; they pin the three books, the
turns, the dead token, the ceiling's refusal and the cost limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sts it

Was: `codeaf <name> <brief>` from a shell refused with "runs from a shell
once its model API is in this build"; `codeaf --help` never named a
carried program, and a typo of one was answered with nothing.

Now: a shell run is the same two processes a chat's run is. The host
reads the person's own profile the way `codeaf exec` does (config.Load,
the crew's work seat, one adapter per model with the service's own id put
on the wire, so an `openrouter/` spelling reaches the router bare), opens
a model API, and starts this executable as the program's child with the
API's address and token and no key, handing it the person's own line with
--json added so a command they named and its own flags arrive intact. It
prints the stage as it changes, each step, each model call as
`model · N in · N out · $X`, and the ending — or, with --json, passes the
records through as records. Every call is written to this machine's
spending ledger once; the conversation, the program record and its stderr
are kept under ~/.codeaf/v3/carried/<name>/<run>/. --max-cost and
--max-hours stop the program from outside (the limit rung), Ctrl-C sends
SIGTERM with its grace (the incomplete rung), a run with no brief starts
nothing, and the exit is carriedExit's.

`codeaf --help` lists every carried program in a group of its own after
the work you hand it — one synopsis line and a one-line summary each,
inside the eighty-cell law, the page's cap moving by exactly that
(carriedPageLines) — and prints no group in a build that carries none. A
program's name joins the typo suggester (commandWords), and a test fails
the build when a program's name would shadow any word run() answers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ZeroPoint95 and others added 20 commits September 24, 2026 19:09
…nd where its ending is kept

PrepareProgramFolder's comment still said a run that went away in the folder
is finished first, and that nothing is changed by a refusal; it is settled
without a commit, and only codeaf's own notes move. The record folder's
comment now names the ending written there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…it, and an earlier-messages question still reaches its page

dev's tab bar now names the home tab with the places bar's word, `home`;
the program room's narrow test and the manual lines that quoted `Home`
read it that way. The senior-dev pages' new words outranked
compacting-over-and-over for "what happened to the earlier messages", so
that page's heading now carries the asker's words.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…task in the folder itself

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… one for a program's ending

endInterruptedProgramRun called a.graph(), which builds a graph, so every
conversation opened got one. An interrupted row only exists where recoverTasks
read a checkpoint back, which already built the graph; read it with tasker().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… enough to fit

Under macOS's $TMPDIR the control path came to 104 bytes, one over the
103-byte socket limit, so the multiplexing options were rightly dropped and the
test failed on a Mac while passing on Linux CI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eRun say so in one sentence

ContinueRun would have rebuilt a program's run on codeaf's own workers, and an
interrupted program row blamed a working copy it never had.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… its card

The @ block told the model to steer running senior-dev work, which reads no
messages; it now names the stop. The task-record card offered enter retry on a
failed program task, which the engine refuses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…its landed card and the tasks tool

A settled run's own row now carries the run's total, as its index row already
did, so the landed card shows the price; a reopen settling a run reads it off
the store's spend rows. The tasks tool's reader of the run's store prints the
price beside the time. No book is summed from rows, so nothing is charged twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ends_on, and any task may name a program's finished run

A program starts the moment it is approved and has nothing to wait in, so its
depends_on was read and dropped. It is now refused before the card while what
it names has not landed. An ordinary proposal naming a program's run was
refused as naming no task; a run that ended done is now a dependency met, and
one still going is refused in its own sentence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… read-only

A program's task writes no worker journal, so the guest page another window
opened onto one read the owner's journal, found nothing, and drew the
[senior-dev] badge over an empty body. The owner view now carries a TaskPage
reader (remote.Agent.ReadPlanTaskPage, which keeps the engine's refusal), the
guest page asks the owner's store once whether the task is a program's, and if
it is the same room gains the program's body: actions under their steps, the
pinned facts line, ctrl+y for the raw calls. It stays a guest page: no stop, no
steer (enter says it is reading), the owner's lanes and trail kept, and a
replaced conversation is its final answer on the page read as on the journal.
The journal beat stops for a program's page, and the owner's landing notice
reads the page once more. MethodPlanTaskPage joins the watcher allow-list; the
page's verbs do not. Ordinary guest pages are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s price is on its card and in the tasks tool

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… so their rows open

The tasks place drew another conversation's running work only from a
presence reading it asked of the agent, and only the in-process agent could
answer. Bare `codeaf` holds a connection to its engine, so on every ordinary
launch no such row was drawn (another window's senior-dev task read `enter go
inside it`) and the reading page behind `enter read it as it runs` could not
be reached from any real window; --no-host drew the row but has no engine to
attach to. The engine launch now binds Options.Elsewhere to
session.ElsewhereOf, which reads the presence files beside this window's
transcript on this machine's disk with this conversation left out, and
refreshElsewhere falls back to it when the agent cannot answer. --host binds
nothing and keeps the card.

Proven live with two windows on one engine and the stub model: the row reads
`another window`, enter opens the program's actions read-only, ctrl+y turns
to the raw calls, and the landing reads when the run ends.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… into senior-dev

Resolved against dev's run road: the start lock and join-or-wait now carry the
program road (a program never joins a run, and nothing joins a program's run),
dev's set-aside ends an abandoned program run in its own words first, a closed
conversation leaves an ordinary run interrupted while a program's run is ended
and its folder finished on its branch, commits carry dev's gitSignature, the
task page takes dev's trail-and-facts head with the program's pinned line in
the facts row, and the prefix waivers sit on the merged measurement.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s the program's badge, the ratchet holds

The task page's trail (dev's new head) draws the program's badge after the
task's crumb. The program's join refusal and the carried program move out of
joinOrWait and publishRunRow, back under the complexity ceiling. Tests that
meant the switch off now say off, since an empty switch is the bash belt; a
limit-ended ordinary run is archived as it ended (dev's one road). The manual
says a /task in a conversation where senior-dev works is refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the snapshot recorder, flag or no flag

senior-dev chose its git recorder unless told --in-place and ended at once
with "workspace is not a git repository" otherwise. codeaf passed the flag
for plain folders, but that made two readers of one question, and any launch
that missed the flag (an older build, a hand-typed run) met an ending the
chat had no lever for. newWorkspaceRecorder now picks git only for a work
tree whose HEAD is a commit, and the snapshot recorder for everything short
of that. --in-place still forces the snapshot recorder over a real
repository, which codeaf keeps doing under a home-folder repository.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed, sent back at most twice, one branch

A program's ending was a sentence on a row, read by the chat only if the
person asked something next. Every program run's landing now wakes the
conversation (programLandingNote) with a verdict — passed, unverified,
failed, limit, crashed — which run in its line it was, its cost and one
next step, under a playbook role page (prompts/program-outcome.md) and a
wider bound than a settle turn (16 calls, 15 minutes). The chat checks,
fixes on the branch, or hands the work back with a sharper brief.

Two bounds are code: a hand-off made in the turn an ending woke is refused
past programAutoRetries (2), and after a dollar or time limit, which waits
for the person's word. A hand-off the person asks for starts a new line.
The program's finished verdict (pass / pass-unverified) now reaches the
session (Report.Verdict -> Summary.Verdict -> RunSummary.ProgramVerdict).

A run handed a folder the last run of the same program left on its branch
carries on on that branch (ProgramFolder.carryOn) with that run's home and
start: the ending names the person's real branch, and a run that adds
nothing never deletes what an earlier one committed.

The landed card says `ended` and "senior-dev's ending went to the chat"
instead of the program's status, and is never painted as a fault.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s its brief behind a dropdown, and opens every step

The program's room opened under three titles: the trail's crumb (the
conversation's name, which a conversation named after its work spells the
same), the bold title row, and `Reading: <title>` over the box. The head is
now the title row alone — title, badge, a `▸ brief` dropdown, the pinned
facts — and the dropdown (or ctrl+o) draws the whole brief in the dim ink
between the head's rules, capped at half the frame. The body no longer opens
on a clipped brief, and the Reading label is gone.

Every action with more to show is a press (hitAction): it opens the whole
step under its line — the command or argument and what came back
(delegate.Shown.Detail, filled by senior-dev's presenter from the log's
command and observation) — and the same press folds it.

The page's notes no longer carry the program's status sentence, claim and
observation: a program run keeps `<program>'s ending went to the chat` and
where the work is (programPageNote).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… on senior-dev's page

senior-dev's page said `wrote a.go` and `edited b.go` with nothing about how
much each moved. The file tools already counted lines for edit and
apply_patch; write now counts its own (writeLineCounts, metadata only — the
model reads nothing new). senior-dev's step record reads the counts off the
tool's metadata (lineCounts, apply_patch summed over its files), the
protocol's step record carries them as `added` and `removed` — the emitter
wrote a fixed list of a step's fields and dropped them until it named them —
and the action log keeps them.

The presenter marks a change to the work (write, edit, apply_patch, never
senior-dev's own spec, pinned check or checklist) with its lines, and the
page draws `+N,-M` at the action's right edge, the added lines in the diff's
green and the removed in its red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ZeroPoint95
ZeroPoint95 marked this pull request as ready for review September 25, 2026 14:38
ZeroPoint95 and others added 8 commits September 25, 2026 11:39
Resolves the two conflicts #1495 made: Land takes the run's base and the
copy record carries CheckBase beside Continues. The clock test's slow
landing takes the base too, and places.md says where the here ~/codeaf
chip sat, so the manual's search still reaches it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in #1429 (conversation overview) and #1485 (one task page for
every task). #1485 retired the page senior-dev's stored page was drawn
on, so a program's task now opens its program room from every door —
its row, its card, the tasks place and the run's tab. A program room
draws no transcript/work tabs, the run's own tab stays the one selected
tab, and the branch's guards for the retired overlays are dropped or
pointed at the room. Tests that drove the old page drive the room.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Benchmarks section now carries the ten-harness DeepSWE comparison:
the chart, a table of solved, cost per task, cost per solved issue and
time, the limits of a one-seed run, and the V4.1 Flash and Kimi K3 runs.
The per-harness numbers move into docs/benchmarks/deepswe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V7ShhY74oyWjYGB3SougdE
The README keeps the chart and one paragraph; the table, the later
V4.1 Flash and Kimi K3 runs, the setup and the limits live in
docs/benchmarks/deepswe. The chart's footer now says only "same model".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V7ShhY74oyWjYGB3SougdE
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V7ShhY74oyWjYGB3SougdE
The installer printed a three-line telemetry notice after the receipt.
It prints none now; the binary's full notice still arrives before the
first session's events are sent. The local install marker stays. The
test asserts the installer prints no notice, and docs/TELEMETRY.md
loses the installer's block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V7ShhY74oyWjYGB3SougdE
Nothing the installer prints mentions telemetry now; the marker is still
written when it can be.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V7ShhY74oyWjYGB3SougdE
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants