BOARD_AGENT_COMMANDS is the one setting a headless agent cannot work
around, and since the install stopped asking for it a project the
detector does not recognise starts with it empty — correctly, but
silently, until a run ended with an agent explaining it could not verify
its work.
Say it twice, in the two places it is learnable: a quiet `no agent
commands` chip in the header (`--idle`, like the drive's "no driver",
never `--alarm` — nothing is failing, something is unconfigured), and a
note appended to the ticker line of the launches that would have run
those commands, work and act-pr. Neither blocks anything: an agent that
only edits files is still useful.
What counts as empty is answered once, by `config.agent_commands()`,
which splits exactly as the adapters' own `split_commands()` does — so
whitespace and a lone comma are nothing configured on the board as well
as at the launch, and the page reads the server's boolean rather than
the raw setting.
Starting a phase cuts phase/<stem> from the newest main it can see and
works the list into it: each member branched from the phase's tip, run
headless, merged back when its checks are green, the next one started.
At the end one PR into main, for a human. The human gate moves from
every card to the phase boundary, and the promise survives: the board
merges into a branch it created, inside a scope you opened.
The runner is a beat, not an agent — everything it decides is already
structured state, and an agent paid to poll would be the wrong tool at
the wrong price. It holds no registry of where a phase is. Two durable
things carry the memory, and the board already writes both: git, where
a member is finished when its branch is contained in the phase branch,
and the card, which grows a ## Phase log the runner adds one line to
per decision. The log is what tells "this member has run and it ended
badly" from "the phase has not reached it yet" — without it a
restarted board would relaunch a run that died.
Containment alone is not enough to call a member merged: a clean exit
that committed nothing leaves an empty branch that is contained. The
card has to have settled into review/ too, or a broken launch would
hide exactly where it always tries to.
Five conditions halt, each already a visible state on the card, and a
halt is written once and then held. Running the phase again is the
person's decision and is what clears it — the run is scoped to its own
log line, so a member whose run died is launchable again. A dependency
that has not landed is a wait, not a halt.
Merges are additive throughout: main into the phase branch on every
beat so a long run does not drift into one enormous conflict, members
into it as they go green, nothing rebased and nothing force-pushed. A
conflict aborts, leaves the branch as it was, and halts naming the
files that collided.
The actor rule decides who runs it, written where it already lives:
the phase card's assignee. A replica renders the phase and advances
nothing. Reachable through /api/phase/run and the ticker; the header
chip and the card actions are a separate card.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
REMOTE/UPSTREAM were module constants, so a team-mode board whose remote
is named anything but origin synced nothing at all — silently, with a
healthy header. The remote is now resolved per use (config's answer, the
one PRs already honour) and threaded through fetch, publish, replay and
integrate; every message names whatever was resolved.
The silence is the other half. Where _converge()/push_now() returned a
bare 'no-origin' that both callers discarded, _remote() now _note()s at
stalled level first: no remote at all names both fixes (add one, or set
BOARD_GIT_REMOTE), and a BOARD_GIT_REMOTE naming a remote this checkout
does not have stalls naming it rather than quietly using another. It
clears with a recovery line like the offline path, and install() asks at
startup so the chip is there from first paint, not the second beat.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A project could run bench for months without a manager/local/.env:
everything fell back to core/.env.example, so the two settings that
change what bench is — claim-on-move and syncing through origin/main —
were invisible to anyone who had not read that file.
install.py now writes it on a first run. It asks three questions no
default can be right about (solo or team, which agent adapter, the
project's test command) and writes core/.env.example with the answers
substituted into their lines, comments and all keys intact — so the
written file is where the project reads what else it can change.
- Runs after first_boot_clean: .env is one of the two things the
first-boot guard reads, so writing it earlier would skip the clean.
- Never asks without a terminal on stdin. install.py sits on the path
of start.sh, update.sh and every hook, so no TTY prints one line and
carries on rather than blocking a board start on an invisible prompt.
--dry-run reports the questions and writes nothing.
- An existing .env is never touched; --setup is the only way back to
the questions, pre-filling from the current file and rewriting it in
place, so start.sh's fallback BOARD_PORT line survives.
- Bare Enter takes every default (the result is the example verbatim,
i.e. today's behaviour exactly); Ctrl-D skips the rest.
start.sh needed no change — it already calls install.py before the port
dance, which is the right order. Tests drive the interactive runs over a
real pty and the non-interactive ones with /dev/null on stdin.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AGENTS.md gets the "State syncs; reactions don't" section under Syncing
boards — the actor-only rule, the file-carried backstops behind it, and
the two consequences you can see (a half-done side effect is nobody's to
finish automatically; ownership gates work launches). The Pull requests
section says which board opens one and how ↑ open PR asks afterwards, the
card-actions list gains ▸ take over and ↑ open PR, "Claiming a card" stops
saying the claim refuses nothing, and the merge & clean up paragraph
documents the origin-side merge — including the constraint the local path
hid: whoever clicks needs merge rights on the repo, not just push rights.
.env.example: BOARD_COMMIT_MOVES documents the claim as a launch gate,
BOARD_SYNC the actor-only reactions and the origin-side merge.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AGENTS.md gets the "Syncing boards" section — what pushes, what pulls,
what a lost race looks like, and the two disciplines that make it safe:
local main advances only through the board and origin, and sync never
merges. The module map gains sync.py, the settings paragraph and the
Board view's description gain the sync chip and the author attribution,
and .env.example documents BOARD_SYNC / BOARD_SYNC_INTERVAL, including
the fetch-rate line for rate-limited remotes.
Copilot review on PR #15: the 'gate off' wording in AGENTS.md and
.env.example read as 'no assignee', but read_task() always parses the
Assignee line and the card face always renders it. The gate governs only
whether a move writes/clears the line and commits. Reword both to say so.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every board tab read "Bench — task board", so the moment a second bench
existed the tab bar stopped saying which was which. The title now leads
with the project — "<project> · bench" — because tab truncation eats the
tail and the tail is the same in every bench tab.
The project is config.PROJECT: the repo directory's name, or BOARD_TITLE
from local/.env for people whose checkouts are all called "app". The
server renders it into the served page's <title>, so the tab is right on
first paint with no flicker from generic to named; /api/state carries it
too, and renderTitle() keeps it in step when the view switcher swaps the
tail (sessions, focus). The project stays the first word regardless, and
nothing else writes document.title.
Tests: tests/test_board_title.py covers the server half in fresh
interpreters (BOARD_TITLE resolution, the rendered title, escaping, the
rest of the page untouched) and the browser half as source invariants,
the same way the other board.html tests work.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AGENTS.md gains the convention — claiming is moving, the assignee is who
launches agents on the card, hand-moves bypass the claim and should update
the line by hand, and git identities collide the way git's do — plus the
**Assignee:** header field beside Status/Priority/Type and the
BOARD_COMMIT_MOVES gate with its unpushed-main consequence.
.env.example documents the setting with its default (off).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Resolutions honouring both sides:
- CLAUDE.md: main's pointer form wins; this branch's release-era wording
(tree diagram line, Updating section) ported into AGENTS.md where the
brief now lives.
- update.sh: this branch's release-based rewrite wins; main's additions
to the old file list (AGENTS.md from task 13, LICENSE) are carried by
the release-manifest instead: added copy AGENTS.md and copy LICENSE.
- Starter local/: manifest and release.sh now seed the split pair
(AGENTS.md notes + pointer CLAUDE.md) mirroring task 13's layout;
artifact test updated to match.
- tests/test_update_round_trip.py (main's, aimed at the removed
git-clone mechanism) retired; its two 13-specific intents ported into
test_update_from_release.py as
test_agents_brief_replaces_an_old_vendor_named_copy, its third already
covered there. 147 tests green.
- release.sh's lost executable bit restored.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
README's first instruction becomes the tokenless curl | tar against
the stable latest-release URL; the clone path moves to a "working on
bench itself" note. CLAUDE.md's Updating section and .env.example's
BENCH_SOURCE comment describe release-based updates: the source is
stamped at build time, BENCH_SOURCE is an override rather than a
prerequisite.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rename-vs-edit conflict: task 13 (landed) moved the workflow brief to
AGENTS.md leaving CLAUDE.md as a pointer, while this branch had edited
the brief's work-launch section in place. Resolution: CLAUDE.md keeps
main's pointer form; this branch's documentation of the
branch-from-origin behaviour moved verbatim into AGENTS.md where that
text now lives. agents.py and the rest auto-merged; 103 tests green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BOARD_AGENT_MODEL picks one model for every headless launch;
BOARD_AGENT_MODEL_WORK / _ACT_PR / _REVIEW beat it per intent (review
covers PR reviews and relevance checks). Empty — the default — means
the variable never reaches the adapter and the vendor's own resolution
applies, exactly today's behaviour; a stray AGENT_MODEL in the board's
own environment is stripped rather than inherited silently.
Core carries the name as an opaque string: config resolves intent →
model, _launch passes it as AGENT_MODEL, and each adapter renders it
natively — claude appends --model, opencode sets the generated
config's model key ("provider/model-id", per its docs). The resolved
model (or its absence) is recorded on the agent record, so the
Sessions and Focus views state what a run actually rode instead of
leaving it to whoever's machine the board happens to run on.
Stub-binary tests pin the byte-identical-when-unset guarantee, the
per-intent resolution, the launch-env seam and the record.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A fresh ▸ start work used to branch from whatever HEAD the board's
checkout happened to hold, so anything merged on GitHub or pushed by a
teammate was missing from the agent's world. Now, when an origin remote
exists, the launch fetches origin/main (bounded by BOARD_FETCH_TIMEOUT,
default 10s) and branches from it — without ever touching the main
checkout itself. No remote, a failed fetch or a timeout all fall back
to branching from HEAD exactly as before: launching is never blocked
by network weather.
The ticker narrates honestly either way: when origin/main is ahead of
the local checkout the launch line says by how much, and when the fetch
had to be skipped it says that too.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
What counts as a definition-of-done check was the origin project's
stack (pytest / lint-imports / frontend) frozen into core: emit.py
classified against inline literals and the Focus panel carried three
fixed rows with duplicated regexes. Per the three-layer law that is
project knowledge, so it now lives in one file: core/checks ships the
old rows as the default, and a checks file in manager/local/ replaces
it wholesale — the same filename-wins resolution as prompts.
- core/checks: '<label>: <command regex>' per line, self-documenting;
read fresh on every use.
- emit.py classifies Bash commands against the resolved file (kind
'check', the label carried into the summary) and judges pass/fail
generically — counted results, broken totals, OK/FAILED verdict
lines — since the hook payload carries no exit status. The runtime
moved under a __main__ guard so the classifier is importable.
- config.checks() mirrors the parser (the bridge stays standalone) and
httpd serves it in /api/state; the Focus panel renders one row per
served entry, matching events with the same patterns — no fixed
rows, no duplicated regexes.
- manager/local/checks gives bench its real definition (unittest), so
the self-hosted board shows a check that can actually run.
- The default BOARD_AGENT_COMMANDS drops its pytest prefix: core no
longer names any stack outside the shipped checks default, and a
test walks manager/core to keep it that way.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Work agents were launched under acceptEdits with no Bash allowlist, so
headless runs could edit files but never run tests or commit — the exact
contradiction card 05 documents. Each launch intent (work, act-pr,
review) now carries a permissions.allow list granting exactly what its
prompt demands, delivered through the same generated settings JSON as
the event hooks. Project test/check commands arrive as neutral prefixes
via BOARD_AGENT_COMMANDS; a new opencode adapter renders the same three
stances in its config language as the portability proof. A clean agent
exit with an empty branch no longer advances the card to review.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Everything core from cicero-pas's .task-manager, with instance data
reduced to skeleton: empty stage directories, the task template, empty
local/ scaffolding, and a README covering install (clone into
.task-manager/, vendored on purpose) and update (update.sh replaces core
wholesale; local/ and tasks/ survive).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>