---
name: paired-plan
description: Create, resume and hand off a plan made of two files, a Markdown file for the reasons and a JSON file for the state, so several coding agents (Claude Code, Codex, Copilot, a local model) and their subagents can continue work that another agent started. Use when asked to "create a plan", "make a plan", "resume the plan" or "hand this off", when a task spans several sessions, repositories or pull requests, or when you are close to a token limit.
license: MIT
---

# Paired plan

A plan that lasts longer than one session is two files, because they answer different questions:

| File | Answers | Read by |
| --- | --- | --- |
| `docs/<name>-plan.md` | **Why**: problem, architecture, decisions, risks, rollback | A person, or an agent that starts cold |
| `docs/<name>-plan.json` | **Where we are**: status, findings, who has each task, what is next | Every agent, at the start of every session |

**The JSON is the source of truth between sessions and between agents.** A task list, a chat
summary or the memory of an earlier session is not. When one agent runs out of tokens, or you
change tool, the next agent reads the two files and continues.

Everything you need is in this folder: the rules (this file), the JSON schema
(`references/plan-state.v1.schema.json`, also published at
`https://miguelcolmenares.com/schemas/plan-state/v1.schema.json`), a worked example
(`references/example-plan.json`) and a validator that uses only Node
(`scripts/validate-plan.mjs`). Nothing to install.

## Which lifecycle

Use `"lifecycle": "multi-session"`, and the JSON is **mandatory**, when **any** of these is true:

1. The request mentions phases, stages or several sessions.
2. The work touches more than one repository.
3. The work will land in more than one pull request.

Otherwise the plan is `single-pr`, or it is a short Markdown note with no JSON. **Say in your
answer which trigger applied**, or that none did, so the choice can be corrected.

- **`single-pr`:** line 1 of the Markdown file is `<!-- paired-plan:temporary -->`, with
  nothing above it. The file is temporary: delete it when the pull request is opened, together
  with a same-name JSON that also says `single-pr`. Nothing here deletes it for you, so the
  marker is how a person or a script of yours finds the files to remove.
- **`multi-session`:** commit **both** files. Do **not** put that marker on the Markdown file.
  Keep updating the same JSON in place until the work is explicitly complete.

## Create a plan

1. Read the project instruction file (`AGENTS.md`, `CLAUDE.md` or the equivalent). List
   `docs/*-plan.json`. If a multi-session plan for this work exists, **extend it**. Do not
   start a second plan for the same work.
2. Read the code that the work touches. Write down what you observed and what you propose,
   and what you did not check.
3. Choose the lifecycle (above) and say which trigger applied.
4. Write `docs/<name>-plan.md`: problem, current state, the decisions with one reason each,
   risks, phases, testing, rollback. State each decision once.
5. For `multi-session`, write `docs/<name>-plan.json` (shape below). Give every task an
   `acceptance` check and the `files` it changes.
6. Run `node scripts/validate-plan.mjs` and fix every error.
7. Commit the Markdown and the JSON together.

## The JSON file

```json
{
  "$schema": "https://miguelcolmenares.com/schemas/plan-state/v1.schema.json",
  "planId": "feature-name",
  "title": "Feature name",
  "lifecycle": "multi-session",
  "createdDate": "2026-01-15",
  "lastUpdated": "2026-01-15",
  "relatedDocs": ["docs/feature-name-plan.md"],
  "handoff": { "nextAction": "Start with the task write-helper." },
  "taskTemplate": {
    "steps": [
      { "id": "implement", "description": "Make the change and its tests.", "status": "not-started" }
    ]
  },
  "workItems": [
    {
      "id": "phase-1",
      "title": "Phase 1",
      "status": "not-started",
      "findings": [],
      "tasks": [
        {
          "id": "write-helper",
          "status": "not-started",
          "acceptance": "npm test -- helper passes",
          "files": ["src/helper.ts", "src/helper.test.ts"],
          "dependsOn": []
        }
      ],
      "log": []
    }
  ],
  "changelog": []
}
```

A complete example, validated, is in `references/example-plan.json`.

**Required at the top:** `planId`, `title`, `lifecycle`, `createdDate`, `lastUpdated`, `relatedDocs`,
`taskTemplate`, `workItems`, `changelog`. Each work item needs `id`, `title`, `status`,
`findings`, `tasks` and `log`. Each task needs `id` and `status`. `taskTemplate.steps`,
`workItems` and every `tasks` list must not be empty.

**Optional, so older plans still validate:** `handoff`, and on a task `notes`, `acceptance`, `files`,
`dependsOn`, `claimedBy`, `claimedAt` and `externalRef`, and on a log entry `agent`. Anything
custom goes in a `metadata` object, never in new top-level keys.

**Statuses:** `not-started`, `in-progress`, `blocked`, `completed`. Nothing else.

