specs/018-orchestrator/tasks.md
Tasks: task orchestration
Input: spec.md (US1–US7), plan.md, data-model.md, contracts/ipc.md,
quickstart.md
Tests: TDD-first throughout — test tasks precede implementation in every phase and MUST fail before the implementation lands.
Lanes (for parallel implementers; shared files are handed off, never
co-written): A server tasks (store.rs, persist.rs, router.rs
task handlers, params.rs task params) · B pty/term/submit
(pty.rs, headless.rs, router.rs pane handlers) · C git/finish
(git.rs, file_diff.rs, worktrees.rs, paths.rs data dir) · D CLI/GUI/docs
(signaltty-cli, signaltty-gui, docs). router.rs/params.rs are
single-writer: A and B sequence their edits (B's submit/reads first, then A's
task handlers, or vice versa — agree at phase start).
Phase 1: Setup (shared fixtures + E2E skeleton)
- T001 [US6] Write the deterministic E2E skeleton in
crates/signaltty-server/tests/orchestrator_e2e.rs(temp repo → 3 async starts back-to-back → waitworking→ fake hooks → reports (worker C via the input_required leg: turn ends without reporting → settled wait ends → follow-up submit → report) → context wait → reads → restart → merge/discard asserts); MUST fail (notask.*methods yet). Disabled (#[ignore]) until Phase 8 greens it. - T002 [P] [US6] Add testkit fixtures in
crates/signaltty-testkit/src/lib.rs: temp git repo helper, synthetic hook-event driver, server restart-on-same-socket/state helper.
Checkpoint: E2E skeleton compiles, fails honestly, documents the bar.
Phase 2: Foundational (entity + protocol — blocks all stories)
- T003 [P] Core
Task/TaskState/transitions + contract/result validation (objective 1…32 KiB, summary …8 KiB, artifacts ≤ 32) with in-module unit tests incrates/signaltty-core/src/model.rsandcrates/signaltty-core/src/state.rs. - T004 [P] Protocol constants in
crates/signaltty-proto/src/lib.rs:task.*methods,pane.submit,attention.pending,task.*events +task.*glob, codesNO_SUCH_TASK,AGENT_BUSY,AGENT_NOT_READY,MERGE_CONFLICT; extendALLlists + schema output. - T005
Store.tasks+ transition methods (create/report/cancel/fail/ finish-record/input_required-on-turn-end/background-ready, each pairing mutate + emit per the AGENTS.md rule — NO direct task-state writes from hook handlers, the background step, or recovery) incrates/signaltty-server/src/store.rs;Snapshot.tasksserde-defaulted + restore incrates/signaltty-server/src/persist.rs; Pane lineage fields (parent_pane_id,root_pane_id,label,relationship,task_id) serde-defaulted incrates/signaltty-core/src/model.rs;target_branchrecorded at start; legacy-snapshot load test incrates/signaltty-server/tests/orchestrator_tasks.rs. - T006 [P]
max_parallel_tasks(default 4) server flag + config incrates/signaltty-server/src/config.rsandcrates/signaltty-server/src/main.rs.
Checkpoint: Entity + protocol exist; legacy snapshots load; nothing callable yet.
Phase 3: US2 submit + US3 reads (lane B; pane primitives)
Independent Test: submit accept/refuse/stall on synthetic-hook panes; rendered/incremental reads on TUI fixtures.
- T007 [US2] Failing tests: submit accept on idle/done,
AGENT_BUSYon working/blocked with zero bytes written (read-before/after),AGENT_NOT_READYon unknown, on never-started panes, and on a worker pane whose task is stillpending(background first-submit in flight),PANE_EXITEDon exited, activity-gateTIMEOUT, fast-completion match incrates/signaltty-server/tests/orchestrator_submit.rs. - T008 [P] [US3] Failing tests: tail-vs-rendered equality on TUI repaint
fixtures, incremental delta-only second read, dropped-range signal after
eviction/restart in
crates/signaltty-term/tests/(new) +crates/signaltty-server/tests/orchestrator_read.rs. - T009 [US2]
pane.submithandler + typed params incrates/signaltty-server/src/router.rs/crates/signaltty-server/src/params.rs(gate → baseline → bracketed paste → delay →\r→ activity gate via existingWaitBaseline; accepted submit on aninput_requiredtask's worker pane moves the task toworkingvia the Store transition method; submit to apendingtask's worker pane refusedAGENT_NOT_READY). - T010 [US3] Rendered ring: vt100 scrollback 5000 +
content_seq+ grid extraction incrates/signaltty-term/src/headless.rs; rebasetailon it; addmode: "rendered"(after_seq/lines→text/seq/next_seq/dropped/truncated) incrates/signaltty-server/src/router.rs+crates/signaltty-server/src/params.rs. - T011 [P] [US2]
signaltty pane submitCLI incrates/signaltty-cli/src/main.rs. - T012 [P] [US3]
pane read --mode rendered --after-seqCLI incrates/signaltty-cli/src/main.rs.
Checkpoint: US2 + US3 green on panes without any task involved.
Phase 4: US1 task start (lanes A+C; sync cap → base → worktree → pane, async background ready + submit)
Independent Test: async start in temp repo (returns pending, background
moves to working); assert worktree/branch/base/target_branch/pane/
lineage/env; over-cap refused with nothing created.
- T013 [US1] Failing tests: async happy-path start (returns
pending, thentask.wait --until workingsucceeds after background ready + submit),RATE_LIMITEDover cap (nothing created), empty objective refused, bad base ref refused, existing branch adopted as pre-existing, concurrent same-path starts serialized, default path under$XDG_DATA_HOME/signaltty/worktrees/(explicitpathwins),target_branchrecorded (detached HEAD → unset),task.get/task.listround-trip incrates/signaltty-server/tests/orchestrator_tasks.rs. - T014 [US1]
data_dir()(XDG_DATA_HOME,~/.local/sharefallback — same pattern asstate_dir/config_dir) incrates/signaltty-core/src/paths.rs; base resolution +target_branchrecording + locked worktree create + branch adoption incrates/signaltty-server/src/git.rsandcrates/signaltty-server/src/worktrees.rs(per-path lock, defaults for branch/path,fetch_first). - T015 [US1]
task.starthandler + typed params incrates/signaltty-server/src/router.rs/crates/signaltty-server/src/params.rs(cap → base → worktree → spawn with lineage/task env incrates/signaltty-server/src/pty.rssynchronously, returning{task, pane}atpending; ready-wait + submit as a server-owned background step via Store transitions — survives client disconnect, cancelled by cancel/discard/shutdown; failures → failed-with-evidence with{stage, …}). TheStop-with-no-report →input_requiredtransition lives in the hook handler (lane A, Store transition method); theStop-vs-report race resolves by checking report arrival first (report wins). - T016 [P] [US1]
pane.spawn --parent-pane/--label/--relationship+task startCLI incrates/signaltty-cli/src/main.rs. - T017 [P] [US1]
task.get/task.listhandlers + CLI incrates/signaltty-server/src/router.rsandcrates/signaltty-cli/src/main.rs(failing round-trip tests already in T013).
Checkpoint: US1 green; E2E legs 1–3 pass.
Phase 5: US4 results + needs-user (lane A; report/wait/attention)
Independent Test: reports store + emit + transition; context wait ends on all-terminal; permission block → ranked listing → native answer → resume.
- T018 [US4] Failing tests: report stores + emits
task.resultand moves state per status; second report refused; unknown task refused;task.waitsingle + context + empty-context-immediate + timeout + default-settledending atinput_required; turn-ended-without-report →input_requiredwith{reason, last_message?}→ follow-up submit moves back toworking→ report completes;attention.pendingrank order with mixed panes;task.*glob subscribe +task_ids[]filter with replay; native-permission block/answer leg (reuse theprovider_fixturepattern fromcrates/signaltty-server/tests/native_permissions.rs) incrates/signaltty-server/tests/orchestrator_tasks.rs. - T019 [US4]
task.report+task.wait(defaultuntil: settled; single-flight, EOF/shutdown cancel) +attention.pendinghandlers + typed params +task_ids[]subscribe filter incrates/signaltty-server/src/router.rs/crates/signaltty-server/src/params.rs. - T020 [P] [US4]
signaltty report,task wait,attentionCLI incrates/signaltty-cli/src/main.rs.
Checkpoint: US4 green; E2E legs 4–5 pass.
Phase 6: US5 diff + finish (lane C; review gate)
Independent Test: diff-vs-base incl. untracked with clean index; merge happy/conflict/dirty paths; discard scoping; branch rules; cleanup-error separation.
- T021 [US5] Failing tests:
task.diffshows only own change vs base (tracked + untracked), index untouched (porcelain clean apart from test edits); merge lands + disposition; conflict →MERGE_CONFLICT, target clean, files named; dirty source/target refusals; staged-work refusal; pre-existing branch never deleted; merge into non-recorded checkout refused (BAD_PARAMS{expected, actual}); detached-HEAD start requires explicittarget_refat finish; cleanup-error separation; discard removes only the task path; second finish refused;task.cancelkeeps checkout incrates/signaltty-server/tests/orchestrator_finish.rs. - T022 [US5]
task.diff/task.file_diff(base-SHA baseline, direct untracked reads, noadd -N) incrates/signaltty-server/src/git.rsandcrates/signaltty-server/src/file_diff.rs+ handlers incrates/signaltty-server/src/router.rs. - T023 [US5]
task.finish(merge/discard rules incl. recorded-target checkout check, abort-on-conflict, guarded cleanup, disposition recording) +task.cancelincrates/signaltty-server/src/router.rs(+ git helpers incrates/signaltty-server/src/git.rs/crates/signaltty-server/src/worktrees.rs). - T024 [P] [US5]
task diff/file-diff/finish/cancelCLI incrates/signaltty-cli/src/main.rs.
Checkpoint: US5 green; E2E legs 6 + 8 pass (restart leg still red).
Phase 7: US6 restart survival (lane A; recovery wiring)
Independent Test: restart mid-run → completed intact, running-with-dead-pane failed with evidence, checkouts preserved; pane exit fails task exactly once.
- T025 [US6] Failing tests: restart with mixed tasks (completed intact +
results + diffs work; running → failed with evidence + checkout preserved;
pending→ failed with{stage: "restart"}); pane kill → failed exactly once (no double emit on close-after-exit) incrates/signaltty-server/tests/orchestrator_e2e.rs. - T026 [US6] Pane-exit hook → task fail in
crates/signaltty-server/src/store.rs; restart recovery incrates/signaltty-server/src/persist.rs(apply); read cursors reset as dropped after restart incrates/signaltty-term/src/headless.rs.
Checkpoint: US6 green; full E2E (incl. #[ignore] removal) green.
Phase 8: US7 GUI rows (lane D; minimal surfacing)
Independent Test: worker pane row shows label + state; updates on
task.updated without terminal rebuild; light + dark screenshots.
- T027 [US7] Headless GUI tests for the task chip (present/absent/update)
in
crates/signaltty-gui/src/sidebar.rs(orapp_tests.rsif the harness fits better — keep display tests out unless no other seam exists). - T028 [US7]
task.*subscription +task.getcache + row chip render incrates/signaltty-gui/src/actor.rsandcrates/signaltty-gui/src/sidebar.rs.
Checkpoint: US7 green with screenshot evidence.
Phase 9: Polish & cross-cutting (lane D)
- T029 [P] Docs rows: Task in
docs/02-data-model.md, one row per new/ changed method and event indocs/08-ipc.md(task.start/get/list/wait/ report/diff/file_diff/finish/cancel,pane.submit,pane.spawnlineage params,pane.readrendered mode,attention.pending,task.*events — re-check every method has both an integration-test task above and a docs/08 row), plus the self-printing schema extension (specs/002 rule: every schema method must dispatch; extend the drift test). Implementers update; ADR-0020 already written by the spec author — amend only if implementation diverged, with reasons. - T030 [P] Agent-facing orchestrator loop doc IN the shipped skill
(
crates/signaltty-cli/assets/SKILL.md, served bysignaltty skill— checkskill.rsembedding): worked example — start 3 tasks back-to-back,task waittosettled, incrementalpane read --mode rendered, handlinginput_required(follow-up submit),task diff,task finish --merge/--discard— with thesignaltty schemapointer for the exact contract. - T031 Run
quickstart.mdend-to-end manually, thenscripts/verify.sh full; attach evidence; independent diff review; un-ignore the E2E test.
Dependencies & execution order
- Phase 1 → Phase 2 → (Phase 3 ∥ Phase 4 supports 5/6/7; Phase 3 is pane-only and can run ∥ Phase 4 after Phase 2) → Phase 5 → Phase 6 → Phase 7 → Phase 8 → Phase 9.
- Within a story: tests MUST fail before implementation; core before handlers; handlers before CLI.
- Shared-file protocol:
router.rs/params.rssingle-writer at a time (Phase 3 lane B, then Phases 4–7 lane A/C sequential edits). Test files are owned per area — implementers never co-edit:orchestrator_e2e.rs(acceptance scenario + restart),orchestrator_submit.rs(US2),orchestrator_read.rs(US3, plussignaltty-termcrate tests),orchestrator_tasks.rs(US1/US4/US6 task-state tests),orchestrator_finish.rs(US5). Shared fixtures/helpers live insignaltty-testkit(T002).headless.rslane B only until Phase 7's cursor-reset line (one hunk, coordinate). - Suggested MVP cut: Phases 1–5 (spawn/submit/read/report/wait) already orchestrate; 6–7 complete review + survival; 8 is the GUI chip.
Parallel example
| 1 | # After Phase 2: two implementers, disjoint files |
| 2 | # Lane B: T007+T009 (router pane paths) then T008+T010 (headless.rs) |
| 3 | # Lane C: T014 (git.rs/worktrees.rs) |
| 4 | # Lane D: T011+T012+T016 (cli/main.rs) once params are stable |