---
description: "Phase tracker mandatory contract  -  every mode depends on this. Native TaskList widget renders the phase tiles to the user; phase-tracker.sh holds the state file (powers :resume / :log / :status)."
---

# Phase Tracker  -  Mandatory Contract

> **TLDR**  -  At every phase boundary the agent does two things: (1) writes to the state file via `phase-tracker.sh` (identical on every CLI), (2) drives the visual channel for the CLI it's running in  -  `TaskCreate`/`TaskUpdate` in Claude Code, `phase-tracker.sh render` in every other CLI. The state file alone is not enough; the user must see the phases progress.

## Why two channels

| Channel | Job | Lives in |
|---|---|---|
| **Visual** (CLI-specific) | The widget or card the user actually sees | CLI-specific (TaskList, chat output, stdout) |
| **`phase-tracker.sh` state** | JSON state file (`~/.claude/logs/multi-agent/<task_id>/tracker-state.json`)  -  read by `:resume`, `:log`, `:status` | Disk  -  survives the session, **identical across CLIs** |

Both channels carry the same state. If one is missing:
- Visual only → `:resume` does not work; the run history is lost
- State file only → the user sees no progress, only bash stdout snapshots

## Visual channel  -  chosen by the agent based on the host CLI

The agent detects which CLI it's running in and uses the appropriate visual mechanism. Detection order:

```
1. system prompt mentions "Claude Code"             → claude-code
2. system prompt mentions "Copilot" / "GitHub Copilot" → copilot
3. system prompt mentions "Codex"                    → codex
4. None of the above                                 → generic (bash stdout)
```

Visual mechanism per CLI:

| CLI | What the agent calls at every phase boundary |
|---|---|
| **claude-code** | `TaskCreate({subject, activeForm})` then `TaskUpdate({status, activeForm})`. Native sticky widget; ⏺ tiles, spinner header. |
| **copilot** | Inline call: `bash phase-tracker.sh render`. The bordered ANSI card lands as the last tool result in the chat. |
| **codex** | The native `update_plan` tool: one plan step per phase, `status: pending \| in_progress \| completed`. Rewrite the whole step list on each boundary  -  the tool takes the full plan, not a delta. |
| **generic** (plain shell, Git Bash, WSL, tmux) | Same bash render  -  the bordered ANSI card prints to terminal stdout in place. |

**Common to every CLI**: `phase-tracker.sh add/update/meta/tokens` calls run identically → the state file is always correct, and `:resume` / `:log` / `:status` work on every CLI.

**Claude Code only**: in addition, `TaskCreate` / `TaskUpdate` native tool calls → the sticky widget pins the phase stack in the user's view.

### Codex specifics

Two constraints on `update_plan`, both of which fail quietly if ignored:

- **Never call it in parallel with another tool.** Codex states this explicitly. A
  phase boundary that batches `update_plan` alongside other calls loses the update.
- **It is unavailable in Codex plan mode.** Fall back to
  `bash phase-tracker.sh render` there rather than skipping the visual channel.

The plan step is the phase label, not a restatement of the work: `Phase 4  -  Review`
in the step list, with detail going to the agent log. A plan that mirrors the phase
list is legible; one that mirrors the task list duplicates what the log already
holds.

## Call pattern

At every phase boundary the agent takes **two steps**:

1. **State**: `phase-tracker.sh add/update/meta/tokens` (identical on every CLI, never changes)
2. **Visual**: CLI-specific  -  TaskList API on claude-code, `phase-tracker.sh render` on every other CLI

### Phase 0 startup  -  pipeline init

```bash
TASK_ID="<id from input parsing>"   # PROJ-12345 / repo#316 / cwd-derived
bash $HOME/.claude/scripts/phase-tracker.sh init "$TASK_ID"
```

Phases by mode:

| Mode | Phases |
|---|---|
| `/multi-agent` | 0,1,2,3,4,5,6,7 |
| `/multi-agent:local` | 0,1,2,3,4,6,7 (Phase 5 User Test needs a worktree checkout; local has none) |
| `/multi-agent:autopilot`, `/multi-agent:local-autopilot` | 0,1,2,3,4,6,7 (always Full; autopilot drops the interactive Phase 5 gate) |
| `/multi-agent:analysis` | 0,1,2,4,6,7 (no code is written, so no Dev and no Test) |