| Task field | What it is for |
| --- | --- |
| `acceptance` | The check that makes the task done, as a command or something you can observe. Write it so a new agent can run it without your context. |
| `files` | The paths the task changes. |
| `dependsOn` | Ids of tasks in the same plan that must be `completed` first. |
| `claimedBy` | The agent and model that took the task, for example `claude-code/sonnet` or `codex`. |
| `claimedAt` | The full ISO 8601 timestamp of the claim, with date and time. A date alone does not let the takeover rule run. |

## Resume a plan

Use this when you start a session, or when another agent stopped.

1. **Pick the plan.** List `docs/*-plan.json`. Keep the `multi-session` plans that still have
   work that is not `completed`. If more than one is left, ask which one. If none, say so.
2. **Read the state.** Read the whole JSON, then every file in `relatedDocs`. The Markdown holds
   the reasons. Trust `findings` and `changelog` over your memory.
3. **Validate.** Run `node scripts/validate-plan.mjs <file>` and read every warning on stderr.
4. **Check the handoff against git.** If the plan has a `handoff`, run `git branch --show-current`,
   `git log -1 --format=%h` and `git status --short`, and compare them with `branch`, `lastCommit`
   and `uncommitted`. **If one differs, stop**, say which, and ask before you change the branch,
   reset anything or commit changes you did not make. With no handoff, run the same commands and
   report what you see.
5. **Pick the next task**, in this order: an `in-progress` task (ask first if another agent claimed
   it), then a `blocked` task whose blocker is gone, then a `not-started` task whose `dependsOn`
   tasks are all `completed`. If the task has no `acceptance`, write one before you start.
6. **Claim it** (next section).
7. **Do it.** Change the files in `files`. If you need another file, add it to `files` first.
   Record other problems you find in `findings`, not in the change.
8. **Verify.** Run the `acceptance` check. If it fails, keep the task `in-progress`, or set it
   `blocked` with a log entry that names the cause. Never mark a task `completed` on intent.
9. **Record and commit.** In the same commit as the work: set the task `completed`, append a log
   entry, set `lastUpdated`, validate, and commit the work and the JSON together. Go back to step 5.
10. **Hand over** when your context is nearly used up (below). Do not start a task you cannot finish
    and verify in this session.

## Claim, update, hand off

These are conventions between agents. **No command enforces them.**

**Claim before work.** In one commit, set the task `in-progress`, set `claimedBy` and set
`claimedAt`. Do the work after that commit.

- Do not work on a task that another agent has claimed. Take another task, or stop and say so.
- A claim older than 24 hours can be taken over when its task has no log entry from the last
  24 hours. Set your own `claimedBy` and `claimedAt`, and append a log entry that names the
  takeover. The takeover relies on the claiming agent's own timestamp.

**Update after every finished task or commit**, in the same commit as the change:

1. Set the `status` of the task or work item.
2. Append a dated entry to the work item's `log`, with `date`, `summary` and `agent`.
3. Append to the top-level `changelog` when it matters beyond one item.
4. Record durable discoveries in `findings`.
5. Set `lastUpdated`.

Never delete or rewrite old entries in `log` and `changelog`. Append. Do not create progress-note
files, and do not rewrite the Markdown prose to track progress. The task list inside your own
agent session is a private copy of the JSON. It never replaces it.

**Hand off** after each finished task, and always when your context is nearly used up or you
must stop. Replace the whole `handoff` object:

| Field | Content |
| --- | --- |
| `nextAction` | One sentence that names the next task and the next step. **Required.** |
| `branch` | The git branch that holds the work. |
| `lastCommit` | The short hash from `git log -1 --format=%h`. |
| `uncommitted` | A short summary of work not committed yet, or `none`. |
| `verify` | The command or check that shows the state is sound. |
| `updatedBy`, `updatedAt` | Your agent and model, and today's date. |

The next agent starts from the handoff and runs `verify` before it continues. Then tell the
user that the handoff is written, and give the `nextAction`.

## Write the entries

The JSON is read by an agent that remembers nothing. Each text field should stand alone:

- One fact per entry, in a sentence of at most 25 words that names who did it.
- A `log` summary says what changed and how it was verified: "Added the lint, 149 tests pass."
- A `findings` entry starts with its kind (observed, measured or estimated) and gives its source:
  a file, a command or a ticket.
- Never write a secret value. Write its name, its type and the count.
- State a decision once. The reason lives in the Markdown, and the JSON points to it.

## Several agents

The plan is the shared state, so the agents can differ. One way to split the roles:

- **One agent plans.** It has the large context window and writes the two files.
- **Another agent runs the tasks**, often a cheaper model that follows the plan.
- **A different agent reviews**, so the code is read by a model that did not write it. On GitHub
  you can also ask the Codex and Copilot reviewers for a second and third reading.

Agents read their own entry file, so the rule must be there. Add this block to the file of each
agent you use (`AGENTS.md` for Codex and others, `CLAUDE.md` for Claude Code,
`.github/copilot-instructions.md` for Copilot), because a rule that is not where the agent reads
is not a rule:

