Skip to content

fix(workspace): name a workspace by its resolved path - #1798

Draft
ningmao-hlyz wants to merge 3219 commits into
Agent-Field:devfrom
ningmao-hlyz:fix/1761-symlink-workspace-keys
Draft

ningmao-hlyz wants to merge 3219 commits into
Agent-Field:devfrom
ningmao-hlyz:fix/1761-symlink-workspace-keys

Conversation

@ningmao-hlyz

Copy link
Copy Markdown

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.EvalSymlinks before keying:

  • engineWorkspace (cmd/codeaf/engine.go) — the resolver every engine
    command goes through (--daemon, --status, --stop) and the source of
    the host's workspace name. It stopped at filepath.Clean, so
    sha256(Clean(...)) in enginehost.where minted one host directory per
    spelling: --status <link> missed the engine started at <dir>.
  • /project (internal/tui3/projectcmd.go) — the pin stored the raw typed
    spelling, so home keyed the folder under the alias while a launch from the
    resolved path (already canonicalized by v3OpenTarget) keyed it under the
    target: two project rows, one folder.

Regression test: cmd/codeaf/engine_symlink_test.go drives a symlink through
engineWorkspace and asserts both spellings land on the same directory, and
that a plain path still resolves to itself.

Changelog entry: docs/rules/changelog.md names the entry for its pull
request, 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 on check
is 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 tests
    pass: TestEngineWorkspaceResolvesSymlink,
    TestEngineWorkspaceKeepsNonSymlinkPathUnchanged (ok … 2.434s). To run
    this on Windows the other cmd/codeaf test files were moved aside for the
    run and put back — see the limitation below.
  • GOOS=linux go vet ./cmd/codeaf ./internal/tui3 — exit 0, the platform CI
    runs, so every test file (including the added one) typechecks there.
  • gofmt on the committed blobs — clean (engine.go, projectcmd.go,
    engine_symlink_test.go).
  • Reproduction at base 8761488a: a stdlib probe mirroring enginehost.where
    showed sha256(Clean(<link>)) ≠ sha256(Clean(<real>)) — two spellings,
    two host directories — and that EvalSymlinks collapses both spellings to
    one host directory.

Honest limitation: cmd/codeaf's test package does not build on Windows at
all — carried_child_test.go uses fakeFolderProgram, which lives behind
//go:build !windows; the identical failure exists at base 8761488a. So
the full suite cannot run on my Windows checkout and defers to CI on Linux
(where GOOS=linux go vet passes).

Checklist

  • A change entry — arrives in the next push, named for this pull request
    (the number does not exist until the pull request does; see
    docs/rules/changelog.md).
  • The manual knows about it — no slash command, key, tool, default, limit
    or refusal moved (keying fix only), so internal/manual/chat/ is
    unchanged.
  • No new line in .github/known-red.txt.
  • Only my own paths are staged — cmd/codeaf/engine.go,
    cmd/codeaf/engine_symlink_test.go, internal/tui3/projectcmd.go.

AbirAbbas and others added 30 commits September 14, 2026 12:46
…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>
ZeroPoint95 and others added 8 commits October 15, 2026 14:26
…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>
@AbirAbbas

Copy link
Copy Markdown
Collaborator

@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:

  1. Engines started by older builds. I rebuilt your upgrade fixture: an alias-keyed host dir with a live socket and a real flock. Held, Dial, Stop, --status, --status-all, --stop-all and idle/busy takeover all reach it from both spellings (10 door × spelling cases), and the built binary passes the same check. That turned up one more gap. Once the old socket closed, retirement only checked the new canonical lock, so it could report success while the old engine was still holding its lock and flushing journals. Retirement now waits for the legacy lock too (73262a5). The test fails on the previous head and passes now.
  2. Existing conversations. An old alias-bucket conversation resumes in place through v3Workspace → v3ResolveSession. One edge case remained: if an empty canonical conversation existed, hello picked it while boot resumed the spoken legacy one, and two real socket hellos booted two agents onto the same journal. Hello now uses the same spoken-history priority as boot (58b3a98), and the regression shows one boot with two connections. Canonical spoken history still wins, and explicit sessions are unchanged.
  3. macOS fixtures. Running with TMPDIR and HOME both pointed through symlinks (the /var → /private/var shape), both hostlatest hello tests, the empty-workspace status test and the typed /project test pass, with their assertions intact. The old versions of those fixtures reproduce your four failures in the same setup.

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 EvalSymlinks equals the requested folder. It skips symlinked state dirs, and foreign, missing or looping citations never match. A Dial miss scans the host dirs linearly, which is fine for normal inventories; I'd only index it if someone accumulates hundreds.

@AbirAbbas
AbirAbbas requested a review from ZeroPoint95 October 20, 2026 13:54
AbirAbbas
AbirAbbas previously approved these changes Oct 20, 2026

@AbirAbbas AbirAbbas left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

ningmao-hlyz and others added 15 commits October 21, 2026 01:04
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>
@AbirAbbas

Copy link
Copy Markdown
Collaborator

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:

  • path-owned project memories drop out of the project's scope,
  • owned conversations under a symlinked HOME stop resuming,
  • saved unsent drafts aren't found,
  • a starting legacy engine doesn't block a second engine on the same folder.

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 current -> A retargeted to B, opening B by its plain path pulls in A's historical memories, including correct and forget on them. It also restores A's unsent draft into B and refuses to start B's engine while A's is running. Separately, the CLI normalizes the workspace before taking the lock, so the raw key never gets locked in the window before a legacy daemon starts listening.

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:

  1. Durable association. When a host, conversation, memory owner or draft is created, record the physical target (or a stable project ID) next to it. Treat an old key as an alias only when that recorded target matches.
  2. One-time migration. At first launch after upgrade, re-own path-keyed memories, drafts and buckets whose recorded workspace resolves to the current folder, and record that the migration ran. Never re-resolve later.
  3. Narrow scope. Canonicalize only the engine host key, which is what workspace: a folder reached through a symlink becomes two projects and two engines #1761's duplicate engines are about, and leave conversation, memory and draft keys alone for now.

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:session The engine — turns, tasks, the toolbelt, checkpoints bug Something the code does that it should not sev:serious Wrong or missing behaviour a person meets in ordinary use

Projects

None yet

Development

Successfully merging this pull request may close these issues.

workspace: a folder reached through a symlink becomes two projects and two engines