13 Commits
Author SHA1 Message Date
istos 64f3e2b3bd A board whose agents cannot run anything says so
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.
2026-08-02 13:42:38 +02:00
istosandClaude Opus 5 da8984d0e6 A phase runs itself, on a branch of its own
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>
2026-08-01 09:43:57 +02:00
istosandClaude Opus 5 8194a043da config: one resolver for which remote this board rides
github.remote() decided it for PRs; sync assumed 'origin'. The answer
moves to config, which already owns BOARD_GIT_REMOTE, resolved on demand
so config still shells out nothing at import. github.remote() becomes a
thin call to it, and git_remotes() lets a caller check whether the name
resolves to a remote that exists rather than falling back silently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 07:33:32 +02:00
istos 9d98e2c55a sync: origin/main as the shared board, behind BOARD_SYNC
Team mode's second half. A board-made move already commits itself (18);
now that commit publishes, every board pulls on a beat, and a card two
boards move at once resolves the way git resolves everything else — the
push race is the concurrency control.

- core/sync.py: push is event-driven (a new state.COMMIT_HOOKS registry
  fires it from taskfiles, so taskfiles stays left of everything that
  reacts to it); pull is a beat that fast-forwards, or replays this
  board's own commits on top when the two diverged. A replay that
  conflicts on a task file drops the local move — origin is the
  linearizer — and toasts who took the card.
- The piggyback guard stands in front of every push and every replay:
  each local-ahead commit on main must be `board: `-prefixed, so a
  human's unpushed work is never published as a side effect of a card
  moving. Uncommitted changes, a checkout off main and an unreachable
  origin all stall or degrade rather than risk anything, each narrated
  once instead of once per beat.
- watch.py names the commit author instead of "disk" for moves a pull
  brought, via the arrivals sync files for it; its narration moved out
  of the loop into narrate() so it can be tested directly.
- The board grows a sync chip that appears only when sync stops
  converging, and the SSE stream grows a toast type so the server can
  say something to the person, not just to the ticker.

Gate off (the default) means no fetch, no push, no thread, no change.

Verified with tests/test_boards_sync.py: two real clones of a real bare
upstream race each other through every case above.
2026-07-30 09:47:38 +02:00
istos 48a8d6aa9c Merge branch 'task/18-cards-claimed-on-move' 2026-07-30 09:17:19 +02:00
istosandClaude Opus 5 de5cedb31f Card 22: the tab names its project
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>
2026-07-30 09:11:58 +02:00
istosandClaude Opus 5 22255f2842 Cards are claimed on move: the assignee written, the move committed
Moving a card out of backlog/ or to-do/ now claims it. taskfiles.move_task
writes an **Assignee:** line from `git config user.name` — first claim only,
an existing assignee is never overwritten — and clears it when a card is
walked all the way back to backlog/. The same move then commits itself: one
commit, the move and the claim together, staged by pathspec so a developer's
unrelated staged work is neither committed nor unstaged, messaged
`board: <number> → <stage> (<name>)`. Hooks are skipped (bookkeeping, not
code) and nothing is pushed — that is task 19's job. A commit that fails is
narrated in the ticker; the card has already moved, and disk is the truth.

All of it sits behind BOARD_COMMIT_MOVES, off by default, so a
single-player board moves cards byte-identically to before.

The card face shows the owner instead of "nobody yet" in every stage — on
done/ cards the line reads as history — and the who row now escapes what
the file said.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 09:05:48 +02:00
istosandClaude Fable 5 1bd2cf533b Merge origin/main into task/14: CLAUDE.md became the AGENTS.md pointer
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>
2026-07-30 07:46:26 +02:00
istosandClaude Fable 5 9fed08d174 Let the board choose agent models, per launch intent
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>
2026-07-30 07:37:39 +02:00
istosandClaude Fable 5 4a91e5a841 Card 14: fresh work branches start from fetched origin/main
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>
2026-07-30 07:25:49 +02:00
istosandClaude Fable 5 7df286ba2e Let the project define the Focus checks, resolved like prompts
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>
2026-07-30 07:15:54 +02:00
istosandClaude Fable 5 f8d1bf252d Launch headless agents with permissions that allow their own contract (task 05)
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>
2026-07-29 19:46:23 +02:00
istosandClaude Fable 5 29cbb11eb5 Bench 1: the task board extracted as its own distribution
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>
2026-07-29 17:57:07 +02:00