Skip to content

Commit d9fe358

Browse files
committed
feat(plan): hand an approved plan to the worker that implements it
Planning and implementing were already two workers, but the seam between them was the human: read the plan on a verdict card, then hand-write a task for a second worker and hope the paraphrase kept what mattered. A plan is not a new kind of object — it is a deliverable that proposes work instead of carrying it — so the handoff can reuse frozen bodies, versioned approval, revision notes, and provenance as they are. - architect kind: read-only, shared workspace, no worktree. Publishes via docket_done outcome: proposal, never parks a plan in a docket_wait question, and writes the whole plan in the response that calls docket_done, since only that body is frozen. - plan-contract.ts: pure parser for Goal / Constraints / numbered Steps with files: / Verification / Risks. Accepts markdown headings and bare `Plan:` label lines (what Pi's plan mode emits). No numbered step means no parse, and everything degrades to ordinary proposal behavior. - Kind-aware handoff. Both Use paths — live worker and stored deliverable — now share startHandoffWorker. Use on an approved plan offers Implement / Parent / Worker: Implement picks worker.implementKind (default implementer), seeds the task from the plan goal, inherits model and thinking, and shows one confirm card. The generic Worker path gained a kind select (name · authority), which fixes the same kind-blindness there. - The plan gate is discharged, not dropped. task.md names the approval and decision id that satisfied it, then re-arms for unnamed files, destructive or external writes, dependency changes, scope growth, or a step that proves wrong. planAuthorized is ignored on the worker-commands and worker-store paths, so only a reviewed handoff can discharge a gate. - Discharged-gate text requires publishing the plan's steps as docket_todos before the first edit, so the plan becomes the progress board without new machinery. - Plan coverage: readPlan parses the byte-exact launch sidecar when the card opens and compares planned files against changeSet.files. The verdict card and Report show a warning-colored `plan <ref> · 2 steps · 1/2 planned files touched · 1 off-plan`. Also adds the "plan" card presentation (heading reads Plan) and the worker.implementKind config key. Documented in ADR-0007, CONTEXT.md (four terms, two resolved ambiguities — including that plan mode itself stays Pi's job), the README, full-reference, and configuration.
1 parent 208454e commit d9fe358

22 files changed

Lines changed: 838 additions & 61 deletions

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
- **plans hand off to implementation without a second gate**: an approved plan deliverable now offers `Use → Implement`, which resolves `worker.implementKind` (default `implementer`), seeds the task from the plan's goal, inherits parent model/thinking, and carries the plan as the byte-exact launch sidecar. Approving a plan and handing it off no longer produces a worker that immediately asks you to approve a plan.
6+
- **plan gate discharge is recorded, not skipped**: only a reviewed handoff can discharge a gate. `task.md` names the approval and decision that satisfied it, tells the worker to publish the plan's steps with `docket_todos`, and re-arms the gate for unnamed files, destructive/external writes, dependency changes, scope growth, or a step that proves wrong. `planAuthorized` without a source deliverable is ignored.
7+
- **two bundled kinds for the loop**: `architect` (read-only, shared workspace) publishes an approvable plan through `docket_done outcome: proposal` instead of parking it in a `docket_wait` question; `implementer` keeps its plan gate and scopes rights to the files the approved plan names.
8+
- **plan contract**: `## Goal`, `## Constraints`, `## Steps` (numbered, each optionally `files: a.ts, b.ts`), `## Verification`, `## Risks`. Markdown headings and bare `Plan:` label lines both parse. A body with no numbered step simply does not parse and reviews as an ordinary proposal — no publish-time validation anywhere.
9+
- **plan coverage on the verdict card and Report**: a ready deliverable that executes an approved plan is compared against the launch sidecar, showing `plan <ref> · N steps · X/Y planned files touched · … off-plan · … untouched`, warning-colored when either drift figure is non-zero. Derived at card-open from data already on disk: no new storage, no decision type, zero model context.
10+
- **kind-aware handoffs**: `Use → Worker` now selects a kind instead of silently landing on the default one. Worker and stored-deliverable handoffs share one path.
11+
- **no new plan object**: a plan is a Worker Deliverable, reusing immutability, versions, generation-bound approval, review notes, and provenance unchanged. See [docs/adr/0007-plan-to-implementation-handoff.md](docs/adr/0007-plan-to-implementation-handoff.md).
12+
313
## 0.8.0
414

515
- **human-started workers only (breaking)**: workers no longer receive `docket_spawn_child`; `can_spawn` is ignored with a migration diagnostic, persisted hierarchy fields are ignored, and `worker.maxSpawnDepth` has no effect. Workers are independent: list/respawn are flat and delete/prune removes only requested worker.

CONTEXT.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,22 @@ _Avoid_: latest answer, worker result, live output.
3838
One accepted ready generation of a Worker Deliverable. `v1` never changes when a revision produces `v2`.
3939
_Avoid_: edit, overwrite, current output.
4040

41+
**Plan deliverable**:
42+
A Worker Deliverable that proposes work instead of carrying it: no frozen change set, and either `proposal` outcome or a body following the plan contract (Goal, Constraints, numbered Steps with `files:` clauses, Verification, Risks). It is a shape and a presentation, never a separate artifact class or store.
43+
_Avoid_: plan artifact, plan record, spec object.
44+
45+
**Implement handoff**:
46+
The Use destination offered only for an approved Plan deliverable. It selects the configured implement kind, seeds the task from the plan's goal, inherits parent execution, and carries the plan as the byte-exact sidecar. Still human-started and still confirmed.
47+
_Avoid_: auto-implement, execute plan, run plan.
48+
49+
**Plan gate discharge**:
50+
A plan gate resolved at launch because the human approved that exact Deliverable Version and started the worker to execute it. The gate is recorded as satisfied in `task.md` with the approving decision, and re-opens for anything the plan does not cover. Only a reviewed handoff can discharge one.
51+
_Avoid_: skipped gate, disabled gate, trusted worker.
52+
53+
**Plan coverage**:
54+
A derived comparison between the files an approved plan named and the files the resulting change set touched, shown on the verdict card and in Report. Read from the launch sidecar at card-open time; it stores nothing and records no decision.
55+
_Avoid_: plan compliance, conformance check, plan lint.
56+
4157
**Approval**:
4258
A generation-bound verdict accepting one exact Deliverable Version. It never injects context or starts work; patch promotion remains an explicit verdict action.
4359
_Avoid_: use, handoff, auto-promote.
@@ -125,6 +141,8 @@ _Avoid_: continue, resume, restore.
125141
- A **Worker** starts from a **Pre-flight brief** and may be constrained by **Decision rights** or a **Plan gate**.
126142
- A **Worker** produces **Artifacts** and one primary **Worker Deliverable** per accepted ready generation; supporting artifacts remain evidence.
127143
- An **Approval** judges one **Deliverable Version**. **Use / Handoff** is separate and remains human-started.
144+
- A **Plan deliverable** is a Worker Deliverable, not a new type; only an approved one offers the **Implement handoff**, and only that handoff produces a **Plan gate discharge**.
145+
- **Plan coverage** compares an approved plan against the change set that claims to execute it. It is evidence on the verdict card, never a gate or a verdict of its own.
128146
- A **Deliverable** freezes exact body bytes plus structured result data, source provenance, approval, and review history.
129147
- **Save** = copy an approved worker generation or author selected content + write one immutable record.
130148
- **Load** = **Mount** the deliverable under a `d<N>` slot at zero model-context cost.
@@ -144,3 +162,5 @@ _Avoid_: continue, resume, restore.
144162

145163
- "checkpoint" made Docket sound like a session-resume feature. Resolved: canonical term is **Deliverable**; old bundles are compatibility-only.
146164
- "continue" duplicated Pi's session vocabulary. Resolved: Docket has **Save**, **Load**, and explicit **Use**; Pi owns continuation.
165+
- "plan" risked becoming a second artifact class beside Deliverable, forking versioning, approval, and storage for no new capability. Resolved: a plan is a **Plan deliverable** — a shape and a presentation — and the first-class addition is the *transition* (**Implement handoff**, **Plan gate discharge**, **Plan coverage**), not a new object.
166+
- "plan mode" could have meant Docket restricting the parent's own tools. Resolved: out of scope. Pi owns session and tool policy; Docket owns the resulting artifact, its approval, and its handoff.

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,12 +227,44 @@ Bundled worker kinds:
227227
- `default`: plan-gated general work in a fresh isolated workspace.
228228
- `scout`: read-only investigation.
229229
- `patcher`: plan-gated edits in an isolated worker workspace.
230+
- `architect`: read-only planning; publishes an approvable implementation plan.
231+
- `implementer`: executes an approved plan handed to it.
230232

231233
A plan gate lets a worker inspect first, then requires it to ask before its first edit or mutating command.
232234
Configured `worker.defaultKind` values are deliberate power-user overrides: Docket preserves the selected kind's declared rights instead of adding an implicit policy on top.
233235

234236
Ready review loop: accepted `docket_done` freezes a Worker Deliverable version → verdict card (Evidence → Worker says → Actions) → `r` Report if needed → `d`/`h` for exact frozen diff/Hunk → promote, approve, or reject. Approval never injects context. After approval, `u Use` either queues one full immutable body for next parent prompt or starts one fresh worker with `source-deliverable.md` input and recorded provenance. Attach is a secondary debug escape hatch, not the normal path.
235237

238+
## Plan, then implement
239+
240+
> A note on why this exists: often, a pattern I use with workers is to send a read-only one out first, make it come back with a plan, argue with it for a round or two, and only then let anything touch the repo. It works, but the seam between those two steps was me — reading the plan in one card, then hand-writing a task for a second worker and hoping I had not paraphrased away the part that mattered. Therefore: the architect kind, and an `Implement` action on an approved plan that carries the exact bytes I approved into the worker that does the work. The approval *is* the handoff. Everything about reviewing the result stayed the same.
241+
242+
A plan is not a separate kind of object: it is a deliverable that proposes work instead of carrying it. That means it gets everything a deliverable already has — one immutable frozen body, versions, approval bound to one exact version, revision notes, provenance, and zero-token mounting.
243+
244+
```text
245+
/docket spawn --as architect add rate limiting to the public API
246+
# worker publishes the plan with docket_done outcome: proposal
247+
/docket verdict # read it, approve it, or request a revision
248+
u → Implement # start the implementation worker from that exact plan
249+
```
250+
251+
`Implement` appears on an approved plan only. It picks the `implementer` kind, seeds the task from the plan's goal, inherits the parent's model and thinking, and writes the plan byte-exact to the worker's `source-deliverable.md`. One confirmation card shows the resolved launch before anything starts. `Worker` remains the explicit path where you choose kind, model, and thinking by hand; `Parent` still just queues the body for your next prompt.
252+
253+
**The plan gate is discharged, not skipped.** You already approved this exact plan version, so the implementation worker executes it instead of stopping to propose it again. The gate stays armed for everything the plan does not cover: files it never names, destructive commands, dependency changes, scope growth, or a step that turns out to be wrong. Its `task.md` says which approval discharged it and which decision recorded that.
254+
255+
A plan the architect writes in the shape below is machine-checkable at review time — steps become the implementer's progress board, and the verdict card compares the files the plan named against the diff that came back:
256+
257+
```text
258+
## Goal one sentence
259+
## Steps 1. numbered — files: path/one.ts, path/two.ts
260+
## Verification exact commands
261+
## Risks what could invalidate this
262+
```
263+
264+
Off-plan or untouched files show as a warning-colored `plan … 3/4 planned files touched · 1 off-plan` line on the verdict card and in Report. A plan that does not follow the shape still works everywhere — it just reviews as an ordinary proposal.
265+
266+
Writing the plan yourself in the parent works the same way: `/docket save --from <artifact-id>`, choose **Proposal**, then `u → Implement`. Docket does not restrict the parent's own tools while you plan; that is Pi's job, and a plan-mode extension composes with this cleanly.
267+
236268
## Durable deliverables
237269

238270
`/docket save --from w<N>` copies an approved exact Worker Deliverable—including its body, frozen patch, approval, review notes, refs, and provenance—to an immutable durable record. Re-saving the same worker generation is idempotent.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# ADR-0007: Carry an approved plan into implementation
2+
3+
## Status
4+
5+
accepted
6+
7+
## Context
8+
9+
Planning first, then implementing, is a routine two-worker loop, and Docket handled it badly at exactly the seam between the two.
10+
11+
There were two ways to produce a plan and only one of them produced anything durable. A plan-gated worker presenting its plan through `docket_wait` left it in `question.text`: ephemeral, unversioned, unapprovable in a generation-bound way, and impossible to hand off. A worker publishing through `docket_done` with `outcome: proposal` produced a real Worker Deliverable. The built-in plan-gated `default` kind pushed users toward the first path, and nothing said so.
12+
13+
The handoff itself was kind-blind. `Use → Worker` resolved model, thinking, context, and workspace but never a kind, so every handoff worker landed on `worker.defaultKind` or the builtin `default` — plan-gated. Approving a plan and handing it off therefore produced a worker that immediately called `docket_wait` to ask for approval of a plan. The human answered the same question twice.
14+
15+
Nothing connected the two decisions afterwards. The implementation deliverable carried `sourceHandoff`, and Report printed one line of provenance, but no surface compared what the approved plan said with what the diff did.
16+
17+
The obvious-looking fix — make Plan a first-class artifact class — would fork Deliverable. Immutability, versioning, generation-bound approval, review notes, provenance, zero-token mounting, and the decision ledger all already exist there, and ADR-0001 → ADR-0005 records the cost of the last object that duplicated that vocabulary: bundles had to be demoted to compatibility-only data.
18+
19+
## Decision
20+
21+
A plan is a Worker Deliverable that proposes work rather than carrying it. Add no artifact class, store, slot prefix, decision verb, or top-level command. Make the *transition* first-class instead:
22+
23+
- **Plan contract** (`extensions/plan-contract.ts`, pure): Goal, Constraints, numbered Steps each optionally declaring `files:`, Verification, Risks. Markdown headings and bare `Plan:` label lines both parse. A body with no numbered step does not parse, and every consumer degrades to ordinary proposal behavior. The contract is a shape, never a schema, and is never validated at publish time.
24+
- **Two bundled kinds.** `architect` is read-only, so it derives a shared workspace and costs no worktree; it is told to publish plans through `docket_done outcome: proposal`, never through `docket_wait`, and to write the whole plan in the same response that calls `docket_done` because only that body is frozen. `implementer` keeps `plan_gate: true` and scopes its rights to the files the approved plan names.
25+
- **Implement handoff.** `Use` on an approved plan offers `Implement` ahead of `Parent` and `Worker`. It resolves the kind from `worker.implementKind` (default `implementer`), seeds the task from the plan's goal, and inherits parent execution, so the plan path costs one select, one prefilled editor, and the existing launch confirmation. `Worker` gains an explicit kind select and otherwise keeps choosing model and thinking by hand.
26+
- **Plan gate discharge, not gate removal.** A gate is discharged only when a reviewed handoff supplies the plan and the human chose Implement. `task.md` then states which approval and which decision discharged it, instructs the worker to publish the plan's steps with `docket_todos`, and re-arms the gate for unnamed files, destructive or external writes, dependency changes, scope growth, and any step that proves wrong. `planAuthorized` without a source deliverable is ignored at both the command and store layers.
27+
- **Plan coverage.** When a ready deliverable carries handoff provenance, the byte-exact launch sidecar is parsed at card-open time and the files the plan named are compared with the change set. The verdict card and Report show steps, planned-files-touched, off-plan, and untouched, colored as a warning when either drift figure is non-zero.
28+
29+
Docket does not restrict the parent's own tools during planning. Pi owns session and tool policy; a plan-mode extension composes with this because Docket only needs the resulting artifact.
30+
31+
## Consequences
32+
33+
Approval and use stay separate (ADR-0003): approving a plan starts nothing, and Implement remains a human action against one exact version. Generation-bound approval means a stale plan version cannot be implemented after a revision was requested.
34+
35+
The redundant second gate is gone without weakening the first: a worker that leaves the approved plan still has to stop and ask, and the ledger still shows which approval authorized the launch.
36+
37+
Plan coverage turns two disconnected decisions into one reviewable chain, at zero storage and zero model-context cost, because it is derived from data already on disk.
38+
39+
The cost is a shape convention that only pays off when plans follow it. That failure is soft everywhere: an unparseable plan still hands off, still discharges its gate, and still reviews — it just shows no coverage line and reads as a proposal.

docs/configuration.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Both optional. Defaults below.
1717
"worker": {
1818
"maxActive": 8,
1919
"defaultKind": "default",
20+
"implementKind": "implementer",
2021
"parentSeedPolicy": "none",
2122
"dockIdleHideMinutes": 30,
2223
"pruneAfterHours": 24,
@@ -45,14 +46,15 @@ Each record carries a `schemaVersion`. Older versions are upgraded when read; a
4546
|---|---|---|
4647
| `worker.maxActive` | 8 | reject `/docket spawn` once this many workers are starting/active/idle/needs_input. |
4748
| `worker.defaultKind` | `default` | kind used when `/docket spawn` omits `--as`. |
49+
| `worker.implementKind` | `implementer` | kind used by Use → Implement on an approved plan. |
4850
| `worker.parentSeedPolicy` | `none` | `"full"` seeds parent JSONL when no per-spawn context flag is present; explicit `"none"` keeps workers fresh and overrides legacy kind seeding. |
4951
| `worker.dockIdleHideMinutes` | 30 | hide ended workers from dock after this many minutes; 0 keeps them. |
5052
| `worker.pruneAfterHours` | 24 | auto-prune ended worker dirs after this many hours; 0 disables. |
5153
| `worker.guardrailsPath` | bundled | absolute or cwd-relative universal guardrail replacement. |
5254

5355
`worker.maxSpawnDepth` is removed. Existing JSON keys are ignored. Workers cannot create workers, and delete/prune affects one requested worker only.
5456

55-
`worker.defaultKind` preserves that kind's declared rights; Docket does not add an implicit plan gate. `worker.guardrailsPath` replaces packaged `extensions/worker-guardrails.md` for every worker.
57+
`worker.defaultKind` preserves that kind's declared rights; Docket does not add an implicit plan gate. `worker.implementKind` names the kind Use → Implement launches; an approved plan discharges that kind's plan gate at launch, so point it at a kind whose declared rights you accept for plan-scoped edits. An unregistered value warns and falls back to the builtin default. `worker.guardrailsPath` replaces packaged `extensions/worker-guardrails.md` for every worker.
5658

5759
### Per-spawn execution
5860

0 commit comments

Comments
 (0)