What changed in v16.0.0: the two picker entries (`/multi-agent`, `:local`) register their FULL set even when the run turns out to be Short, and there is a timing reason. The tracker boots at Step -1, the first thing in every run, while the depth picker cannot run before Step 7.5 - its recommendation needs `taskType`, which needs the fetched issue and the branch. So Phases 1 and 2 are registered `pending` like any other and flipped to `skipped` at 7.5 if Short is chosen. See "Late skip" below.

Register each phase:

```bash
bash $HOME/.claude/scripts/phase-tracker.sh add 0 "Init"
bash $HOME/.claude/scripts/phase-tracker.sh add 1 "Analysis"
# ...
```

### Claude Code  -  also register a TaskList tile per phase

```text
For each phase in the current mode, IN PHASE-NUMBER ORDER (0 → 1 → 2 → ... → 7):
  TaskCreate({
    subject: "Phase 0: Init",
    description: "Repo discovery, branch, worktree, identity bind",
    activeForm: "Initializing pipeline"
  })
  → returns taskId
  bash phase-tracker.sh meta 0 tasklist_id "<taskId>"
```

The `tasklist_id` meta field is persisted so that `:resume` can rebuild the TaskList from the state file in a fresh session.

#### TaskCreate ordering (strict)

**The native TaskList widget renders tiles in TaskCreate creation order, NOT by phase-number metadata.** Therefore:

> **All TaskCreate calls for the active mode's phase set MUST fire in strict phase-number order BEFORE any TaskUpdate is applied. No "pre-mark skipped phases as completed before Phase 0" reasoning is permitted  -  even when the agent knows in advance that a phase will be skipped.**

Why: an agent reasoning "the user picked Short, so phases 1/2 will be skipped  -  let me TaskCreate them as completed first" produces tile IDs `1, 2, 3, ...` for phases 1/2/4, then the Phase 0 tile gets ID `4` and visually drops below them. The user sees `1, 2, 4 ✓ · 0 ▶ · 3 ☐ · ...` instead of `0 ▶ · 1 ✓ · 2 ✓ · 3 ☐ · 4 ✓ · ...`.

The correct sequence is **always**:

```text
# Step A  -  register every phase tile in the ACTIVE MODE'S SET, in phase-number
# order. A phase outside the mode's set gets no TaskCreate at all  -  the example
# below is the full pipeline, whose set happens to be all eight.
TaskCreate(Phase 0)  → taskId₀
TaskCreate(Phase 1)  → taskId₁
TaskCreate(Phase 2)  → taskId₂
TaskCreate(Phase 3)  → taskId₃
TaskCreate(Phase 4)  → taskId₄
TaskCreate(Phase 5)  → taskId₅
TaskCreate(Phase 6)  → taskId₆
TaskCreate(Phase 7)  → taskId₇

# Step B  -  only AFTER every tile is created, apply status updates
TaskUpdate(taskId₀, status="in_progress")              # Phase 0 starts
# A phase that IS in the set but short-circuits at runtime flips here, e.g. a
# full-pipeline run whose Phase 5 test gate is suppressed by autopilot:
TaskUpdate(taskId₅, status="completed", activeForm="[SKIPPED]")
```

Mode-specific phase sets:

| Mode | TaskCreate set (in order) |
|---|---|
| `/multi-agent` | 0 → 1 → 2 → 3 → 4 → 5 → 6 → 7 (all 8; 1 and 2 flip to skipped at Step 7.5 if the user picks Short) |
| `:local` | 0 → 1 → 2 → 3 → 4 → 6 → 7 (same late skip; Phase 5 is not in the set at all) |
| `:autopilot`, `:local-autopilot` | 0 → 1 → 2 → 3 → 4 → 6 → 7 (7 phases  -  always Full, and the interactive Phase 5 gate is dropped) |
| `:analysis` | 0 → 1 → 2 → 4 → 6 → 7 (6 phases  -  no code is written, so 3 and 5 are not in the set) |

