diff --git a/tasks/backlog/48-a-phase-is-a-card-that-lists-its-cards.md b/tasks/backlog/48-a-phase-is-a-card-that-lists-its-cards.md new file mode 100644 index 0000000..15aa927 --- /dev/null +++ b/tasks/backlog/48-a-phase-is-a-card-that-lists-its-cards.md @@ -0,0 +1,110 @@ +# 48 — A phase is a card that lists its cards + +**Status:** Backlog +**Priority:** High — everything else about phases reads this; get the +shape wrong and three cards inherit it +**Type:** Feature + +A phase is a group of related tasks meant to run one after another. Rather +than a new directory, a new stage or a registry, a phase is **a task card +like any other** — `**Type:** Phase`, with a `## Cards` section naming its +members in the order they run. This card is the model and its rendering: +after it, the board knows what a phase is and shows it. Nothing runs yet. + +## Context + +- `tasks/` holds only state and has to keep working as a plain folder + kanban even if `manager/` is deleted. A phase that lived anywhere else + would be a second source of truth; as a card it is diffable, + greppable, reviewable and syncs like everything else. +- **`Type` already exists and is already read.** `TYPE_RE` + (`manager/core/taskfiles.py:27`) matches any value and `read_task` + surfaces it (`:70`), so `**Type:** Phase` needs no parser change to be + stored — only somewhere to mean something. +- **`Depends on` is documented but parsed nowhere.** `AGENTS.md` + describes it as informational — "the board does not enforce it; it + informs whoever picks the next card" — and a grep of `manager/core/` + finds no reader. This card gives it one. +- Ordering *within* a lane is task `../backlog/25-order-within-a-lane.md` + and is not needed here: a phase's order comes from its own list, not + from where its cards happen to sit. + +**Affected areas:** `manager/core/taskfiles.py` (reading the section and +the dependency line), and `manager/core/board.html` (the chip on a member +card). + +## What to build + +- **`**Type:** Phase`** marks a card as a phase. The name of the phase is + the card itself — its number and title — so nothing new has to be + named, spelled twice or kept in step. +- **A `## Cards` section** listing members in run order, one per line, + each beginning with a task number: + + ```markdown + ## Cards + + - 31 — Stand up site/ and its build + - 32 — Serve it from a Cloudflare Worker + - 33 — The landing page + ``` + + Document order is run order. The number is what is parsed; the title + after it is for the reader and is never matched against anything. +- **One direction only.** The phase card lists its members; member cards + say nothing about phases. Membership therefore cannot disagree with + itself, and there is exactly one place to edit when it changes. +- **`Depends on` is parsed at last, and it guards rather than orders.** + The list says what runs next; a member's dependencies say whether it + *may* — a card whose dependency is not finished is not startable even + if the list reached it. Expose it on the task; do not act on it here. +- **Membership is derived for the member card.** The board reads every + task file already, so a member's phase and its position are computed + from the phase cards rather than stored twice. +- **A `⟶ 3/5` chip** in the member card's footer row, in the dim + register beside `CI` and `PR ↗` — a destination, not a status — opening + the phase card. +- **Drift is flagged, not swallowed**, in the same spirit as `status + drift`: a listed number with no card, the same card listed by two + phases, a card listed twice by one phase. Each is an authoring mistake + that would otherwise surface much later as a runner behaving oddly. + +**Out of scope** — tempting neighbours left alone: + +- Running anything. No branch, no launches, no advancing — card 49. +- The header chip and the run/hold actions — card 50. +- The board action that adds a card to a phase — card 51. +- Nesting. A phase card is not a member of another phase. + +## Acceptance + +- [ ] Given a card with `**Type:** Phase` and a `## Cards` section, when + the board reads it, then its members and their order are available + in the order the section lists them. +- [ ] A member card shows `⟶ /` in its footer row, and + clicking it opens the phase card. +- [ ] A card in no phase shows no chip and is unchanged in every way. +- [ ] Given a `## Cards` line naming a number no card has, the board + flags it rather than silently skipping it. +- [ ] Given the same card listed by two phase cards, both are flagged. +- [ ] `Depends on` is parsed into a list of numbers and exposed on the + task; nothing yet acts on it. +- [ ] Edge case: a `## Cards` section that is empty, or a phase card with + no such section, reads as a phase with no members rather than + failing. +- [ ] Edge case: a number in the list written as `#31` or `31 —` or bare + `31` all resolve to the same card. + +## Notes + +The phase card earns its keep beyond membership: it is where the phase's +own brief lives (what this group of work is for), it is what the board +will later hang a branch and a PR on, and it is what makes the whole +feature reviewable — a phase arrives in the repo as a file you can read +in a diff before anything runs. + +Most phase cards will be written whole, with their members already +listed and each member's `Depends on` already filled in, rather than +assembled on the board. The authoring path (51) is the convenience, not +the primary one — which is a good reason for the file format to be +pleasant to write by hand. diff --git a/tasks/backlog/49-a-phase-runs-itself-on-its-own-branch.md b/tasks/backlog/49-a-phase-runs-itself-on-its-own-branch.md new file mode 100644 index 0000000..d286942 --- /dev/null +++ b/tasks/backlog/49-a-phase-runs-itself-on-its-own-branch.md @@ -0,0 +1,146 @@ +# 49 — A phase runs itself, on a branch of its own + +**Status:** Backlog +**Priority:** High — the point of the whole thing: work that continues +without you until something is genuinely wrong +**Type:** Feature +**Depends on:** 48 — the board has to know what a phase is first + +Give a phase its own integration branch and work the list into it: each +card branched from the phase's tip, run headless, merged back when its +checks are green, and the next one started. One PR at the end, into +`main`, for a human. The runner is a beat on the board, 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. + +## Context + +The problem this exists to solve, precisely: + +- `_fresh_branch_point()` (`manager/core/agents.py:167`) branches every + new task worktree from `origin/main`. For *related* tasks — the whole + premise of a phase — card two branched from main cannot see card one's + work while card one sits unmerged in `review/`. It will conflict, or + quietly build the same thing twice. +- Gating on a merge into `main` would fix the branch point and destroy + the point: `main` is merged by a person, so the phase would stall on + every card. + +So the phase gets **its own branch**, and the human gate moves from every +card to the phase boundary. The promise bench makes — *nothing merges +without you* — is about `main`, and it survives intact: the board merges +into a branch it created, inside a scope you opened, and `main` still +waits for your click. + +What already exists and should be used rather than rebuilt: + +- The board reacts to state without an agent: a card entering `review/` + opens a PR "mechanically, by the board", and the PR poller is a plain + thread checking reviews, CI and mergeable state every 60s. +- `github.public_state()` (`:378`) exposes `{verdict, ci, copilot, + conflicts, url}` per card — the advance condition is readable, not + judged. +- `watch.py` answers *am I the actor?*, and `AGENTS.md` says every future + automation hung off a stage transition inherits it. +- Every failure mode a card can have is already a state it wears: + `NOT READY`, `run failed`, a clean exit that committed nothing. +- `complete_task()` (`manager/core/github.py:387`) is the model for a + careful multi-step git operation that narrates and aborts cleanly. + +**Affected areas:** a new `manager/core/phases.py` — right of `agents` in +the module map, since it needs `taskfiles`, `github` and `agents` — plus +the branch-point change in `agents.py` and a beat wired in `board.py`. + +## What to build + +- **A phase branch.** Starting a phase cuts `phase/` from the + newest `origin/main` it can see, by the same rule and the same timeout + a task branch uses. +- **Members branch from the phase tip**, not from main. This is the one + change inside `agents.py`: where a launch is part of a phase, the + branch point is the phase branch rather than `origin/main`, and the + ticker names it as it already names an unusual branch point. +- **A stateless beat.** On each pass, recompute: which members are + finished, which is first unfinished, what does it need. Hold no + registry — a board restart then resumes a phase by looking, and the + same logic answers "what now?" whether the last event was a launch, a + merge or a crash. +- **Advance on green.** A member is finished when its card is in + `review/` and its CI has passed. Then merge its branch into the phase + branch — additively, never rebasing, never force-pushing — and start + the next member whose `Depends on` are all finished. +- **Halt, never skip.** The five conditions, each already a visible state + on the card: `NOT READY`; a non-zero exit; a clean exit with no + commits; CI red; a merge into the phase branch that is not mechanical. + A phase that steps over a failed card builds the rest on a foundation + that never landed. +- **Keep the phase branch fresh.** Merge `main` into it on the beat, + additively, so a phase that runs for hours does not drift into one + enormous conflict at the end. A conflict there halts the phase like any + other. +- **Finish into a PR.** When every member is finished, push the phase + branch, open one PR into `main` with the member list as its body, write + the `**PR:**` line into the phase card, and move the phase card to + `review/`. From there the existing apparatus applies unchanged — CI + chip, `◔ review PR`, `⚑ copilot`, and drag-to-`done/` for **merge & + clean up**. +- **One board runs it.** The actor rule decides; a replica renders the + phase and advances nothing. + +**Out of scope** — tempting neighbours left alone: + +- Running members in parallel where dependencies allow. Sequential + first; parallelism is a second card once the sequencing is trusted. +- Any UI. The header chip, the run and hold actions and the narration + are card 50 — this card is reachable through the API and the ticker. +- Auto-merging anything into `main`, under any condition. +- Nested phases, and a member belonging to two phases at once (48 flags + that as drift; here it simply must not run twice). + +## Acceptance + +- [ ] Given a phase whose members are unstarted, when it is run, then + `phase/` exists at the newest `origin/main`, and the first + member's worktree is branched from it. +- [ ] Given member one is in `review/` with CI green, when the beat runs, + then its branch is merged into the phase branch and member two is + launched from the new tip — and member two's worktree contains + member one's work. +- [ ] Given a member whose `Depends on` names an unfinished card, it is + not launched even when the list reaches it. +- [ ] Given a member that exits `NOT READY`, the phase halts, the card + walks back as it does today, and no further member starts. +- [ ] Given a member whose run fails, or exits clean with no commits, or + whose CI is red, the phase halts in each case. +- [ ] When every member is finished, one PR is opened from the phase + branch into `main`, the phase card carries its `**PR:**` line, and + the card is in `review/`. +- [ ] Restarting the board mid-phase resumes it without repeating a + launch or a merge. +- [ ] With `BOARD_SYNC` on, a second board shows the phase advancing and + launches nothing itself. +- [ ] Edge case: a phase whose list is empty goes straight to `review/` + with nothing to merge, or refuses to start — either, but not a + branch left behind and a card stuck in `in-progress/`. +- [ ] Edge case: a member card moved by hand while the phase is running + does not cause a second launch on the same task. + +## Notes + +**`AGENTS.md` will need a sentence it does not have.** "Merging remains +yours — the board never merges" is quoted as an absolute. After this it +needs to say what it always meant: the board never merges into `main`. A +phase branch is the board's own, and merging into it is bookkeeping in +the same family as committing a move. + +A lever worth considering once this works: run `◔ review PR` on each +member and require `APPROVE` as well as CI green, with one automatic +`↻ act on PR` round before halting. That would put an agent's review +inside the phase and keep the human's for the boundary — more autonomy, +more tokens, and a bigger change than this card should carry. + +**Risks** — this is the third place in the codebase that merges git +branches, after `complete_task` and `↻ act on PR`'s conflict resolution. +It must behave like both: abort cleanly, leave no half-merged branch, and +narrate every step. A phase branch that ends up in a broken state is +worse than a phase that refuses to start. diff --git a/tasks/backlog/50-the-board-shows-a-phase-running.md b/tasks/backlog/50-the-board-shows-a-phase-running.md new file mode 100644 index 0000000..e9ca3c1 --- /dev/null +++ b/tasks/backlog/50-the-board-shows-a-phase-running.md @@ -0,0 +1,105 @@ +# 50 — The board shows a phase running, and shows it stopping + +**Status:** Backlog +**Priority:** High — a phase that runs unattended is only trustworthy if +its halt is impossible to miss +**Type:** Feature +**Depends on:** 49 — there must be something to watch + +Give the phase the three pieces of interface it does not already inherit: +an action to start and stop it, a header chip while it runs, and the same +chip in `--alarm` when it halts. Everything else — the PR chip, CI, the +review actions, the merge-and-clean-up sheet — the phase card already +gets by being a card. + +## Context + +- The phase card moves through the stages like any other card, and each + stage already means the right thing: `to-do/` queued, `in-progress/` + running, `review/` all members landed and a PR open, `done/` merged. +- Card actions live in the status pill's slot on hover, at most two per + state, and anything costing tokens or stopping work arms on the first + click and fires on the second (`board.html:1191` onwards). + `▸ start work` sits in that slot on an in-progress card, and `‖ hold` + is already the word for stopping while an agent runs. +- The header carries what is happening *across* the board: the live + agents chip, and the sync chip which appears only when sync stops + converging and then holds until a human settles it. A running phase is + the same kind of fact. +- The design system's colour law: `--accent` an agent alive, `--alarm` + blocked or failed, and one looping animation ("breathe") meaning work + is happening. +- A failed run is deliberately told three times at three altitudes — a + state the card wears, a toast, and a line in the ticker — because it is + the outcome a person must not miss. + +**Affected areas:** `manager/core/board.html` only, plus whatever +`/api/state` must carry to describe a running phase. + +## What to build + +- **`▸ run phase`** on an `in-progress/` phase card, in the slot + `▸ start work` occupies on an ordinary one, arming and firing like + every other launch. Moving the card to `in-progress/` stays the + commitment; this is the second half of it. +- **`‖ hold`** while it runs, meaning what it means everywhere else: + stop, without unwinding what has already landed on the phase branch. +- **A header chip while a phase runs**, beside the agents chip, breathing + in `--accent`: the phase, its progress and the card it is on — + `⟶ auth-rework · 3/5 · on #33`. Present only while a phase is running, + the way the sync chip is present only when there is something to say. +- **The halted state, in `--alarm`, holding.** `⟶ auth-rework · halted at + #35 — not ready`, staying until the phase is resumed or stopped rather + than scrolling away. With a toast, because a halt is rare and + actionable, and a ticker line that survives in the log. The member card + wears its own failure independently — that duplication is the point. +- **Narration of each advance** in the ticker: the member that finished, + the merge into the phase branch, the member starting next. A phase that + advances silently is a phase nobody can debug afterwards. +- **The phase card's sheet lists its members in order** with each one's + current stage, so the card answers "where is this up to" without + hunting across five columns. + +**Out of scope** — tempting neighbours left alone: + +- Filtering the board to a phase's cards, or dimming everything else. + Useful, bigger, and better judged once the chip exists. +- A Focus view for a phase. Focus is a heads-up display for one session; + one for a phase is a real idea and a separate build. +- Anything that changes what the runner does. This card watches. +- The `⟶` member chip, which is card 48's. + +## Acceptance + +- [ ] Given a phase card in `in-progress/`, hovering it offers + **▸ run phase**, which arms on the first click and starts on the + second. +- [ ] While a phase runs, the header carries a breathing chip naming the + phase, its progress and the member in flight. +- [ ] Given a member that fails, declines, commits nothing or goes red, + the chip turns `--alarm` and names the reason and the card, a toast + fires, and the chip holds until the phase is resumed or stopped. +- [ ] **‖ hold** stops the phase and leaves the phase branch and every + landed member exactly as they were. +- [ ] With no phase running, the header is exactly as it is today — no + empty chip, no placeholder. +- [ ] The ticker names every advance: what finished, what merged, what + started. +- [ ] Opening a phase card lists its members in run order with each + one's stage. +- [ ] Edge case: two phases could in principle run on one board — either + the chip handles more than one, or starting a second is refused + with a reason. Not a chip that silently shows one of them. + +## Notes + +The reason the halted state gets this much attention is that the feature +is asking for trust: you start a phase and stop watching. Everything in +bench that runs unattended already pays that back the same way — the +failed run wears its state, sync says when it stalls — and a phase that +halts quietly at 2am would undo the whole argument for having one. + +Worth checking while building: the header is getting crowded. Agents +chip, sync chip, phase chip, theme, refresh. If they collide, the phase +chip is the one that can fold into the agents chip — both answer "what +is happening without me" — rather than shrinking the ones already there. diff --git a/tasks/backlog/51-add-a-card-to-a-phase-from-the-board.md b/tasks/backlog/51-add-a-card-to-a-phase-from-the-board.md new file mode 100644 index 0000000..ad53c9a --- /dev/null +++ b/tasks/backlog/51-add-a-card-to-a-phase-from-the-board.md @@ -0,0 +1,101 @@ +# 51 — Add a card to a phase without opening the file + +**Status:** Backlog +**Priority:** Low — the convenience path; phase cards mostly arrive with +their lists already written +**Type:** Feature +**Depends on:** 48 — the list this appends to has to exist and be read + +A `⟶ phase` action on a backlog or to-do card, offering the phase cards +currently in `to-do/` and appending the card to the one you pick. It is +the small path: most phases are written whole, members and dependencies +already in place, before anything reaches the board. This is for the card +you decide belongs after all. + +## Context + +- The phase card's `## Cards` section is the single place membership + lives (48), so adding a card means appending one line to *that* file — + not editing the card being added. +- The board already writes into task files where the file must stay the + source of truth: the `**PR:**` line goes in through `commit_edit()` + (`manager/core/taskfiles.py:242`), which commits under the same gate a + move does and, in team mode, reaches the other boards. +- A choice between several options has a pattern too: the drag-to-`done/` + sheet (`completeSheet`, `board.html:1362`) puts a short list of named + outcomes in front of you rather than guessing. +- Card actions arm before they fire, and the status pill's slot holds at + most two per state. On backlog and to-do cards that slot is close to + free — `◔ still true?` is often the only one there. +- Task `../backlog/47-an-archive-button-on-the-card.md` puts a chip in + the footer row for a similar reason; the two should not fight for the + same corner. + +**Affected areas:** `manager/core/board.html` (the action and its sheet), +`manager/core/httpd.py` (a route), `manager/core/taskfiles.py` (the +append). + +## What to build + +- **A `⟶ phase` action** on cards in `backlog/` and `to-do/` that are not + already in a phase, and not themselves phase cards. +- **A sheet listing the phase cards in `to-do/`**, each with its number + and title and how many cards it already holds. Picking one appends the + card to the end of that phase's `## Cards` section and closes the + sheet. +- **`to-do/` only.** A phase in `in-progress/` is running: its branch + exists, its members are being worked in the order the list had when it + started, and appending mid-flight is a different feature with different + questions. Offer it and someone will find out the hard way. +- **The append goes through the board's own write path**, so it commits + itself under `BOARD_COMMIT_MOVES` and syncs like every other board-made + change to a task file. An addition that never leaves one working tree + is not an addition the phase will run. +- **Written the way a person writes it** — `- 33 — ` — so the + section stays something you would have typed. A machine-shaped line in + a file people author by hand is how a format stops being pleasant. +- **Nothing else moves.** The card stays in its stage; joining a phase is + not a commitment to start it, and the phase decides when it runs. +- **No phases in `to-do/` → the action is absent**, not present and + empty. The board offers what it can do. + +**Out of scope** — tempting neighbours left alone: + +- Removing a card from a phase, and reordering the list. Both are file + edits for now; both are better judged once phases have run a few + times. +- Adding to a running phase. +- Creating a phase card from the board. Phase cards are written, and + writing them is where their brief comes from. +- Multi-select — adding several cards in one gesture — which needs a + selection model the board does not have. + +## Acceptance + +- [ ] Given a card in `backlog/` or `to-do/` and at least one phase card + in `to-do/`, hovering offers `⟶ phase`; choosing a phase appends + the card to that phase's `## Cards` and the member chip from 48 + appears on it. +- [ ] The appended line names the card's number and title, at the end of + the section, and the rest of the phase card is untouched. +- [ ] With `BOARD_COMMIT_MOVES` on, the append is committed on its own, + messaged like the board's other bookkeeping; with it off, nothing + is committed and the file is simply edited. +- [ ] Phase cards in `in-progress/`, `review/` or `done/` are not + offered. +- [ ] A card already in a phase does not offer the action. +- [ ] With no phase in `to-do/`, the action does not appear. +- [ ] Edge case: a phase card with no `## Cards` section yet gets one, + rather than the line being appended to the end of the file. +- [ ] Edge case: two boards adding to the same phase produce two lines, + not a lost one — the second append reads the file as it is on disk + rather than as it was rendered. + +## Notes + +The reason this is Low and not Medium: the intended way a phase arrives +is fully formed — a card written with its members listed and each +member's `Depends on` filled in — because that is a thing you can read in +a diff and reason about before any of it runs. This action is for the +afterthought, and it should stay small enough not to become the way +phases are assembled.