```markdown
## Plan State (source of truth)

A plan that spans several sessions, agents, repos or PRs is two files: `docs/[feature]-plan.md`
explains why, and `docs/[feature]-plan.json` tracks where we are. The JSON is the source of truth.

- Triggers: the JSON is mandatory when the request mentions phases or several sessions, the work
  touches more than one repository, or it lands in more than one pull request. Say which applied.
- Resume: list `docs/*-plan.json`, read every `multi-session` plan and its `relatedDocs`, and start
  from `handoff` when it exists.
- Claim: before the work, set the task `in-progress` with `claimedBy` and `claimedAt`, in one commit.
- Update: after each task, update the JSON in the same commit. Mark `completed` only after
  `acceptance` passed.
- Hand off: after each task, and when your context is nearly used up, write `handoff`.
- Your session task list never replaces the JSON. Validate with
  `node scripts/validate-plan.mjs`.
```

A local model, for example one served by Ollama, can read the same two files and follow the same
steps, because the plan does not belong to any platform.

## Subagents

A subagent starts with no memory of the conversation, so the plan it receives must be complete
by itself. A working setup, as one repository defines it:

- The **main agent** keeps the whole picture. It decides the order, writes the plan for each
  task, reviews what comes back and orchestrates the rest.
- **Subagents** run the plan they are given, **one task each**. Cap how many run at once (that
  repository allows five).
- Each subagent works in **its own git worktree, on its own branch** from the base branch, and
  never in the main checkout, so two of them never edit the same files on disk. In Claude Code you
  ask for it with `isolation: "worktree"`. Keep the "do not touch" list anyway: two branches that
  change the same file still conflict when they are merged.
- The brief says: the issue, the branch and the base branch; the files to change, and the files
  **not** to touch because another subagent is using them; what "done" means; the checks to run
  before the pull request; and the commit and pull request rules.
- The subagent **stops at the pull request**. It does not merge. The main agent reviews the pull
  request and the checks.

### Cheap models, small context

The main agent has the large context window and the better model. A subagent gets the opposite:
**a cheap model and a small, bounded context.** This saves tokens and context, and it works
because the plan carries the thinking. The subagent only follows it.

- **Claude Code:** pin the model for each subagent, and pin the smallest one (`haiku`). A rule in
  the project instruction file makes it a habit: "a subagent never uses a model above Haiku".
- **Codex and Copilot:** there is no pin for a cheap subagent model in this setup. The model comes
  from the session (`codex --model` for Codex, the model picker for Copilot). So the rule is a
  written instruction to the orchestrator, and you choose the cheap model yourself.
- **Bounded context:** give the subagent only the plan for its task, never the conversation. The
  main agent keeps the whole picture.
- **If a task is too hard for the cheap model,** split it into smaller tasks, or let the main agent
  do it. Do not give the subagent a bigger model.

The task fields carry most of that brief: `acceptance` is what "done" means, `files` is what it may
change, and the `files` of the other `in-progress` tasks are what it must not touch. `dependsOn`
tells you which tasks can run at the same time: those whose dependencies are all `completed`.

**Not defined by the convention:** who edits the plan JSON when several subagents run in parallel.
They work in separate worktrees and on separate branches, so decide one rule for your team and
write it in the entry file.
A single writer, the main agent, is the simplest.

## Validate

```bash
node scripts/validate-plan.mjs                  # every docs/*-plan.json in the current folder
node scripts/validate-plan.mjs docs/x-plan.json # one file
node scripts/validate-plan.mjs --strict         # warnings exit 1 too
```

It reads the schema in `references/` (`--schema <path>` uses another one), exits 1 and prints
the failing path when a plan does not match, and exits 1 on a missing file or malformed JSON. It
prints **warnings** on stderr for: an `in-progress` task with no `claimedBy`, a multi-session plan with
open work and no `handoff`, a `completed` task with no log entry, a `relatedDocs` file that does not
exist, and a `dependsOn` id that names no task. A warning does not change the exit code unless you
pass `--strict`.

## Red flags

- A status written in the chat and not in the JSON.
- A task marked `completed` and no one can say how it was checked.
- A `handoff` written two sessions ago.
- Two plans for the same work.
- A claim older than a day with no log entry.
- The JSON is the only file in the commit, and no work was done.

## Verification

- [ ] Both files exist and are committed together (multi-session), or the marker is on line 1 (single-pr).
- [ ] `plan-state validate` exits 0, with no warnings that belong to this work.
- [ ] Every task has `acceptance` and `files`.
- [ ] The `handoff` names a task, a branch and a `verify` command.
- [ ] The "Plan State" block is in the entry file of every agent that will work on the plan.

## Feedback

This skill is a proposal and it is not finished. If you use it, I would like to know how it went:
what broke, which rule was missing or unclear, and how you do this yourself. Write to
[me@miguelcolmenares.com](mailto:me@miguelcolmenares.com).