A phase outside the mode's set gets no TaskCreate at all; the `[SKIPPED]` pattern applies only to a phase that IS in the set and short-circuits at runtime. Phase 4 is in every mode's set as of v14.0.0. The authoritative per-mode set is the `for p in ...` init block in each mode's own entry doc, generated by `gen-mode-dispatch.mjs`; this table mirrors those blocks.

#### Late skip  -  the depth picker

The two picker entries cannot know their phase set at Step -1, and the ordering rule above forbids pre-marking. The contract already has the answer, and it is the only permitted one: register the tile in order with the default `pending` status, then flip it when the phase actually short-circuits.

```text
# Step -1, before anything else: all eight, in order, all pending
TaskCreate(Phase 0) ... TaskCreate(Phase 7)

# Step 7.5, after the depth answer. Short only:
TaskUpdate(taskId₁, status="completed", activeForm="[SKIPPED]")
TaskUpdate(taskId₂, status="completed", activeForm="[SKIPPED]")
bash $HOME/.claude/scripts/phase-tracker.sh update 1 skipped
bash $HOME/.claude/scripts/phase-tracker.sh update 2 skipped
```

Order holds because every tile was created before any update. Nothing is pre-marked: at creation time the run genuinely does not know, and the flip happens at the moment it learns.

**Enforcement**: `smoke-tasklist-ordering.sh` scans the dispatcher (`commands/multi-agent/SKILL.md`) and every mode entry point doc (`commands/multi-agent/{autopilot,local,local-autopilot,analysis,resume-local}/SKILL.md` + the Copilot full-inline orchestrator mirror) for the explicit "in phase-number order" rule. Inventory drift fails the smoke.

### Other CLIs  -  call render after every state change

There is no TaskList outside Claude Code. Instead, after each state change the agent calls:

```bash
bash $HOME/.claude/scripts/phase-tracker.sh render
```

This prints a bordered ANSI card to the chat. The user sees the up-to-date phase table as the last tool result. Do not wrap in markdown  -  raw bash output is correct.

### Phase entry

**Claude Code:**
```text
TaskUpdate({taskId: <saved>, status: "in_progress"})
bash phase-tracker.sh update <N> in_progress
```

**Other CLIs:**
```bash
bash $HOME/.claude/scripts/phase-tracker.sh update <N> in_progress
bash $HOME/.claude/scripts/phase-tracker.sh render
```

### Phase exit

**Claude Code:**
```text
TaskUpdate({taskId: <saved>, status: "completed"})
bash phase-tracker.sh update <N> completed
```

**Other CLIs:**
```bash
bash $HOME/.claude/scripts/phase-tracker.sh update <N> completed
bash $HOME/.claude/scripts/phase-tracker.sh render
```

`failed` and `skipped` follow the same pattern, with the matching status.

### Active phase  -  "what is it doing right now"

**Claude Code**  -  `TaskUpdate({activeForm: ...})` updates the spinner header:
```text
TaskUpdate({taskId: <dev_task>, activeForm: "Editing TopBarView.swift"})
TaskUpdate({taskId: <dev_task>, activeForm: "Running xcodebuild test"})
```

**Other CLIs**  -  the dedicated `now` action (quiet: writes `meta.Now`, never renders; the card picks it up at the next boundary render/update):
```bash
bash $HOME/.claude/scripts/phase-tracker.sh now 3 "editing TopBarView.swift"
```

The `Now` value shows up in the bordered card as `Now: editing TopBarView.swift` (truncated to 60 chars by the script).

**Progress-line mirror (required).** Every `normal`-tier progress line from the progress contract's canonical "When to emit" set is ALSO mirrored to the active phase, so the tracker always answers "what is it doing right now":

- Claude Code: `TaskUpdate({activeForm: "<line text, arrow prefix stripped>"})`
- Other CLIs: `phase-tracker.sh now <N> "<line text, arrow prefix stripped>"`

Throttling rules: mirror only canonical-set lines (`verbose`-tier internals are not mirrored); at most one mirror per emitted line; skip the mirror when the text is identical to the last mirrored value; the mirror itself never triggers a render. Text is truncated to 60 chars.

### Delegated phases  -  mirror limitation + chunked dispatch (required)

When a phase's work is delegated to a subagent (Phase 3 Dev on Opus in a Short run, `create-component` plugin dispatch, Phase 1 explorers, Phase 4 reviewers), the visual channel freezes for the duration of the Agent call: the orchestrator is blocked while the call is in flight, so it cannot fire `TaskUpdate` / `now` / `tokens`, and a subagent cannot drive the parent session's TaskList (its own TaskCreate/TaskUpdate calls land on an invisible child list). The progress-line mirror above can therefore only fire while the orchestrator holds control. Rules:

1. **Pre-dispatch marker.** Immediately before every Agent call, set the active-phase line to the delegation itself, so the frozen interval at least states what is running and on which model:
   - Claude Code: `TaskUpdate({activeForm: "Dev subagent (opus): <task subject>"})`
   - Other CLIs: `phase-tracker.sh now <N> "dev subagent (opus): <task subject>"`
2. **Chunk long delegations.** A phase whose delegated work spans multiple tasks MUST NOT go out as one monolithic Agent call. Dispatch per task (the Phase 2 task graph, or in a Short run the self-generated task list, is the natural chunk boundary) so the orchestrator regains control at each boundary and refreshes `activeForm`, `tokens`, and `now` between chunks. Single-task phases and inherently atomic dispatches (one reviewer, one explorer) are exempt.
3. **Post-chunk accounting.** When each chunk returns, record its token estimate (`phase-tracker.sh tokens <N> <in> <out>`) before dispatching the next chunk  -  not accumulated once at phase end.

### 5. Token accounting  -  automatic (manual top-up optional)

The native TaskList header automatically prints `↑Nk tokens · thought for Ns`  -  Claude Code uses session-level metrics. For per-phase token attribution:

```bash
bash $HOME/.claude/scripts/phase-tracker.sh tokens <N> <input_count> <output_count> [cached_count]
bash $HOME/.claude/scripts/phase-tracker.sh model  <N> <model_name>
```

Token counts are additive  -  multiple calls accumulate. `input_count` is FRESH input (cache-exclusive); the optional 4th arg is the prompt-cache-read count, priced at the discounted rate. `model` tags the phase so the card and the cost helper can price it. Stored per phase in the state file; surfaced in `:log` reports, on the bash card tile (`<elapsed> · <tok> tok · ~$<usd>`), and in the card footer (total USD + cached tokens).

**MANDATORY: per-phase token narration on completion (v9.10.2).** The native
TaskList widget cannot display per-phase tokens  -  it shows name, status, and
duration only. Without this rule the user sees durations and nothing else
until the Phase 7 Cost Breakdown. So whenever a phase transitions to
`completed`, in addition to the `TaskUpdate` call, print ONE narrator line in
`outputLanguage` immediately after, using the same totals just written via
`phase-tracker.sh tokens`:

```
Phase <N> <name> tamamlandi - ~<in> giris / ~<out> cikis token (<model>, ~$<usd>)
Phase <N> <name> done - ~<in> in / ~<out> out tokens (<model>, ~$<usd>)
```

USD comes from `phase-tracker.sh cost <N>` - the single pricing implementation
(cost-table.json rates, cached tokens at the discounted rate, floored to
cents; prints `-` when the model is unknown). Do not re-implement the math
inline. Phases with zero recorded tokens print `(no LLM calls)` instead of
fabricating numbers.

**Completion tile suffix (Claude Code).** The native widget cannot show
per-phase tokens, but subject edits after creation are safe (tile order is
fixed at TaskCreate time). So on phase completion, in addition to the
narration line, append the spend to the tile subject:

```text
TaskUpdate({taskId: <phase_task>, subject: "Phase 3: Dev · ~35k tok · ~$0.26"})
```

Skip the suffix when the phase recorded zero tokens (subject stays clean).

**Honesty note:** on Claude Code the orchestrator does not receive its own
usage metering, so per-phase counts are content-size estimates (chars/4 for
prompts dispatched + responses received, subagent payloads included). Prefix
estimates with `~`. The authoritative end-of-run numbers remain the state file
and the Phase 7 Cost Breakdown; the narration line exists so the user sees
live per-phase spend instead of duration-only tiles.

### 6. Phase context  -  rich summaries via `meta`

Store a per-phase summary in the state file:

```bash
bash $HOME/.claude/scripts/phase-tracker.sh meta <N> "<key>" "<value>"
```

`:log` and `:status` reports surface this meta. Suggested keys:

| Phase | Keys |
|---|---|
| 0 Init | Account, Source, Repos, Branch, Worktree, Identity, Board |
| 1 Analysis | Explorers, Findings |
| 2 Planning | Steps, Risks |
| 3 Dev | Model, Files, Tests, Build, WIP |
| 4 Review | Gates, Reviewers, Verdict |
| 5 Test | Scenarios, Result |
| 6 Commit | Branch, Push, PR, Jira |
| 7 Report | Channels, Artifacts |

Meta keys are overwritten  -  values update as the pipeline progresses.

### 7. Sub-phase (optional, e.g. Phase 1 explorers)

There is no nested TaskList for sub-phases  -  sub-progress is reflected in `activeForm` plus a `sub` record in the state file.

```bash
bash $HOME/.claude/scripts/phase-tracker.sh sub <N> <sub_id> "<sub name>" in_progress
```

```text
TaskUpdate({taskId: <phase_task>, activeForm: "Running explorer: repo-map"})
```

## Resume behaviour

When `/multi-agent:resume <task_id>` is called:
1. The state file is loaded and the phase list is read.
2. `TaskCreate` is called again for every phase (new task IDs are issued for the new session).
3. Each phase's status is re-applied via `TaskUpdate` based on the state file.
4. The active phase is determined and the agent picks up where it left off.

The `tasklist_id` meta from the previous session is replaced with the new IDs during resume.

## Continuation runs (finish / manual-test)

A pre-existing `tracker-state.json` for the task is never re-initialized. Rules for any command that continues an earlier run (`/multi-agent:resume-local`, `/multi-agent:manual-test`, resume):

1. `init` runs ONLY when no state file exists for the task. Otherwise the existing file is kept - phase history (elapsed, tokens, model, meta) survives.
2. The continuing command re-declares its phase set with `add` - `add` is idempotent, so existing phases keep their name, status, and token history; only genuinely new phases are appended. The card renders phases sorted by numeric id, so mixed sets stay in order.
3. If Phase 5 was left `in_progress` with `Now: awaiting local test (user)`, the continuing command marks it `update 5 completed` + `meta 5 Result "local test done (user)"` before its own work starts (finish may re-open it with `update 5 in_progress` when its build+test gate runs; elapsed keeps the original `started_at`, which is acceptable).
4. On Claude Code, rebuild the FULL TaskList from the state file (completed tiles included) in phase order before any `TaskUpdate`, refreshing every `tasklist_id` meta - exactly the Resume behaviour above.
5. Print ONE line in `outputLanguage` summarizing the inherited history, e.g. `Continuing PROJ-12345: phases 0-3 finished earlier (12m, 38.4k tok, ~$0.74)` (USD via `cost total`), then `render`.

## Anti-patterns

❌ **TaskList only** → `:resume` will not work; no run history is preserved.
❌ **`phase-tracker.sh` only** in Claude Code → the user sees no widget, only bash stdout snapshots  -  the #1 source of "I see no phases" complaints.
❌ **Wiring `phase-tracker.sh render` to `statusLine` config** → it drops below the screen as a placeholder; the user complained, removed in v7.9.0.
❌ **One monolithic subagent call for a whole multi-task phase** → the tile freezes for the entire phase (no `activeForm`, no `tokens`, no `now`)  -  the user sees a dead widget for 20+ minutes. Chunk per task; see "Delegated phases" above.

## Verification

`smoke-tracker-contract.sh` greps every mode dispatch file for the contract reference. Adding a new mode means adding the same contract section.

## Cross-reference

- `$HOME/.claude/multi-agent-refs/phases.md` section "Visual Phase Tracker" (canonical spec)
- `$HOME/.claude/multi-agent-refs/progress-contract.md` (one-line stdout format, runs alongside the tracker)
- `$HOME/.claude/scripts/phase-tracker.sh` (state file CRUD)
- `$HOME/.claude/scripts/run-aggregator.mjs` (post-hoc cost summary)
