senior-dev: a coding agent built into codeaf takes a whole task, in the folder itself - #1488
Draft
ZeroPoint95 wants to merge 155 commits into
Draft
ZeroPoint95 wants to merge 155 commits into
ZeroPoint95 wants to merge 155 commits into
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>
… for a program's badge out of the title Home's needs you rows, its since you left lines and the @ list wrote a program's badge onto the end of the title as plain text, and each of them cuts a title from its right to keep the age at its edge, so the badge was the first thing lost: a senior-dev landing with a 56-cell name read `…session store [seni… 1h` on a two-column home and wore no badge on a three-column one, and the @ list lost it under about seventy columns. Now a home cell carries the program beside its title and measures the badge as part of it, and the badge keeps its long spelling while the title keeps twelve cells, its short one after that, the rule every other list of the work keeps. A since you left line wears the badge between the name and what the work came to, which gives way first. The @ list fits its label to the room its row will give it before the row cuts it; the row's own label budget is one function now and both read it. Ordinary rows are measured and cut exactly as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…passes only after the model has read the bounce The bounce was once per message of the person's, and the mark was spent by whichever proposal took the lock first. "fix issues #31 and #32 with senior-dev" is two proposals in one reply, staged before the model reads either result, so the first was turned back and the second passed as though the bounce had been read: it went up with no `via` and its countdown admitted it to codeaf's own worker against the person's ask. A bounce withdrawn with a cut reply also kept its mark, so the next proposal passed unread. Now the mark carries the step that made it: every proposal without a `via` in that step is turned back, a proposal from a later step (after the model has read the bounce) passes as it is, and a withdrawn bounce takes its mark back. The programs and senior-dev pages say so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…program the person named The bounce and the floor's lift read only the newest thing the person typed, and a steer is a new message. "fix the dropped-retries issue with senior-dev" followed by "the failing test is TestRetryUnderLoad" left nothing that named senior-dev, so a proposal without `via` went to codeaf's own worker, and a trivial steer such as "fix this file only" held senior-dev on the floor. Now every message typed into a turn is read for the programs this build carries, where the person's words are recorded: the newest one named, the message that named it, and every one named. The bounce is counted against the message that named the program, so a steer naming nothing earns no second one; the floor lifts for any program named in the turn; a woken turn keeps the reading it was woken under; and the next message that opens a turn starts it again. The programs page has a section of its own for it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…in the conversation, and only an ask for the program lifts the floor The floor lifted whenever the person's words contained the program's name. "revert senior-dev's commit" and "commit senior-dev's changes" are trivial asks that name senior-dev in passing, so a proposal with `via` passed the floor, and one without was first bounced toward senior-dev: a billed run on a branch of its own, which can never do the revert or the commit the person asked for. Now a commit, an undo or a revert is held on the floor whatever `via` says, before the bounce, since a program never moves the person's branch. For a one-file fix or a single read the floor lifts only where the words ask for the program: its command typed as a word of its own, its name first in the message, or its name right after a word that hands it the work (with, via, using, use, give, hand, to, have, let, ask, get, want), never as a possessive. A trivial ask that only mentions a program meets the floor without a bounce. The bounce itself still hears any mention, since the model can answer it. The programs, senior-dev and tasks pages say so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the person's branch is safe codeaf's two switches in a program's folder ran with the repository's hooks, and a post-checkout hook that failed made `git switch -c` exit non-zero after HEAD had moved: the refusal said nothing was changed while the person's checkout sat on an orphan branch, and a switch back that git reported badly left the empty branch behind under a sentence saying it could not go back. A lock git could not take after it made the branch left a stray `task/…` branch. The ending said "your branch X is as it was" without reading it, so commits a program's shell put there were reported as nothing, and a run that changed nothing switched the checkout onto them. And the finishing commit named the notes folder in an exclude pathspec, which makes `git add` exit 1 whenever `.senior-dev/` is there and ignored, so every repository with notes from an earlier run failed its finishing commit and kept its empty branch. Now both switches run with core.hooksPath at the null device, and a failed switch is read again: a cut that happened is carried on with, a stray branch is deleted, and a switch back that arrived goes on to drop the empty branch. The person's branch is read before the receipt and the ending promise anything, and one that moved is said to have, from where to where; a run that changed nothing stays on its empty branch rather than switch onto it. The leftovers are staged whole and the notes' path reset out of the index, and a merge the program's shell left half done is never committed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ithout a commit A run whose process went away — codeaf closed, crashed or killed, a shell run's second ctrl-c — was finished by the next codeaf that found it with `git add -A` and a commit under codeaf's name with the hooks skipped, before the dirty-checkout check. The folder had been the person's since the process died, so a day of their own edits on the run's branch, untracked files and all, went into that commit, a merge they were resolving was concluded with its conflict markers, and the next run was cut on top; the reopen did the same without even the repository's git lock. Now codeaf commits leftovers only for a run whose end it saw. A run found owed, at a conversation's reopen or by the next run in that folder, is settled without a git write: its notes are moved into its record folder, and its record ends with where its work is — its branch, checked out as it was left, and how many files are not committed, read with git's optional locks off. The next run then meets those changes like anybody's, refused, and is told they may be the earlier run's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… one another run holds The hold on a program's folder was keyed by its exact path, so a run on a plain folder of projects and a run in one of those projects each held their own and started together; once the outer run submitted, its checkpoint restore put back what the inner run had changed and removed the files it had added, and counted them as its own. Now a folder is busy when a held folder is it, holds it, or is inside it. The hold file says which folder it holds as well as whose run, a run takes its own hold before it reads the tree around it (so two runs readying a parent and a child can both be refused and never both start), the folders above are asked by name and the holds below by what they say they hold, and the refusal names the held folder and how it stands to the one asked for. The hold lives in programhold.go, which the next change's guards read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…m's run holds Without the copy a program works in the person's live folder, and the hold on it only kept out a second program. The chat's own write and edit tools, a quick task, a typed or proposed ordinary task, and the landing of one already running all still wrote there. Once senior-dev had submitted, its restore put back what they changed and deleted what they added, with no copy kept; an ordinary task merged into the program's branch was undone while its row said it landed; and a task cut from the held repository recorded the program's branch as the person's. Now, while a program holds a folder from any conversation, window or shell, every tool that saves a file at a path it names refuses a path inside it, naming the file, the folder and the run (reads stay open). An ordinary task whose folder is it, inside it or around it is refused before it starts, at the card, a typed `/task`, a quick task, a node's start and the run road. A task already running keeps its branch rather than merge, a mirror is not laid, and `/land` waits. The receipt tells the chat the folder is the program's until it ends. `bash` and the person's own editor are not fenced, and the manual says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ogram ends, and a hangup stops it A shell run finished its folder only after the model API had waited up to seventy seconds for the price of a call the stop cut short, and the second ctrl-c the manual offers during that wait killed the host first: the folder stayed on the program's branch with its work uncommitted and nothing said. A closed terminal or a dropped ssh connection sent SIGHUP, which nothing caught, so the host died on the spot; and a host that died without a word never stopped its child, which runs in a process group of its own and so worked on in the folder after the hold had gone. Now the folder is finished, and the program's end written to its record, as soon as the program has exited, before the wait for prices. SIGHUP stops the run the way the first ctrl-c does, and a second hangup is ignored rather than allowed to kill the finishing. A child looks for its parent once a second and stops, as a SIGTERM would stop it, when the process that started it is gone. A folder a killed host still leaves unfinished is settled by the next run there without a commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd the receipt names a repository at home A reopen that settled a program's folder left the row `interrupted` for good when the store called the program's task done, on the reading that the work was never landed and that was a person's call; a program's work is never landed by anybody, and the folder had just been settled. A folder another process had ended — the run itself before codeaf closed, or the next run in that folder, which then wrote its own record over the one beside the hold — left the reopen finding nothing owed, so the row said only that codeaf closed. And the receipt of a folder inside a repository rooted at the home folder told the chat it "has no git history". Now how a run's folder was left is also written into the run's own record folder, and a reopen that finds nothing owed reads it back and puts it on the page once; a done task's row settles done, with the program's result, where the work is and the branch that holds it. The receipt for a folder under a repository at home names that repository and says no branch is cut there and nothing committed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hold waits by a deadline The run door's new refusal for an ordinary run on a held folder took startKnownTaskRunVia past the task engine's ceiling of fifteen decisions, and the hold asked again for a taken lock a counted number of times, which the session's one-deadline law forbids. Now readyRunFolder answers a new run's folder — a program's readied and held, an ordinary run refused a held one, a program that only answers given none — and the run door calls it first; and a run asks again for a taken hold for a tenth of a second, by the clock, rather than five times. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A child that saw its host go stopped as a SIGTERM would stop it, but nobody was left to send the SIGKILL a host's own stop sends after the grace, so a program still at work on its way out — a restore, a last test run — could go on in a folder the next run had already taken. Now the ladder a host's stop keeps is kept without the host: the stop, the grace, then the child is ended outright, unless it has finished by then. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…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>
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.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