The first full cycle closes: all seventeen cards from two days of
self-hosted dogfooding are done, and the earlier done pile moved to
tasks/archive/ — out of every column, never deleted. The board starts
its next cycle empty.
Co-Authored-By: Claude Fable 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>
The hover actions were easy to miss and mute when hit: a ~20px target
that slid in on hover and reshaped when arming swapped the label, then
zero feedback between the second click and the SSE redraw seconds later.
board.html, one state machine for every action button (hover actions and
$-command chips alike), its truth in S.acts so SSE re-renders mid-window
redraw the same picture:
- Stable geometry: rest/confirm/busy labels stack in one grid cell so the
button is born at its widest state's width; the slot fades in place
(opacity only); buttons are 24px tall and the toprow reserves that
height, so nothing moves or grows under the pointer.
- Visible armed window: alarm colour plus a draining underline over the
disarm timer, lengthened to 5s; a rebuilt button rejoins the drain
partway via a negative animation delay.
- Instant busy state: firing disables the button, breathes the glyph and
shows a participle (starting…, holding…) until the redraw replaces the
card, the runner reports failure, or a 15s timeout restores it with a
toast naming the action — never a silent revert.
- Misses fall harmlessly: the slot pads its hitbox (padding + negative
margin) and swallows clicks itself, so gap clicks neither fire an
action nor open the card sheet.
tests/test_card_actions.py holds the source-level invariants; the full
suite (119 tests) passes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Refuse assets whose tar members or manifest copy paths reach outside
their root, checked before anything is replaced; create parent dirs for
future nested copy entries. Test both refusals hermetically. Replace
setUpClass bare asserts with explicit checks that survive python -O.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both adapters' act-pr stances gain git fetch and git merge so a
conflicted PR can be resolved by merging main into the branch. History
rewriting is denied outright rather than merely unlisted — the plain
'git push' allow would otherwise cover the force spellings — via deny
rules in the claude settings and last-match-wins deny globs in the
opencode config. git rebase stays absent and denied in both.
act-pr.md teaches the flow: merge origin/main, honour both sides'
intent, tests green, the resolution in its own commit naming files and
choices, covered explicitly in the report — and refuse semantic
conflicts (main made the branch's premise false) by aborting the merge
and naming the collision for a human. The adapters README records the
widened contract.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The poller now reads GitHub's mergeable field alongside reviews and
checks. A CONFLICTING PR drops any approved-green verdict (as
changes-needed-by-you, not a CI failure), wears an alarm-coloured
conflicts chip in the card's footer row, and narrates the flip in the
ticker. GitHub computes mergeability lazily, so UNKNOWN keeps the
previous reading instead of flapping the chip.
The poll fold is now a pure function (_fold), so the verdict logic is
testable without gh.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both stops precede any gh call, so they run hermetically in a scratch
git repo carrying only release.sh and a VERSION.
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>
The repo is two things at once — bench-the-project and bench-the-
distribution — and install/update conflated them by cloning the repo
and subtracting what should not have come along. Invert it: one
curated artifact, correct by construction.
- manager/core/release-manifest: the whole shipping list in one place
(copy/tree/once/keep/seed classes). update.sh's hardcoded top-level
file list, promoted.
- release.sh: builds bench.tar.gz from the manifest (contents at the
tarball root, stable asset name — the tokenless latest-release URL
depends on both), stamps the source repo into the shipped update.sh,
refuses on dirty tree or existing tag, tags v<VERSION> and publishes
via gh release create. Never ships in the artifact.
- update.sh: downloads the latest release (BENCH_REF pins a tag) via
gh with an anonymous curl fallback; refuses when the asset's VERSION
disagrees with its tag; replaces manager/core/ wholesale plus the
artifact manifest's `copy` files; touches nothing else. No release
published -> says so and changes nothing; no silent git fallback.
- tests: the tarball equals exactly the manifest (no cards, no local/
content beyond the generated starter, no state/tests/.claude), the
artifact installs pristine and boots the board, and update.sh's
replace/survive/refuse paths run hermetically against PATH-stubbed
gh and curl.
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>
Ships with the distribution: update.sh's core-owned file list carries
LICENSE so installed projects receive it, and the release manifest
(task 15) inherits the list.
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>
The ticker note says the branch point is N ahead of this checkout, but
the count compared origin/main to the local main branch — misleading
whenever the board runs from another branch or detached HEAD. HEAD is
the fallback base, so HEAD..origin/main is exactly what launching from
it would have missed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AGENTS.md is the cross-vendor name (opencode, Codex, Gemini CLI read it
natively; so does current Claude Code), and the brief goes to every
vendor's agents — an opencode work agent previously launched with no
project brief at all. The content moves verbatim to AGENTS.md at the
root and in manager/local/; each CLAUDE.md becomes a load-bearing
compatibility pointer (@AGENTS.md import) for older Claude Code CLIs.
update.sh's core-owned file list now carries both names, so updating an
old-layout install lands AGENTS.md and replaces the full CLAUDE.md with
the pointer instead of resurrecting it. All four core prompts, README,
the board.py/taskfiles.py docstrings, the task template and the adapter
contract docs now name AGENTS.md; adapters/README.md notes that vendors
reading AGENTS.md from the working tree need no adapter work.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Five fixed 296px columns overflowed 13" MacBooks (~1556px footprint vs
~1440 viewport), clipping the Done column. Columns now grow/shrink from
a zero basis, capped at --col (pixel-identical at >=1556px) and floored
at a new --col-min token (240px: five columns + gaps + board padding =
1276px, fitting 1280px viewports). Below the floor the existing
overflow-x:auto fallback is unchanged.
Sessions, Focus and the header are already fluid at 1280px — checked,
nothing to fix there.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The canonical brief still said fresh worktrees branch from current
HEAD; keep it truthful now that a launch fetches origin/main first and
falls back to HEAD only when it must.
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>
The first-boot guard is now its own predicate gating both the clean and
the local/state/ stamp, so a disarmed run (pre-existing local/.env, or
self-hosted) touches nothing at all — before, it still created the stamp
directory on any vendored run. Removal unlinks symlinks instead of
following them into rmtree. Tests filter BOARD_* out of the subprocess
environment, assert the stamp stays absent when .env disarms the guard,
and cover the symlink case.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CLAUDE.md and the task template recited pytest / lint-imports /
frontend as *the* definition of done in core-owned text; both now say
'the project's configured checks' and point at the checks file. Bench's
own local/CLAUDE.md records that its definition of done is the stdlib
suite, kept in the checks file beside it.
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>
Written and then brought into line with the enriched template task 07
shipped — affected-areas line, out-of-scope bullets, a Given/When/Then
edge case for the update.sh round-trip, and a Risks note naming the
pointer file's import as load-bearing for older CLIs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A vendored clone of bench arrives carrying bench's own task cards, plans
and reference documents, so a new user's first board opened pre-loaded
with our backlog. install.py now detects first boot — a vendored install
(project root != manager root) whose manager/local/ has never been
populated (no .env, no state/) — and only then clears the stage
directories, tasks/archive/, plans/ and reference/, keeping
task-template.md and the .gitkeep files and printing every removal.
--dry-run lists instead of removing. After cleaning it stamps
manager/local/state/, so the guard is permanently false on every later
run even if the adapter wire fails, and a host's own cards are never
touched. Self-hosted repos (bench itself, including fresh dev clones)
are never cleaned: their tasks/ is the repo's history.
Covered end-to-end in tests/test_install_first_boot.py by running
install.py as a subprocess against scratch host layouts; README notes
the first-boot behaviour under "Install into a repo".
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Twelve task cards from the first self-hosted dogfooding sessions: 02,
05, 06, 07 done; 08 in review; 09 queued; the rest in backlog. Review
launches no longer pass the retired MultiEdit tool name as a deny rule
(it killed every review/relevance launch at flag parsing — task 10
keeps the follow-up test work). Gitignore .worktrees/ and personal
.claude/settings.local.json; commit the portable .claude/settings.json
that task 02's wiring fix now generates.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Focus 'Right now' well advertised a report with the › chevron but was
a static div. When the rendered event carries detail it is now a real
disclosure — a details.fold whose summary is the well itself, keyed into
S.openFolds like the Sessions timeline's folds so the open state survives
the SSE-driven re-renders, with the report in the fold's mono <pre>,
scroll-bounded when long.
Wells where clicking does nothing no longer wear the chevron: the board
cards' live line and the empty-focus well lead with · instead (the drive
wells' ✳ already meant something else). One glyph, one meaning.
tests/test_focus_well.py encodes the invariants as source-level checks:
› only inside the wellfold summary, detail rendered preformatted, fold
state keyed rather than DOM-only, Sessions folds untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Merge the highest-value slots from a fuller development-task template
into bench's lean one: an optional Depends on header line, an Affected
areas line in Context, Out of scope bullets in What to build, edge-case
and Given/When/Then guidance in Acceptance, and a Risks slot in Notes.
Each slot says who consumes it and is marked deletable — empty
boilerplate is worse than absence.
The load-bearing contracts are unchanged: exact Status values, the Open
questions heading the NOT READY gate keys off, Acceptance as the review
target, and the template staying off the board. Organisation process
(staging, sign-offs, approvals, contacts) is deliberately absent; a new
contract test pins both the gained slots and what must not grow back.
CLAUDE.md's Task file format section documents the Depends on line.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All four core prompt templates now end with the same reader-first
report contract: name the audience (a teammate deciding the card's
fate in under a minute), lead with the state of the work, put action
items in their own list, complete sentences and repo-relative paths,
and 'short but self-sufficient' in place of 'concise'. Machine-parsed
marker lines (NOT READY, RELEVANCE REVIEW, PR REVIEW, ADDRESSED) are
untouched, and a new test locks the block identical across templates,
keeps the markers verbatim, proves str.format still renders, and
checks local/prompts/ overrides still win.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three layout defects broke ./start.sh in the self-hosted repo, one of
which (missing local/state/) broke every fresh vendored install too:
- install.py resolved the project as TM.parent, a hardcoded vendored-
layout assumption. It now asks git for the toplevel from the manager's
directory (same resolution as config._repo_root), falling back to the
parent when git is unavailable — vendored installs still find the host
repo, self-hosted bench finds itself instead of its parent.
- adapters/claude/wire hardcoded ".task-manager/" into the emit hook
command and plansDirectory. Both are now derived from the manager's
path relative to the project root, so vendored installs keep the
.task-manager/ prefix and self-hosted bench gets prefix-free paths.
_is_ours also recognises the emit.py suffix, so settings wired with
the old literal path count as stale and are repaired idempotently.
- board.py and state.py created the sessions/agent dirs without
parents=True; local/state/ is gitignored and ships empty, so a virgin
checkout died with FileNotFoundError before serving. Boot now creates
the whole chain, wiring or no wiring.
tests/test_self_hosting.py (stdlib unittest) covers both layouts' wiring,
stale-path repair, idempotent re-runs, refusal without .claude/, root
resolution with and without git, and an integration boot of board.py
from a scratch checkout with no local/state/.
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>