Plan state
Version 5.0.0. Status: Stable. This document specifies the machine-readable plan state layer of the Deep Work Plan methodology, now aligned with the DWP standard’s own version — no existing requirement is weakened by the renumbering. This revision also documents the guarded state updater, verified plan publication, and the evidence-truth rules a completed plan must satisfy (see below). The keywords MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY are to be interpreted as described in RFC 2119.
Two JSON artifacts — manifest.json (the plan’s static identity) and state.json (the live, per-task execution state including validation-gate results) — that every plan MAY carry alongside its markdown files, and that unattended execution (see Agent protocol) and non-git workspaces (see Archetypes §3) MUST carry.
The markdown plan remains the human-readable source of truth. The JSON layer is a derived projection: it is regenerated by the agent at defined protocol points, never hand-edited, and never allowed to silently disagree with the markdown. Its purpose is interoperability — linting, conformance checking, diffing, dashboards, registry discovery, and synchronization with external session infrastructure — none of which can be built reliably on prose.
Why this exists
Through v1.1, plans were prose markdown only. That kept them auditable and agent-agnostic, but left nothing a tool could validate, diff, or consume: no conformance gate, no desync detection between README.md and PROGRESS.md, no way for a daemon or cloud session to know a plan’s state without parsing prose. v1.2 adds the JSON projection without demoting markdown — the projection is derived from the markdown, in the same way a lockfile is derived from a manifest.
Placement
A plan using the state layer has this layout:
.dwp/plans/PLAN_{name}/
├── README.md ← human source of truth (unchanged)
├── PROGRESS.md ← narrative log (unchanged)
├── PROMPTS.md ← unchanged
├── manifest.json ← static identity (written at materialization)
├── state.json ← live state (rewritten at protocol points)
├── analysis_results/
└── {N}.task_{...}.md
manifest.json MUST be written exactly once, when the create flow materializes the plan, and MUST NOT change afterward except for a spec-version migration recorded in PROGRESS.md.
state.json MUST be rewritten by the agent at each of these protocol points: plan materialization (all tasks pending), task start (in_progress), each validation-gate run (gate record appended or updated), task completion (completed, as part of the task completion protocol in DWP specification), a checkpoint before any planned interruption, and a blocked stop.
Both files MUST be written atomically: write to a temporary file in the same directory, then rename over the target. A crashed write MUST NOT leave a truncated JSON file in place.
When the layer is required
- For interactive execution in a git repository, the state layer is RECOMMENDED for new plans and OPTIONAL for pre-v1.2 plans. A plan without it remains conformant.
- For unattended execution, the state layer is REQUIRED.
- In an agent workspace without git, the state layer is REQUIRED:
state.jsoncarries the recovery information that the git log carries in a repository.
manifest.json — plan identity
{
"schema": "https://deepworkplan.com/schema/plan-manifest/v2.json",
"spec_version": "2.4.0",
"name": "PLAN_payment_webhooks",
"title": "Add payment webhook handling",
"archetype": "individual",
"rigor": "standard",
"plan_format": "full",
"created_at": "2026-06-09T14:00:00Z",
"created_by": { "agent": "claude-code", "model": "claude-fable-5" },
"tags": ["backend", "payments"],
"task_count": 7,
"parent_plan": null
}
schema, spec_version, name, archetype, rigor, created_at, task_count, and plan_format are REQUIRED.
archetype MUST be one of individual, orchestrator-hub, agent-workspace.
rigor MUST be one of micro, standard, deep (see Proportional rigor).
plan_format MUST be one of lite, full — the representation chosen at creation (see Lite plans). It is immutable at the manifest level: a later promotion from Lite to Full is recorded in state.json, never by rewriting the manifest.
parent_plan links a child plan to its orchestrator plan ({repo}:{plan_name}, or null).
created_by SHOULD identify the creating agent and model. It MUST NOT contain secrets, tokens, or user identifiers beyond a display name.
state.json — live execution state
{
"schema": "https://deepworkplan.com/schema/plan-state/v2.json",
"plan": "PLAN_payment_webhooks",
"updated_at": "2026-06-09T16:42:10Z",
"updated_by": { "agent": "claude-code", "model": "claude-fable-5" },
"status": "in_progress",
"completed_count": 2,
"task_count": 7,
"format": "full",
"materialization": "ready",
"approval": "approved",
"promotion": null,
"tasks": [
{
"id": 1,
"locator": { "kind": "file", "value": "1.task_webhook_endpoint.md" },
"title": "Create webhook endpoint",
"status": "completed",
"started_at": "2026-06-09T14:10:00Z",
"completed_at": "2026-06-09T15:02:33Z",
"commit": "a1b2c3d",
"gates": [
{
"command": "pnpm run test",
"passes": true,
"exit_code": 0,
"last_run": "2026-06-09T15:01:50Z",
"evidence": "42 passed, 0 failed"
}
],
"outcome": {
"tried": ["raw body parsing via middleware"],
"failed": ["initial signature check used wrong header"],
"worked": "verify signature against X-Sig header before JSON parse",
"notes": "stripe-style HMAC; see analysis_results/webhook_notes.md"
}
},
{
"id": 3,
"locator": { "kind": "file", "value": "3.task_retry_queue.md" },
"title": "Add retry queue",
"status": "in_progress",
"started_at": "2026-06-09T16:30:00Z",
"gates": []
}
],
"checkpoint": {
"task": 3,
"step": "instructions:4",
"at": "2026-06-09T16:42:10Z",
"note": "queue table migrated; worker loop not yet wired"
},
"blocked": null
}
A Lite plan’s task entries use an inline locator pointing at the task’s anchor in README.md instead of a separate file — everything else about the entry (gates, outcome, status) works the same way:
{
"format": "lite",
"materialization": "ready",
"approval": "pre_approved",
"promotion": null,
"tasks": [
{
"id": 2,
"locator": { "kind": "inline", "value": "#task-2" },
"title": "Add retry queue",
"status": "pending",
"gates": []
}
]
}
Format, materialization, approval, and promotion
format MUST be one of lite, full and mirrors the manifest’s plan_format — mutable here, unlike the manifest, because a Lite plan MAY later promote to Full. materialization MUST be one of materializing (the plan folder is being written), ready (materialization is complete), or promoting (a Lite-to-Full promotion is in progress). approval MUST be one of pending, approved, pre_approved; it is OPTIONAL in this schema so that a plan written before it was recorded still validates — when it is absent, treat the README’s Approval row as the value, and pending when neither is present. promotion is null outside a promotion, or an object recording the promotion’s intent and destination tasks while materialization is promoting. See Lite plans for the full lifecycle these fields encode.
Task entries
Every task — a separate file in a Full plan, or an inline {#task-N} record in a Lite plan — MUST have exactly one entry in tasks, keyed by its number (id) and its locator. locator.kind MUST be file (Full — value is the task’s filename) or inline (Lite — value is the task’s anchor, #task-N).
status MUST be one of pending, in_progress, completed, blocked, skipped. skipped is valid only when the user explicitly removed the task from scope via refine; state.json MUST NOT be used to skip work silently.
A completed entry MUST carry completed_at and, where the plan commits, the short commit hash — this is the plan-to-code traceability link.
Gate records
Each run of a validation command SHOULD be recorded as a gate record: command, passes (boolean), exit_code, last_run, and a short human-readable evidence string (a summary line or a path under the plan’s own analysis_results/ (inside the plan’s folder, never the repository root), never full command output).
A task MUST NOT be marked completed in state.json while any of its gate records has passes: false and no later passing run. Gate records are the machine equivalent of “never mark complete without evidence” — the pattern of a per-item passes flag guarding premature completion.
Outcome records as episodic memory
A completed task SHOULD carry an outcome record: what was tried, what failed, what worked, and free-form notes. Keep each entry to one line.
Outcome records make a finished plan retrievable episodic memory: an agent (or a memory-indexing platform) can later recall how a problem was solved, not just that it was. They feed task-local skills dispositions and the Final Review’s skills reconciliation, which reads them when mining patterns. On platforms such as Hermes that index agent memory, outcome records in state.json make completed plans directly retrievable across future sessions.
Checkpoint and blocked state
checkpoint records the finest-grained resume point inside the current task: the task id, a free-form step locator, a timestamp, and a one-line note. An agent SHOULD update it whenever it pauses inside a task; it MUST update it before any planned interruption in unattended mode.
blocked is null or { "task": N, "reason": "...", "since": "...", "needs": "..." }. An unattended agent that hits a stop condition MUST populate blocked before halting — this is how a daemon’s next heartbeat, or a human, learns why the plan stopped.
Projection and reconciliation
The markdown MUST win every disagreement. If state.json says task 4 is completed but the plan README shows an unchecked box, the state file is stale.
A resuming agent MUST compare the README checkbox list against state.json before continuing. On desync it MUST regenerate state.json from the markdown (and the git log, where available), record the reconciliation in PROGRESS.md, and only then proceed.
The verify sub-skill MUST treat desync as a conformance finding: report which tasks disagree and in which direction.
Tools other than the executing agent MUST treat both JSON files as read-only.
Guarded state updates
Ordinary progress writes go through a shipped, targeted updater rather than a full-file rewrite. It rejects malformed state outright, and it refuses to mark a task completed without nonempty gate evidence attached — a --gate-json form is available for a command whose own output contains pipe characters, and the updater accepts the same closed gate object described above. Retries supersede only their own command; a different command keeps its own separate record. --block-reason records a blocker; --resolve-blocker resolves only the current task’s blocker, never another task’s. Skipped work can never make a plan completed. --reopen-reason records a caller’s intent to amend the plan through refine — the amendment and any evidence it invalidates MUST be recorded in the task log first. --expected-sha256 rejects a write against a state snapshot that has since moved on. A cooperative .lock directory serializes concurrent writers; a crashed writer’s lock MUST be inspected before removal, and no protection is claimed against an editor that bypasses the lock entirely. These records assert results — they do not themselves prove a command executed, or that its output was semantically accepted.
Verified plan publication
Before announcing completion, the finished task logs (each carrying its Skills disposition and, in the Final Review, its Documentation decision), the README index, and PROGRESS.md MUST be authored from earned source and acceptance results. The plan’s final task then closes through the shipped finalizer: its terminal transition validates the completed candidate against every plan artifact before writing state, verifies the files afterward, and records a analysis_results/FINALIZATION.json receipt. An invented passing gate MUST NOT back this transition — the receipt is external evidence of what was actually checked, never its own prerequisite. bash ../verify/conformance.sh --plan PLAN_name runs next, against the real artifacts on disk.
An interrupted publication leaves a .finalizing.json marker in place; normal verification fails until the evidence is inspected and the recovery helper succeeds against the same candidate — nothing resumes a publication by assumption. A stale cooperative lock requires confirming no writer remains active before removal. Nothing in this layer commits, pushes, executes a stored gate command, or silently repairs the plan’s markdown. A missing Python interpreter yields UNVERIFIED, never completed.
Evidence truth and amendments
Every change to a task’s scope, acceptance criteria, or deferral carries one durable amendment record: the original criterion verbatim, what was observed, the disposition, the reason, the authority behind it (user, developer, or evidence), the affected tasks, and which evidence was invalidated or preserved. Amendments are appended, never backdated; manifest.json keeps its creation provenance and is never rewritten to match a changed live scope.
Five evidence states describe what a task record may close against:
- Completed investigation — real recorded work; it closes a task only against a revised criterion that names it, never against the original as written.
- Unexecuted scenario — recorded as not performed; it contributes no passing evidence in any era.
- Deferred requirement — the criterion moves to a named destination task with recorded authority; only that amendment closes the source.
- Failed gate — remains failing until the same acceptance intent is re-run and passes; a retry supersedes only its own command.
- Achieved product outcome — the criterion as written, verified by its own gate; the only state that completes a task unchanged.
Enforcement is mechanical wherever the records allow it. Gate evidence marked “invalidated by refine” is retained history, never passing evidence, and a completed task that still relies on it is reported by the checker. A passing record whose own text admits the check never ran (for example “never entered,” “did not run,” or “cannot be measured”) is a contradiction, reported the same way — as is a completed state task whose own log still reads Status: pending. Narrative contradictions beyond these — a report whose conclusions disagree with its own checklist — require a human reviewer; the checker reports what the records say, not what the prose means. A user MAY explicitly accept a bounded exception with recorded authority; unattended pre-approval is never blanket permission to abandon a core objective, and an unmeetable mandatory criterion is a blocker, never completed work.
Schema versioning
Both schemas are versioned by URL. Additive fields are allowed within a version; renaming or re-typing a field requires a new schema version and a migration note in the spec changelog. This revision introduces /v2.json for both schemas: the task entry’s file field becomes a typed locator ({"kind": "file" | "inline", "value": ...}), the manifest gains plan_format, and the state file gains format, materialization, approval, and promotion — together the fields Lite plans need (see Lite plans). /v1.json manifests and state files remain valid and are never silently rewritten to v2; a refine session MAY migrate one deliberately. The spec_version field in the manifest pins the DWP spec version the plan was created under; an agent encountering a newer plan than its installed spec SHOULD say so rather than guess.