senior-dev: a coding agent built into codeaf takes a whole task, in the folder itself - #1488
Merged
Merged
Conversation
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>
… made, and the engine reaps its whole process tree Two gaps an adversarial pass found in the first round of fixes. The rescue before a restore listed untracked files under the CURRENT ignore rules, so a person who added an ignore line and created the file mid-run lost it; and it skipped a tracked file the person deleted, which the restore then silently recreated. Every path changed after submission is now accounted for against the rules recorded at the run's start: modified and new files are copied as before, deletions are listed in a plain manifest in the rescue folder, paths ignored at the start are still never touched, and no rescue folder is made or named when there is nothing in it. Processes were found at the end only by an environment marker a command could drop (setsid env -u ..., env -i), so such a process outlived the run. On Linux the senior-dev engine, and only it (never the codeaf process), is a child subreaper and walks /proc parent links to kill and reap its descendants before it exits, on a normal ending, a stop and a deadline; the host's marker sweep remains only as the best effort after the engine itself was killed. A person's own processes and codeaf are never touched. Review of #1488, verification findings V1.1, V1.2, V1.4. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
tasks.md said a task has no dollar limit of its own and models-and-cost.md that a task is never stopped on its own dollar count; both now say that of an ordinary /task and point to senior-dev's run ceiling (0 and 3h by default). The senior-dev page states the restore's deletion manifest, the estimated-cost refusal as a limit, and each platform's boundary on background processes. Review of #1488, verification finding V1.7. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…true now The fix round had left a "Fix round" section keyed by review labels (C1, E2, F1–F5) under the entry. Each item is now an invalidates line in the entry's own shape, saying what somebody may still believe and what is true instead. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… its credit In a real run the model ran git reset --soft main and git commit through its own bash. That commit took the person's git identity from the repository and had no Assisted-by, because codeaf signs only the finishing commit it makes itself and there was nothing left to stage. The model's shell commands now carry the same author and committer identity codeaf's run commits use (one identity, now in internal/gitidentity), and when the finish has nothing to stage and the tip of the run's branch is a commit the run made without the credit, codeaf amends that tip's message to add it, keeping its tree. A commit that existed before the run is never amended. Review of #1488, verification finding V2.3. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The missing ended card seen on one real engine-road run did not reproduce once the landing stood outside the work fold (d2415a8): through the proposal road with a stub model the card showed before and after the wake, with the task page open or not, and after reopening. This pins the order that test did not cover, a landing delivered after the chat has already answered, and that it survives a reopen. Review of #1488, verification finding V2.4. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…says so /budget conversation 1.5 wrote the profile and answered "per conversation · $1.50", but the open agent kept the spend rail it was launched with, so the next chat turn, ordinary task and senior-dev run in that conversation used the old figure; a senior-dev card showed $10 beside the chat's $1.50. The agent now publishes a live override that every rail reader uses, the engine road carries it over a new remote call, and /budget and the settings panel wait for the bind before they show the new reading. When the bind fails (an older engine host that does not know the call), the receipt says the saved figure applies to the next conversation. /budget has no time row, and the manual no longer says it changes a run's wall-clock limit. Review of #1488, verification finding V2.2. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The ship road built its terminal before restoring the frozen candidate, so the person's later edits were saved under codeaf's state root but the shell ending, the task's end record and the chat's landing never said where. It now restores first and then builds the one terminal, with the rescue folder and the deletion manifest. A test holds the post-submit verification in a real child process under the delegate host, edits and deletes files meanwhile, and proves the path crosses the protocol into the stored end action; the failed-suite road (which already restored first) and an unchanged tree are covered too. Review of #1488, verification finding V2.1. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The fix that keeps generated caches out of a run's commits had internal/session
call util.GeneratedRunPath, and every file of internal/seniordev/util is
!windows, so the Windows build of codeaf stopped compiling ("build constraints
exclude all Go files"). The pull-request gate cross-builds nothing; ci-full does,
on the way into staging. The generated-path list and the engine's commit
identity now live in runshape.go, the one file of the package without a build
constraint; GOOS=windows builds again.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…nd a pushed one The finish credited a run-made tip only when it carried codeaf's identity, so in the common ending, where every write was already checkpointed by the engine as senior-dev and nothing was left to stage, no commit on the branch carried Assisted-by (seen on a real run). A tip made under either of the run's two identities is now credited. The amend also now requires that HEAD is the run's branch and that no remote-tracking ref holds the tip: a model that pushed its commit used to have the finish rewrite it, leaving the local branch diverged from the remote. A person's commit on the run branch is still never amended. Review of #1488, final verification. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
When the folder changed after submission and the rescue copy could not be made, the restore refused (correctly: nothing is lost) and the folder kept the later changes, but the run still ended "submitted a change, and the project's own build and tests passed", about a tree nothing had checked. A failed restore on the ship road now turns a pass into pass-unverified, which the chat treats as work to check, and both restore roads record why it failed; the ending adds that the folder could not be put back, and why, so it also holds later changes that nothing checked. Review of #1488, final verification. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ndows build Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…side senior-dev Brings in #1494 (team delegation), #1511 and #1436 (per-task worker, planner and checker routing). A senior-dev run keeps its own road beside them: it works alone in the folder it holds, so a via proposal starts one program run and does not go through the team split; it keeps the models it was asked for, or the pinned or profile worker, and #1436's per-task routing and $5 task cap apply to codeaf's own tasks, while senior-dev keeps its finite per-run ceiling. Both sides had claimed remote protocol version 18; the combined wire is version 19. Program badges fit #1494's grouped side column, the manuals describe the merged model choice, and the prompt-size ledger records the combined fixed prefix (57,218 bytes). dev's unbounded-launch test now holds its six workers at a barrier instead of guessing with a sleep, which failed on a loaded machine. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The previous fix kept the Windows build compiling by giving internal/seniordev/util one file without a build constraint, which broke the PR's own law that no file of senior-dev reaches a Windows build (TestNoFileOfSeniorDevReachesAWindowsBuild). The engine's commit identity and the list of caches a run's tests leave now live in internal/gitidentity, beside codeaf's own run identity, which builds on every platform; the engine keeps its names for them as aliases, and internal/session no longer imports the engine at all. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ut exception The dev merge made TestNoFileOfSeniorDevReachesAWindowsBuild allow one file of internal/seniordev/util to build on Windows, the one that held the run facts codeaf reads on every platform. Those facts now live in internal/gitidentity, so the law is back to its original form: no file of senior-dev builds there. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e name the rest The environment-pin law found two CODEAF_ names nothing had registered. The strip list named CODEAF_API_KEY, which codeaf never reads and which the *_API_KEY suffix rule in the same function already removes (the older spelling too), so it is no longer named. CODEAF_DELEGATE_RUN, the mark codeaf sets on a program's process to find what its commands left behind after the engine is killed, is plumbing like the model API's own two names, and is registered beside them. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… does TestSoloRecoveryCrossesThePersistedEngineBoundaryWithoutReplayingToolEffects drove soloConverse without the recorder's Prepare, which every real run calls first and which installs git's exclusion for the live session store. Without it the store's temporary files could race git add during submit, the submit failed, the model was nudged that it had stopped without submitting, and the stub saw a fourth request it did not expect. Under load it failed 2 runs in 40; with the recorder prepared it passes 40 of 40, and 10 of 10 with -race. Review of #1488. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With senior-dev's pages beside dev's new team-manager page, the probe "what happens if it will not stop" reached home, tasks, team-manager and senior-dev instead of the keys page that answers it. The heading already answered it as "will not let go"; it now says both. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ommit it makes The finish amended the run branch's tip to add Assisted-by when nothing was left to stage. The final verification showed that rewrote a commit the model had pushed by direct URL (no remote-tracking ref records such a push) and dropped the signature of a signed commit; guarding the amend further would always miss a case. The amend is gone: commits senior-dev makes as it works keep the run's identity and are never rewritten, and the credit is carried by the finishing commit codeaf makes when there is something left to commit. Review of #1488, final verification V4.1, V4.2. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
After the Pareto-crew merge, codeaf senior-dev run --high <model> on a fresh
profile with a key but no crew rows was refused before its first call ("no
allowed model on a connected provider can start"), because the profile seat was
resolved before the explicit model was applied. The seat is now resolved only
when the run needs a default, which keeps that refusal and its /crew door for a
run with no model named.
Review of #1488, final verification V4.4.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Three ways a run could claim or cost more than it should. A comparison of the folder with the submitted candidate that failed (git gone, say) left the run at pass, and even a failed restore could keep a "build and tests passed" reason beside its pass-unverified status. Both now end unchecked, and every field of the terminal record agrees with the final status. A verification command that ran zero tests counted as a pass: a real run's unittest fallback ran plain discovery from the root, found nothing in a tests/ folder without __init__.py, printed "Ran 0 tests ... OK", and the run said its build and tests passed. An empty run from any runner the discovery picks now ends unchecked with a sentence saying so, and the unittest fallback discovers the project's test folders. The rescue before a restore copied whatever had changed, of any size, with the project's permissions. It now checks a 25 MiB per-file and 250 MiB total limit before it touches anything; over them nothing is restored and the run ends unchecked, and what it does copy is private to the user (0700 folders, 0600 files). The manual states all of it, and a test keeps its figures on the constants. Review of #1488, final verification V4.3, V4.5, V4.6. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…he task room Brings in #1516, #1517, #1518 and #1519. The two conflicts were dev editing the inline run-road code that this branch had moved into helpers: the --one-model seat (#1517) now rides the precomputed crew factory in enginewire, and the hand-off's named model reaches the crew router as a worker pin (#1518) through the crew wish set before commitProposalToRun. That wish is read only by the ordinary task's crew routing, so a senior-dev run keeps the models it names. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ary" Dev's #1518 pinned three sentences that carry the crew's default per-task limit ("No task may cost more than its limit: $5 ..."). With senior-dev in the tree those sentences are false for a senior-dev run, which is not a crew task and keeps its own ceiling, and the pages already say "an ordinary /task" there. The pins now match the qualified sentences; the figure still comes from config.CrewTaskCapDefault. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
AbirAbbas
added a commit
that referenced
this pull request
Sep 26, 2026
…1530) * senior-dev: a plain folder starts with a readable safety list again On dd0fcc6 (#1488 merged) a senior-dev run in a folder with no git crashed at bootstrap before its first model call: "read start-time ignore list: open .../ignored-at-start: no such file or directory". Folder preparation returned on the plain road (and the gitignored-subfolder road, which takes it too) before writing the start-time ignore list, while the shell and the chat both still handed its path to the child, which refuses an unreadable list. Preparation now writes an empty, readable list there first, so the run works in place and commits nothing again; a repository's list is written and honoured as before. Tests start the real child on the shell road and as the chat's run worker in a plain folder. Found by a real-model run of the merged build. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * senior-dev: shell model words resolve before the child starts codeaf senior-dev --high z-ai/glm-5.3-flash failed after launch with 'models.dev: provider "z-ai" not found' because the value went to senior-dev as typed and its first segment was read as a provider; only openrouter/z-ai/... worked. The shell now resolves --high and --asked through the same model matcher and connected-service check the chat's proposals use, and hands the child the qualified id. A bare OpenRouter id, a service-prefixed id and a short crew word all work; a model no connected service can serve is refused before the child starts, naming /crew and codeaf connect. The manual says what --high accepts. Found by a real-model run of the merged build. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * changes: the plain-folder and shell model-name fix is #1530 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * changes: #1530's entry carries its number Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Draft: not ready to merge. The test of the chat's routing against a real model has not been run yet.
How
/senior-devworkscodeafbinary, 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 areinternal/seniordev, and about 37k are tests. Its solution process is unchanged; codeaf hosts it and meters it./senior-dev <brief>.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.codeaf senior-dev <brief>runs it without the TUI.[senior-dev]badge ([sd]when narrow), beside ordinary/tasks.ctrl+yshows the raw model calls.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./costand home.Added
/senior-devcommand, theviafield onpropose_task, andcodeaf senior-devin the shell.internal/seniordev(the program),internal/delegate(the protocol a built-in program uses to talk to codeaf; spec indocs/design/delegate/PROTOCOL.md), andinternal/run/delegateworker.go(the host).[senior-dev]) wherever a task is named. Ordinary tasks wear none.ctrl+yfor the raw calls.internal/manual/chat/senior-dev.mdanddelegates.md.scripts/clean-run.sh(themake clean-runtarget), which opensbin/codeafon a fresh state root.Changed
via, it is sent back once.write,editand file-generating tools refuse paths inside it, and so does every ordinary task road.bashis not fenced.SIZE-BUDGETrises from 54,600,000 to 57,400,000, the size the engine adds; PERF.md shows the figures.devsantos/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.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 saysendedand "senior-dev's ending went to the chat" instead of the status.▸ briefdropdown that shows the whole brief in grey (alsoctrl+o), and the pinned facts. The trail crumb and theReading: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.+N,-Mon senior-dev's page, the added lines in green and the removed in red. The file tools count them (writenow counts its own, aseditandapply_patchdid), and the protocol'ssteprecord carries them asaddedandremoved.bind().Fixed
runningfor ever. It now reads ended, with its time stopped.taskstool 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.depends_onnames 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.@block or a retry on the task card.ContinueRunand the task row refuse to carry a senior-dev run on, in one sentence.session.ElsewhereOf). This also switches on the "another window is already in these files" warning for/taskon a normal launch.--in-placein a folder with no git history. It now chooses git only for a work tree with a commit, and its snapshot recorder otherwise.endInterruptedProgramRuncalledgraph()rather thantasker().Known gaps (not in this change)
incomplete, instead of its actions view./stopon a page opened from another window does nothing and says nothing.tasks.mdquotes the stale-status line differently from the code (current status unavailable — showing the last known state)./taskdefaults 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/taskor 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, on2f2dc745d, andtest-touchedagain after the gap fixes (rebased ontodevf101ad2e7):test-quick,shard-test_test.sh, andtest-touchedall pass.test-touchedcoveredinternal/session(8 shards),internal/tui3(8 shards),cmd/codeafand every other package this branch touches.make test-lawspasses in all 28 packages.devon this Mac too:scripts/one-suite_test.sh, because this Mac has noflock;TestTheWarmTranscriptEncodeCostsTheSameAtEightyTurnsAsAtEight.endInterruptedProgramRuncalledgraph()rather thantasker(). That was the first CI failure, inTestTheSessionAcceptanceDoesNotMintChecksFromProse.$TMPDIR.devsantos/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) atfff90333c: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.interrupted; a senior-dev run is ended and its folder finished on its own branch.dev'sgitSignature, and the task page usesdev's trail and facts head with senior-dev's badge and pinned line.test-touchedpasses for session (8 shards), tui3 (8 shards) and every other touched package except three tests that also fail on a cleandevon this Mac: twocmd/codeaf/varvs/private/varpath tests, and the provider allocation test./taskrefused while it runs, the landed card at$0.05, and the stub's $0.1353 matching the conversation's $0.14.$0.05for 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;enter read it as it runson another conversation's senior-dev run. It opens the actions view read-only,ctrl+yshows the raw calls and back, typing is refused in a sentence, there is no stop, and the final page shows once the run ends;[sd]row at 110 columns, the actions view andctrl+y;/taskrefused while senior-dev holds the folder;Checklist
docs/changes/unreleased/1488-senior-dev.md.internal/manual/chat/is updated in this change..github/known-red.txt.git add -A.🤖 Generated with Claude Code