Skip to content

santos/dev: crew family, Pareto crewing and the Model Pool, custom connections - #1108

Draft
santoshkumarradha wants to merge 693 commits into
devfrom
santos/dev
Draft

santoshkumarradha wants to merge 693 commits into
devfrom
santos/dev

Conversation

@santoshkumarradha

@santoshkumarradha santoshkumarradha commented Sep 17, 2026

Copy link
Copy Markdown
Member

Integration branch santos/dev into dev. Everything below through #1192 landed on dev as #1194 on 2026-09-18 (merged by Abir with his own fixes on top: a zombie-aware process-group liveness check and a CI-mode guard in the pool test), and dev was merged back into santos/dev at 631237c, again at 2ac2360 (the Model Pool mirror workflow, #1116) and at c8d0046 (the /connect custom row fix, #1195), and at ae07e93 (the v0.3.0 changelog roll-up, #1196), so the branch is level with dev. #1203 makes two planner tests count their fake client's calls atomically, removing a test-only data race that failed one full check under load. #1109 brings the worker harness: one loop under /task and codeaf do, with a plan store per run, a review round after each piece of work, and the task tree on the rail. It is off unless CODEAF_TASK_BELT=bash is set, so the default road does not move; today a /task the person types and codeaf do take the new road, and a task the chat starts by itself joined it in #1205. #1207 seats a review check on the crew's checking seat instead of the thinking seat, so the model a person picks to check is the one that checks; the door resolves that seat from --check-model, then CODEAF_CHECK_MODEL, then a pinned plan seat, then the profile's careful row. The two harness waves landed at 164548f (full check on Spark green, 114 packages): #1204 draws the plan as a tree on the chat rail with a dot row, a line saying what is happening now, a bounded read-only question about a run, and a work tab that lists runs; #1205 gives the chat one way to put work out, a landing that wakes no model turn, and a reply only when an answer is owed, and a task the chat starts now takes the new road too. #1210 reads a run's result from the store when the root's worker returns after the run ended, which removes a race that lost the answer under load. #1209 shapes answers to lead with the action, number their steps and end on one next thing. #1211 joins a test's background writer before its state directory is torn down (test-only, landed at 7cda67c, full check green). Three fixes from a first real session on the new road landed together at b94df33 (full check green, 114 packages): #1212 counts a landing's report as evidence that the ask is finished, so a correct answer is not followed by an empty turn, and a check refused for a leading directory change is told the form that passes; #1213 keeps a reopened conversation's tasks, by reading back a task born from a run's plan, never overwriting a checkpoint it could not read, never reusing an id, and saving a run's ending with its row; #1214 tells a refused proposal every problem at once and settles where a task works from the folders its brief names before asking the person. #1215 reads a declared check as one command the way a shell reads its quotes, so a search for one of several words is admitted, run and judged by one shared reader, and a refusal names the character and the form that passes; #1221 judges the router scenario on scripted first tokens so its verdict no longer depends on how busy the machine is (both landed at 39ffbc5, full check green). Seven small PRs landed together at fe9eb62 (full check on Spark green, 114 packages): #1222 lets the conversation's tasks tool read a run's tasks from the run's own record, so a finished hand-off is asked about and not redone; #1223 says once, at load, which top-level profile config keys nothing reads, so a nested object written where a dotted key was meant is no longer silent; #1225 stops a dispatch pass from reclaiming the leaf it just released, which ran the same piece of work twice and spent its budget twice; #1227 seals the place sweep's note at close, so nothing is written under state after the process closes and quitting never waits on the folder walk; #1226, #1228 and #1229 make three tests independent of machine load (a wait helper that checks its condition before the clock, a usage writer joined before cleanup, a first token ordered by a signal). Three more landed together at 4e176a5 (full check on Spark green, 114 packages, first run): #1230 lets a conversation whose engine runs in another process, which is the default, read and steer its run: the rail's rows, a task's page and the steering verbs now cross the wire, a compile-time line fails the build if a later method is not carried, and the plan is re-read on a beat only while a row is queued or running, so a conversation at rest reads nothing; #1231 opens a task's page from its row, gives a run a row of its own with its parts under it, and answers a click from the same drawn view the frame used; #1233 seats the review of a codeaf do run whose root finished itself before its worker returned, which used to end the run with no check and failed the full check under load. Five more landed together at 608c459 (full check on Spark green, 114 packages): #1220 makes a worker's declared checks the review contract: a holds verdict rests on a recorded, zero-exit run of every declared command, the worker records each command's own exit, a record opens with a line marking it as one that records exits, so a declared check that was never observed is refused, and the basis of every verdict is kept with the task (replaces #1206); #1232 gives every run a copy of its own, so a hand-off no longer works in the person's folder, and brings the work home as one commit when the run ends; #1235 paints a wait whose stream has stopped arriving at the spinner's own pace, measured at about 0.6 points of a core less on the wait path, with its method and harness under bench/idle-surface; #1240 stops a letter typed into a note on a running task's page from raising the stop card, and a finished task's page no longer offers verbs the store would refuse or shows a store id; #1237 takes the connect tests off the fixed callback ports, so nothing else on a machine can fail them. Two more landed at 1cbcb4b (full check on Spark green, 114 packages, first run): #1239 tells the person once, in the conversation, which config.json keys nothing reads, in hosted and unhosted launches alike, again only when that set of keys changes, and never for a key the product itself writes; #1238 identifies a surviving process in a test by its identity and start time, since a bare process number can be reused by an unrelated process. Four more landed at 12840ee (full check on Spark green, 114 packages, first run): #1234 keeps every run a conversation has made readable, and closes a lock the plan reader could leave held when a conversation had no run yet, which stopped the engine; a new guard law now fails the build if a reader of that shape can return while still holding its lock; #1242 makes two profile writes in one process compose, so a second write no longer drops the first one's key; #1243 adds the regression test for seating a check after a worker whose root finished itself returns; #1241 adds a repeatable drive of a run on the real binary in hosted mode under scripts/, outside the full check. Five more landed at 443c661 (full check on Spark green, 114 packages, first run): #1244 gives what a person types after pressing a run's row to that task's page, so a note meant for a task is never sent to the model as a message; #1245 keeps the unread-key notice quiet about keys the product itself retired, with a ledger law so a key cannot be retired silently; #1236 lets a reconnect reuse its registered identity, reports a finished sign-in only after its listener is released, and hands the product a held listener in tests; #1247 and #1246 make two tests independent of machine load. Two more landed at 8ed67b7 (full check on Spark green, 114 packages, on the second run; the first run failed one temp directory cleanup in a package the stack does not touch): #1248 keeps both keys when two codeaf processes write the profile at once, with a lock that holds on every platform the repo builds for, a wait bounded at two seconds, and no stale lock to reclaim after a crash; #1249 makes the job shell tmux tests read one marker line, so a large environment can no longer cut the line they assert on or let an absence pass unproven. Still to come as small PRs: the task's own page and its reader over the wire, a copy of its own for every run, and the review contract where a worker declares its own proof command and the engine judges it by what ran (replaces #1206). This PR now carries what came after #1194: the lazy lexer registry (#1193) and whatever lands next. This PR is the running index of what the branch carries at the product level. It stays a draft until every feature below has landed on santos/dev, the independent end-to-end pass on Spark is green, and Santosh has e2e-tested it.

