Repository navigation
fix(workspace): name a workspace by its resolved path - #1798
ningmao-hlyz wants to merge 3219 commits into
Conversation
…he same turn carries on (Agent-Field#1037) * approval: let account discovery reach its own question The shipped prompt blanket put approval questions in front of both account discovery and the connect card. Account listing now lifts as a read, while the connect tool lifts because its own card is the one consent question; named rules, deny, and the existing floors still win. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * session: carry account connections through the same turn Connect answers used to collapse refusal, silence, and typed instructions into one no, while the tool description told the model to wait for another turn. Answer provenance now survives to distinct sentences, one shared instruction says to continue on the next request of this turn, and the belt teaches the conditional discovery path. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * tui3: settle browser waits when their turn ends A browser row could keep waiting and animating after its turn had closed, and words typed beneath a browser question opened a key-check card. Turn settlement now closes those rows with the existing incomplete wording, and only typed-key questions can open the check card. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * session: hide browser sign-in exchange details Browser callback failures used to pass their raw exchange text, including a state nonce, into the transcript and tool result. The session boundary now keeps vendor refusals in plain words and replaces internal exchange failures with one honest, non-sensitive sentence. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * manual: tell the truth about account connections The manual said the browser stayed closed, concurrent credential writers could lose a change, and account bootstrap tools followed the ordinary approval path. It now describes the opened browser and retained address, locked store, single connect question, same-turn continuation, and the distinct ways a question can finish. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * session: let a connect answer beat the clock The five-minute branch could call an answer silence after its resolver had already claimed the question. The clock and resolver now share one atomic winner, and a claimed answer is returned even when its send is still in flight. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * connect: hide every failed browser exchange The session hid familiar OAuth failures, but unknown text passed through and the Connections tab still drew raw callback errors. Browser failures now cross one deny-by-default boundary; only a short vendor-shaped refusal is retained. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * session: prove account pickup on the shipped posture The same-turn acceptance ran under blanket allow, where its zero-consent claim could not fail. It now runs under the shipped prompt posture and proves the connect card is the only question before the newly armed tool runs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * manual: keep account answers inside the search window The privacy heading traded away the old asker's words, the SearchBoth guard was rewritten into a plain search, and the MCP inventory was split under false counts. Both vocabularies now retrieve, the rescue direction is loud again, and the original 28-row section is whole. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * session: the account cue pays for itself out of what was said twice The new account sentence and the same-turn wording pushed both prefix arms 79 bytes over their caps, and the sentence named `services` without the belt fact claiming that name. The fact now lists both tools it spells, says only what has to be true before the call, and `use_service` no longer repeats the ordering the fact already gives — leaving both prefixes below where they were on dev. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * changelog: the connect-on-demand entry for Agent-Field#1037 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…e keystroke (Agent-Field#1039) Home's ctrl+e writes the archive flag and then re-reads the world on the spot, so that the row moves under the hand rather than on the next sweep. The seam it re-read through answers from what is held and refreshes behind itself, and the write went straight past that cache to the engine — so the rebuild redrew the row exactly as it was, and the row went only when a later beat's fetch had returned and a later beat rebuilt. Two seconds of allowed staleness plus up to two three-second beats is the several seconds a person saw between pressing the key and watching the row go. hostWorld.archive is the seam now: the write over the wire, then the held copy corrected and the entry aged out so the next reading still asks the engine what it really thinks. That is hostStanding.save's correction, which already existed for the ambient side, applied to the one write home makes against the world. A refused write changes nothing here, and bringing a row back is the same correction in the other direction. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Agent-Field#1041) * module: the import path is github.com/Agent-Field/codeaf The repository is being renamed from aforge-v2 to codeaf; the module path moves first, on its own, so the mechanical import rewrite is one reviewable commit. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * build: the binary, the commands and the corpus tag are codeaf The commands move with the product name: cmd/aforge becomes cmd/codeaf, and the five neighbours beside it — census, changes, demo-home, release, replay — follow. `make build` writes bin/codeaf, the packed-manual build tag is codeaf_packed_manual, the release artifact is codeaf-release, the CI scratch root is /tmp/codeaf-ci, and .gitignore's stray-binary line is /codeaf. The manual page starting-aforge.md is starting-codeaf.md, and every reference to it moves with it. release.yml stops naming the repository: RELEASE_REPOSITORY is now ${{ github.repository }}, which is right before the GitHub rename, right after it, and right in a fork that publishes to itself. The wide textual rename of the product's own words is the next commit. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * rename: the product in the Go sources is codeaf The wide textual pass over every Go file: identifiers, strings, comments and test expectations spell codeaf, CODEAF_ and ~/.codeaf where they spelled the old name. It is the mechanical half of the rename and it is deliberately plain — no fallback reads the old environment variable, no adoption moves an old state root, no shim answers to the old command. Those arrive next, so that they are added to a tree with one name in it rather than a tree with two. Four things the machine could not do on its own, and a person did: - `AForge` inside an ordinary English word is not the product. A blind pass turned `AForgedTool` and `AForgettablePreference` into rubble; both are English again, spelled so the letters of the old name no longer occur. - The folder search ladder was documented and tested entirely in queries derived from the old name — its lead, its initials, its loose reading and the transposition the last rung exists for. `codeaf` shares four letters with the `~/code/` every one of those paths begins with, so several of those queries now reach a higher rung and would never have tested the rung they were written for. Every example is rederived from the new name and the ladder's own comment says what it now says. - The wordmark on the first screen had no letterform for `c` or `d`, and [wordmarkRows] SKIPS a letter it cannot draw — so the first screen of a fresh install would have spelled the name with two holes in it. - The guard that failed any first-screen sentence still carrying the retired name compared against a literal the same pass had renamed, so it asserted the opposite of its intent. That duty is the whole tree's now, and the name law is where it goes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * rename: the manual, the docs and the tooling say codeaf The same wide pass over everything a person reads or runs: the two manual corpora, the system prompt, the READMEs and design notes, the workflows, the installer, the bench harnesses and their self-tests. The pieces that are plumbing rather than prose, and had to move with it: - `.gitignore` ignores `.codeaf/` where it ignored the old repository-local directory. - CHANGELOG.md keeps every word it recorded and loses one: the HTML comment `codeaf-changes roll` inserts beneath is the tool's anchor, not a release note, and the tool had just been renamed out from under it. - `internal/exec/testdata/rtk` is a stub, and the only thing it shares with the test driving it is the name of the variable it logs to. Renaming one and not the other left the stub logging to /dev/null and the test reading an empty file. - The realissue bench rig names the arm it runs. `bench/conversation`'s own runner had already moved to `codeaf`, so a candidate still declaring the old arm was one the runner would refuse to start. `test/remote`, `internal/furrowbin` and the two `docs/design` capture scripts are renamed too. They sit outside the paths the wide pass was pointed at, and each of them names the binary, the state root or an environment variable that had already moved — a stale spelling there is a broken command, not a stale word. furrow is still furrow throughout. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * manual: the pinned-version install line gives VERSION to bash `VERSION=<tag>` written before `bash` in a pipeline sets the variable for `curl`, which does not read it, and not for the script on the other side of the pipe — so the documented way to pin a release installed whatever `latest` resolved to. README.md, docs/rules/promotion.md and release.yml all spell the piped form already; this page was the one that did not, which is why internal/release's TestPublishedPinExamplesGiveVersionToBash was red on dev. The example is now a runnable line rather than a sentence describing one. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * manual: the page says what codeaf used to be called `## What it is called` carried a sentence contrasting the wordmark's spelling with the prose's, and the mechanical rename collapsed both halves onto one word — it read "it used to say codeaf in some of those places and codeaf in the others", which says nothing and is the kind of sentence a reader stops trusting the page over. The history moves to its own `## The old name` section, which is where it can name the retired spellings honestly, and which is the one place in this corpus the name law lets them be written. It answers the five questions somebody actually asks — what happened to aforge, is this the same thing, is this the CodeAF from the benchmarks, why is the folder .codeaf now — and it states the three things on a person's machine that keep working: the adopted state folder and the link left behind, the AFORGE_ variable still read when the CODEAF_ one is unset, and .aforge-v3/config.json still read inside a repository. The section is written long rather than dense on purpose. A short section whose heading carries every one of those words outranks its own neighbours under BM25: the first draft took the top result for "how do I start codeaf" away from `## Starting it`, which is the page talking over itself. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * chat: three comments and a failure message that named two retired products Each of these was written when the surface spelled one product in the letterforms and another in the prose three rows under it, and each named both. The mechanical rename wrote the same word into both halves, so all three now said a thing happened and then described it happening to itself. They are rewritten to keep the anecdote — which is the ONE SOURCE OF TRUTH law's own worked example and worth keeping — without naming a product that no longer exists. The name law covers internal/tui3, so restoring the dead spellings is not available to them, and a `legacy-name` marker is for a compatibility seam rather than for prose. TestTheWordmarksRightEdgeIsNeverAHoleBetweenTwoStrokes failed with "the name would read `aforga`", which was the old name with its `e` drawn as an `a`. It derives the word from [product] now, so the message stays true through the next rename as well. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * namelaw: a live surface cannot quietly say the old name again A rename is finished when the tree stops being able to undo it by accident. A wide `sed` makes every literal say the new word once; nothing then stops the next wave writing the old one back into a manual page, a workflow or a first-run sentence, and nobody finds out until somebody runs the wide search again. So the law is a package of nothing but the law, found by scripts/laws.sh through its go/parser import and therefore on the pull-request gate the day it lands. It reads every Go source under cmd, internal and bench; go.mod, the Makefile, the ignore file and the size budget; the workflows, the issue and pull-request templates and the rulesets README; the scripts and the bench tooling; both manual corpora and the session prompts; and the documents an agent is pointed at. A failure names the file and the line. The exemptions are records rather than holes, and each says why beside itself. CHANGELOG.md, docs/changes, docs/design, the captured frames, bench-results, audit-notes and everything under a testdata directory say what was true on the day they were written. A line marked `legacy-name` is a compatibility seam — inline it exempts only itself, and on a line of its own it reaches forward to the first blank line, which is how a multi-line string or a two-line shell fallback is marked once. A Markdown section headed with the words "old name" is the one page that explains the history to a person who asks. The root Markdown set is an allow-list and not a glob, so a lane's untracked spec or notes at the root is not silently promoted into a product surface. Fixture tests prove every boundary on a temporary tree rather than on this one, so the repository-sized assertion going green means the surfaces were read rather than that the walk found nothing convenient. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: the agent-facing guides name the product, and the name it replaced CLAUDE.md and AGENTS.md are one file reached through a symlink, so they stay byte-identical by construction. They gain `## The name` — codeaf, lowercase, since 2026-09-14, and the law that enforces it — and `## The old name, and the three places it is still allowed`, which names the retired spellings outright. Naming them is the point: an agent whose memory predates today will spell them, and a section that refuses to say the word cannot tell that agent it is wrong. The section's own heading carries the words "old name", which is the law's exemption, so the guide is allowed to say what it has to say. Two sentences the wide pass left standing on one word: PERF.md's owner ruling had become a tautology about builds, and docs/CHAT-V3.md still described the rename as a refactor that had not happened yet while spelling the new name for both sides of it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * tui3: the folder-search comments reason from the new name Four comments still explained the ladder with queries derived from the old name (an initials example with a letter the new name does not have, and a slip example that no longer slipped). They reason from the rederived examples now. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs/design/polish: the three audits keep their record The wide rename made both sides of a recorded comparison the same word, so sentences that once said the wordmark spelled one name while the prose spelled another became false. A dated audit is a record; it keeps the words it saw. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * env: one door for the product's own variables, and the old spelling behind it Every CODEAF_ name the product reads now goes through internal/env, which answers the CODEAF_ spelling and falls through to the AFORGE_ one when the new name is unset or set empty — so a machine whose shell still exports the former names keeps working for one release. There are two doors on purpose. Get and Lookup are for a name the caller knows at compile time, and they PANIC on anything that does not start with CODEAF_, because a constant naming somebody else's variable is a programmer error. Value and LookupValue are for a name that is a value at runtime — a settings row's Env field, a knob table, a seat, an onboarding row — where the same table can hold OPENROUTER_API_KEY beside CODEAF_MODEL; an owned name there gets the same fallback and a foreign one is read plain. The remaining call sites move in the commits that follow, and the law that keeps them there lands with the last of them. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * home: both names resolve the state root, and the first real start adopts the old folder CODEAF_HOME wins; AFORGE_HOME is honoured when it says nothing. With neither set the root is ~/.codeaf when that exists, ~/.aforge when only that exists — a legacy root the adoption below could not move — and ~/.codeaf otherwise. THE ADOPTION IS ONE-WAY, ONCE, AND NEVER FATAL. On a machine with no ~/.codeaf and a real ~/.aforge directory, the first start of the real binary renames the folder and leaves ~/.aforge behind as a symlink to it, so an old PATH line (~/.aforge/bin), an old unit file, a running daemon and anybody's own script keep resolving. A second start changes nothing. Any failure is one line and the process continues — the resolution above then reads the old root, which is why the fallback exists rather than being dead weight. It is called from main and from nowhere else, and the exported door returns immediately inside a test binary: a test that resolves the state root must never move the state of whoever ran it (internal/home/undertest.go says why). The machinery underneath is driven directly by its own tests. `codeaf help env` says once that the AFORGE_ spelling is still read for one release, and the demo home now strips both spellings from the child it launches — an inherited AFORGE_HOME would otherwise point a throwaway launch at the owner's own state root, which is the single thing that file exists to prevent. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * project: a repository's own files are read under either name and written under one The three places codeaf keeps something inside somebody else's repository move to the new name and keep reading the old one: project settings are <workspace>/.codeaf/config.json with .aforge-v3/config.json read when the new file is absent, a conversation's autonomy rules are <workspace>/.codeaf/ autonomy.json with .aforge/autonomy.json behind them, and a repository's subharness shelf is <repo>/.codeaf/subharnesses with <repo>/.aforge/ subharnesses behind it. READS FALL BACK; WRITES NEVER DO. Every writer names the new path and leaves the old file exactly where it is — nothing here rewrites a person's repository on its own, and somebody who renames the folder themselves simply stops using the fallback. The comment above ProjectConfigDir said this directory was deliberately not on the product's final name because the rename would happen once, at the end. This is that rename, so the paragraph now says what is true instead. The orientation digest skips .aforge and .aforge-v3 beside .codeaf, so a repository that still carries either is not read to the model as source, and .gitignore knows about the new directory as well as the task system's own. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * tasks: the work is committed as codeaf and recognised under either name A task commit is authored codeaf <codeaf@localhost>. Every reader that has to tell the task system's own work from the person's accepts the former identity too, because an existing repository carries commits made under it: a branch that has only ever moved under one of those two identities has not been moved by the person, and a landing must not be held back from it. THE SAME RULE ONE DIRECTORY OVER. The folders the task system leaves inside a repository are still written as .codeaf-v3 — live worktrees are registered under that path by git itself and moving it is not something a compatibility layer may do — but every reader of them now recognises .aforge-v3 as well: branch protection, the audit walk, the mirror copy, the dirt reading, the left-behind scan, and the clean and reset that must not sweep an old folder away. One list, in one place, rather than a name spelled into nine files. An empty commit range is also no longer read as the person having moved the branch. The early returns above it mean the range normally holds a commit, but a reading that comes back with nothing has observed nothing — and the reading- failure case beside it already answers that the same way. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * standing: one timer per machine, and a child hears both spellings Installing the timer now stops and removes an aforge-tick unit on the same machine before writing codeaf-tick, on systemd and on launchd alike. A machine that had the old one would otherwise tick twice — two processes doing the same standing pass against the same state root, five minutes apart forever. The units it writes carry the state root under BOTH names, and the reader that digs the root back out of a unit file on disk accepts either, so a unit written by the old build still resolves after an upgrade and a unit written by this one still works if something downgrades. The same for every variable the product hands to a program it starts: a skill's check.sh, a skills-bin shell, an rtk shell. They receive the CODEAF_ name and the AFORGE_ one with the same value, because a script somebody wrote last month reads the name it was written against and this program cannot edit it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * remote and install: the far command is codeaf, and the law holds the door shut A host that answers "command not found" is told the program is called codeaf now — it was aforge before 2026-09-14 — and must be installed there under that name. There is no retry under the old name: a machine that has the old binary has the old program, and starting it would be a conversation against something else. The recogniser reads either word out of the far shell's own wording, because the sentence a person needs is the same either way. The installer pulls Agent-Field/codeaf, installs into ~/.codeaf/bin, marks the PATH line `# codeaf installer`, and reads CODEAF_* variables with the AFORGE_* ones as fallbacks. Two transitional roads, each with a comment saying when to delete it: a release lookup that 404s under the new repository is retried under Agent-Field/aforge-v2, and a release with no codeaf-<os>-<arch> asset installs the aforge one — under the new name, since what is installed is this program. AND THE LAW ARRIVES WITH THE LAST CALL SITE IT GOVERNS. internal/env's law reads the tree with go/parser — so scripts/laws.sh runs it on every pull request — and fails any product read of an owned variable through os: a CODEAF_ or AFORGE_ literal, a constant anywhere in the repository that holds one, or either pulled out of os.Environ. The table-driven families that cannot be seen through statically are held by a count of their dynamic-door calls instead. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * resident: the skill-check test names the old spelling where the law can see it The marker inside the embedded shell script was a string to the name law, so the tree was red on a line that was a legitimate shim. The old spelling is a Go constant on its own marked line now, interpolated into the script. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * pair and relay: a name on disk or on the wire is not renamed with the product A device key, a relay machine name, a registration label, two protocol tokens, two HTTP headers and the pairing PAKE's two identities are bytes an EXISTING machine already wrote down or already speaks. The wide pass renamed all of them, which unpairs every paired device (`decodeSeed` rejects a key written by any earlier build, and `Load` answers "is not a device key any more — remove it … which un-pairs every device it was paired with"), changes the machine name a person types after `--at`, and leaves an old relay unable to see either header. They keep their former spelling permanently, each marked `legacy-name`: these are not a one-release seam, they are the wire. The device-key reader accepts both prefixes and keeps writing the old one, so a key written by this build is still readable by a machine that has not upgraded. Only labels a person reads say codeaf. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * install and release: the folder is adopted before it is made, and an old release still resolves Three things the install road got wrong, each of which meets a person on the day they upgrade. THE INSTALLER MADE THE NEW STATE ROOT BEFORE THE BINARY COULD ADOPT THE OLD ONE. `mkdir -p "$INSTALL_DIR"` creates `~/.codeaf/bin`, and adoption declines the moment `~/.codeaf` exists — so every existing person who upgraded through the documented road looked brand new, with their conversations, projects, memories and keys on disk and unreachable. Proved against the real binary. The verified download is now run once from the temporary directory first, while `~/.codeaf` is still absent, and only when the install directory is under the state root. A PINNED VERSION COULD NOT REACH THE FORMER REPOSITORY. `VERSION=<tag>` skips the API call that carries the repository fallback, so a tag published before the GitHub rename resolved against a repository that does not exist yet — which is exactly the window this release sits in. The asset download falls back to the former repository too, keeping the checksum in the same one it chose. THE FORMER PATH LINE WAS LEFT BEHIND AND DUPLICATED. A shell file carrying `# aforge installer` kept a PATH entry for `~/.aforge/bin` — through the adoption link, the OLD binary — and gained a second line beside it. The old marker is recognised and its line is replaced in place. Also: the Release workflow read `.github/known-red.txt` under `set -euo pipefail`, and that ledger burned to zero in Agent-Field#1012. Every non-dev release job died before its tests. The Makefile already guarded for it; the workflow does now too. That line is byte-identical on dev — it is not this wave's regression, but this wave's release runs through it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * standing and wake: a timer written under the old name is seen, repaired and removed Installing a timer already removed its former-name twin, but nothing ever reached Install on the machine that needed it. `Timer.read` looked only at the current unit path, so a machine carrying `aforge-tick.*` reported no timer at all: the standing page said nothing was installed while the old unit went on firing, startup had no drift to repair, and turning standing off left it running. Reading falls back to the former units and reports them as installed and stale, and Uninstall removes both generations on both platforms. `internal/watchdog` — the v1 resident's wake timer — had no former-name spelling anywhere in the file. Status, install and uninstall each knew one generation, so an upgraded machine kept an active `aforge-wake` unit whose ExecStart still resolves through the adoption link to the OLD binary, invisible, and installing the current one gave it a second timer against one state root. It now carries the same shape internal/standing has. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * home: an unusable current root falls back, and the page stops promising the link Adoption and resolution both treated ANY existing object at `~/.codeaf` as the state root. A regular file there declined the adoption and was then returned as the root, so a person with real state in `~/.aforge` got `.codeaf/graph.db is a file, and a folder was needed there` and no way to their own things. Both now require a directory: anything else logs one line and the former directory goes on serving. The manual said codeaf leaves a link at the old path AND that a failure means it carries on reading the old folder. Those cannot both be true of the same run — the link is best effort AFTER the move, so a move that succeeds and a link that fails leaves no old path at all. The page says it tries to leave the link, and that it is the MOVE failing that keeps the old folder in use. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * environment and caches: the one door is used everywhere, and a cache is not thrown away A key reference a person STORED was read with `os.Getenv`, so a stored `CODEAF_X` got none of the fallback the environment package exists to give — and the law could not see it, because the law only knows a read whose argument it can read at parse time. The reference goes through the dynamic door, and the law gains injected fixtures for every shape it claims to refuse plus a sentence saying a runtime-supplied name is beyond it and `env.Value` is the door for one. The extracted-document cache headers were renamed, and the reader compares them exactly — so every `.extracted.md` on an existing machine became a miss and the document was extracted again, which on the OCR and model engines is money per document. The former headers are accepted on read; the current ones are still what is written. Four surfaces said an `AFORGE_*` name is read when the `CODEAF_*` one is "unset". The code also falls back on an empty one. They say "unset or empty". Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * namelaw and English: one marked line is one exemption, and "an codeaf" is not a word A line that was exactly `// legacy-name` exempted every following non-blank line — a hole wide enough to hide a paragraph, and a different law from the one CLAUDE.md states, which is that each line carries its own marker. Nothing in the tree relied on the run, so only same-line markers open the exemption now, and the fixtures that blessed the run refuse it instead. Two markers sat on lines with nothing retired on them and are gone. `aforge` takes "an" and `codeaf` takes "a". Twelve sentences the wide pass left mid-sentence now read, including the two a person meets — the terminal page's "An `codeaf do` errand" and the models page's — and `internal/furrowbin`'s promise that "every codeaf is an codeaf with furrow". Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * tasks and media: the repository's own folder is .codeaf, and the former one is still read Stage 1's wide pass turned `<workspace>/.aforge-v3/` into `.codeaf-v3/` — a spelling that has never existed on any disk — while the settings layer had already put the repository's own file at `<workspace>/.codeaf/config.json`. Two folders for one product is the thing this wave exists to remove, and every reader written as the LEGACY rung was spelled `.codeaf-v3`, so it found nothing anywhere. One folder now. Every dropping — tasks, images, music, audio, video, jobs, stubs, attachments, the ground baseline, the release receipt, the left-behind record — writes under `<workspace>/.codeaf/<kind>`, and every reader of a kind accepts `.aforge-v3/<kind>` beside it, marked `legacy-name`. Reads prefer the current path and fall back only when it is absent. Nothing rewrites somebody's repository on its own. THE TASK WORKTREE WAS THE ONE THAT BIT. `tasksDirName` was the only spelling used to build a worktree path and the git-root lock, so a task registered under `<repo>/.aforge-v3/tasks/<session>/<id>` — every task on every machine that has run this program — was not found, not resumed and not cleaned, and an old binary and a new one took DIFFERENT locks on one repository. Both paths are tried, and a fixture registers a real `git worktree` under the former path and carries it through reload, resume and removal. Nine manual pages told a person to use `.codeaf-v3/config.json` for a repository's settings — a path the code has never read. They say `.codeaf`, and the tasks page names both spellings once where it says what is never committed. `.gitignore` drops the line that never named anything, and the orientation digest stops walking a directory it could not recognise. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * changes: the entry for the rename carries its pull request number Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * config: the receipt names the spelling of the variable that answered A person who set AFORGE_MODEL was told the model came from CODEAF_MODEL, a row they never wrote. The env door now says which spelling supplied a value, the seat records it when the environment rung answers, and the receipt says that word. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* codeaf-changes: a notes command renders one version's section to fit a GitHub release page GitHub refuses a release body over 125,000 characters, and a rolled-up section carries every fold of every entry — the v0.2.0 section renders at 1.7 MB. `codeaf-changes notes <tag>` prints the section whole when it fits, the headline layer with the folds removed when it does not, and a cut at a line boundary when even that is too long, always ending with the line that says the record is in CHANGELOG.md. A tag with no section is a non-zero exit and nothing on stdout. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * release: the stable notes go through codeaf-changes notes, not an awk copy of the section The publish job copied the whole `## <tag>` section of CHANGELOG.md into the release body; the first roll-up would have failed the stable publish at the last step, after six binaries were built. The workflow now asks codeaf-changes for notes that fit, the workflow test refuses the awk copy coming back, and the two rules say what the page carries when the section is bigger than a body may be. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs/changes: the entry for Agent-Field#1044 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* docs/changes: v0.2.0 rolled up into CHANGELOG.md The 613 loose entries in docs/changes/unreleased/ — every pull request since v0.1.0, the codeaf rename included — are now the `## v0.2.0` section of CHANGELOG.md, headline lines with the claims one fold away, and the loose files are gone. This lands on dev before the promotion, as docs/rules/promotion.md orders, so the stable release reads its notes from this section. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * exec: the removed-worker law reads CHANGELOG.md as the record it already reads docs/changes as The law skipped docs/changes because a changelog entry says what was removed, by name, and rewriting it would falsify the record. The first roll-up moved those entries into CHANGELOG.md, which the law did not skip, and went red on the laws. The rolled-up changelog is the same record. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs/changes: the entry for Agent-Field#1045 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…Agent-Field#1047) * release: the release-surface test survives an absent known-red ledger The ledger burned to zero and was deleted (Agent-Field#1012). Under set -euo pipefail the step's first line — an awk over the missing file inside an assignment — exited 2 before go test ran, on every channel but dev, which skips this job. The first staging build after the deletion published nothing that way. The file is read only when it is there, as scripts/laws.sh reads it, and a test runs the real prologue under the real shell options with the ledger absent, empty, comments-only and carrying a name. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs/changes: the entry for Agent-Field#1047 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…ue on release day (Agent-Field#1050) * Make the release-day install story true Describe each release channel and the conditions on both installer roads. Disclose the bounded background rtk fetch used by codeaf do, and state when GitHub can enforce the checked-in branch rules. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs/changes: the release-day truth entry for Agent-Field#1050 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…Field#1051) * Third-party notices, generated from the build and guarded by a law Nothing in this repository reproduced the licences of the code compiled into the binary. internal/pair/cpace is BSD-3-Clause and linked into every build, whose clause 2 asks for the notice in the materials that go out with a binary distribution; internal/connect/ampcatalog/providers.json is an embedded copy of the amp-labs catalog; and every module in the build carries the same shape of obligation. None of them had a notice anywhere. THIRD-PARTY-NOTICES.md is that notice, and cmd/codeaf-notices generates it from the build itself — the modules `go list -deps ./cmd/codeaf` names, plus the two components this tree carries its own copies of — so it cannot be written by hand and cannot quietly go stale. Like codeaf-changes it is a developer's binary and never a verb on codeaf, so the product pays nothing for it. The law beside it reads the same module set and fails when a compiled module has no section, when either copied component is missing, and when the committed CPace licence text is no longer reproduced word for word. It needs no network and decides in a third of a second. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * A release carries the third-party notices beside its binaries The release workflow published dist/codeaf-* and checksums.txt and nothing else, so the notice existed in the repository and reached nobody who installed the thing. A licence that asks for its text in the materials provided with a binary distribution is not satisfied by a file on a branch. The build job now copies THIRD-PARTY-NOTICES.md into dist/ before the checksums are written, so checksums.txt covers it and the publish step — which already uploads dist/* — attaches it to every release on every channel. The installer needs no change: it looks up only the row naming its own asset and ignores the rest. internal/release asserts both lines, because a copy that quietly stops happening is exactly the kind of omission nobody notices until somebody asks for the notice. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * README and the promotion runbook name the notices Somebody reading the install section had no way to know a notice ships, and somebody following the release runbook had no way to know it needs regenerating when the dependencies move. One line each, where the reader already is. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs/changes: the change entry for the third-party notices Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Agent-Field#1056) * The installer takes the newest channel build, not the first one listed `--rc`, `--dev` and `--staging` read `releases?per_page=100` and took the first tag that matched the channel. GitHub does not return that list in publish order: with `--dev` today it chose `dev-20260915-e2ae913b7d0c` (13:43Z) while `dev-20260915-56a22c20ec53` (14:56Z) and `dev-20260915-4b6ec83cfbfa` (15:21Z) both existed, and a person got a stale build with no sign anything was wrong. The whole list is read now and the candidate with the greatest `published_at` wins, falling back to `created_at` when a release has no publish time. The refusal named only one of its two causes. A private repository answers an anonymous call with a 404 and took the same path, so a person was told about a rate limit that was not the problem; the sentence names both now. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * The change entry for the installer's channel pick Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
The chip above the message box walked five stops and wrapped from `max` to `low`, so neither `ctrl+v` nor a press on it could reach `auto` — the state a shipped install is in. Putting a dialled conversation back meant leaving the control in front of you for `/effort`. It walks the clearing wheel now, the one the model picker's `ctrl+t` and a task's own rung have always turned: one press past `max` hands the scope back. Clearing on a machine whose own `thinking` row is set says so honestly rather than blaming a level on the model and pointing at a chord that would not move it. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…orking directory (Agent-Field#1042) * tui3: an empty target is nothing to open, and never a window on the working directory `open ""` on a Mac is not an error. The platform resolves the empty path to the calling process's own working directory and puts a Finder window on screen — so `startOpener("")` did not fail, it opened a file manager, and said nothing. The caller that did it is the sign-in handoff. `connectAuth` passes `ev.AuthURL` straight to `openConnectFlow`, which hands it to the platform; an EventConnectAuth carrying no URL therefore opened a window onto whatever folder codeaf was started in. Under `go test` that folder is the package's own source directory, and `salience_test.go`'s table — which drives EVERY event kind through the conversation's pump, EventConnectAuth among them, with no AuthURL and no stub over `processOpener` — did exactly that on every run of `./internal/tui3`, in every worktree. Four of them are in this laptop's Finder recents, one per checkout, each at `internal/tui3` and nothing else in the tree. `open` raises an existing window rather than making a second one, so it read as one window shoving itself in front of the person over and over. The guard is in `startOpener` and not at the call site, because all six doors into the platform deserve it and a number that appears in two places drifts. It says the sentence a PATH miss already says: a person is owed the same answer either way, and the link written under every handoff is still the way through. `salienceRun` now takes `watchOpener`, so no row of that table can reach the machine running the test whatever door a future kind grows. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs/changes: the change entry for the opener's empty target Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…r walks your home (Agent-Field#1058) * tui3: the folder walk's budget bounds every entry, and the suite never walks your home Two halves of one fault, and the second is what made the first visible. THE BUDGET WAS NOT A BUDGET. `folderIndexWalk`'s clock check stood below the walk's `!entry.IsDir()` return, so a FILE never consulted it and the walk got a turn to stop only BETWEEN DIRECTORIES. The comment over the constants said so out loud — "checked before traversal and at each directory" — and that reading was the defect rather than the description of it: the directory that ends a home walk on a Mac is `~/Library/Caches`, one directory holding hundreds of thousands of files, and a bound never read while those stream past is not a bound. Measured on the owner's laptop: `scanFolderRoots` took 5m58s under a three-second budget and returned 1144 roots. The check moves above the file return; the constants' comment now says what the code does. THE SUITE WALKED THE DEVELOPER'S OWN HOME DIRECTORY. tui3_test.go's TestMain moves the state root and says in so many words that it leaves HOME alone, because this package DRAWS `~` in front of paths and those readings are about the real one. `scanFolderRoots` does not draw `~`, it WALKS it, with os.UserHomeDir — the one thing that seam does not cover. So any test that opened the folder picker indexed the whole home of whoever ran it: six minutes on a laptop, nothing at all on a runner whose home is empty. That is a test that passes there and fails here, which is exactly what TestMain exists to prevent, and it is how `TestThePicksLandingMidBrowseKeepTheChoicesAndThePreview` came to panic on the driver's five-second budget while CI stayed green. `folderRootScan` is now the seam, for opener.go's [processOpener] reason, and TestMain answers it with nothing. The manual said the index is built in the background and refreshed about once a day, which stayed true, and said nothing about the walk being bounded — which was fine while it ran to the end of the disk and is not fine now that it stops. `choosing-a-folder.md` states the three stops and the way past them. `folderIndexWord` already writes "there was more to look through than this list holds" for exactly this case and NOTHING DRAWS IT. That is left alone here: wiring a sentence onto the picker is a different change from making the number it reports true. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs/changes: the change entry for the folder walk's budget Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * tui3: the sentence about a bounded walk is deleted, because nothing ever said it `folderIndexWord` wrote two sentences — `there was more to look through than this list holds`, and a count of the folders a permission refused — and no surface has ever called it. Three tests asserted its wording, which is what kept it looking alive. A person-facing sentence no door reaches is worse than absent: it reads, to anyone grepping for it, as a promise the product keeps. It does not. The facts are [folderIndexAnswer.Bound] and `.Denied`, they are still set, and the three tests now stand on those instead — including the prefix-of-the-truth law, which was written about the answer and is now attached to it. A picker that says out loud what it could not see is a real thing to want, and it is a change that adds a door rather than one that deletes a corpse. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* tui3: ctrl+c leaves on the press that lands At rest ctrl+c ends the session on the keystroke that lands, over every picker, panel, room, page and paste bracket — leaving is never modal. The arm is gone: no first press that only warms the door, no 1.5-second window, no `ctrl+c again to quit` in the hint slot. Mid-turn nothing changes. ctrl+c is still the interrupt, and that press is spent on the model, so a two-tap mid-turn stops the answer and then leaves. WHAT THE ARM WAS PROTECTING IS STILL PROTECTED, by the exit path rather than by a keystroke: the draft and every parked message are written down by app.quit itself, which a real SIGINT and a closed window already took; work a session host is running outlives the window whatever this surface does. What is genuinely spent is the WARNING — nothing on the way out names running work any more. The close-a-tab card still does. quitarm.go goes; leavingDraft, all that is left of it, is leaving.go. The work-counting helpers move to tabclose.go as workCount/workCountWord, which is the one card that still reads them. The manual pages say the new rule in the asker's own words, and three retrieval probes that went red on the way were fixed in the pages rather than in the test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: the change entry takes the pull request's own number Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…open the thing (Agent-Field#1043) The spend place drew a chart and two ragged tables, and its doors went to the wrong screen. It draws ONE CUT of the ledger now — `by topic` or `by model`, the same money added up two ways — with the heading as the control that swaps them. Four columns each, the name left-aligned and every figure right-aligned into a block that ends where the chart ends. The bars are gone: the list is sorted dearest first and every row says what it cost, which is the same comparison in figures. `enter` on a row opens the thing itself. A task opens its own record card, found by the pair that identifies one — its id and the conversation that ran it — and a conversation is brought forward or opened beside the one you are in. A thing the record no longer holds refuses where it stands. Money is a column of cents with a floor; the head line, /cost and the Spending tab keep the exact arithmetic. Thousands are marked everywhere and token volumes gained a rung above the million. A row gives up its words before its figure. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* tui3: a home panel stands where its content puts it (Agent-Field#1046) Home's law 2 was `act left, watch right` — the person's three panels on the left, the machine's four on the right, whatever any of them held. On a quiet machine that drew six whispering headings over a screen that was five sixths scaffolding, with the one panel that had rows boxed into a third of the width. The column is now a reading of the panel's content. A panel with rows stands in the FIELD, the columns left of the last one, filled from the top left corner down; a panel with nothing in it stands in the RAIL, the last column, flush right, as its heading and its whisper. `projects` and `spend` are pinned to the top of the rail because their height is the same on every machine on every day, and one blank row separates them from what is only quiet today. The rank inside a column never moves, which is what keeps the cost to one axis. The field fills rather than balances, so the cursor's panel is in the same column at every width the content fits in — a landing that moved with the frame would be a landing nobody can build a habit on — and a wide frame leaves its middle empty rather than spreading two short columns over it. `←→` now step over a column with nothing to stand on. Without that the rail would be out of the arrows' reach on any wide frame whose field fits in one column: projects and spend drawn on the screen with no key that walks to them. DESIGN.md carries the amended laws 2, 3 and 6 and says what each used to say. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * tui3: the resting box keeps a caret on the cell the first letter lands on Home's box is the screen's one primary action (DESIGN §1 law 1), and with nothing typed into it the caret was switched off — on the reading that home at rest is a dashboard somebody reads rather than a thing they type at. The owner's reading is the other one: a box with no caret in it does not look like somewhere to type, which is the same complaint that made the box three rows tall in Agent-Field#1000. The hazard the old comment names is real and is answered by PLACING the caret rather than by hiding it. Unplaced it blinks at the frame's origin, over the `home` heading; it now stands on the first cell a character will land on, which is the dim sentence's own first letter — a placeholder behind a caret, as in any other text field. It is the same caret the conversation's box has: one blinking tea.CursorBar, wherever the frame puts it. The resting rows keep their empty press span, so a click on them still falls through to the place underneath (Agent-Field#1000). Every place's resting box gains the caret, not only home's — it is one box with one behaviour. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * tui3, demo-home: a standing row names enter only where there is a conversation to open Two findings from one report — `enter` on a `needs you` watch row did nothing. THE LEGEND PROMISED A DOOR THE ROW DID NOT HAVE. drawKeysBand named `enter open where it was asked` on every item, and an item with no origin has nothing to open — [app.homeItemEnter] refuses it in words. The legend's own law, stated three paragraphs above the branch that broke it, is that it names keys that work; the design's is that a capability that cannot work is absent, not broken. The clause is now drawn only where Origin.Transcript is set. THE REST WAS THE FIXTURE, twice over. Every standing order the demo seeder wrote had an empty Origin, so the demo never exercised the door at all. seed.go already keeps every conversation's id by title so that rows ABOUT a conversation name one really on disk — "a join that points at nothing draws a row a person cannot open" — and the standing orders were the one table left out of that discipline. Each now names the conversation it was asked for, and the seeder fails loudly if that title is not one it writes. And the fixture's own directory was two project buckets. On a Mac `/tmp` is a symlink to `/private/tmp`: os.MkdirTemp hands back the `/tmp` spelling, a codeaf launched inside resolves its own working directory to `/private/tmp`, and home drew the demo project twice — `…/codeaf` with the fixture's conversations and `~/codeaf` with the window's own — while the bucket guard refused every door between them. demoDir now resolves its symlinks before anything is written. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * tui3: the middle column is what the selected row says about itself The field spilled into the middle column when it ran out of vertical room, so the middle was a panel sometimes and blank other times — and on the frame a person actually has, a tall terminal and a quiet machine, it was blank always. THE FIELD IS NOW ONE COLUMN AT EVERY WIDTH and what will not fit in it folds, which is the squeeze that has always run at the widths with no second column to spill into. That buys a middle column that means one thing at every size: the description of whichever row is selected, wrapped over as many lines as it needs, with the row's answers under it. It is assembled at PAINT time and not at build time, which is the whole reason it can follow the cursor: the grid's lines are settled when the room or the width moves and an arrow key moves neither. It follows the pointer through the same previewLine the card beside the search uses, and it starts ON THE ROW IT IS ABOUT — a sentence at the top of a column while its row is eight lines down is a sentence whose owner a person has to work out. Nothing in the column is a stop, so `→` steps over it to the rail rather than parking the cursor on a sentence about the row it just left. A ROW IS THEREFORE ONE LINE, and the panel neither draws the second line nor reserves the room for it. Both kinds move: the needs-you question that was always drawn, and the sentence a to-check landing grew only under the cursor. At two columns and at one there is no middle, and every row keeps its own second line exactly as before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * tui3: needs you keeps its question under its row Everywhere else a row's second line is a GLOSS on the row — what a piece of work is doing, the first sentence of a report — and a gloss is exactly what a column about the selected row is for. On `needs you` the second line IS the row: the question is the thing that stopped, and the panel's whole purpose is that a person reads what is waiting on them without walking the cursor onto it. A question you have to select to read is a question you can miss. So a permanent sub on `needs you` stays under its row, keeps its reserved line in the panel's height, and the description column draws nothing for it — the same words twice on one frame is the reader wondering which is which. The `to check` landings in the same panel grow their sentence under the cursor and have never been readable at a glance, so they move to the column like every other row's. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * tui3: the description column is every row's note, aligned to its row Three things the owner asked for, and they are one mechanism. `enter` ON A NEEDS YOU ROW IS NAMED ONLY WHERE IT WORKS. `subRight` was set on every row of the panel, so a watch made from home's own box — whose origin is an EXCHANGE under the item's folder rather than a session ([standing.Origin]) — drew `enter` beside a question while [app.homeItemEnter] answered `made from home — no conversation to open`. A conversation's question always has a door and keeps the key; a watch has one only where it was asked for in a conversation. This is the legend's law said on the row itself: NAME ONLY KEYS THAT WORK. THE NEEDS YOU QUESTION IS IN THE COLUMN NOW, AND ALWAYS. It was inline as the one exception; it is instead the one row whose note is drawn whether or not the row is selected ([homeCell.alwaysSaid]), because the panel exists so a person reads what is waiting on them without walking the cursor onto it. Every row is one line again. WHICH WORKS BECAUSE EVERY NOTE STANDS ON ITS OWN ROW'S LINE. Which row a sentence belongs to is a fact about where it is rather than something the reader works out, so a permanent note and the selected row's can share the column without either being ambiguous; a note runs down only as far as the next note's row, so two never overlap. AND TWO MORE PANELS HAVE SOMETHING TO SAY. A `where you were` row draws the last thing this person said in that conversation — the one fact that tells two conversations with similar names apart, and the reading this window's own row has always drawn. It is asked for THE ROW BEING READ and cached, off the draw: a screen that read forty journals to fill a column about one row would be paying for thirty-nine nobody looked at. A `next up` row draws the arrangement in the person's own words, and nothing where those words are already the row's title. Both are carried only on a frame that HAS the column. A sub reserves a line in its panel's height, and paying that on a narrow terminal for a column that is not on the screen costs a row a person could have read. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * tui3: a row's keys are its own, and enter on a home-made watch opens it on standing THE KEYS BELONG TO THE SELECTED ROW AND NOBODY ELSE. A `needs you` question is in the description column whether or not its row is selected, and it was drawing `enter` beside it the whole time — a key advertised on a row a person is not standing on is a key that would do something else if they pressed it, which is law 7 inverted (a row draws its answers so the key is never a guess). The sentence now stands alone until its row is the one being read, and the key joins it at the right of its own line rather than on a second line below. A PERMANENT NOTE IS ONE LINE, SELECTED OR NOT. It shares the column with rows above and below and may not grow into them; only a note that is there BECAUSE it is selected has the column to itself and wraps. AND `enter` ON A WATCH MADE AT HOME OPENS IT ON STANDING. It refused in a sentence and left the cursor where it was, which is the worst of both — nothing happens AND the screen explains why on the row somebody is trying to leave. Such an item has no conversation behind it (its exchange is kept under the item's folder rather than as a session) but it has a page, and that is the honest answer to "show me this thing". The row and the card both say which door it is: `enter open where it was asked` where there is a conversation, `enter open it on standing` where there is not. THE WAIT ON A `where you were` NOTE IS REMOVED RATHER THAN INDICATED. The owner asked for a spinner while the journal tail is read; law 8 gives the screen's one moving cell to work and nothing else, and TestExactlyOneRowIsGivenTheSpinner pins it. So the peek is asked on the KEY that moved the cursor instead of on the next beat — refreshHomeCard already ran after every key and returned early on the grid, on the reading that the grid has nothing about the row under the cursor, which the description column has now made false. The sentence lands in well under half a second, and there is no wait left to announce. Reading every conversation on the screen up front was the other way and costs a dozen whole-transcript scans, 33ms each, for the one row somebody is looking at. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * tui3: a question home raises is drawn in the description column `enter` on a conversation another window holds asks before taking it — codeaf does not move somebody's conversation out from under them silently, and the answers carry what it costs ("its reply stops there; its tasks come here"). The question is raised as the block's own card, and on the grid that card had NOWHERE TO DRAW: it belongs to the detail column beside the typed search, which is not up at rest. So all a person got was the one-line foot version, appended to the resting sentence — where it read as a run-on and was missed. Pressing enter looked like it changed the bottom of the screen for no reason. The description column is where it goes now, beside the row whose enter raised it, drawn by [app.questionPanelRows] like every other question on this surface — the frame, the mark, the head, the answers in a column with what each one costs, and the safe one marked. It takes the column WHOLE: notes stacked around a decision a person has to answer are the screen talking over it. Where the card is taller than the room under its row it is pulled up rather than cut, because a decision with its last answer off the bottom of the screen is worse than one drawn a few lines above its row. And the foot keeps its own sentence. A question said in both places is one decision drawn twice, which is the defect the block exists to end — [sayHomeAsk] already refused to double up beside a card and simply did not know the grid had one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * tui3: a watch asked for in another project opens from here `enter` on a `needs you` watch answered nothing but a clause at the end of the foot — `elsewhere · /private/tmp/codeaf-demo/codeaf` — on every window that was not launched inside the folder that made the watch. The owner read it as a dead key, twice, and it was: the sentence it wrote is the last clause of a line whose first four clauses are key hints, which is not where a person looks for the answer to a keystroke. THE GUARD WAS THE REPEALED RULE. [app.homeItemEnter] refused any origin outside the window's own project bucket, on the reading that a conversation in another project is out of reach. That reading is gone, and the word the refusal borrowed says so itself — [homeElsewhereWord]: "IT NO LONGER MARKS A ROW THIS WINDOW CANNOT OPEN, because there is no such row: enter opens any project on this screen". A `where you were` row in another project has opened from here for as long as the grid has existed ([app.homeOpenLine] asks the capability and the lock, never the bucket); this path was the last one holding the old rule. So the bucket check goes and the capability check stays, in the sentence a conversation row already says when this window has no door at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * tui3: a question raised by the launch draws in the column too The card stood beside the row whose enter raised it and drew nothing at all where there was no such row — which is every question the LAUNCH raises, a window meeting a lock on the conversation it was opening. The foot stops saying a question once this column can draw one, so that combination put a decision on the screen with nothing on the screen about it. A question with no row of its own stands at the top of the column. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * tui3: the ? leads the question, not the row that asked it `GNeedsHuman` means "this has stopped and is waiting on you", and the thing that is true of is the QUESTION rather than the conversation it came from. While the question was the row's own second line the mark had nowhere else to be; now that the description column draws it, the mark goes with the words. So on a frame with that column the row keeps its two blank cells — every title on the screen still starts in the same column — and the note stands in the mark instead. Narrower than three columns nothing moves: the question is back under its row and the mark leads the row, as it always has. It is still ONE mark and one accent on the row's line (law 8), drawn through the vocabulary's one door, and iconlaw is green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * tui3: a note in the description column stands in three cells, not two The mark and the first letter of the sentence it leads were touching. A note's lead is one cell wider than a row's now, and EVERY note takes it whether it wears a mark or not — notes are read down the column against each other rather than against the rows in the column beside them, so the one that starts with a `?` and the one that starts with a word have to begin in the same place. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * demo-home: the fixture has been looked at before, so `since you left` has rows Six of home's seven panels drew rows on the demo and `since you left` whispered — on a fixture holding eight landed tasks, three made files and a fortnight of memory changes. Nothing was missing from it. The look stamp was. THAT PANEL IS A DELTA AND A DELTA NEEDS AN ORIGIN. `.last-look` under `v3/projects` is when home was last closed, every `…Since` reader measures news from it, and a machine that has never had home closed on it has none — `ArtifactsSince` and `LandedSince` answer nothing at all rather than everything, which is look.go's stated law: "the first look marks NOTHING as news, rather than everything". Correct for a real first launch, and exactly wrong for a fixture whose whole job is a row on every place. So the seeder stamps it twelve hours back — the figure DESIGN.md §1 draws — far enough that the landed work and the made files are all inside the window. AND THE HANDOFF NOW SAYS WHY `running` NEEDS THE HEARTBEAT, which cost this session an hour of looking at an empty panel. It is `Presence.Fresh(now)` and nothing else, so a demo opened with a bare `HOME=<dir> bin/codeaf` shows it whispering however much work the fixture seeded: the rows are on disk and every reader has already given up on them. Through the seeder (`--launch`, which is what `make demo-home` uses) a beat restamps them and the panel fills. An empty `running` on the demo is a picture of the missing heartbeat, not of the panel. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * tui3: a question card is drawn whole or not at all, and the foot answers when it is not "What happens if we run out of vertical space?" — the card was CUT. At twelve rows its closing edge, its keys and one of its two answers were off the bottom of the screen, which is a decision a person cannot answer. A card taller than the room is not drawn now, and the one-line foot version — which exists for exactly the frames with no room for a card — takes it. Both sides ask ONE predicate ([app.homeAskFitsColumn]): two answers to "is the card on this frame" is a question drawn twice on a tall frame and drawn nowhere at all on a short one, and both of those happened while this was two conditions. THE FOOT ASKS IT AT DRAW TIME AND NOT ON A KEY. [app.sayHomeAsk] runs when a key is pressed, and somebody who has just made the window smaller has pressed none — so the card stopped fitting, the foot did not know, and the decision was on no part of the screen at all. [app.placeMsgLine] falls back to the question's own sentence whenever the card cannot be drawn, which a resize reaches on the next frame like everything else. AND THE CARD IS SIZED AGAINST THE COLUMN RATHER THAN THE FIELD'S OWN HEIGHT. It is not a note about a row and is not bounded by how many rows there are; clamping it to the field's length refused to draw one on every quiet machine, which is most of them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * tui3: needs you is its words alone — no counts, no clause, and the landings are unread The heading counted its live questions and the group line under it counted the landings, with `finished, nobody has checked it` at the line's right. The owner cut all three: a figure beside rows that are right there is a second thing to read that says nothing the rows do not. The group is `unread` now, the one word a mailbox uses, and its line is that word. The fold keeps its count, because it stands for rows that are not on the screen. The change entry also takes in the rest of the branch — the description column, the moved mark, the question card, enter on standing and the resting caret — which it had not yet recorded. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * tui3: every row of home's field rests on the one foot, and ctrl+o is true on all of them The foot under a field row said that row's own thing — `ctrl+x stop it` on a running task, `enter open where it was asked · ctrl+e pause · esc close` on a standing order, `enter opens the place this happened in · esc close` on a line of news — so it changed under the hand on every step of the cursor. The owner ruled that the rows under the moving headings all rest on the resting sentence: the four keys and `ctrl+o open folder`. For the chord to be true on every row, every row answers what folder it belongs to: a standing order's workspace, the conversation a landing ran in. The pause and stop chords still work and are on the alt+. map; the foot no longer names them, which also spares the paint the running-verbs walk the frame-disk law had to write around. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * tui3: the right margin of every field row is a time, and the rest is description A landing read `3 files · 1d` beside a landing that read `10h`; a since-you-left task carried its cost where a made file carried its conversation's name; a conversation from another folder wore its project beside its age and one from this folder wore nothing; the own row said `here`; a background job carried its project inside its title. Six facts down one edge, and the owner asked what each of them was. The margin is a time now on every row — waited, landed, last spoke, running for, due in — and every other fact is the row's description in the middle column: the files open a landing's sentence, cost and conversation are a ledger row's description under the cursor, the project is the first clause of a conversation's, and `here` opens the own row's. The words that say what enter will do (another window, coming here, folder gone) keep the time's place. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * tui3: a home panel's fold is a toggle — N more opens the panel, N fewer shuts it Every panel folded its tail into a door: `N more · tasks` opened the tasks place, `needs you` said `N more · M older · tasks` for the landings that aged off it, and `where you were`'s `N more · type to find one` could not be stood on at all because typing was its door. The owner found one line that opened somewhere else and another that was not a stop, and asked for one thing that expands. The fold is one line now, `N more`, a stop on every panel, and enter or a click opens the panel: its cap is lifted, the other panels squeeze to their floors in law 5's order — the open one is taken from last — and it takes the column; the line reads `N fewer` and enter shuts it. One panel is open at a time. needs you's aged landings are counted in its fold and come back when it opens. An open panel taller than the column names the place for what it still cannot show, the one case a fold still names one. It wears no mark, because the grid has two and no other (law 8). DESIGN.md law 9 is amended. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * tui3: a next up row describes what it found, not when it wakes The margin said `in now`, the description said `every five minutes`, and the title usually said the schedule a third time. The owner asked for something better. The margin is the clock — and for a watch that has looked, the time it looked, `checked 3m ago`, since a five-minute probe is always about to be due and the finding beside it is as old as that look — and the description is the finding: the line a watch found or `found nothing`, a reminder's or routine's last outcome, `your call` and why for one stopped on a person. An order that has never woken describes nothing; the row has said its schedule once. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * tui3: next up is scheduled — one word, no clock at the right, one sentence per kind of order The heading named two of the four kinds of order under it, every row carried its clock at the margin beside a title that usually said the schedule too, and an order stopped on a person stood here as well as on needs you. The owner asked for one heading word, no time on the rows, and one standardized way per kind to say when. So: `scheduled`. A row is the person's own words and nothing at its right; its time is said once, in its description under the cursor, in the sentence its kind has — reminder · goes off tomorrow 9:00am; routine · every morning at nine · next … · last: …; watch · every five minutes · last looked 3m ago · found: …; rule · always. A stopped order is off the panel while it waits, so one thing is said once across the columns. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * tui3: the collapse coming back is a fold, not the find word The fold on the grid became a toggle and a shut one is `N more` and nothing else, so this test was still waiting for `N more · type to find one` — a line the grid no longer draws. It reads the panel's own fold off the grid rather than the frame's words, because the words alone would also be satisfied by the `→ more` a legend can carry. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * tui3: a scheduled row says its sentence at every width, a fold names no place, and the manual stops teaching the retired home Review of Agent-Field#1046 found three blockers and four should-fixes; this is all of them. A `scheduled` row drew its one sentence per kind only where the frame had a description column, so at 120 and 80 columns the panel was bare titles with no kind and no time on them once the clock left the margin. The sentence is drawn at every width now, as the line under the cursor's row where there is no middle column, the way a `where you were` row already draws its own. An open fold read `3 fewer · 40 more · tasks` while `enter` on it folded the panel rather than opening tasks — a door drawn on a wall. The fold counts and names no place; the panel's heading is the door (law 10). `homeFindWord` and the slot's `more` column go with it, and the tagged e2e fold test that waited for `type to find one` on a real terminal waits for `N more` instead. `enter` on an order made from home lands the standing place on that order through the margin's door, not at the top of the list. The compiled manual said which panels sit left and right, spelled the resting foot without `ctrl+o open folder` in three places, said the columns were fixed, and still described the `elsewhere · <workspace>` and `made from home — no conversation to open` refusals home no longer makes. DESIGN.md and the change entry stop promising an empty middle column and say the middle is the descriptions'. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * tui3: a question's door word is said under the cursor, not on every row On a frame without a description column every `needs you` question draws its second line, and every one of them ended in `enter` — a column of legends for a key only one row is about to take. The description column already drew the word only for the selected row; the second-line shape now agrees: the answering row keeps its chips, the cursor's row says its door, the rest say nothing at their right. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…ent-Field#1040) * tui3, manual: the switcher's close key says close tab, not put away The `ctrl+k` card's legend read `ctrl+w put away` while the receipt it prints one act later read `tab closed · <title>`. The receipt was the honest one: `hopAway` takes the row off this window's tab row and leaves the conversation running and on the list. `put away` is already home's word for `ctrl+e`, which ARCHIVES a row until its name is typed — a durable act one keystroke away from this one — so the legend was borrowing the heavier verb for the lighter act. The legend now says `ctrl+w close tab`, and `keys.md` stops spelling the switcher's close with home's verb in the four places it did, including the section heading. The section states the distinction outright rather than leaving a reader to infer it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * tui3: taking a conversation from the switcher leaves the place you took it from `ctrl+k` then `enter` on home switched the conversation UNDERNEATH home and left home standing, so the keystroke read as one that did nothing while it had quietly swapped what the screen was drawn over. The card opens on all seven places because it is drawn over the screen rather than being a screen of its own; the cost was that nothing brought the place down again. `hopTake` now ends in `hopLand`, which comes down off whatever place is showing. Quick switching (`ctrl+tab`) and a row taken from below the fold land the same way, and `you are here` — which brings nothing forward — still lands, because from home it is the person saying "that one". A refusal is now said where the person is looking (`hopSay`: home's own sentence, a place's `pageMsg`, the conversation's entry line when nothing is standing over it) instead of into a transcript behind the place. A refused row deliberately leaves the place up, so the sentence can be read. `ctrl+w` on the card is unchanged: closing a tab from home leaves you on home, because that key is about the tab row and not about where you are standing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * tui3: the ctrl+k card lists conversations in the tab row's order The strip and the card are two readings of one set of conversations, drawn one line apart and used together: a person sees `Chats` on the row, presses `ctrl+k`, and looks for the conversation they were just looking at. Ordered by recency the card put it somewhere else — the third tab could be the first row — so the two lines disagreed about the same five conversations and neither position meant anything. `hopReading` still walks the previous-stack to BUILD the rows; `hopStripOrder` then lays them out the way the strip is laid out, leftmost tab first. The walk survives as the tie-break for a conversation with no tab on the row — one dismissed with `ctrl+w` is still held, still running and still on the card, and has no position to borrow, so it sorts after every tabbed row. The sort is stable for exactly that reason. The strip's own order is first-entered and never re-sorts (chattabs.go: a person reaches for the position, not for the word), so the card inherits that stability and the digits become worth drawing — `3` is the third tab. THE CURSOR STILL FOLLOWS RECENCY. Row zero is no longer the one `tab` would go to, so `hopFirstStop` is now a method that walks the previous-stack for the most recent conversation that is not the one in front. `ctrl+k` `enter` still means "the last one", which is the pair of keystrokes this card exists for. It reads the strip that was drawn rather than calling `tabList`, which rebuilds the row in place and would have the card writing to what it is reading. With no strip drawn, the rows keep the walk's own order. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * tui3: a closed tab leaves the ctrl+k list at the same moment it leaves the row The card's open list held every conversation the keeper was holding, so closing a tab — with its `×`, with `ctrl+w` in the conversation, or with `ctrl+w` on the card itself — took the tab off the strip and left the row on the card looking open. Two readings of one set of conversations, drawn one line apart, disagreeing about which of them this window is showing. `hopReading` now splits on `hopTabbed`: the rows above the fold are exactly the tab row, and a conversation with no tab goes BEHIND THE FOLD. It is not closed — still held, still running, still one `enter` from the front, and its tab comes back with it, which is what keeps a closed tab a way back rather than a deletion. `hopTabbed` asks `tabShut` and never the drawn strip. `tabShut` is set the instant a tab goes, by all three doors, and cleared in `rememberOpen` — the one road back to the front. The strip is a frame behind the keystroke, so a card rebuilt from it after its own `ctrl+w` kept the row it had just closed. Two consequences, both deliberate: the head's `3 of 12` is now how many TABS are on the row (`hopOpenRows` is the figure the reading returned, not a walk of rows that gains the machine when the fold opens); and a second `ctrl+w` closes the NEXT row rather than re-saying the receipt for the one already gone, which is what the key does on the strip. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * tui3: the switcher's foot is one instruction and its name column takes the width Three readings off the running card, all the owner's. THE FOOT. It read `▸ 9 more on this machine · → reach them`, which spent a whole row on a figure the head already carries — `3 of 12` says both halves of it — and then said the same thing twice, once in a triangle nobody reads as a verb and once in words. It is now `→ show closed`, and `← hide closed` with the fold open: the key, and what pressing it does. `closed` is what every row down there has in common, whether this terminal never opened it or its tab was shut. THE SUMMARY UNDER THE LIST. It re-wrapped the selected row's title whenever the subject column had abbreviated it — a second reading of the row the cursor was already on, present or absent depending on how long that one name happened to be, and costing the rows under it their places on a short card. It is gone. THE NAME COLUMN. It was fixed at 34 cells while the clause took every cell left over, so a wide terminal spent its width on `nothing new` and cut the name. The clause now has the fixed column and the name takes what is left, up to 64. The narrowing ladder is project, the clause's tail, the clock, then the clause: the name is cut last. And the gutter, which is not about width: a long enough name fills whatever column it is given, so two cells are held back at the end of the column. Without them the name and the clause run together into one word — the `…parsing regres…nothing new` in the report — however wide the card is. keys.md's switcher section was over its size rule after three changes landed in it, and the retrieval gate caught it: a probe that used to reach it started reaching four other pages. The layout and the ordering are now sections of their own. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * tui3: the fold says where the tabs stop, and every refusing row says why Two halves of one report: the conversations a person closes look exactly like the ones that are open once `→ show closed` is pressed, and the `✕` on some rows down there reads as "closed" when it means something else entirely. THE SEAM. With the fold open the card drew one undifferentiated list. A conversation whose tab was closed a minute ago comes back wearing `○` and its live clause — correctly, because it IS still held and still running — so nothing at all said which rows were tabs. One dim `closed` now stands where the tabs stop. It is the fold's half of the `open` the head already says, and it is a word rather than a rule because this surface outlines nothing and separates with ink. It is not a row: no spot, no cursor, no press, and it is not drawn while the list is only tabs. THE MARK. `✕` is `tokens.GlyphFailed` and on this card it means the row REFUSES to open — another window holds that conversation, or its project folder has gone. `hopRest` asked the disk about the folder for the glyph but left the field `hopRestNote` reads false, so the gone case wore the mark with an empty clause: a refusal a person cannot account for. The answer is taken once and both readings use it. The `✕` itself is left alone. That a person presses `✕` to close a tab and reads `✕` as a refusal one line below is a real collision, but the mark comes through the one door every surface uses and moving it is a change to the icon design and its slot table, not to this card. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * tui3: the switcher's `closed` heading is spaced the way its `open` head is Drawn tight against the rows the word sat between two lists and read as belonging to neither. It now has a blank line above it and a blank below — the same three lines the head is: `open`, its blank, its first row — so the two headings sit identically over their own lists. The blank above is not drawn when the seam lands at the top of the visible list, where the head's own blank is already the air above it. The air yields on a card too short for it and the word does not, which is the rule the card's own top and bottom air already follows. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * tui3: ctrl+k kills to the end of the line, and the switcher moves to alt+k ctrl+k is kill-to-the-end-of-the-line in every shell on the machine, and this surface already had the other half of that pair on ctrl+u. Spending the letter on the conversation switcher meant a hand that has typed that chord for twenty years got a card of conversations instead of an edit, in the one box on the surface people type into all day. Every ctrl+<letter> here is spent, so the edit could not move somewhere else and the switcher had to. alt+ is the modifier class the places are already built on, k is the letter people have learned, and the move pays for itself on the reverse gesture: alt+shift+k is escape-then-K, a different byte from alt+k's escape-then-k, where ctrl+shift+k and ctrl+k were the same byte — so walking the ring backwards stops being a thing only a terminal that took the kitty disambiguation flag could do. The edit is bound in the message box and in every typed filter box, it stops at the newline rather than joining two lines, and a whole-box flush goes on the kill ring first exactly as ctrl+u's does. What it costs is a Mac whose Option key composes accents, where the chord types a character and the card does not come. That is the tax every alt+ chord here already pays; chords.go now knows the switcher's dead key, and it is the one character on that table read in a conversation as well as on a place, because it is the one chord bound in both. Two stale claims fixed on the way past: ui.quick_switch has always governed ctrl+tab rather than the binding, and the harness design's room has answered digits since its chord row was deleted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * tui3: the line kills travel together, and a law keeps them together ctrl+k reached the conversation's composer, listNavigate and the settings value editor, and was missed on the five boxes that hand-roll their own key switch: home's box, the errand pane beside it, the tasks and rewind filters and the settings filter. Home is where a launch lands, so the first box anybody tried the new chord in was one of the five without it. The surface has no shared kill vocabulary to have added it to — the maps are per box, and a hand-rolled map is a map that forgets — so the thing that notices is a law. killpairlaw_test.go reads the package with go/ast and fails a key switch that answers ctrl+u without ctrl+k, naming the file and the line. The first-run setup and the onboarding limit row are named exceptions with their reason stated: both keep their text in a plain string with no caret, so kill-to-the-end has nothing to mean there, and the law proves each exemption is still needed rather than trusting its own list. A behavioural test covers home's box beside it, because the law can see the binding and not the route. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * tui3: home's rule names the switcher, and one modifier has one spelling again Two things the owner found looking at one screen. Home's rule gains `alt+k chats`, under the gate the conversation's legend already uses — named where it would act and nowhere else. It says `chats` rather than the legend's `switch` because every clause on that line is a key and the noun it moves, which is the grammar `alt+w folder` and `alt+o model` set, and `Chats` is what the tab bar's own control beside it says. And the ⌥/alt question behind it, which had a real answer: they are one key, and three lines were drawing it under both names. chords.go has always held that chordSpelling.say is the one door every person-facing sentence about a chord goes through — the constants are authored `alt+` and a Mac's `⌥` is substituted in once, at the moment of drawing — but home's rule, the question chip and two rows of the key sheet were built straight out of their constants and never went through it. So a Mac that said `⌥1…⌥7` on the map said `alt+w` on home and `alt+a` on the status line, one keystroke apart, with nothing to tell a person it was one key. All of them go through the door now. The command list needed the spelling threaded through note, menuNote, height, fit and rows, because `⌥3` is two cells where `alt+3` is five and fit counts the lines a row will take before rows draws it: measuring one spelling and drawing the other pushes the status line off the frame. chordspellinglaw_test.go is what keeps it. It renders the conversation, the key sheet, all seven places and the key map on a Mac's spelling and fails on any `alt+` that reaches the frame — a frame test rather than a source test, because every constant is SUPPOSED to say `alt+` and what was wrong was the path from the constant to the paint. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * tui3: one word for the switcher, and the Chats control goes The door had three spellings at once: `alt+k switch` on the legend and the map, `Chats` on the tab row's own control, and `alt+k chats` on home's rule. One word for one door is what audit-words was written to keep, and the noun is the one that survives — it is what the thing IS rather than what pressing it does, and it is the word a person already has for a list of conversations. And the control itself is deleted. It was written when the switcher's chord was something a person had to already know about and nothing on the screen said the other conversations existed at all; the legend under the box names the key now, wherever the card would open, in the person's own keyboard's spelling. A labelled door beside it was a second way to say one thing, and the third spelling of it. WHAT DOES NOT GO WITH IT IS THE COUNT. `+3` says this row could not spell three of the tabs it has, and a strip that went quiet about them would be back to claiming the window holds exactly what fits — the defect the whole header was built to fix. It is dim, inert and drawn whole or not at all, which is what `tabFold` already was on a frame where the switcher could not open. Gone with the button: tabMore, tabsWord, tabMoreWord, tabsMoreWord, tabsShorter, and app.hopOpenAll — that last one a second constructor for a state `→` on the card reaches anyway, left with no caller but tests. The three tests that used it drive the real gesture now. `tab closed · keep running` said `find it under Chats`, pointing at furniture that is no longer drawn. It names the key, through the spelling door so a Mac reads `⌥k`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * tui3: a Mac spells the modifier opt+, not the option symbol Every modifier on a Mac keycap carries both a word and a symbol — Control is `control` and ⌃, Option is `option` and ⌥, Command is `command` and ⌘ — so "what the keycap says" never chose between them, and this surface was taking the word for one modifier and the symbol for another. `ctrl+` beside `⌥`, with nothing predicting which a reader got. `opt+` is `ctrl+`'s own move made twice: the keycap word, cut short, joined with `+`. THE BINDINGS ARE UNTOUCHED. The constants are still authored `alt+` and chordSpelling.say still does one substitution at the moment of drawing; only what is drawn changed. It costs nothing to measure, which is what makes it safe in every ladder on the surface: `opt+` is four cells, exactly as `alt+` is, so a line that fits the chord on Linux fits it on a Mac. The glyph measured as one cell by ansi.StringWidth and was not reliably one cell on screen — U+2325 is East-Asian Ambiguous, which a terminal set to draw ambiguous characters wide renders in two, and a font without the codepoint substitutes at a width nothing measured. It was also the one person-visible mark reached as a string literal rather than through tokens.GlyphSet.Glyph, which every other mark goes through precisely because a literal cannot know the terminal's repertoire. chordMetaBare is the new constant for prose that names the key alone: `your terminal sends opt as a letter`, not a dangling `opt+`. payload.go's prefix list loses its one entry that had to explain why it carried no `+`. ⌘ is still a glyph and is now the only one, which is also why say() must still run before anything measures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * tui3: ctrl+u is a line kill everywhere, and nothing still points at ctrl+k Three things the review on Agent-Field#1062 found, all of them mine. `/settings` told a person `Ctrl+k always opens the list`. That chord DELETES TEXT now, so the sentence was instructing them to destroy their own filter. The `quick switch` row and its config mirror say `alt+k`. Four shipped manual passages still documented the retired binding and the deleted control: keys.md's account-question section, two passages in tasks.md — including a top-bar `▾` picker that does not exist — and permissions.md naming `Chats` as visible furniture. This is the manual-law hole the change entry already names: the gate checks that a name is mentioned, never that the claim around it is true. And `ctrl+u` was not a line kill in five boxes. Home's box, the errand pane, the tasks and rewind filters and the settings filter answered it with reset() — the whole field — so `abcdef` with the caret after `abc` threw `def` away, while the ctrl+k that just landed beside it correctly kept the head. One chord cannot mean "to the start of the line" in the composer and "all of it" on Home. All five call killToStart now. killpairlaw_test.go reads the source and can see that a switch answers both chords; it cannot see that the two answers are the same gesture, which is exactly what was wrong. TestBothLineKillsAreLineKillsInEvery BoxThatHasThem drives the composer and home's box from a caret in the middle, which is the only place the two readings differ. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * tui3: ctrl+w refuses a conversation this window never held, and the seam pays only when drawn Two found in review, both on the fold this branch added. CTRL+W ON A MACHINE ROW CLOSED NOTHING AND SAID IT HAD. `hopTabbed` asked `tabShut` alone, and nothing ever dismissed a conversation this terminal has never held — so the map was silent about one and silence read as "it has a tab". `hopReading` never noticed, because every row it splits is one the keeper is holding. `hopAway` did: it answered a below-fold machine row with `tab closed · <title>`, marked a conversation it had never held as dismissed, and changed nothing on the row. `dev` refused that correctly before this card learned to fold, so it was a regression. `hopTabbed` now asks `row.open` too. THE SEAM'S THREE LINES WERE CHARGED WHETHER OR NOT IT WAS DRAWN. The reservation came off `available` on every card with both groups in it, but the word is only drawn where the scroll window reaches the first closed row — so on a short card the window lost three rows, the rows it lost were the closed ones, and pressing `→ show closed` made the list SHORTER and showed nothing while the foot offered `← hide closed`. Measured at card heights 8 through 13. They are given up in the order they can be afforded: the air first, on the card's own rule that air yields before anything else, then the word — and a card too short even for the word draws the closed rows without it rather than drawing none of them, because a person who pressed `show closed` asked for the rows. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * manual: the stop section answers the question in the asker's own words Merging `dev` brought Agent-Field#1061, which rewrote what stopping does and how long it takes; with this branch's own keys.md edits beside it, the retrieval gate sent "how long does stop take" to the tasks pages instead of the section that answers it. The page had the answer and the heading spelled it "how long stopping takes", which is not how the question is asked. The fix is the page, never the test: the asker's phrasing goes in the heading. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * tui3: ctrl+w on a row with no tab does nothing, and says nothing about it The card refused it in a sentence — `that one is not open here — enter opens it` — and the owner took the sentence out. The key exists to close a tab; a row below the fold has none, whether this terminal never opened that conversation or its tab was closed a minute ago. The state the key was pressed for is the state it found, and a line explaining that is the surface answering a question nobody asked. So `hopNotOpenWord` is deleted, nothing is said, nothing moves, and the card is not even re-read. Its two tests now assert the silence and that the row count is untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * manual: the switcher's fold names the one thing it cannot do on a short terminal A card of about ten lines holds three rows, and where all three are tabs the `→ show closed` keystroke changes only the foot: the closed rows are below the scroll line rather than missing, and `↓` walks to them. The seam's own lines are no longer charged for that — this is the scroll window, not waste. Moving the cursor onto the first closed row would hide it, at the cost of `enter` right after `→` opening a conversation the person had not selected. The owner looked at both and kept the cursor where it is, so the page says what the key does on a small frame rather than leaving somebody to find it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…laces (Agent-Field#1064) * docs: the workspace-foundation constraints stop linking two private places CONSTRAINTS.md linked a chatgpt.com conversation and a personal repository that only its owner can open. Neither link resolves for anyone else, and a public repository should not carry addresses into private accounts. The references stay as prose so the record of where the ideas came from is kept. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs/changes: entry for Agent-Field#1064 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…Field#1049) * README: the launch story, hero and diagrams; the reference README moves to docs/GUIDE.md A new README for the public repository: install, a real first turn, the benchmark slot, why a factory rather than a copilot, the seven places, tasks and review, specialist tasks, standing orders, ssh and phone, the open-model crew, the one binary, a section for people arriving from other harnesses, and the roadmap. Screenshot slots carry placeholder frames until the real captures land. The hero and diagrams come from the brand package in ~/Documents/agentfield/codeaf. LICENSE is Apache 2.0, the same text as agentfield. The old README is kept whole as docs/GUIDE.md. * changes: entry for Agent-Field#1049 * README: lead with the handoff, tighten the copy, answer the relay and permissions questions * README: CodeAF in prose, the control room, the six seats, headless as a front door, a diagram that argues in lines * README hero: the model roster fits left of the fan * README hero: shorter roster * README: built for open models, in the hero roster and the intro * README: hero alt text names the models * README: real product screens for home, conversation, tasks, questions, models and phone Six staged terminal captures replace the placeholder frames. The phone is shown as a mobile terminal over ssh, not an app, and the anywhere diagram no longer says a phone attaches through the relay, which the manual says it cannot. * README: remote section says what --host is, and claims match the manual --at needs a hosted relay that is not running, so it moves to the roadmap and the relay and pairing paragraph goes. The section now pitches the split surface and engine: typing never waits on the link, the conversation outlives the lid, files cross both ways, two screens share one turn. Checked work lands by itself and never on a protected branch, so 'nothing merges without you' is gone; the review keys on home are 1 accept, 2 not right. The review placeholder is removed. * README: one window for every project, talk becomes tasks, trivia cut Leads with the gold bento under the intro. New sections for one window across projects (tabs, ctrl+k) and a conversation that becomes tasks. Drops the per-turn price, rewind, crash safety and the permission keys every agent has. Real standing and projects screens replace placeholders; the unverified headless claim about titles is gone. * README: chat-to-factory overview, subharnesses section, sharp WebP screens The overview under the intro is now the layered chat, tasks and home shot on the warm ground. A subharnesses section positions specialists built for one job, with PR-AF coming as a native one and the benchmark run by the developer subharness. Every product screen is re-rendered from exact pixels (the image model polish softened text) and shipped as WebP. * README: subharness diagram; change entry lists the sections plainly * README: CodeAF logo on every image, tasks-across-projects shot for one window, diagrams as WebP * README: early preview note and where new work shows up first * README: Discord link and badge; tracked byline link * README: intro leads with open-model efficiency and the change from juggling agents to one place * README: add a recorded demo under the overview Home, a message that becomes two tasks, the rail, home with them running, a larger conversation's task tree, a task opened and switching chats. Recorded live on open models, waits sped up. Animated WebP, 8.9 MB. * README: byline reads 'Built by AgentField team' (Agent-Field#1059) Co-authored-by: OG <oktaygoktas@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> * readme: the demo is a 92 second video at real speed, with sound * readme: drop the unverified and the placeholders, say coming soon, home and overview on the current home --------- Co-authored-by: OG <33333646+oktaygoktas@users.noreply.github.com> Co-authored-by: OG <oktaygoktas@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…/status names the gate (Agent-Field#1063) The three Safety rows — `ask before running`, the tool exceptions and the shell command rules — wrote the profile and left the running gate alone, while the YOLO badge read that profile live. So the badge and the gate disagreed in both directions: `allow` lit the badge over a gate that kept asking, and `prompt` put it out over a gate that was still wide open, which is the false-safety claim Agent-Field#322 and Agent-Field#325 exist to prevent. `applySetting` now pushes those three rows through `ApplyApprovals` — the seam /permissions already drops a banked rule through — and refreshes the badge on the same keystroke, on the branch where the push landed. A rebuild that could not be done is said out loud on the panel's foot line in /permissions' own words, `saved · from the next session`, and the badge stays with the gate in force rather than following the file. And /status carries the posture it never carried: `approvals prompt`, `allow` or `deny`, in the `ask before running` row's own words, in the note, in --json and on the phone's status sheet, from the one posture reading the badge uses. The status row keeps its negative-space badge and the segment is no longer written into the page twice. The note's label column is measured from the widest label it carries, so three crew assertions that spelled `"\ncrew"` plus exactly five spaces were assertions about every other row on the page. They read the crew line by its label now. Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Agent-Field#1075) * attribution: the footer names CodeAF, and still links to agentfield.ai The pull request, issue and comment footers read "Drafted with agentfield ai". They now read "Drafted with CodeAF"; the link, its utm parameters and the "reviewed and owned by the author" tail are unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GNBScbyNAbbEQ5jn9G6auy * docs: the change entry for the CodeAF footer Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GNBScbyNAbbEQ5jn9G6auy --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…change], and the answer stands (Agent-Field#1065 round 1) (Agent-Field#1067) * session, prompt: a wrong completion check is answered with [no change], and the answer stands (Agent-Field#1065 round 1) The end-of-turn reader saw 600 bytes of the last message, so a long table read as cut off; the carry-on told the model to explain itself, and the explanation became the only answer left in view under the fold. The reader now sees up to 8 KB of the last words, and a cut it still has to make ends with `[clipped by codeaf: N of M bytes]`, which its ask says is never evidence of a cut-off answer. The carry-on lead says who wrote the note and offers one exact reply, `[no change]`: it ends carrying on for the ask without another reading, the transcript keeps it for the model, and neither replay nor the live feed draws it — so the settled answer before it is what the fold finds. system.md trades `# Tone` and `# Delivery` for `# The answer`, `# Words or work`, `# When corrected` and `# Messages from codeaf`, and the two lines that pushed action on questions are qualified. The lean page keeps all four; the prefix waivers rise by the 1,705 bytes that cost, dated and named. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: the change entry takes the pull request's own number Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * session: a quiet landing is asked about the notes it was handed, not the page that names them The worker's system page now names [silent] and [stuck] under `# Messages from codeaf`, so a search of the whole request found the tag on the page. childWasHanded reads the conversation and leaves the page out. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * session, prompt: [no change] answers the note it follows, and a long ask leaves the last words cut by name (Agent-Field#1065 round 1) Self-review of Agent-Field#1067 found the token honoured on the wrong replies. The gate read the turn's carry-on count, so a model that made calls in answer to the note and then said [no change] ended the turn unread on an answer those calls may have made stale, and a token after a load or ask nudge counted too. It now reads the transcript: the token must be the very next reply to the reader's own lead (answeredNoteUnchanged), and the dead declined flag is gone. An ask up to the whole digest rides in its head, so eight kilobytes of last words behind a long ask reached the backstop clip and ended in a bare `…`. checkpointClipSaid takes the room that is left. A turn that ends on the token no longer starts the route judge or hands the token to the memory reflex, and a later reading's last words skip it. `# Messages from codeaf` named `[still asked]`, which only the reader ever receives, and left out `[stuck]` and `[held]`, which the conversation does; it now names the tags that arrive and says any other opening, such as `[image Agent-Field#1]`, is the person's. The waivers sit on the measured 1,779 bytes, and the manual and the change entry say what the gate actually honours. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * prompt: the new sections keep the rules the old two carried, and say what stays in view (Agent-Field#1065 round 1) Owner review of Agent-Field#1067. Replacing `# Tone` and `# Delivery` had dropped five rules; each is back inside the new sections: match the person's task and vocabulary, choose the safe option under uncertainty, evidence and blocking details complete, never a compiling scaffold or a narrowed test, never half-solved work. `# The answer` said only the last message reaches the person, which `# Planning` contradicts: the plan and the per-batch sentence are shown while a turn runs. It now says that once a turn ends only its last message stays in view. `# Words or work` sat beside `## Work or words`, which spends the same two words on hand-off sizing; it is `# Answer or change`, and it says an answer asked for in chat is written in the reply, not a file, unless the person asks for one. The waivers sit on the new measurement, 2,035 bytes over dev on both arms. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * prompt: the messages-from-codeaf list names every note codeaf writes The system prompt now distinguishes conversation user-role notes from private reader and tool-result markers. A structural test follows the production constants, and the byte waivers record the exact fixed and lean measurements. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * session: no change skips judgment only when it answers the reader The completion loop now uses the transcript context before suppressing the route judge or substituting the prior answer for memory. Tests cover the withdrawn and ordinary-token memory paths, and the manual separates counting from display. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test: the model stub replays the no-change fold The OpenAI-compatible stub can now drive the real binary through eleven read rounds, a long table, a rejecting reader, and the exact token reply. The recipe records the tmux setup and the expected log and screen evidence. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * prompt: the standing-news tag stays with the event, not on the page The messages-from-codeaf list had named [something you set up fired], and TestTheSteeringLineReadsAsNewsAndNotAsARequest refused it: that line says under the news itself what it is and what not to do about it, so the page's copy is one law bought on every request of every turn for a turn most sessions never have. The list keeps the compaction tags, the structural test now files the standing frame with the private voices the page must not name, and the waivers fall to the new measurement (fixed 55,280, lean 47,055). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Abir Abbas <abirabbas1998@gmail.com>
…t-Field#1068) * chore: clean internal experiment records and revive test/ux for codeaf Public-repository cleanup ahead of opening the docs to outside readers. - Remove four internal DOE/experiment record directories that reference private issue and pull-request numbers and are not current-product documentation: docs/design/pin-doe, plan-gate-doe, turn-wall-share-doe, and workspace-foundation. - Neutralize internal machine references in benchmark and design docs: the bench host ("spark"), its secrets.env path, and a personal working branch name, all now generic placeholders. - Revive test/ux/, the user-facing journey suite, for the renamed product: collapse the removed v1/v2 dual-surface logic into the single v3 surface, repoint the binary to cmd/codeaf and state to ~/.codeaf/CODEAF_*, and delete gate.sh whose premise was the v1-vs-v2 parity table. Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com> * chore: drop internal conversation-runtime wave records, keep the measurement contract The conversation-runtime directory was a local development wave: day-by-day status, /private/tmp paths, "owner authorized" process notes, draft PR numbers, and local change logs. None of it is current-product documentation a public reader needs. Keep PARETO.md, which is the benchmark's cited measurement contract, trimmed of its dangling calibration link and owner language. Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com> * chore: neutralize the remaining bench-host and work-machine references A wider sweep found the internal bench host ("Spark"), its secrets path (fleet/secrets.env), and a scratch ops directory still named in live docs, bench READMEs and bench scripts that the earlier redaction had not touched. Rename the host to "benchhost"/"the bench host", drop the secrets path, and replace "the laptop" as the owner's own machine with a neutral "work machine" where it meant the internal one. The manual's first-person "the laptop you are sitting at" is public-facing and left alone. Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com> * chore: fix review findings in the public-cleanup diff - PERF.md: drop a private tracker issue reference (Agent-Field#846). - PARETO.md: restore a complete sentence dropped when the calibration link was removed, and align tense with the remaining present-tense claim. - home-mission-control/DESIGN.md: drop the internal "fleet" system name and an owner-framing phrase, consistent with the secrets-path scrub elsewhere. Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com> * docs/design: the two records live code still cites stay in the tree plan-gate-doe/REPORT.md is the measured evidence behind the plan-gate default that twelve comments in cmd/codeaf, internal/config, internal/session and internal/splitgate point at, and turn-wall-share-doe is the run internal/session/turnwall.go says it was added from. CLAUDE.md keeps the record on purpose for exactly this reason, and neither folder names a machine, a secret or a private address. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test/ux: the revived journey suite goes back out Agent-Field#991 removed it because it drove the v1 and v2 surfaces, and the revival did not change that: its journal verb reads graph.db tables (messages, nodes, usage, commands) that the v3 chat never writes, lib.sh still waits for the resident, nothing runs it from the Makefile or a workflow, and its only evidence was bash -n. docs/JOURNEY.md says internal/e2e is the one suite that drives the binary, and that stays true. A suite that has been run green against bin/codeaf can come back on its own pull request with a target that runs it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs/changes: entry for Agent-Field#1068 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs/changes: the Agent-Field#1068 entry says why the journey suite is out, correctly The first draft said the v3 chat never writes the graph.db tables the suite reads. Running the suite showed both message rows landing, so that sentence was wrong; what is true is that nothing runs the suite, it had never been run, and its first journey fails on its first screen check (Agent-Field#1072). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: codeaf <agentfield-bot@users.noreply.github.com> Co-authored-by: Abir Abbas <abirabbas1998@gmail.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…alhost The author codeaf stamped on the commits it lands was `codeaf <codeaf@localhost>`, which no GitHub account can link, so every branch codeaf landed failed the CLA check. It now writes `codeaf <agentfield-bot@users.noreply.github.com>` - the identity the Co-Authored-By trailer already used - and keeps `codeaf@localhost` and `aforge@localhost` recognised as legacy by taskCommitIdentity. GLM 5.3's review caught an unmarked retired name in permissions.md that reddened the namelaw gate, and seven stale test fixtures; both fixed. Laws gate, session, exec and cmd are green. Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com>
Agent-Field#1103) * README: show the pinned-version install line the release test requires The Install rewrite dropped the VERSION=<tag> pin example, and TestPublishedPinExamplesGiveVersionToBash reads every published install surface for `| VERSION=` because `VERSION=<tag> curl ...` is an easy mistake to copy — it sets the variable for curl instead of the install script. The pin goes back inline on the same host the section already uses, with the releases page still linked for tags. Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com> * docs: the change entry for the README pin example Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com> --------- Co-authored-by: codeaf <agentfield-bot@users.noreply.github.com>
…nt-Field#1095) * telemetry: the package behind anonymous usage counting, per the wire contract Four typed events (first_run, session_started, session_ended, fault) with the contract's allowlisted, bucketed props; the opt-out ladder with its off reasons; a hashed install identity at 0700/0600; a fault fingerprint over codeaf function names only; a JSON-lines spool with a deadline-bounded flush of up to 50 events per POST, 7-day age drops and a 1000-line cap; the notice constant with NoticeShown/MarkNoticeShown gating the flush; and Show() for a future codeaf telemetry show. A library only: nothing in cmd/codeaf, internal/session or internal/config reads it yet. Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com> * telemetry: tests, doc, and contract fixes for the usage-count package Adds the package's tests — bucketing edges, the opt-out ladder, install identity, the notice, fingerprinting, the spool against an httptest relay (cap, age drop, 50-per-POST splitting, a hard deadline against a hanging server, silence on failure) and the privacy law that no sentinel string reaches the wire. docs/TELEMETRY.md documents the notice, what is sent and how to turn it off, with a test that fails if the doc's property table drifts from the Go allowlist. Fixes four drifts from the wire contract found running those tests: the <1m duration bucket was missing, the fingerprint's argument stripping cut too much, the rc channel suffix must be a single integer, and under go test a config-off rung outranks the test rung in the reason reported. Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com> * telemetry: fix review defects 1-8 with tests Fingerprint stability (last-( cut, .go:/main./created-by frame rules), counted-batch flush, guarded and multi-process-safe spool, BucketDuration edges, Configure-gated write paths, O_EXCL install id, install.json preservation, buildinfo.Dirty accessor. Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com> * installer: record the install locally and print the telemetry notice Following the telemetry wire contract: a successful install now writes telemetry/install.json under the state root (dir 0700, file 0600) with the install method, channel and UTC timestamp, leaves any existing install_id alone, and makes no request the download steps did not already make. At the very end the installer prints the contract's notice to stderr after one blank line, or one line saying telemetry is off when CODEAF_TELEMETRY/DO_NOT_TRACK opts out. README gains a plain Telemetry section pointing at docs/TELEMETRY.md, and a new test/installer-telemetry.sh proves the marker, the notice and the opt-out without a network. Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com> * installer: four review corrections to the telemetry commit The review of 71ff95a6b found four things. Revert the one line that changed `if [[ -z "${TAG:-}" ]]; then` back to `${TAG:-}`: under set -u an unset TAG must reach api_problem, not crash. In write_install_marker the channel lands in JSON verbatim and CHANNEL can come from the environment, so only stable, rc, staging or dev is recorded and anything else becomes "unknown" — with test cases for a valid channel and for we"ird, both asserting the marker parses (python3 -m json.tool, or jq, else a strict channel check). Fence the README Telemetry notice in a ```text block so Markdown keeps its alignment, wording byte for byte. Clear the two shellcheck findings in test/installer-telemetry.sh with one block disable for SC2016/SC2034 — ok() evals those strings, so the quoting is deliberate — and export CODEAF_TELEMETRY/DO_NOT_TRACK where a subshell needs them, without weakening any assertion. Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com> * release-downloads: daily PostHog report of public GitHub release download counts A scheduled workflow reads the repository's releases from the public GitHub API and sends one codeaf:release_downloads event per binary asset (codeaf-<os>-<arch>[.exe]) — sidecars like checksums.txt are not reported. Each event carries release_tag, channel (stable or the prerelease suffix), prerelease, os, arch, the cumulative download_count and the UTC snapshot_date; the event id is derived from tag, asset and date, so a same-day rerun deduplicates instead of counting twice. No code in the binary; counts come only from public release data. Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com> * release-downloads: tests cover the workflow, docs and pagination The dry-run unittests now also assert the identifiers the wire spec spells out where the script and the workflow spell them: the schedule, dispatch, contents-read permissions, built-in token, project-key secret and host variable in .github/workflows/release-downloads.yml, the public-data paragraph in docs/rules/promotion.md, stdlib-only imports, pagination across API pages, the host override and the /batch/ endpoint, sidecar skipping, draft skipping, and the channel words stable, rc, dev and staging. Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com> * release-downloads: four review corrections to the script Channel detection now follows the tag grammar in cmd/codeaf-release/version.go: stable, rc, dev and staging tags match their four patterns and anything else reports "unknown", so dev-20260917-09299019bcdc is no longer mislabelled "stable". --dry-run works without CODEAF_POSTHOG_PROJECT_KEY, the local and test path: the batch prints with api_key "<unset>" and exits 0, and only the sending path stops with the notice. Every event carries "$geoip_disable": true next to "$process_person_profile": false, since the sender is a CI runner whose location is meaningless. The module docstring now names release-downloads.yml, the file that exists. The fixture holds one tag of each shape plus an unrecognised one, and the tests assert the channel for each, including the two real dev and staging tags. Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com> * release-downloads: test runs on Python 3.9 too ScriptSourceTest read sys.stdlib_module_names unguarded; the attribute arrived in Python 3.10, so the suite errored on macOS stock python3 (3.9.6). Guarded with getattr so the pinned import-set check stands on its own where the attribute is absent; 32 tests pass on 3.9, 3.10 and 3.14. Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com> * installer telemetry: the notice is read from docs/TELEMETRY.md The test read .telemetry-contract.md, a working note that is not in the repository and never will be, so it aborted with "telemetry contract is missing" in a clean checkout. It now extracts the fenced block under "## The notice" in docs/TELEMETRY.md — the same bytes as the Notice constant in internal/telemetry/notice.go — and holds the installer's TELEMETRY_NOTICE to it as before; a new assertion holds the README's telemetry fence to it too, byte for byte, so the invariant the suite existed to guard now has its third leg. Comments in scripts/install.sh, internal/telemetry/telemetry.go and this test now name docs/TELEMETRY.md as the source of the notice. No Go code changed and no Go tests were run. shellcheck is clean; the suite's 33 passing assertions include the two new notice checks, and the same two stat(1)-related assertions that already failed on this GNU machine before the change still fail with identical output. Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com> * release-downloads: the download-counts paragraph lives in docs/TELEMETRY.md docs/rules/promotion.md was the only home the paragraph had, and it came with a fallback pinning the branch to the state before the telemetry doc existed. The paragraph moves, byte for byte, into docs/TELEMETRY.md as a final "## Download counts" section beside the rest of the telemetry story, and promotion.md returns to its origin/dev content, so this branch now touches no release docs. test_no_telemetry_doc_on_this_branch asserted docs/TELEMETRY.md did not exist and is deleted. The two tests that read the paragraph read it where it now lives: both require a "## Download counts" section in docs/TELEMETRY.md naming .github/workflows/release-downloads.yml, scripts/release_downloads.py and the checksums.txt sidecar, and the promotion.md fallback is gone. No Go code changed and no Go tests were run; the property table in docs/TELEMETRY.md is untouched. The suite passes: 31 tests, OK. Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com> * installer telemetry: the mode check asks GNU stat first On Linux `stat -f` succeeds and reports the filesystem, so the BSD-first order never reached the GNU form and both permission checks failed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CqfhxLrrBNQxwPNvEgoBGR * telemetry: refuse to send to the production relay from a test binary Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com> * telemetry: testHome points the endpoint at a dead loopback Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com> * telemetry: the orphan-adoption test names the dead relay it now gets Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com> * telemetry: a test pins that no test binary reaches the production relay Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com> * telemetry: three tests pin the test-binary relay refusal Each asserts through real calls, not a re-implemented effect: the newRelay override lands after testHome and a flush reaches that recorder; a default-endpoint flush is refused like a dead relay (zero round trips, event still spooled); forcing the ladder on cannot reopen the path. Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com> * telemetry: allocation-free session counters Snapshot returns the SessionStats fields the SessionEnded constructor takes, so the wiring hands the struct straight over. Cost is stored as integer micro-dollars and divided back once, in Snapshot. Count* never ask whether telemetry is enabled and never allocate. Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com> * telemetry: gofmt the counters test One alignment fix in the many-goroutine test; no behaviour change. Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com> * config: telemetry setting row (CODEAF_TELEMETRY) Key "telemetry", on|off, default on, env pin CODEAF_TELEMETRY, in the interface group beside the history row. TelemetryAtIn walks the ladder — pin, project file, profile config, default — and TelemetryOffReason answers whether config turned the pipe off and names which rung did. The row sits in ProjectKeys so a repository may answer for itself. Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com> * cmd/codeaf: the telemetry command, and the setting it writes Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com> * cmd/codeaf: session lifecycle events at execute (work in progress) Configure, notice, first_run and session_started at the start of every session command, session_ended and a one-second flush at its exit. The end event does not yet carry the session counters. * provider: count answered model calls and their spend at the log door * session: count sealed turns and executed tool calls * cmd/codeaf: the session-ended event carries the run's counters telemetryEnd builds the end event from telemetry.Snapshot so turns, model calls, tool calls and cost reach the spool; the stop-reason switch becomes a map over the exit ladder; and both lifecycle constructors are gated on the ladder being on so a disabled run touches no state. Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com> * cmd/codeaf: the counters test says the tally is process-wide The exact turn, call and cost assertions hold for one run of the package; a repeated run counts into the same atomic tally again. Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com> * guard: a fault hook Recover calls, for a reporter above this package Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com> * cmd/codeaf: the fault event at both crash sites, off the current session Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com> * cmd/codeaf: a test that the crash site spools a fault and not the panic text Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com> * docs: finish the anonymous usage counts contract and add the 1095 changelog entry * guard: the fault hook sits behind an atomic pointer A goroutine recovering a panic read the hook while a test on another goroutine wrote it, which the race detector reported. SetOnFault stores it once; Recover loads it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * cmd/codeaf: a task's interrupt word comes from the signal, not the cancelled context stopSignals cancels the signal context at the end of every errand, so the flag read from its Err said interrupted for a run that finished cleanly. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * tui3, installer: the telemetry row has a tab, and a custom install writes no marker TestEverySettingRowHasATab wants every registry row on the settings panel. TestCustomInstallOutsideTheStateRootDoesNotRunAdoption wants an install outside the state root to leave the login's state folders alone, so the install marker is written only when the root exists or the install is in it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * cmd/codeaf: the counters test zeroes the process tally first Other tests in the package drive model and tool calls through the doors that count, so the exact bands held only when the test ran alone. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * cmd/codeaf: the version test drives the telemetry lifecycle, not execute execute starts writers under the state root that outlive the test, and the temp dir's cleanup raced them. The lifecycle alone is what the test pins. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * cmd/codeaf: gofmt the lifecycle test Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: codeaf <agentfield-bot@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…ace the scheduler (Agent-Field#1104) * session: the carry-ladder fixtures wait for the mark's drawing, not race the scheduler TestASuccessfulHandoffJournalsTheRungThatSuppliedTheBrief failed once in a full make check at line 118: the worker's brief was the running model's draft, not the mastermind's written handoff. THE CAUSE IS NOT A STOLEN STEP. Reproduced under load (60 CPU burners, the fixture run 1500 times), every request the fixture makes is either the turn's own or one the aside answers: the queue carries only `round`, `draft` and `writer`, and the errands beside the turn (title, namer, the mark's drawing) never touch it. What moves is the ROUND COUNT — 41 to 54 across the runs — because the mark's drawing is a sidecar (sidecar.go) whose landing the scheduler places, and the turn is never held for it. On a busy machine the turn runs its rounds off faster than the drawing's goroutine is scheduled onto a core, the gap between the net firing and the drawing landing outruns the slack the script carries ([checkpointSlack] says so in its own words), the writer's ask lands PAST the end of the 53-step script, the scripted completer answers it with its past-the-end line `(unscripted)` — which is not prose — the handoff rung reads degenerate, and the carry ladder falls onto the draft. That is Agent-Field#392 exactly, one mechanism along. WHAT FIXES IT IS WAITING FOR THE READING. checkpointAgent now puts a watch on the turn's own context ([watchReadings]) and the handover fixtures submit through [watchedContext], so [answerWhenQuiet] holds each request that carries the belt until every reading beside it has landed — the order a real turn has, where a real model spends seconds on a step and the small readings land inside it. The gap is then one boundary whatever else the machine is doing, and a script long enough to cross the mark is long enough. THE REGRESSION TEST holds the drawing back by a fixed wall time — standing in for a scheduler that does not reach it in time — and insists on the behaviour: the turn does not spend its script racing the drawing, and the writer's rung still supplies the brief. It fails without the watch (the turn makes 54 rounds) and passes with it. Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com> * docs: the change entry for the carry-ladder drawing race Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com> --------- Co-authored-by: codeaf <agentfield-bot@users.noreply.github.com>
…1076) * docs: the install proxy is live, and the manual says so `https://agentfield.ai/get/codeaf` has served `scripts/install.sh` since 2026-09-16, the day the repository went public, so the compiled-in manual and docs/GUIDE.md stop saying it is "not serving yet" and stop calling the repository private. The proxied line goes first, as the README already has it; the raw GitHub line is described as the script under it. Both pages say to pipe into `bash`, not `sh`, because the script uses pipefail and `[[ ]]` and dash rejects them, and both state the channel path (`/dev`, `/staging`, `/rc`) beside the flags. The manual also says what the proxy refuses: a script without its channel line exactly once, or a body that is not a shell script, is a 502 rather than the wrong thing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs/changes: entry for Agent-Field#1076 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…arts (Agent-Field#1060) * The release-tag grammar has one home, and it is internal/update Stable, rc, dev and staging tags are parsed, compared and bumped in exactly one place now. cmd/codeaf-release keeps its next, kind and prune behaviour and its tests unchanged; it reads the grammar from the new package rather than owning a second copy of it. A second parser is the thing that drifts: the updater about to land needs the same answer to "is this tag newer than that one" that the release tool gives, and one source of truth is the only way they cannot disagree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * internal/update: look up a release, check it, and replace this binary with it The launch check answers only for a build that could sensibly be behind: a stable tag, or a release candidate whose stable line has since been published. A dev or staging build, a make build sha and an unstamped binary make no request at all, because a channel build is deliberately newer than stable and a source build belongs to whoever built it. The answer is cached for a day beside the profile, and every failure is silence. The install downloads into the target's own directory, matches the sha256 from checksums.txt, and only then renames over the running file, so a refused download, a wrong checksum or an unwritable directory all leave the original binary exactly where it was and no temp file behind. The API token never leaves the API host: the redirect policy strips it the moment a redirect changes host, which Go does not do on its own for another port or a subdomain. Windows has no atomic rename over a running image, so it goes through ReplaceFileW and puts the original back when Windows reports the one failure that moves it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * tui3: a launch says a newer codeaf is out, and /update installs it The launch line is one dim transcript note carrying both versions, /update, and the curl line, because the command is what the owner asked to be given. It arrives as a command rather than an opening read, so the first frame is drawn before the network is asked anything and a slow or refused answer draws nothing. /update — /upgrade is the same road — names the tag, then the checksum and where it landed, then quits with a restart plan for the door to carry out. Already newest says so and stays. It refuses while a turn or a task is running, and once it has started, one shared guard keeps a second /update and a new turn from beginning underneath it: the restart must never quit on top of live work, and the home page's Enter roads go through the same guard chat's does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * codeaf update, and the door that restarts the same conversation codeaf update --check prints one line and exits 0 when this release is newest, 3 when a newer one exists, 1 when it could not ask. Without --check it installs stable, or whatever --rc, --dev, --staging or --version names, choosing a channel build by its own publish time the way the installer does. A binary built from source refuses in place and names its path rather than replacing something a developer is holding. The restart happens here and not inside Bubble Tea: the surface fills a slot, the door reads it once the frame is down and the terminal is back, and then execs the new binary with this launch's own arguments plus --session pointing at this transcript, so the conversation reopens where it was. A linked-local engine is asked to retire only after the surface has detached, and it still refuses while another window or any live work is there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * remote: a window that says it is leaving needs no torn-link grace The twenty-second grace exists for a terminal that was killed without saying so, and a deliberate detach is the opposite of that — the window has just announced its departure. Carrying the grace anyway left a cleanly closed window looking attached for two more call deadlines, which kept a freshly installed codeaf waiting behind the engine the old one had already left. It clears only once the last view is gone. Turns, tasks, unanswered questions, other views and live work all still refuse retirement, and a refusal still never stops the work it protects. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * The installer and the updater agree on what a channel tag looks like Two roads now install codeaf, and they were reading the same release list by different rules: the shell accepted any dev-* or staging-* tag and semver parts with leading zeroes, while internal/update accepts only the shapes the release workflow actually publishes. A malformed tag newer than a real one would send the two roads to different builds. One helper carries the grammar to both channel selection and the line that names the channel, and an unrecognised tag for the asked-for channel now says so instead of installing whatever came back. The publish-time ordering Agent-Field#1056 added is untouched: only which candidates are eligible changed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * The manual stops saying that nothing self-updates That sentence was true until this change and is now the opposite of true, so it goes, and a section written in the words people bring takes its place: is there a newer version, how do I update codeaf, why does it say this every time I start. It states who gets the launch line and who never does, the opt-out variable, the day the cached answer lasts, the shell exit codes, and both refusals — a build made from source and an executable nobody may write. The command table names /update and /upgrade with the argument they take. Four probes reach the new section, and the remote page keeps its own asker's words in its heading so the new section cannot take that question from it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs/changes: the update check and /update, for Agent-Field#1060 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * update: --check answers for the channel it was asked about, and a newer build is never downgraded Two things a hand verification caught. --check parsed whatever tag it had selected with the stable grammar alone, so --dev, --staging and --rc could never be "newer" and every one of them answered "you are on the newest codeaf" and exited 0 while a newer build sat in the list. The check now keeps its channel all the way through the decision and always names the tag it is talking about: stable and rc compare as semver, a dev or staging tag is the same tag or a different one, and --version is equal or it is not. And a build AHEAD of the newest stable was quietly installed backwards — the check said newest, the install downgraded it. Both roads refuse now, naming both versions and the deliberate way past: this codeaf is v0.3.0, ahead of the newest stable v0.2.0 — pass --version v0.2.0 to install it anyway An explicit tag IS that deliberate road, so --version and /update <tag> still install exactly what was named. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * update: a failure names the asset and the status, and ends with the curl line A download that failed printed the whole fallback URL, which put the retired product name on somebody's screen. The line names the asset it asked for and the status it got back, and nothing about which repository answered — the compatibility attempts still happen, they are just not the person's business. Every terminal failure now ends with the curl line, which D9 asked for and the download path had been missing. The / list declared no argument for /update while the command page documented one, so the list gains the second row the way /cache has one. The manual section now says what the code does about a build ahead of the newest stable, where it had said such a build "gets nothing" — true of the launch line, and false of the install until this wave. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * update: the curl line is agentfield.ai/get/codeaf, the supported road The launch line, the refusals and the terminal door offered the raw GitHub address of scripts/install.sh. The repository is public and agentfield.ai/get/codeaf has served that script from main since 2026-09-16, so the constant now spells the supported line, the manual's update section names it in place of "the curl line below", and the change entry says so. Asked for on the pull request by Santosh. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ld#1096) * codeaf: a run's own note is offered outside the tree it is judged in leafOutputHint invited a job root to write its overflow document at a workspace-relative name (exec.SuggestPathFor's task-2-x1-toolchain-checks.md shape), so a run working in a person's directory left that note untracked at the root of their working tree. The delivery gate holds the run's record — the artifact registry, joined by jobArtifacts, plus the delivered tree — against the request, read the note as part of what was handed over, and refused the delivery; the run then ended incomplete over complete work. There is no separate mechanical clean-tree check to exempt, so the fix is the writer's: where the workspace belongs to the person, the root's file invitation points at the run's scratch beside its recorders and job logs, and the message keeps carrying the deliverable. * codeaf: a person's own file still joins the record the gate reads TestARootLeafInAPersonsDirectoryPutsItsNotesInScratch baselined the tree after the note was already written, so its landing read could never have observed anything: the record stayed empty because of the ordering, not because of the address. The baseline now happens first, in the executor's own order, and a hint that ever points into the tree again fails on the observation, not only on the spelling. And the other half of the law is pinned down: a file that turns up in the person's tree while the leaf runs — even at a name wearing the run's own task-<n>-<slug>.md shape — is still observed and still lands in the record the delivery gate is held to. Only the run's own note moved; nothing else about what the tree says was loosened. Co-Authored-By: codeaf <agentfield-bot@users.noreply.github.com> * docs: the change note for the root's overflow file moving out of the tree --------- Co-authored-by: codeaf <agentfield-bot@users.noreply.github.com>
…p line (Agent-Field#1106) `┌─┐ / ├─┤ / └─┘` is not an `a`, it is two closed bowls of equal size stacked on each other, and two equal closed bowls are an `8`. That was the second-to-last glyph of the product's own name on the first screen of every fresh install. The owner's answer is to have the two letters at the end stand up out of the word as capitals: `A` is an apex, a crossbar and two legs; `F` is the one letter whose case is told by its arms alone, so its top arm is the longer one. A CAPITAL'S TOP IS THE ASCENDER'S TOP, and three rows of box-drawing could not give it that. A bar is drawn at the middle of its cell and a stem reaches the cell's edge, so the `d` stood half a row over any capital however it was formed — and a stem in the bottom row hung half a row under the bowls' baseline the same way. The fourth row is that half a row made whole: the x-height letters live in the lower three and leave the top row blank; the capitals and the `d`'s ascender start in the top row, the ascender as the half-stroke `╷` so its top is the capitals' bar; and every stem that ends on the baseline ends as `╵`, which stops where `└─┘` does. A `│` in the bottom row is a descender and nothing else. THE WORD IS SPELLED `codeaf` AND DRAWN `CodeAF`. `product` is unchanged, the glyph table is still keyed by its lowercase letters, and a terminal that cannot draw boxes still gets the word `codeaf`. The greeting's centring test read the unit's left edge off the wordmark's first row, which is now the cap line the x-height letters leave blank; it reads the second row. The one manual page that draws the wordmark had been drawing the retired `aforge` letterforms under the label "the codeaf wordmark" — the name law walks text and cannot see a name spelled in box-drawing — and draws the shipped forms now. Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…nt-Field#1546) (Agent-Field#1842) * session: the daily spend ceiling binds task and errand turns too (Agent-Field#1546) railBlockLocked skipped the daily budget check for InTask runners and the home Errand, and the crew guard that also enforces it is absent under --one-model and for an InTask agent with no crew. So a chat /task past the day's limit, and a task under --one-model, kept spending with no refusal while headless codeaf do and standing firings honoured the limit. Remove the InTask/Errand carve-out so the daily ceiling is checked on every turn-starting path. It is a ceiling read of the shared ledger, never a tally, so a path already held to the crew's own day cap is refused in two places, never charged twice. * docs: change entry for Agent-Field#1842 (daily ceiling on every path) * address review: carry the selected daily budget and spend preauthorization into workers ZeroPoint95's review on Agent-Field#1842 caught that the daily check read the default profile for workers (which carry no ProfileDir), so a worker refused under a higher selected limit and spent past a lower one, and that it re-applied the limit --yes-spend deliberately lifts. - Resolve the daily ceiling once at the root (where ProfileDir is set) and carry it as Config.DailyBudgetUSD; dailyCeilingUSD reads live from the profile for a root and the carried figure for a worker, never a profile at an empty path. - Carry Config.SpendPreauthorized (--yes-spend / CODEAF_PREAUTHORIZE_SPEND=1) into workers; it lifts the daily check and nothing else, matching the crew guard. - Thread both through all three worker constructors: the task-child (task_run.go), the adaptive-run worker (orchestrate.go) and the plan-run bash worker (run.CrewFactory, which gains a preauthorized parameter). Adds regressions: a worker held to its carried selected budget (higher allows, lower refuses) not the default, and preauthorization lifting the daily ceiling while a conversation limit stays in force. * fix: preserve daily budget authority in roots and task checks --------- Co-authored-by: ZeroPoint95 <329227198+ZeroPoint95@users.noreply.github.com>
…ld#1877) * docs: record DeepSWE harness versions and release context * docs: add DeepSWE version evidence change entry (Agent-Field#1877) * docs: use public senior-dev lineage and simplify version records
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Keep upgrade retirement behind the original host lock until its journals finish closing. Exercise status, stop, sweeps and idle or busy takeover through both workspace spellings. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Use the same spoken-conversation priority for hello discovery and boot. Prove that two real socket connections reuse one agent for an alias-keyed conversation even when the canonical bucket contains an empty session. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…#1870) * fix: refresh Codex catalog client version * docs: record Codex catalog version fix for Agent-Field#1870 * test: cover version-gated Codex discovery through both catalog doors Pin the backend fixture independently of the shared client version so reverting the advertised version hides newer models in both direct and translated listings. Preserve visibility filtering and cached reasoning metadata. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Abir Abbas <abirabbas1998@gmail.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix: build valid SQLite file URIs on Windows * docs: record Windows SQLite URI fix for Agent-Field#1868 * test: preserve POSIX SQLite URIs and cover Windows path forms Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Abir Abbas <abirabbas1998@gmail.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
@ningmao-hlyz thanks for working through two rounds on this. Since nobody had re-reviewed after your last push, I've taken it over. I verified each point and pushed two fixes on top of your branch (no history rewritten). @ZeroPoint95, all three of your points from the 14th:
I also merged current dev. The PR gate passes on Go 1.26.5: build, vet, fmt, packed manual, changelog, laws, carried help and belt pages, plus the codeaf / rest / tui3 legs. The only noise was a TempDir cleanup flake in an unrelated shell subtest, which passes on retry and on dev. On the security side, the compatibility lookup only matches a recorded workspace whose |
AbirAbbas
left a comment
There was a problem hiding this comment.
Verified at 58b3a98: alias-keyed hosts and conversations from older builds stay reachable from both spellings, the two edge cases found during takeover (legacy-lock retirement, hello vs boot priority) are fixed with failing-before tests, and the four macOS-shape fixtures pass under symlinked TMPDIR/HOME. PR gate green.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Use LaunchDir only for owned conversations so hello and boot find their old HOME or temporary bucket and retain their private workspace. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ogether Use one directory inventory for Attach, Run, startup probing and retirement. Recheck under the canonical lock and retain legacy locks for the host lifetime so a listener gap cannot allow a second engine. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Accept either citation for owned conversations while keeping launch-only matches unavailable to borrowed sessions. Cover both HOME and temporary owned launches. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Carry canonical and historical owner keys through launch, resume, anchoring and workers. Read and edit old rows in place while keeping new writes canonical, including non-Git folders and repositories without origin. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Search canonical and cited historical prefixes together. Match local workspace and transcript aliases while preserving machine, conversation, live-window and newest-record guards; save restored composers before pruning their old records. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Use the same resolved path classification at launch and sweep, including symlinked HOME and TMPDIR outside /tmp. Retain the temporary citation and verify the sweep can still classify it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Describe owned launch citations, path-memory aliases, draft restoration, lifetime-lock handoff and normalized temporary provenance without promising a merge of historical buckets. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Keep the store graph owner invariant for corrections, with new independently remembered lines still written canonically. Carry a proven raw workspace through the existing local hello field so flat-session memory owners stay readable without metadata. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Retain validated launch spellings in the surface even when a flat session has no metadata citation. Propagate that read provenance with each composer and clear it alongside draft storage on takeover. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Use a physical target in the citation-filtering fixtures so macOS temp-root symlinks do not introduce an extra valid incoming alias into their expected sets. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…1845) * fix(session): require explicit work orders for auto tasks * docs(changelog): add entry for PR 1845 * test(session): tolerate prompt line wrapping * fix(session): distinguish polite work requests from questions * test(session): cover routing authorization and stopped-result scope Separate feasibility questions from explanation requests in the shared route contract and the searchable chat manual. Pin examples with their complete classification clauses, and prove stopped-result suppression does not block fresh requests or normal remediation. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(manual): make stopped-result routing reachable through search Give the stopped-result rule its own section so the manual context includes both the restart guard and the fresh-human-request exception. Pin the actual retrieved answer, since merely finding the long tasks page hid the rule beyond the section limit. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(session): judge human steering during stopped-result wakes Read current-turn person provenance and judge the newly consumed instruction, including a verbatim repeat of the stopped request. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(session): scope stopped-result routing to each delivered task Keep attempt snapshots and effective obligations distinct even when sibling tasks inherit the same request. Skip result-only cancelled work before classification, and show both readers only the independent eligible work with explicit cancellation boundaries. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(session): restore task cancellation from its saved ending Derive the stopped runtime flag from the persisted stopped ending. Keep restored cancelled results out of routing while ordinary failed results retain repair eligibility. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(session): limit stopped-result guard to unambiguous result-only wakes Keep dev's routing inputs, floors and rulings whenever a human speaks, results are mixed, or provenance is incomplete. Skip both classifiers only when every delivered result is stopped for the current request in a result-only wake. Remove scoped eligible-work questions, attempt tags and snapshot deduplication. Retain frozen stop provenance and restored cancellation for the stop-only case. Preserve untagged news through batching without changing existing owed asks. Compare production wake, Submit, retry, cancel and recovery probes against dev, and narrow the manual and change entry to the conservative guarantee. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(session): drain steering before reading routes in stopped-wake tests Both fresh-human stopped-wake tests submitted a steer from the answer hook but let the turn race the boundary drain, so CI could count one route reading instead of two. Drain inside the hook, as the mixed-result steering test already does. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Abir Abbas <abirabbas1998@gmail.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Status update: I'm holding this one and moving it back to draft. Neither the original head nor the current head is safe to merge. The fix itself is right: a folder reached through a symlink should be one workspace. The hard part is the upgrade. Two independent reviews found that changing the key with nothing else hides data existing users already have:
I pushed a round (79b8171..de11a6b) that reads old keys as aliases when a recorded citation resolves to the same folder today. A third review found that approach is unsafe too, because a symlink can be repointed. With This needs a decision before more code: old keys can only be trusted if the project that owned them is recorded durably. The options I see:
@ZeroPoint95, which direction do you want? The reproductions for every finding above are written as Go tests, and I'll attach them to whichever route you choose. |
a2f0486 to
d68bcf8
Compare
Fixes #1761.
What changed
A folder reached through a symlink became two projects, two conversation
histories and two engines. Both doors where a person's path becomes a key now
resolve symlinks with
filepath.EvalSymlinksbefore keying:engineWorkspace(cmd/codeaf/engine.go) — the resolver every enginecommand goes through (
--daemon,--status,--stop) and the source ofthe host's workspace name. It stopped at
filepath.Clean, sosha256(Clean(...))inenginehost.whereminted one host directory perspelling:
--status <link>missed the engine started at<dir>./project(internal/tui3/projectcmd.go) — the pin stored the raw typedspelling, so home keyed the folder under the alias while a launch from the
resolved path (already canonicalized by
v3OpenTarget) keyed it under thetarget: two project rows, one folder.
Regression test:
cmd/codeaf/engine_symlink_test.godrives a symlink throughengineWorkspaceand asserts both spellings land on the same directory, andthat a plain path still resolves to itself.
Changelog entry:
docs/rules/changelog.mdnames the entry for its pullrequest, and the number exists only once the pull request does — so it
arrives in the next push as
docs/changes/unreleased/<this-pr>-symlink-workspace-keys.md(
kind: fixed,surface: [engine, workspace]). The first red oncheckis that workflow, not a mistake.
How it was checked
go build ./cmd/codeaf ./internal/tui3— exit 0 (go1.26.5).go test ./cmd/codeaf -run TestEngineWorkspace -count=1 -v— both testspass:
TestEngineWorkspaceResolvesSymlink,TestEngineWorkspaceKeepsNonSymlinkPathUnchanged(ok … 2.434s). To runthis on Windows the other
cmd/codeaftest files were moved aside for therun and put back — see the limitation below.
GOOS=linux go vet ./cmd/codeaf ./internal/tui3— exit 0, the platform CIruns, so every test file (including the added one) typechecks there.
gofmton the committed blobs — clean (engine.go,projectcmd.go,engine_symlink_test.go).8761488a: a stdlib probe mirroringenginehost.whereshowed
sha256(Clean(<link>))≠sha256(Clean(<real>))— two spellings,two host directories — and that
EvalSymlinkscollapses both spellings toone host directory.
Honest limitation:
cmd/codeaf's test package does not build on Windows atall —
carried_child_test.gousesfakeFolderProgram, which lives behind//go:build !windows; the identical failure exists at base8761488a. Sothe full suite cannot run on my Windows checkout and defers to CI on Linux
(where
GOOS=linux go vetpasses).Checklist
(the number does not exist until the pull request does; see
docs/rules/changelog.md).or refusal moved (keying fix only), so
internal/manual/chat/isunchanged.
.github/known-red.txt.cmd/codeaf/engine.go,cmd/codeaf/engine_symlink_test.go,internal/tui3/projectcmd.go.