10 Commits
Author SHA1 Message Date
istosandClaude Opus 5 b690060be2 Finishing a phase finishes its cards, and clears up after them
Merge & clean up on a phase card now ends the phase: after the merge into
main has actually succeeded, every member the phase merged moves to done/
and its workspace is cleared the way completing an ordinary card clears
its own — worktree removed, local branch deleted, branch on the remote
deleted.

Only what the phase merged. A member is swept when its card settled into
review/ or done/ and its branch is contained in the phase branch (or
there was never a branch to bring) — the same pair the runner reads a
member as merged by. One that halted, was held or was walked back keeps
its card, its worktree and its branch: there is work in them.

Nothing uncommitted is thrown away: a member's worktree comes out without
--force, and a dirty one is reported in the ticker and kept with its
branch rather than forced.

One ending, told once. taskfiles.move_together moves the cards in a
single commit naming all of them (`board: 47, 52 → done with phase 53`,
so it publishes in team mode like any other board commit), and the moves
are marked quiet so the watcher does not also scroll five identical move
lines behind the one line the ending gets.

The other endings are untouched: "just move the card" and archiving the
phase card move no member, because neither puts anything in main.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 10:08:00 +02:00
istosandClaude Opus 5 463b35bdaf A card joins a phase without the file being opened
Phases are meant to arrive whole — members listed, dependencies filled in, readable in a diff before any of it runs. This is the other case: the card you decide belongs after all.

⟶ phase sits on backlog/ and to-do/ cards that are not already in a phase and are not phase cards themselves, and opens a sheet naming the phase cards waiting in to-do/ with what each already holds. Picking one appends `- <n> — <title>` to the end of that phase's ## Cards — the way a person writes it, because the section is authored and read by hand and a machine-shaped line is how a format stops being pleasant.

Only to-do/. A phase in in-progress/ is running: its branch exists and its members are being worked in the order the list had when it started, so appending mid-flight is a different feature with different questions. Offer it and someone finds that out the hard way. No phase waiting there and the action is absent rather than present and empty.

Nothing else moves. One line into the phase card, nothing at all into the card added — membership runs one direction and joining a phase is not a commitment to start it. The append goes out through append_to_section, the same door the phase log uses, so it commits itself under BOARD_COMMIT_MOVES, reaches the other boards, and reads the phase card off the disk rather than off a render: two boards adding to one phase produce two lines, not a lost one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 11:21:27 +02:00
istosandClaude Opus 5 ecde8abb53 A member of a phase branches from the phase, not from main
Two pieces the runner needs, in the modules that own them.

taskfiles grows a third door: append_to_section, one line under one
heading. The phase log is a running record, and append_to_task would
scatter its lines through the file as other sections landed between
them — the record would stop being readable in the one place a person
looks. It commits like every other board-made write.

agents learns where a phase member starts. That is the whole reason a
phase has a branch: related cards run one after another, so card two
branched from main could not see card one's work while card one sat
unmerged in review/ — it would conflict, or quietly build the same
thing twice. A card in no phase, or one whose phase has not been
started, takes the ordinary fresh branch point, and the ticker names
the unusual base as it already does.

claim_for_launch loses its underscore: a phase run claims its card the
same way starting work on one does, from another module.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 09:43:35 +02:00
istosandClaude Opus 5 9621437479 A phase is a card that lists its cards
`**Type:** Phase` plus a `## Cards` section is the whole model. The phase
card is the only authority on membership: it lists its members in run
order, and a member's phase and position are derived from that list
rather than stored on the member — one direction, one place to edit.

- taskfiles.py reads the section (numbers only; `31`, `#31` and `031` are
  one card, indented lines are a member's own continuation) and parses
  `Depends on` at last, into the numbers it names. Nothing acts on it:
  the list says what runs next, the line says whether it may.
- collect() weaves the board: members resolved in document order, each
  member given its phase and 3/5 position. What will not resolve is
  flagged rather than skipped — a number no card has, a card two phases
  both list (both flagged), a card listed twice, a line naming no number,
  a phase listed by a phase.
- board.html gives a member card a `⟶ <phase> 3/5` chip in the footer
  row beside CI and PR ↗, opening the phase card, and wears phase drift
  as a pill next to status drift.

Nothing runs yet — no branch, no launches, no advancing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 09:01:37 +02:00
istosandClaude Opus 5 b462b83a7f Archiving a card reaches git, like every other task-file write
An archive renamed the file on disk and stopped there, leaving an
uncommitted deletion of a tracked file — exactly what sync._clean()
refuses to run over, so one drag onto the tray stalled every later board
commit, kept the archive off the other boards, and left the card in a
single working tree. Appended agent reports had the same gap.

taskfiles now has one door out of a stage directory, _relocate(): it
registers the expectation, writes, moves, and commits under the same
COMMIT_MOVES gate a move uses — so archive, unarchive and move all commit
by construction rather than by each caller remembering. The messages read
like the others (`board: <n> -> archived (<name>)`, and the stage slug on
the way back) and carry the `board: ` prefix sync's piggyback guard
requires; routing through _commit means state.task_committed() fires, so
the event-driven push publishes it.

The second door is append_to_task(), which writes into a card where it
stands and commits via commit_edit(); agents._file_report() uses it
instead of touching config.TASKS itself, so a run's closing report is
recorded rather than left modified.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 16:57:09 +02:00
istosandClaude Opus 5 755d40dd33 etiquette: the actor's board acts, every other replica renders
Task 19 gave every board the same truth; this makes exactly one of them
react to it. State syncs; reactions don't.

- watch.py: attribution is now the trigger gate. _actor returns (who,
  remote), and a move a pull applied — the arrivals sync files — renders
  and narrates but opens no PR. A plain mv on this disk still acts: inert
  means "happened elsewhere", not "unattributed".
- github.py: the file-carried gates behind that rule, so the rare double
  is harmless rather than loud. The **PR:** line commits itself in team
  mode (taskfiles.commit_edit, sharing the move's pathspec-scoped commit),
  so it reaches the other boards instead of sitting in one working tree —
  where it would also stall sync outright; and a `gh pr create` that races
  anyway adopts the PR GitHub already holds.
- No board finishes the actor's half-done side effect on its own: the
  startup reconcile stands down in team mode and a review card with a
  branch and no PR carries ↑ open PR (POST /api/pr/open), which is a
  person deciding rather than N boards guessing. _open_pr raises its
  reasons now, so the automatic path narrates them and the explicit one
  toasts them.
- agents.py: the claim gates work launches. A card someone else holds
  refuses, naming them; ▸ take over is the deliberate second path (armed
  like everything that costs tokens) and reassigns via taskfiles'
  set_assignee; an unheld card claims itself on launch. Only in team mode
  — with BOARD_COMMIT_MOVES off nothing writes an assignee, so nothing
  reads one as a lock.
- github.complete_task: with BOARD_SYNC on, merge & clean up runs
  `gh pr merge` and lets the beat deliver the result, so local main only
  ever fast-forwards and no board makes a merge commit of its own. A
  branch without a PR is refused with a pointer to ↑ open PR. Sync off
  keeps the local merge path exactly as it was.

Verified with tests/test_actor_acts.py: two real clones of a real bare
upstream and a stub gh — the replica that only renders, the PR line that
travels, the double that adopts, the takeover that reassigns, and both
merge paths.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 11:18:17 +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
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 e10b5cf1ed Move the workflow brief to AGENTS.md; keep CLAUDE.md as a pointer
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>
2026-07-30 07:35:28 +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