What a person gets

Feature What it does for you How you use it Where it is
Crew model family: open or all The three crew words (frugal, balanced, max) answer from open-weight models only, or from every model including frontier ones. models.crew.source = open|all (default all), or the open/all switch in /crew. On santos/dev (#1073)
Pareto crewing: crews computed from the live catalog Crew presets are computed from OpenRouter's prices and quality indexes on the cost-quality front, refreshed daily, instead of a hand-picked table. Any seat can be auto. /crew catalog, or models.crew.pick = catalog. On santos/dev (#1093, squash 2fb296b)
Pick mode: table, catalog, learn Choose the fixed table, today's catalog, or a crew learned from judged runs, per role (worker, checker, planner). /crew table|catalog|learn, models.crew.pick. On santos/dev (#1093, squash 2fb296b)
Learn: judged runs move the picks After each chat /task run an independent judge scores every seat (billed to a judge seat). Scores go to your own sheet and feed the picker next time, blended with the pool by observation count, so your own runs improve your own picks first. Starts from a seed built from 145 real runs, so learning works on day one. A seat you type by hand is judged too. Automatic under pick = learn. codeaf pool show lists what the picker knows. On santos/dev (#1093, squash 2fb296b)
Model Pool: collective, text-free evidence Every install can share its score rows through a relay and pull a signed index back, so everyone picks better because the others ran. Rows are computed numbers and ids under a per-install nonce: never code, prompts, paths or identity. model_pool on|read|off (default on, notice shown once). codeaf pool show|status|verify [--json]. On santos/dev (#1093, squash 2fb296b)
Public relay and GitHub mirror The relay Worker ships in-repo under relay/ with a runbook; it is live at https://codeaf.agentfield.ai/pool. The signed index is mirrored to the model-pool branch of this repo so anyone can watch it update. A team can run its own private relay with its own key (CODEAF_MODEL_POOL_RELAY_URL, _MIRROR_URL, _PUBLIC_KEY). Nothing to do; works out of the box. On santos/dev (#1093, squash 2fb296b)
The paper docs/design/model-pool/pareto-crewing.pdf ("Pareto Crewing"): the objective, estimator and picker, with figures from real runs. Read it. On santos/dev (#1093, squash 2fb296b)
Named, multiple, switchable custom connections Connect several OpenAI-compatible endpoints (local models, proxies), each with its own name, key and model list; switch between them, including mid-session; rename one and every stored pick follows. /model groups models under each connection's name. Switching onto a keyless default service is refused with a reason. /connectCustom OpenAI-compatible API; /settings → Providers → add / edit / active connection. On santos/dev (#1092, squash 825ef2c; Santosh e2e-tested locally against a local OpenAI-compatible proxy)
Worker harness: one loop under /task and do The internal hands (patch, doc, web, image) become one worker loop with a plan store, shared by the chat /task door and codeaf do. No new surface; fewer steps and lower cost per task (smoke: 4/4 parts in 28 steps vs 3/4 in 42). #1109, draft, wave 1 landing

Bug fixes on the branch (question answer-box keys, landing answer fate, retry avoiding the failed upstream, recovery evidence, taxonomy lane read, and, with #1092, mid-session /connect now reaching the live conversation on the engine road, a pre-existing hole) are not listed above; each has its own change note under docs/changes/unreleased/.

Known limits, stated on purpose

  • The learned picker is greedy on its estimate; there is no exploration yet, so a model that is never picked is never scored. The next algorithm pass (Thompson sampling, hierarchical cap, drift) is a follow-up PR.
  • The judge runs on the chat /task door only; codeaf do, ask and remote sessions do not feed the pool yet.
  • The pool index is read at start-up (24 h cache); codeaf pool verify pulls a fresh copy, read at the next start.
  • A pool cell becomes readable at 3 rows, from one install or three (the paper says three installs); follow-up.

Relay status

Live at https://codeaf.agentfield.ai/pool (healthz 200, index signed, codeaf pool verify good). The model-pool branch exists and carries the current signed index (refreshed hourly by a stand-in job until #1116 puts the workflow on dev). The pool is primed with the same measured runs the embedded seed encodes, so the published index carries real cells from day one.

End-to-end pass on Spark (independent session, done)

All 12 steps pass on #1093's head (cf4ea35; final fd29d79 differs by a runbook sentence). Report: spark:~/src/e2e-learn-2026-09-17.md with KNOWN-LIMITS.md and evidence/INDEX.md beside it; 18 capped runs, about $26.

Step Result
1 fresh install, learn, judged tasks, own sheet + outbox flushed pass (first pass failed: judge picked a rate-limited free model; fixed)
2 rows reach the relay, fold, publish pass (index 1789681435, 19 cells)
3 fresh install D reads the pool and picks differently from the seed pass (pool: worker gpt-5.6-sol; seed only: glm-5.3-flash)
4 privacy: rows are numbers/ids/nonce; read and off send nothing pass
5 resilience: bogus relay → mirror → seed; task still runs; verify good pass
6 manual /model override under learn is still judged pass
7 crew words × open/all, table vs catalog, real tasks on frugal-open and max-all pass
8 auto/catalog/learn seats resolve on headless doors pass (first pass fell to the table; fixed)
9 /crew chooser and /settings pool row with notice pass (note: pick mode is a settings row, not a chooser switch)
10 `pool show status
11 codeaf models, --refresh, daily cache pass
12 headless do/exec under a learn crew pass (fixed with 8; judge on headless doors stays a follow-up)

Follow-ups from the e2e, now on santos/dev: a task's thread stops its parts before closing and a machinery cut records its reason (#1118); a decision card under --yolo takes its default (#1117); the auto crew row shows the rung it actually resolved to (#1119); a bare auto row under picked from = catalog reads the catalog's figures alone (#1120). codeaf do seats from the settings the run will use, so a fresh profile with the key in the environment reads the catalog, not the table (#1122); the relay accepts the exec and run doors (#1121); a launch's pool errands stop when the process closes instead of writing into a closed profile (#1124); a launch compiles internal/verify's patterns on first use and the engine host runs on a capped scheduler (#1125); the client's min_installs floor counts installs, as the relay's does (#1127); the relay folds a retried batch once by its nonce (#1128), carries an acceptable metric beside the judge score (#1129), publishes only cells that meet the floor (#1131) and writes once per cell per batch (#1136); a refused row leaves the outbox and the file is compacted (#1130, #1133); the pool errand tracker's critical sections unlock from a defer (#1134); a run a process death left unjudged, and any headless door's landing, is judged on the next chat start (#1132). the finished-tree check reads a project cloned into a subdirectory (#1135); the exec, run and do doors leave a pending record for the sweep (#1137, #1138, #1139); the wall admits a first round it cannot cost and refuses an overrun it cannot hold, so a long codeaf do lands instead of re-planning to the wall (#1144), with the delivery gate over a grown job root pinned by a test (#1147); pool show, pool verify and pool status print both metrics, the pending judge records and the last sweep (#1141, #1142); a test's crash fixture stays in a profile the test owns, and the package's test floor clears any profile the environment named, and the opening-hint test reads a profile it made and pins the greeting contract both ways (#1145, #1150, #1153), and tui3's package law makes every surface-building helper name a profile of its own (#1173); the manual's status row says whose ledger it is (#1143); the seed index is regenerated from the live relay by one in-repo command, so a fresh install's fallback is a verified copy of the pool (#1148); pool show --cells lists the held index's cells with their installs and dims (#1149); a landing that leaves gitignored build products behind stays complete and the reconciled row names them (#1152); codeaf version on an unstamped binary names the condition, no .git directory for the toolchain to read, instead of blaming a Makefile target (#1151). pool status says when the relay dropped a row and why, instead of the row vanishing silently (#1154); a job teardown records the group leader's identity at launch and never signals a process group the kernel has since handed to someone else (#1155); a codeaf do continuation after an overrun continues the plan it has from the recorded turns instead of re-planning from scratch (#1156); the process sets a soft memory limit of half the machine's memory, nothing below a 512 MiB floor, an explicit GOMEMLIMIT untouched, carried into engine --daemon (#1158), and the bound is the smallest finite one of physical memory and the cgroup v2/v1 limits walked from the process's own level to the root (#1162); pool status says when this run cached the index, so the seed-to-cache flip is visible in the run that caused it (#1161). The pool's copy of a seat's id is the bare vendor/id, so a fresh install on the default model (whose seat carries codeaf's alias marker) no longer has its first rows refused by the relay, and the judge picker's same-vendor exclusion works on it (#1163); no cmd/codeaf test can push rows to the default relay, the package floor pins a dead address (#1164); the relay bounds each judge's fitted severity to ±10, clamps every adjusted score to the rubric, publishes mean and sd on one scale and falls back to the median when spread is zero, which is what had published a cell at 103.5 (#1165); the relay can purge stored rows from a fixture vendor and refuse vendors not in ALLOWED_VENDORS (#1167), with ALLOWED_VENDORS set to the catalog's vendors plus codeaf (#1177, Worker version 47102a26; 624 fixture keys purged from the live store); the built-in seed is regenerated from the relay's first clean publish after the purge, 18 cells, no fixture rows (#1180); a job's shell no longer inherits the host's TMUX handle and gets a profile-owned TMUX_TMPDIR, so a command the model runs cannot kill the terminal hosting codeaf (#1166); an unattended run (--yolo, nobody to answer) is refused a git push, merge or rebase that would move a branch it did not create, including the branch the checkout began on, while its own task branch stays pushable and attended sessions are untouched (#1168, the road it took is in #1160's note); pool status says the install's identity, the judged total across all doors, and "none yet" when no landing has been judged (#1169); an exec run's worker spend is minted into the usage ledger at the door, one row per run, so the status row, the cap and the pool row count it (#1170); the task door honours a stored models.crew word (every arm had resolved to balanced) and a pinned tier row survives pick=learn instead of being recomputed (#1171); a landing card raised on the check road under --yolo takes its default and records that the dial answered and that the check ran out of time, instead of parking a headless run forever, while conflict, shift and ground landings still park and now say so in the record (#1174); the model catalog warm is seated on the profile's start-up errand tracker so a process close joins it, instead of a warm outliving the process and writing its cache into whatever state root was current (#1179); the auditor's calls tag themselves auditor with the node they check, and its usage rows carry the auditor role and the task, so a ledger tells an audit call from a session turn (#1181); a headless do --json whose node ends failed or unverified names the kept branch and the verdict in its envelope, so nobody digs through tasks.json for kept work (#1182); the settle turn a landing note wakes under task.settle auto runs under the checker's own bound, a call ceiling and a share of the run's cap, one call for a clean tree with no declared check, and records "not settled within its bound" instead of running to the wall, which had been a one-to-two-dollar tax on most headless runs (#1183); the chat-side tasks text and a resumed session's record line name a failed or unverified node's kept task/<slug> branch and its verdict word, the same two words the headless envelope carries (#1184); the identical-effects test is deterministic, its forty saves carrying distinct call signatures so the turn's repetition guard cannot end the turn and the leash is the only thing the assertion is about, which closes a margin-of-one race that had read as a load flake since #284 (#1185); a non-verified codeaf exec or codeaf run names its kept branch and verdict in its envelope through the one result-envelope seam the three headless doors share, so do, exec and run answer alike (#1186); a task worker parked on a command it started (a bash call the 30-second background clock promoted to a job) waited the node's whole allowance when the job never ended, which is what held a DOE run silent for 53 minutes under a live cap; the park now has a bound of its own, a third of the allowance, posts "the park was not settled within its bound" and hands the turn back while the command keeps running, with the diagnosis at docs/notes/yolo-task-worker-parked-on-promoted-bash.md (#1187); the model pool runbook's plan line says metered, not capped (#1188, #1189, docs only); the status row says working while the door's task subtree turns, read from the same signal the tab strip draws from so the two cannot disagree, with the manual's screen and task-page sections moved to match (#1191); a job subtree is cut when it passes a CPU or process bound, 60% of the machine sustained for 30 s or 128 processes when nothing else bounds it, the process backstop alone under a cgroup or affinity quota, and the run learns it as "the job subtree was not settled within its bound", which is what a model that spawns infinite loops under a dollar cap had escaped (#1190); a note records what a task's audit step can execute and where that boundary is set, the declared checks plus read-only git and coreutils, which is why a brief that declares no check leaves a cautious auditor nothing it may run (#1192, docs only); the prose lexer registry loads a curated set of forty languages on first use instead of parsing all 279 of chroma's lexers at init, plain text for the rest, which takes about 3 ms off cold start and 2 MB off the binary, with chroma's files and licence carried beside the copies (#1193). Still open, PRs in progress: the footprint benchmark method and table under docs/benchmarks (#1126); Pareto crewing v2 with a model-free grader (#1123, Santosh reviews). The "task-branch merge fails 1 in 3" observation was the test driver killing runs mid-check, not a product defect.

Related, not on this branch

Before this leaves draft

santoshkumarradha and others added 26 commits September 18, 2026 02:52
…tree branch (#1178)

Diagnosis: the merge-home is correct and targets the driver's own worktree, but runs only on a verified verdict. A driver 'settled' (its quiet rule) is not the engine's 'verified'; unverified/failed nodes keep the deliverable on task/<slug> by design. Traces the branch-creation, commit, and merge-home paths with file:line, proves the pattern across c193-c221, and argues the smallest driver-side recovery.

Co-authored-by: codeaf <agentfield-bot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…1180)

* pool: regenerate the seed index from the relay (version 1789715873)

seedgen against the relay's 07:17Z publish: 18 cells, min_installs 3, the
relay's judge list; the paper's role_quality cells beside it. seedgen -check
exits 0 against the two files.

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

* docs: change note for the regenerated seed index

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

* pool: the seed count test reads the score metric's own cells

The regenerated seed carries the graded shares beside the judge's scores,
so the role_quality line counts its own cells, not the seed's total.

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

* docs: stamp the seed note with its PR number

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

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* cmd/codeaf: the model warm stops when the process closes

The model catalog warm was started through guard.Go, which joins nothing at
shutdown, and it writes the picker cache through tui3.WriteModelCache, which
resolves CODEAF_HOME at the moment it writes. A warm that outlived the process
that asked for it landed in whichever state root was current when the rows
arrived — the next test's own TempDir, whose clean-up then failed with
"directory not empty".

It is now seated on the profile's start-up errand tracker (the #1124 machinery)
and observes the tracker's context, so v3Process.closeAll cancels its wait and
joins it before the conversations and stores close.

cmd/codeaf/chatv3_modelswarm_test.go holds the catalog fetch open, moves
CODEAF_HOME, closes the process, releases the fetch, and fails if the write
lands in the moved root (deterministic: fails without the seating, passes with).

Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com>

* docs/notes: what the model warm still reads, and the test failing on this tree

A review of #1178 caught its own summary claiming that warmV3Models "no longer
resolves the catalog via ContextLength". It does — chatv3.go:2320 still asks
ContextLength for the window. The sentence was wrong and the code is right, so
the note now says what the function actually does and why the obvious way to
make the sentence true would be a regression: the non-blocking v3ContextWindow
seam matches an id with EqualFold where ContextLength normalizes it through
catalog's own index, so a conversation started on model:high would lose the
window it exists to learn; and FetchedAt on the next line reads through the same
waiting door anyway. What moved is the WAIT, to Catalog.Warmed(ctx), and after
it answers the blocking door cannot block, because LoadLazy closes its channel
inside the future's own body.

The note also carries the evidence the brief asked for and it was missing: the
command, and the deterministic failure against the pre-fix fire-and-forget shape
(0.11s, the write landing in the root CODEAF_HOME had already moved to), beside
the pass with the seating in place.

Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com>

* internal/catalog, cmd/codeaf: the model warm's reads never resolve the catalog

The reported sentence that warmV3Models "no longer resolves the catalog via
ContextLength" was false on the first commit: it still asked
models.ContextLength(started) for the window, and the pull-request check that
reads the sentence held it. The naive fix — read through v3ContextWindow, the
launch's non-blocking seam — is a regression: it matches ids with EqualFold
instead of normalizeID, so a session started on `model:high` answers a zero
window, and it leaves the FetchedAt read on the resolving door behind it.

The warm now reads through two new never-waiting doors on the catalog,
ContextLengthNow and FetchedAtNow, which read rowsNow (the ModelsNow door),
answer zero/nil while a lazy catalog is still warming, and match ids exactly
like the blocking twins. After Warmed(ctx) answers true the rows are always
in hand, so the twins answer the same numbers without counting a blocking
question; the warm's only wait is the context-observing one the close ends.

Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com>

* docs/changes: the entry carries the pull request number it landed as

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: codeaf <agentfield-bot@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…e they check (#1181)

* session: a failing test says the auditor's call is the auditor's

The auditor's provider call reached the model-call log tagged `turn` with no
node, and its usage row named no role and no task. This test drives a real
provider boundary from a checker built by newAuditAgent and reads the call log,
expecting `auditor` and the id of the node it checked; the usage-row test now
expects `role: auditor`, the checked node's `task` and the high seat.

* session: the auditor's calls say they are the auditor's

An agent built to check a node answers for a crew role and is not that node, so
its calls wear the role's own word and name the node they check rather than
falling through to `turn` with no node. Config gains checksNode for the node a
checker CHECKS, set by newAuditAgent from the node it was handed; loop.go stamps
callPurpose(crewRole) and WithCallNode(checksNode) for a crew role, and banks the
turn with that role word so TagUsage writes it beside the high seat the ledger
already billed. usageNode files the row under the checked node, which taskID
deliberately leaves empty.

* docs: change note for the auditor's calls naming themselves

* docs: the change entry says what the pool's record actually carries

It asserted the pool row's contents from the brief and left a reader unable
to tell whether `internal/pool/record` was inspected or skipped. The row is
one per judged seat score — metric, seat, model, score, judge, door, size,
day — written by Recorder.Record from judge scores at the pool's own record
command, never from a provider call, with no call count and no cost in it.
The entry now says that, and says plainly that no file there changed.

Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com>

* docs: stamp the auditor-purpose note with its PR number

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: codeaf <agentfield-bot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: codeaf <agentfield-bot@users.noreply.github.com>
…n's wall (#1183)

* docs: change note for the auditor's calls naming themselves

* docs: the change entry says what the pool's record actually carries

It asserted the pool row's contents from the brief and left a reader unable
to tell whether `internal/pool/record` was inspected or skipped. The row is
one per judged seat score — metric, seat, model, score, judge, door, size,
day — written by Recorder.Record from judge scores at the pool's own record
command, never from a provider call, with no call count and no cost in it.
The entry now says that, and says plainly that no file there changed.

Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com>

* docs: stamp the auditor-purpose note with its PR number

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* session: the settle turn runs under a bound, not the run's wall

A landed-but-unverified task under `task.settle = auto` wakes the
conversation's own turn and tells it to read the work and settle it. That
turn was measured running 28 minutes of 49 tool-call rounds on the high-tier
model, stopped only by the run's wall, over a tree that was already clean.

These tests hold the settle turn to the checker's own contract: a call
ceiling, a share of the run's money when a Steward is armed, the bound and
the count on the hand-back, one call for a clean tree with no declared
check, and no ceiling for an ordinary turn.

Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com>

* session: bound the settle turn at the wake, and say why it came back

The turn a landing note wakes under `task.settle = auto` is the one that has to
read the work and settle it, and it ran with no bound of its own. The wake is
now marked a settle wake where the note is minted, and the turn it starts opens
the checker's own window on its context and stops at `settleCallCeiling`
provider calls — or at its share of the run's money when a Steward is armed.
When a bound trips, the node is handed back with the reason and the count on its
report (a new reason beside `taskAskTimeReason`), not a silent stop.

A node whose check saw a clean tree with no declared check carries both facts
onto the node and settles after one call. A conversation's ordinary turn keeps
no ceiling.

Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com>

* docs: the change entry for the settle turn's bound

Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com>

* session: the settle turn tests build against the bound

Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com>

* session: the settle bound's count says one call in the singular

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

* docs: stamp the settle-turn-bound note with its PR number

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: codeaf <agentfield-bot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…s was

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…'s kept branch and verdict (#1184)

The chat-side counterpart to #1182: a /task node that ended failed or unverified kept its deliverable on its own task/<slug> branch (keptWork) and did not merge home. The tasks tool text, the recovered-graph note, and the TaskIndexEntry record now name that kept branch and the verdict word (session.TaskFailed/TaskUnverified) — the same vocabulary #1182 put on the headless envelope. keptBranchOf gates on the merge word (mergeAborted/Conflicted/Kept) so only genuinely-kept work names a branch. tui3 rendering is untouched; the rail/page read the words off the record.

Co-authored-by: codeaf <agentfield-bot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…ard (#1185)

TestARunOfIdenticalEffectsEndsNothingByItself failed on its len(rounds) < 2
assertion in a full-package run under load and passed alone. It was not the
leash: the run's allowance is 60 minutes and the run lasts 0.21s, so the
deadline branch above the repeat branch in childRun.trip never fires, the step
budget is 200 against seven steps, and every save resets r.idle.

What ended the turn was the turn's OWN repetition guard. Forty byte-identical
write calls are a loop by looped.go's rule, which keys on the call signature
(callSignature hashes the raw argument text), so the guard hands the turn over
at its seventh write, and whether the drain reached a second repeat checkpoint
before that turn closed was already a race without any load at all.

The test now leaves one identical EFFECT and forty distinct call SIGNATURES:
the saves write an empty file, so the leash's fingerprint of it cannot race the
next truncating write, and the JSON key order turns over between calls, so no
two signatures match and the guard cannot fire. Measured on this tree, the
round count moves from 3 against a floor of 2 to 19.

It also holds the behaviour effects.pardon exists for, which nothing pinned
before: the reader is asked once per run of identical effects and never on
every step. Removing that reset gives 38 rounds against a ceiling of 21.

Co-authored-by: codeaf <agentfield-bot@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…anch and verdict (#1186)

* cmd/codeaf: the exec and run envelopes must name a non-verified node's kept branch and verdict (failing tests)

The tests for the last gap in the #1182/#1184 vocabulary: an exec or run run
that ends non-verified carries kept_branch (the workspace's own branch) and
verdict (the record's word) on its --json envelope. They name the shared seam
(buildResultEnvelope/runResult) as the place one change covers both doors,
pin the failed-broke-with-nothing and no-repository/detached cases, and an
end-to-end smoke run proves the door passes its workspace through.

Red on purpose: buildExecEnvelope takes no workspace and subharnessRun has
no workspace field.

Co-authored-by: codeaf <agentfield-bot@users.noreply.github.com>

* cmd/codeaf: the exec and run envelopes name a non-verified node's kept branch and verdict

The last gap in the #1182/#1184 vocabulary. An exec run leaves its landing
for the pool's judge with State session.TaskUnverified (exec.go) and a run
door run does the same on every ending that ran (subharness_run.go), but
neither door's --json envelope said where the run's work stood or what
verdict left it there.

Both now say the two facts do --json has said since #1182, at the shared
buildResultEnvelope/runResult seam (envelope.go) so one merge covers all
three verbs and do's own emission moves onto it unchanged. execVerdict
(exec.go) reads the word off the same condition the pending landing uses:
a run the landing refuses — never started, or broke with no text and no
artifacts — says session.TaskFailed; everything else that ran says
session.TaskUnverified. The run door's sayEnvelope is unverified on both
its endings (the landing is written whatever the stop) and sayFailedEnvelope
is failed: that road writes no landing at all. kept_branch is the
workspace's own branch via keptBranchIn (extracted from errandKeptBranch,
do.go), empty for a detached HEAD or a non-repo, and absent with the key
when no verdict stands. The run door's workspace rides subharnessRun,
resolved at the door from space.Root() exactly as do resolves its own.

exec passes space.Root() at the buildExecEnvelope call site; the
end-to-end smoke pins the door wiring.

Co-authored-by: codeaf <agentfield-bot@users.noreply.github.com>

* docs: the envelope's kept_branch and verdict cover all three headless doors

HEADLESS.md's two rows said the keys ride codeaf do alone; they now say what
exec and run publish — unverified on every run that left a pending landing,
failed on one that broke with nothing to show — and the change entry records
what moved (#1185).

Co-authored-by: codeaf <agentfield-bot@users.noreply.github.com>

---------

Co-authored-by: codeaf <agentfield-bot@users.noreply.github.com>
* docs: what a --yolo task turn waited on when a bash job backgrounded

Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com>

* session: bound a worker's park on a promoted command

Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com>

* docs: change note for the park's own bound

Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com>

* docs: stamp park-bound change note (#1187)

Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com>

---------

Co-authored-by: codeaf <agentfield-bot@users.noreply.github.com>
Four tests for the plan drawn as a tree in the tasks place: a child
indented under its parent, a held row under the task it waits on, the
live step at the node's own column, and a note on a child still landing
through PlanNote. Each fails today but the guard; the code follows.

PlanTaskRow carries `Waits` — the hard dependencies that are not its
parent — so a row can say what holds it besides its parent.

Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com>
A plan row is now nested on the parent the store wrote: planItem answers
the tree walk's Parent from planAnchor — the store's own parent, or, for
a row kept pending, the hard dependency holding it that is not its
parent. The place's existing walk does the rest, so a child sits under
its parent and a held row sits under what it waits on, each wearing the
tasks place's own connector (tasksKin).

A plan row is a node of the plan's tree and not a fold: its children
always follow it, so the graph is on screen without a keypress. A record
row keeps the fold it had.

PlanTaskRow.Waits carries the hard dependencies a row is held behind,
and planWaits names the anchor — the waiting-on task or the parent — so
`queued · waits: <the work>` is one answer, not two.

Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com>
PlanTaskPage carries the task's own children, read one level deep in
store order, and the page draws them under the steps the way the rail
draws a family: the tasks place's connector, each child's state word and
title, and its live step under it while one is in flight. The note
composer and its receipt are untouched.

Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com>
Two answers in the asker's words: the plan is drawn as a tree, a task
sits under the task that requested it, and a task held behind named work
sits under what it waits on, still wearing `queued · waits: <that task>`.
tasks.md carries it after the tasks place, worker-harness.md after its
own pane page.

Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
#1191)

* tui3: a door at rest with a turning task subtree says idle

Failing test first, before the render-site fix: the status row's state
word must say `working` while the conversation's own turn is over and a
task it started is still turning. It currently says `idle`.

Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com>

* tui3: the status word says working while a task subtree turns

A door at rest whose work outlived its turn now says `working` from
[app.stateWord], reading the surface's own frame-safe signal
([app.frontSignal]) rather than `a.state`, whose behavioural reads are
unchanged. The word matches the tab strip's; the figures are untouched.

Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com>

* tui3: assertions, manual and note for the working word

roomstatus_test.go's fixture is a door at rest whose node is still turning,
so the conversation's own reading after leaving the room is the working
word, not a bare idle. The manual gains a row for the at-rest `working` and
loses the sentence that called the conversation idle while a node worked; a
note and a changelog entry record the word, the render site and why the
count is app.frontSignal and not runningTasks.

Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com>

---------

Co-authored-by: codeaf <agentfield-bot@users.noreply.github.com>
* session: a failing test for a job subtree over its CPU bound

The test states a spinner storm and an honest parallel verify as injected
readings — 16 loops then 64, against 34 processes and 8.5 cores — and demands
the storm be cut with a record while the honest peak survives. Nothing bounds a
job subtree's CPU or process count today, so it fails.

Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com>

* session: bound a job subtree's CPU and process count

A run whose model spawns busy loops as bash jobs spends no tokens, so its dollar
cap never fires while the box burns. #1158 bounded a run's memory and nothing
bounded a job subtree's CPU or process count.

The subtree #1155 records at launch (a process group with its leader's identity)
now has a bound: a portable Go-side watch over the subtree's cumulative CPU and
live process count. The CPU RATE is the signal that separates an honest peak (34
processes, 8.5 cores) from the storm (16 loops, then 64 — fewer processes but far
more CPU); a much higher process ceiling catches the fork storm the rate alone
would miss. A subtree over the ceiling for three consecutive readings is cut and
the run is handed the record 'the job subtree was not settled within its bound',
the family #1183 and #1187 speak in.

Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com>

* docs: stamp cpu-bound change note (#1190)

Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com>

* session: draw the CPU ceiling from the effective cores, not the machine

Under a cgroup CPU quota (where cells run) or an affinity mask a subtree tops out
at a few cores, so max(0.6*NumCPU, 12) could never trip: the 64-loop storm passed
the CPU rule and, at 64 processes, the 256-process backstop too. Draw the ceiling
from the effective cores (min of NumCPU, cgroup cpu.max/cfs quota walked from the
process's own cgroup as #1162 does for memory, and sched_getaffinity), share 0.6,
floor 1; lower the process backstop to 128 (about 4x the honest peak). New
internal/processgroup effective-cores reader, injectable for the seam test.

Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com>

* session: the CPU rule applies only when no quota or affinity binds

A share of a cell's four-core quota (2.4 cores) would cut every honest go test
inside the cell: under a binding quota the quota is already the ceiling and #1187's
park bound returns a wedged turn, so the CPU rate rule is off there and only the
128-process backstop applies. When nothing binds, the ceiling is 0.6 x NumCPU as
before. quotaBinds() decides it (effective cores < machine cores). Widen the trip
window to 15 readings (30 s) so an honest build burst is never cut. Remove the now
unused core floor.

Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com>

---------

Co-authored-by: codeaf <agentfield-bot@users.noreply.github.com>
…is set (#1192)

Co-authored-by: codeaf <agentfield-bot@users.noreply.github.com>
#1193)

* internal/tui2/prose: a curated lexer set that parses nothing at init

Importing the dependency's lexers package runs a package initializer that
builds a lexer from every one of its 279 embedded XML definitions, before
main and whether or not a block is ever drawn. On the cold-start benchmark
that parse is about seven of the twelve milliseconds `codeaf --version`
costs, and every command pays it.

So the set becomes ours. embedded/ carries 40 of those definitions — 39
languages plus go_template.xml, which the Go lexer delegates a raw string
to — copied byte-for-byte from the dependency's own copies (all 40 verified
identical with cmp against v2.27.0 in the module cache; NOTICE says do not
edit them here, so a re-copy after an upgrade is a diff with nothing to
reconcile).

lexers.go embeds them (`//go:embed embedded/*.xml` at line 44 into
curatedFS at 45) behind one sync.Once (curatedRegistry, line 129) and
builds the whole set on first use (curated, line 135), because a definition
may delegate to another BY NAME — html.xml leans on CSS and Javascript,
docker.xml on Bash and JSON, makefile.xml on Bash — and one parsed alone
whose delegate was never registered would tokenise the delegated region as
text. Nothing is read at package init. loadCuratedXML (line 118) is the only
XML parse, and curatedParses (line 114) counts every one so the laziness can
be pinned by a test rather than asserted in a comment.

curatedEntries (line 67) is the set, and its order matters in exactly one
place: hcl.xml and terraform.xml both claim the alias `hcl`, and
registration leaves the later holding it, so Terraform follows HCL and a
```hcl fence reaches what it reached before. There is deliberately no second
table of names — curatedGet (line 158) and curatedMatch (line 165) are the
dependency's own lookups over the curated registry, so names, aliases and
bare extensions come from each definition's own Config and cannot drift.

Go and markdown are the two languages the dependency ships as Go source
rather than XML, so there is nothing to copy. lexers_builtin.go rebuilds
both out of the dependency's own rule tables (goRules line 50, markdownRules
line 161), lazily and registered with the rest (newGoLexer line 40,
newMarkdownLexer line 95): goTemplate (line 28) is a sync.OnceValue over the
one XML the Go lexer needs, and markdownLexer.Tokenise (line 112) keeps the
frontmatter delegation to YAML via splitFrontmatter (line 134). Without them
the most common fence in this repository would lose its colour, which is the
one thing the set must not do.

Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com>

* internal/tui2/prose/code.go: the four registry call sites go through the curated set

The import of the dependency's lexers package (line 8) is what ran the 279
definitions' initializer, and this file was the only place in the tree that
imported it. It is gone; only chroma itself remains (line 7).

All four call sites move onto the curated set:

- LexerName (line 318) matched a filename with lexers.Match; it now uses
  curatedMatch (line 330), the same globs and priority ordering over the
  curated registry. It still answers "" for a filename nothing claims, and
  the doc comment now says an uncurated filename is exactly that case, so a
  file preview falls back to what it drew before instead of being painted as
  if it were code. The memoisation stays: Match walks every file pattern of
  every lexer, and the callers draw terminal rows at thirty frames a second.

- highlightPieces (line 349) resolved a fence's language with lexers.Get,
  then lexers.Analyse on the source when the label missed, then
  lexers.Fallback when that missed too. It now uses curatedGet (line 359)
  and, on a miss, draws the body at the ground tier — the same unstyled
  pieces a block under a profile with no ramp already produced. Analyse is
  dropped rather than replaced: an unlabelled or uncurated block is plain
  text, because a wrong guess is a claim about somebody's code. Fallback is
  dropped with it, since the nil-lexer degradation the callers already made
  IS the plain-text answer.

No behaviour change for a curated language; the miss path raises nothing.

Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com>

* internal/tui2/prose: pin the laziness, and the change note

TestMain (lexers_test.go line 22) is the pin, and it runs before any test can
build the set for its own reasons: curatedParses must be 0 and
curatedRegistry.reg must be nil at that point, so an eager parse — a package
variable that calls the loader, a registry built in a var — exits non-zero
before a single test reports.

That is a run, not a claim. Adding `var _ = curatedGet("go")` to the package
makes it fail:

    the curated lexer set parsed 39 definition(s) from XML before any use;
    the registry is eager
    FAIL  github.com/Agent-Field/codeaf/internal/tui2/prose  0.006s

and deleting that one line returns the package to green (ok, 0.420s). 39 is
the count of XML definitions in the set; go_template.xml is parsed through
goTemplate rather than loadCuratedXML, so it is not one of the 39.

TestCuratedSetBuildsOnceAndAnswersNames (line 37) pins the other half: first
use builds the set, a later use re-parses nothing, names and aliases and bare
extensions all resolve (go/golang/python/py/rust/ts/yaml/yml/bash/shell/hcl/
terraform/markdown/md/text), filenames resolve (main.go, script.py,
styles.css, Dockerfile, x.yml, notes.md), and a miss is nil for both a
language and a filename — plain text, no error.

TestCuratedLanguagesHighlight (line 78) is the no-regression half: the set
carries real lexers and not non-nil placeholders, so python, rust,
typescript and markdown fenced blocks still emit code-slot colours.

The change note carries the measured cold start (three best-of-7 rounds of
the benchmark's own startup phase, before 15/13/14 ms and after 10/11/10 ms)
and the measured binary sizes (53,739,785 to 51,511,561 bytes, both
`make build` of this tree).

Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com>

---------

Co-authored-by: codeaf <agentfield-bot@users.noreply.github.com>
…ot on the wall clock (#1221)

The scenario where the default lane goes slow and the router moves compared
measured wall times, so on a busy machine a healthy router could miss the
design's gate. Each request is now scored on the scripted first token of the
lane that answered it, and both of the design's clauses stand on that figure:
a p90 first-token improvement of at least thirty percent, and the moving arm's
p90 below the pinned arm's median. A router that does not move fails by
construction. Measured wall times stay in the log as diagnostics. Test-only.
A config file whose keys the reader does not know, including a nested object
written where a dotted key was meant, read the same as no config at all, with
no word to the person. Loading a profile now names every top-level key that
nothing consumes, once per load, as a log line and never as an error, so a
session still starts. A correctly shaped config says nothing.
Asked about work it had just handed off on the new road, the conversation's
tasks tool answered that no task had run, because it read only the old task
graph. It now lists a run's tasks from the run's own store, named the way the
rail names them, a task by its number and a part by its place under it, each
with its state and the first line of its result, and an id reads one of them.
The shipped listing of earlier tasks is kept beside it. The manual no longer
describes an ask box that no screen opens.
…operties, with the verdict basis recorded (#1220)

The worker declares its checks, and a holds verdict rests on a recorded, auditable, zero-exit run of every declared command. The worker records each command's own exit status, an absent status reads as unknown and earns nothing, and a record opens with a line that marks it as one that records exits, so a declared check that was never observed is refused, including when the record is missing or empty. A record written before exits were recorded still holds by reading and names the checks it did not observe. A does-not-hold verdict is ungated, a task with no declaration gets a reading verdict, and the verdict basis is persisted. One quote-aware reader of "is this one command" lives in internal/approval and is shared by the proposal door, the checker's runner gate and the store.
…her's keys (#1248)

A profile write now takes a cross-process lock on a stable config.json.lock
for the whole read, copy and rename, inside the in-process write mutex. The
lock rides internal/filelock, so it holds on every platform the repo builds
for. A write waits at most two seconds for another process and then fails
with a plain timeout error. The lock file is never unlinked: a crashed holder
is released by the operating system closing its file, so there is no stale
lock to reclaim.

Tests run two real processes forced to contend and show both keys survive,
a bounded timeout, and recovery after a holder exits without unlocking. Those
forced tests are unix-only; their clocks are sanity bounds, not load bounds.
…ump (#1249)

Both tests dumped the whole environment through the capped output collector
and read variables out of it. On a machine with a large ambient environment
the dump is cut before the TMUX_TMPDIR line, so the test read it absent while
the job shell had it, and the two checks for stripped variables passed without
proving anything. The shell now prints the three variables on one short line
with an explicit word for unset, through the same login shell road the product
uses, and the test fails if that line is missing from the captured output.
Test only; the product was already correct on every platform.
…llows on a beat (#1251)

The run's summary is a read nobody pressed for, and its refresh can wait on a
model for seconds. It was asked through the one ordered line that carries a
person's gestures to the engine, so a press on a run's row, or a stop, waited
behind it: 2.4 to 10.1 seconds on a real screen, 0.09 to 0.18 after. The
summary now goes beside the line. What may leave the line is decided by
property: the ask was not a gesture and nothing a person does next depends on
the engine having seen it first. A law lists every door that stands in the
line and fails when one is added without a reason.

An open page on a task that can still move was re-read on every paint tick
once the last read answered, 509 wire reads in ninety seconds for one page. It
now follows on the rail's own beat, counted from the last read for any reason,
and a page on an ended or held task is never read again. The follow stays in
the line because its fold replaces the page and must not overtake a note.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants