Interactive planning agent for building implementation plans. Produces plans with a precise protocol contract and UI specs for parallel daemon/web execution.
Installs into .claude/skills of the current project.
Are you the author of Plan Work?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/zalaras-plan-work)
---
name: plan-work
description: "Interactive planning agent for building implementation plans. Produces plans with a precise protocol contract and UI specs for parallel daemon/web execution."
argument-hint: "<plan-name> \"<description>\""
allowed-tools: Read, Write, Edit, Grep, Glob, Bash
---
> Maintainer note: a skill, not an agent — a multi-turn interview needs the main session (a subagent returns one message). Don't re-create an agent twin.
Work with the developer to build an implementation plan and write it to `plans/<plan-name>/plan.md`.
The plan is what lets the daemon and web agents work in parallel: its protocol contract and UI
specifications must be detailed enough that neither needs to see the other's code. You are done
when the developer has approved it and the Approval steps below have run.
## Arguments
This command was invoked with: **$ARGUMENTS**
Expected format: `<plan-name> "<description>"`
- `plan-name`: kebab-case identifier (e.g., `session-list-ui`)
- `description`: a quoted string describing the work
If no arguments are provided, ask the developer for a plan name and description.
## How to plan
Work through the sections below with the developer as a conversation, not a dump: ask clarifying
questions, propose approaches, and get confirmation before moving on.
- Read the code to state current behaviour (an edge case's "because" included), what a change
touches and which tests it breaks, citing real paths — never to design it. A plan fixes only what
crosses an agent boundary (protocol, schema, Testable UI, invariants, ADR'd decisions); the shape
is the implementer's (`docs/conventions.md` § Design, kb:adr/process-plan-fixes-boundaries-not-shape).
- When the description or the spec is vague on a point, ask pointed questions rather than guessing.
- Save the plan file after each major section so progress isn't lost.
- The plan stays `draft` until the developer explicitly approves it.
### 0. Check for Existing Spec
Check whether `plans/<plan-name>/spec.md` exists, then create `plans/<plan-name>/` if it doesn't.
All plan artifacts live there. If a spec exists:
- Read it thoroughly — it contains requirements gathered during a prior spec interview.
- **A spec whose `**Status**` is not `Approved` is not a starting point.** Stop and ask the developer
to finish `/spec <plan-name>`, or to say explicitly that they are abandoning it — a `Draft` spec
means the interview never reached agreement, and planning on it silently adopts whatever it guessed.
- Use it as the starting point. The Goal, Requirements, Scope, Edge Cases, and Acceptance Criteria sections are already defined.
- Carry its `**Features**` header and its `## Feature Spec Delta` forward — the delta becomes this
plan's `## Doc Delta` (§10), amended for anything planning changes.
- Skip questions the spec already answers clearly. Focus on the **implementation-specific details** it doesn't cover: the protocol contract delta, schema changes, UI specifications, affected files and their owners.
If no spec exists, proceed normally — the spec step is optional.
### 1. Understand the Scope
Ask the developer to elaborate on what this adds or fixes and which `TODO.md` entry or issue it
closes. Settle the plan's `**Features**` (names with a `docs/features/<name>/spec.md`) — carried from
the spec, or agreed here when there is none. A feature whose files the plan edits but whose
behaviour it leaves alone (a call-site repair, a fixture, a shared helper) goes under `**Touches**`
instead: it packs as spec and contract only, so it costs the agents a page, not a library
(kb:adr/process-touched-features-widen-without-stopping).
Then run `go run ./tools/kb pack --plan <plan-name> --role planner` (`--features a,b` before the
header exists) and read it before planning; never contradict it: accepted ADRs are settled,
`rejected` ones are why a cut feature stays cut, facts beat Claude Code's docs, `contract.md` is the
protocol you state deltas against, conventions choose the stack (a plan never picks libraries).
`CLAUDE.md` hard rules bind too — a plan that requires violating one is wrong by construction. Read
relevant existing code (Grep/Glob) and the pack's feature `spec.md` and ADRs to understand current
patterns.
### 2. Determine Work Type
Establish which tracks are affected. Ask the developer to confirm:
- **daemon**: Go only — ingest, state machine, storage, tmux/PTY, HTTP/WS server
- **web**: TypeScript dashboard only
- **full-stack**: both
This determines which agents the orchestrator will invoke later.
Also settle the **E2E Scope** header explicitly — the orchestrator must not infer it from prose:
- **new-specs**: the plan adds Playwright specs (any plan with user-visible behaviour)
- **harness-only**: no new spec, but an edit to `web/e2e/helpers/*` or fixtures is part of the deliverable (kb:lesson/orchestrator-work-spawned-as-agent) — e2e-specs still runs, reporting `harness-only` then sweeping the full suite
- **none**: nothing E2E-observable; Steps 1 and 5 are skipped
And the **Fixture plan** header (docs/conventions.md §Testing) when E2E Scope is not `none`: per new
spec, `daemon` (fresh per test — the default, and mandatory when any test asserts daemon-global state
such as rail/grid order or counts, prefs, usage, theme, recents, auto-focus on the only session, or
restarts/kills the daemon), `startDaemon` (spawn options computed in the test) or `fileDaemon` (every
test title-scoped), with the one-line reason — e.g. `**Fixture plan**: drop.spec.ts daemon (auto-focus
needs the sole session)`. Write `none` when E2E Scope is `none`.
### 3. Define Requirements
Work with the developer to create clear, testable requirements. Each requirement should be:
- Specific enough to write a test for
- Scoped to a single behavior or outcome
- Labeled with a priority (must-have, should-have, nice-to-have)
### 4. Identify Affected Files and owners
Affected Files is your impact read, not a fence. Per impl track, list **existing files by exact path**
and **new code by the feature glob it falls under** (`` new under `internal/claudecode/launch*.go` ``)
— never a bare directory, which plan-lint's ownership check skips. Each line names the REQs it
carries and the behaviour that changes there, never a signature, type, field, helper or constant.
What reading the code taught you that could save the implementer time (an existing helper, a bound
that suffices) goes under Implementation Notes → Hints, which are non-binding. Then list **Existing
tests this breaks** per owning test agent — the tests asserting behaviour the plan changes, with line
numbers when known; the impl log's `## Handoff` confirms or corrects it.
**`TODO.md`, `docs/adr/`, `docs/diagrams/`, `SPEC.md`, `docs/features/*/spec.md` and generated kb files are never listed under an impl track** (the hand-written part of a touched package's `CLAUDE.md` is the exception and belongs to that impl track). They are the orchestrator's (Doc-Upkeep Backstop / Completion), and the review rules forbid an impl agent from touching `SPEC.md`. Put the required upkeep under Implementation Notes → Doc upkeep, addressed to the orchestrator (kb:lesson/plan-gave-no-single-owner).
A composition root (`web/src/main.ts`, `internal/server/server.go`) may appear under Affected Files only for a one-line registration; anything more is a new feature module (`docs/conventions.md` § Composition roots).
**Every requirement's test coverage names exactly one owning test agent — no conditional routing.**
"A unit test if the logic is unit-testable, otherwise E4 covers it" resolves to nobody
(kb:lesson/conditional-test-routing-resolves-to-nobody). If you cannot tell at
planning whether the logic is unit-testable, route the test to the unit agent and require the logic
to be unit-testable; where it lives and the seam's shape are the implementer's, reported as a
`design:` line the tester reads. A seam must decide something its caller could not write as the
same branch (kb:lesson/plan-placement-never-overrides-directory-rule). A genuinely E2E-only requirement says so and names the `E*` criterion that carries it.
**Tooling/config files belong to an impl track, never to a test agent.** `web/playwright.config.ts`,
`web/e2e/helpers/fixtures.ts` and `web/scripts/e2e-lint.sh` are **web-impl**'s (e2e-specs may not
edit them — the agent judged by the suite cannot hold the knobs that define passing). List a needed
config change under the owning impl track's Affected Files explicitly, never in an E2E subsection
where ownership is ambiguous (kb:lesson/plan-gave-no-single-owner).
### 5. Define the Protocol Contract
The daemon and web implementation agents run in parallel and use the daemon↔UI protocol as their shared interface — neither reads the other's code. The contract must be complete and unambiguous.
State this plan's **delta against `docs/protocol.md`**: every WS message and HTTP endpoint added or changed, with full shapes. For each:
- **WS messages**: direction (daemon→UI / UI→daemon), `type`, full JSON shape with types, which fields are optional/nullable and exactly when (e.g. null before a session's first API response), ordering/delivery caveats
- **HTTP endpoints**: method and path, request body, response body, error responses with status
codes, auth (localhost token per `kb:spec/connection`). Every example body is the **exact wire shape**: error
examples sit inside the `{"error": {"code", "message", …}}` envelope `kb:anchor/transport` mandates, extra
fields (a `paths` list, a `retryAfter`) inside that object.
`plan-lint.sh` flags an unenveloped example.
Example of sufficient detail:
```
WS daemon→UI session_state
{ "type": "session_state", "target": "muster:3.0", "state": "needs-input",
"since": "ISO8601", "title": "string" }
Sent on every state transition. `target` is the tmux target — the session key.
```
The delta is merged into `docs/protocol.md` at approval (see Approval). After that, no agent may change the contract unilaterally mid-pipeline — a contract problem stops the pipeline and comes back to the developer.
### 6. Schema Changes
If SQLite schema changes are needed:
- New tables or columns with exact types and constraints (schema direction per `kb:adr/lifecycle-migrations-add-tables-when-written`)
- A numbered, forward-only migration (no down migrations)
- Note: don't plan tests for what SQLite guarantees — constraint enforcement is the platform's job
### 6a. Diagrams (optional)
Draw one only when the plan adds or changes a state machine, a hook/protocol sequence, a
schema or a component boundary, or when a picture would measurably help implementation.
Kinds and keywords are in `docs/conventions.md` § Knowledge records; a Code-level
`classDiagram` is allowed in a plan, never in a record. Read the kb's diagrams for the area
first (`kb ls --type diagram`, and the feature spec's inline fences) and mark a changed one
`delta of kb:diagram/<slug>` so the orchestrator applies it. Unsure whether one helps? Ask
the developer rather than decide. plan-lint fails a fence whose keyword is outside the closed list.
### 7. UI Specifications (for web or full-stack)
Define the user-facing behavior clearly enough that the web agent can work without seeing the daemon code:
- Which views/screens are added or modified
- The DOM structure E2E tests and the design system rely on (`<template>` elements — no framework)
- User interaction flows
- **The three mandatory states for every view**: no data yet (render "unknown", never an empty gauge — `kb:adr/usage-unknown-renders-word-not-track`), data, and daemon-down
**Design system**: `docs/design/design-system.md` and `docs/design/ux-flows.md` are binding; cite the sections and the mockup that govern every surface this plan touches.
#### Testable UI Elements
For each key interactive element that E2E tests will target, define its expected accessible name. This is the shared contract between the planning, E2E, and web agents: `web-impl` implements these names exactly, and `e2e-specs` writes its locators against them without seeing the code.
| Element | Role | Name / Text Pattern | Notes |
|---------|------|---------------------|-------|
| e.g., New session button | `button` | `New session` | — |
| e.g., a `<details><summary>` disclosure | — | `<its summary text>` | no implicit role — locator strategy is e2e-specs' call |
**Only assert a `Role` you can actually guarantee.** A role you assert here becomes a `getByRole()` in the E2E suite, and a wrong one produces a locator that can never match — which reads as a mysterious test failure rather than a plan defect. This codebase is hand-rolled HTML (no component library), so assert a role only when:
- a native element guarantees it (`<button>` → `button`, `<a href>` → `link`, `<input>` → `textbox`, `<select>` → `combobox`), **or**
- you are explicitly mandating an attribute that creates it (`role="status"`, `aria-label="Usage"` on an `<aside>` → `complementary`)
Bare `<details><summary>`, `<div>` and `<span>` carry no implicit ARIA role. When unsure what
role a piece of markup exposes, put `—` in the Role column and describe the intent in Notes —
`e2e-specs` verifies its locators against a real DOM and picks a working one. Never resolve the
uncertainty by requiring an extra `role=` attribute so a test can find the element — that trades
native semantics for test convenience.
**Transcribe text patterns from the reference render, never compose them from memory.** When a
mockup is the design authority, open it and derive each Name/Text Pattern from the markup it
actually contains — separators, spacing, element boundaries. A composed pattern invents details the
mockup lacks and the implementation (correctly following the mockup) won't ship (kb:lesson/plan-asserted-surface-nobody-defined). `textContent` concatenates adjacent
elements with no whitespace — a pattern spanning sibling spans must not assume spaces.
#### Invariants
If the plan or protocol states a rule that must hold **at all times** — an "iff", an
"always", a "never", a failure that must *surface* (e.g. "`attention` is non-null iff state
is `needs_input`") — list it as a **named invariant**, not
just in a requirement's prose. Invariants get a different test shape: assert them from **every
reachable source state**, not the convenient one. Before approval, walk every state-changing
path in the code Affected Files names against each named invariant: a path or
existing test the plan calls "unchanged" that an invariant or User Flow now reaches costs a
fix wave (kb:lesson/invariant-missed-by-per-transition-tests). A "run every
input against every starting state, assert the invariant after" table is cheap; write it into
the acceptance criteria explicitly.
**Source states include multi-instance configurations.** When a per-session resource
lives on shared infrastructure (a tmux socket, a registry, a pool), "every reachable
source state" includes *with other sessions present* — and the invariant covers the
bystanders ("killing one session never touches another's client/pane/socket")
(kb:lesson/tiles-never-refit-behind-pattern-match, kb:lesson/detach-on-destroy-misrouted-keystrokes). If the plan ships
an interactive surface in more than one view, the invariants section must name each
hosting view as a state the round-trip is asserted from.
#### Carried-over measurements (kb:lesson/detach-on-destroy-misrouted-keystrokes)
A measured value is evidence **for the configuration it was measured in**. When a
requirement applies a value under a different tool, permission mode, auth state, version or
topology than it was measured in, that fact record's value must be re-examined against
that change before it becomes a requirement — in writing, per value, in the plan
("re-checked against decision N: still valid because …"). The measurement was real; its
applicability had expired.
### 8. Edge Cases and Error Handling
Discuss edge cases and failure scenarios on both sides. For Muster, always cover the standing ones
that apply: hook loss/duplication/reordering, `/clear` minting a new `session_id` in the same pane,
daemon restart mid-session, no-data-yet nulls, tmux pane death without `SessionEnd`.
**Write the late-arrival cases out:** for every rule keyed on `session_id`, one edge case per event of the `/clear` pair arriving *after* the other was applied, and one for a straggler from the previous turn arriving after the rebind.
**Every numbered edge case ends with the criterion that checks it** — `→ E<n>` / `→ W<n>` / `→ D<n>` (written in §9), or `→ untested: <reason>` when it genuinely cannot be driven. An unpinned edge case is tested by nobody until the reviewer tries it; `plan-lint.sh` refuses the plan.
### 9. Acceptance Criteria
Define clear acceptance criteria that the review agent will check against. Write them as prose, one behaviour per criterion, covering daemon-verifiable and web-verifiable outcomes.
**Three rules:**
1. **One clause per criterion.** Never mix a runnable command with a judgement call in one item:
"`make test` and `make lint` pass; no `any` types; the gauge shows unknown before first response"
is four criteria — a reader who watches the build go green marks the whole thing done and
discards the constraints most likely to be violated. If you find yourself typing `;` or `and
also`, start a new criterion.
2. **Number criteria uniquely across the whole section**, not per subsection. Prefix by area: `D1, D2…` (Daemon), `W1, W2…` (Web), `E1, E2…` (E2E). Restarting the count per subsection makes "criterion 12 passed" ambiguous in a review.
3. **Every UI element a criterion asserts must be defined somewhere in the plan** — a Requirement, the UI Specifications, or the Testable UI Elements table. Cross-check each `E*` criterion against those sections before approval: a criterion *tests* the plan's surface, it does not introduce new surface (kb:lesson/plan-asserted-surface-nobody-defined).
Then, with the developer, distil the criteria into an **Automated Checks** block: the subset where
"satisfied" is exactly "this one shell command exits 0". You author this deliberately — only you
know which backticked things in your prose are commands and which are identifiers, so it cannot be
left to a parser. Anything needing a human read stays in prose under `### Reviewer-Verified`, so the
non-runnable half of a split criterion is assigned rather than lost — but *no `<string>` survives in
`<dir>`* is a negative grep and belongs in the block (`plan-lint.sh` flags it in prose).
The orchestrator and the review agent execute the block **verbatim**, so every line must run from the project root with no arguments, no environment setup and no interactive prompt. Prefer the Make entry points (`make test`, `make lint`, `make web-build`, `make web-test`, `make e2e`) over ad-hoc pipelines.
**Negative grep checks (`! rg …`) need one authoring decision: test-file scope.** State in the
check's prose twin (or a note beside the block) whether `_test.go` / `*.test.ts` / `e2e/` files are
inside the grep's net, and why. Tests often legitimately need the banned strings (a boundary test
POSTing a real wire body) — if test files are in scope, the plan must say how tests obtain those
strings legally (from the boundary package, in a shape the implementer picks), or agents contort around
the check (kb:lesson/banned-string-split-to-dodge-gate). `plan-lint.sh` runs the dry-runs against
the plan text and the tree; put every hit it lists under **Affected Files** against the agent that
owns it, or re-scope the check.
### 10. Doc Delta (mandatory)
Every plan states what its work does to the **present-tense documents** — `docs/features/*/spec.md`
and `docs/protocol.md` — because after review approves the code, `doc-reconcile` promotes exactly
these sentences and nothing else. Carry the spec's `## Feature Spec Delta` forward when there is one;
author it here when there is not.
Per feature: the sentences that **become true**, and the sentences that **stop being true**. Write
each as an assertion about the target file, not an instruction, so it can be re-verified rather than
merely re-run. The deletions half is required — a spec body is capped at 800 words and
`check-kb` hard-fails past it, so a delta that only adds will eventually break the build at the very
end of a run.
A plan that changes no doc claim writes `No doc change.` — `plan-lint` fails on an absent or empty
section, never on an honest one. The Protocol Contract (§5) stays where it is; this section covers
the prose the contract does not generate.
## Plan Document Format
Write the plan to `plans/<plan-name>/plan.md` using this structure. The template is wrapped in a four-backtick fence so the triple-backtick blocks inside it nest correctly — in the plan file itself they are ordinary triple-backtick fences.
````markdown
# Plan: <Plan Name>
**Created**: <date>
**Status**: draft | approved | in-progress | completed
**Work Type**: daemon | web | full-stack
**E2E Scope**: new-specs | harness-only | none
**Fixture plan**: <spec>.spec.ts daemon | startDaemon | fileDaemon (<why>) [; …] | none
**Features**: <name>[, <name>] — each has a docs/features/<name>/spec.md
**Touches**: <name>[, <name>] — files edited, behaviour unchanged; packs spec and contract only (omit the line when none)
**Description**: <one-line summary>
## Overview
<2-3 paragraph description of the work>
## Requirements
### Must Have
- [ ] REQ-1: <requirement description>
### Should Have
- [ ] REQ-2: ...
### Nice to Have
- [ ] REQ-3: ...
## Protocol Contract
Delta against `docs/protocol.md` (merged there on approval; `docs/features/<f>/contract.md` regenerates). "No protocol changes" if none.
### WS: <direction> `<type>`
```json
{ "field": "type — description, nullability and when" }
```
<when it is sent, ordering/delivery caveats>
### HTTP: <METHOD> <path>
**Auth**: <requirement>
**Request:**
```json
{ "field": "type — description" }
```
**Response 2xx:**
```json
{ "field": "type — description" }
```
**Errors:**
- 400: <when and response shape>
## Schema Changes
<migration details or "No schema changes required">
## Diagrams
<omit the section, or one mermaid fence per diagram with a line saying what it is for and, when it changes a kb diagram, `delta of kb:diagram/<slug>`>
## UI Specifications
### Views
- <view> — <what it shows>
### User Flows
1. <step-by-step user interaction flow>
### States
- No data yet: <what "unknown" looks like here>
- Daemon down: <behaviour>
### Testable UI Elements
| Element | Role | Name / Text Pattern | Notes |
|---------|------|---------------------|-------|
| <element description> | `<role or —>` | `<accessible name or regex>` | <optional notes> |
## Affected Files
The planner's impact read, not a fence: existing files by path, new code by feature glob; REQs and behaviour, never shape.
### Daemon (daemon-impl)
- `internal/session/machine.go` — REQ-1: <behaviour that changes>
- new under `internal/claudecode/launch*.go` — REQ-2: <behaviour>
### Web (web-impl)
- `web/src/render/sessions.ts` — REQ-3: <behaviour that changes>
### Existing tests this breaks
- <owning test agent>: `path/to/existing_test.go:<line>` — <the behaviour it asserts that stops being true>
## Edge Cases
1. <edge case and how to handle it> → E<n> | W<n> | D<n> | untested: <reason>
## Acceptance Criteria
IDs are unique across the whole section — `D*` daemon, `W*` web, `E*` e2e. One clause per
criterion; never mix a runnable command with a judgement call in one item.
### Daemon
- **D1**: <one verifiable criterion, single clause>
### Web
- **W1**: <one verifiable UI criterion, single clause>
### E2E
- **E1**: <one end-to-end user flow that should work>
### Automated Checks
Every line below is `<ID> <single-line shell command>`, run from the project root. A check passes
iff its command exits 0. Write "must not exist" checks so success is exit 0 — prefix the grep with
`!`. IDs match the prose criterion above where one exists; a check with no prose twin (e.g. a build
gate) is fine and shares the same ID namespace. The orchestrator and the review agent run these
verbatim; nothing else in this section is executed automatically.
```checks
D1 make test
D2 go build ./...
D3 make lint
D4 ! rg -n "hook_event_name" cmd/ internal/ --glob '!internal/claudecode/**'
W1 make web-build
W2 make web-test
E1 make e2e
```
### Reviewer-Verified
Criteria that are not a single exit-code check. The review agent verifies these by reading code or
exercising the app. List them explicitly — this is where the non-runnable half of a split compound
criterion goes, so it is assigned rather than dropped.
- **W3**: no `any` types in new web code
- **W4**: <a rendering/state claim that needs a browser>
## Doc Delta
**<feature>** — becomes true:
- <assertion about docs/features/<feature>/spec.md or docs/protocol.md>
**<feature>** — stops being true:
- <the sentence that must come out, or "nothing">
<or "No doc change." when this plan changes no present-tense doc claim>
## Out of scope
- <a nearby thing this plan deliberately does not do, and why>
<This is the ONLY route by which a run may add an open `TODO.md` item, copied verbatim, so the
user approves the wording here or not at all (kb:adr/process-backlog-entries-are-the-users-to-file).
Anything the run discovers later is proposed, never filed. "Nothing." is acceptable.>
## Implementation Notes
**Hints (non-binding):** <what reading the code found that may save the implementer time>
<any additional context and gotchas — cite `kb:fact/<slug>` for every measured
Claude Code quirk handled and `kb:adr/<slug>` for every decision this plan makes — a decision with
no ADR is not yet a decision>
````
## Approval
When the developer explicitly approves the plan, in this order:
1. Run `.claude/skills/orchestrate/scripts/plan-lint.sh <plan-name>` and fix every `FAIL` first.
2. Mark the plan's `**Status**` `approved`.
3. Merge the Protocol Contract delta into `docs/protocol.md` under the anchors of the plan's
`**Features**`, and let `make gen-kb`
(step 5) regenerate `docs/features/<f>/contract.md` — both agents code against it.
4. Write each decision the plan makes as a `status: proposed` ADR in `docs/adr/`
(`refs: [plan:<plan-name>]`, one decision each).
5. `make gen-kb && make check-kb`.
6. Hand the developer the next two commands, verbatim: `make worktree NAME=<plan-name>` from this
primary checkout (it commits these planning edits onto `plan/<plan-name>` and builds
`../muster-<plan-name>`), then start the `/orchestrate <plan-name>` session **in that directory** —
orchestrate pre-flight refuses to run anywhere else (kb:adr/process-pipeline-runs-in-sibling-worktree).