Make the one plan of a business intent that /shape-slice has served with a slice, once — the second play of the shaping loop (ADR 049: /shape-slice, then /shape-plan, then /shape-eval). Run as /shape-plan <intent-issue> (for example /shape-plan 788). It holds every slice the spine links to the intent, and for each slice it makes one epic (an issue of type Epic, no label) and one story for each feature (an issue of type Story, no label, a sub-issue of the epic), each story written from its fea...
Installs into .claude/skills of the current project.
Are you the author of Shape Plan?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/intent-driven-ai-shape-plan)
---
name: shape-plan
position: both
argument-hint: "<business intent issue number>"
description: 'Make the one plan of a business intent that /shape-slice has served with a slice, once — the second play of the shaping loop (ADR 049: /shape-slice, then /shape-plan, then /shape-eval). Run as /shape-plan <intent-issue> (for example /shape-plan 788). It holds every slice the spine links to the intent, and for each slice it makes one epic (an issue of type Epic, no label) and one story for each feature (an issue of type Story, no label, a sub-issue of the epic), each story written from its feature''s ICE and that feature''s capability''s ICE: its scope of work (goals, limits, expectations) and its Context guidance, with no failure condition and no recovery condition. When the intent already has a plan file, it stops at pre-flight, before it opens a change or writes anything, and points to /update-plan, which changes a plan that exists. Within its one run it never makes an epic or a story twice; a feature whose ICE is not ready is skipped and named. The epic holds the feature-to-story map, which always agrees with the plan file''s tree. Nothing reaches the tracker and the plan file is not written before the product owner types yes at one pinned checkpoint, after a brief that shows every write. Writes only the plan file, the epics and the stories; nothing in the evals folder or the product model. Position both: it opens its own change and lands it through commit-change, propose-change, check-change and merge-change, on the plan''s branch when the intent has one.'
user-invocable: true
---
# shape-plan
A person names a business intent that `/shape-slice` has served with a slice. /shape-plan
makes the intent's **one plan**, once: the plan file
(`.garura/project/plans/<intent-issue>.yaml`), one **epic** for each slice the plan holds, and
one **story** for each feature of that slice. Each story is written from its feature's ICE and
that feature's capability's ICE, and holds no failure condition and no recovery condition:
those stay in the slice's hidden file, which `/shape-slice` wrote. When the intent already has a
plan, the play stops at pre-flight and points to `/update-plan`, which changes a plan that
exists. The eval play (#798) comes next, once it is built.
The run reads the slices from the spine (the link "slice serves business intent"); it never
picks, orders or tests a slice. Within its one run it never makes an epic or a story twice,
and it knows a work item by the plan file's `represents` marks, never by a title. It
shows the product owner one brief with every tracker write and the plan-file change, and
writes nothing before the typed yes.
**Pipeline position: both.** start-change opens the change at the head (on the plan's
sub-branch when the intent has a plan); the end sequence commit-change → propose-change →
check-change → merge-change lands it.
## Compiled From
This play was compiled from the shape-plan ICE (`reference/ice.md`) by create-play. Intent
defines constraints (C1–C3, C5–C11, C13–C14) and failure conditions (F1–F5, F7–F13); the
expectation defines success scenarios (S1, S6–S9), a Done means (D1–D5, baked to `stop-condition.yaml`), and one recovery entry
per failure condition. To modify this play, update `reference/ice.md` and recompile with
edit-play.
Do NOT edit this file manually — it is a compiled artifact.
## Role
You are the orchestrator. You own the step order, the one brief and the checkpoint. The play
reads no memory itself. The `product-os-keeper` agent gathers context and calls the skill
`author-work-items`, which writes the words of each epic and story; the agent passes the
skill's output on as it is and never writes content. `template-manager` hands out the approval
prompt and the close templates. You run all mechanical work through bundled scripts: the
plan-exists check (`check_plan_absent.py`), the gather and the clean ICE copies (`gather_plan.py`, which runs the ready check
`check_ice_workable.py`), the find (`find_work_items.py`), the draft check (`check_drafts.py`),
the brief (`render_brief.py`), the checkpoint record and the write gate (`record_approval.py`),
the tracker writes (`write_work_items.py`, through `platform_adapter.py`), the plan file
(`write_plan_file.py`), the checks (`check_plan.py`, `check_tracker.py`), the write guard
(`scoped_write_guard.py`) and the stop condition. Every script writes its files through a
temp file and a rename (`plan_common.py`). The play never writes the plan file, an epic or a
story by hand.
**Forbidden:** a second plan for one intent (C1); picking, ordering or testing a slice, or
leaving out a slice the spine links to the intent (C2); matching a work item by its title, or
making an epic or a story twice within the run (C3); reading a "Fails if" line, or a story,
an epic or a plan file that holds a failure or recovery condition (C5); an epic title that
says how the product is built, or a map that disagrees with the tree (C6); writing outside the
plan file, the epics and the stories — the evals folder and the product model above all (C7);
a tracker write before the typed yes, a write the brief did not show, or a checkpoint skipped
by config (C8); a story built from an ICE that is not ready, or a skipped feature not named
(C9); landing outside the change plays or on the wrong branch (C10); a real person's name, or
text a person cannot follow (C11); a label on an epic or a story, a wrong issue type, or a write
while an epic holds work that is not its slice's (C13); going on past pre-flight when the intent
already has a plan file (C14).
**Agent boundaries:**
| Agent | Domain | Skills it uses | Phases |
|-------|--------|----------------|--------|
| `product-os-keeper` | Gather context and pass it on: hand the skill only `skill-input.json` and the clean ICE copies it names (never `gather.json`, never an ICE page), check that no clean copy holds a failure line, call the skill, and pass its `drafts.json` on as it is | `author-work-items` | Draft |
| `template-manager` (utility) | Hand out the approval prompt and the close templates (evidence file, delivery report, Next-line map) into short-term memory | — | Checkpoint, Close |
`product-os-keeper` is the single **domain agent**; `template-manager` is the one utility
agent. The skill `author-work-items` was built for this play. The start and end sequence are
injected sub-plays; everything else is a script.
## Pre-flight
| Check | Constraint | Action on Failure |
|-------|-----------|-------------------|
| Resolve config, `stm_base`, `product_base` (`.garura/core/config.yaml`) | — | Hard halt |
| A business intent issue number was given (`/shape-plan <intent-issue>`, digits only); the plan's ID is that number | C1 | Hard halt — ask for the intent's issue number |
| **The intent has no plan yet** — `check_plan_absent.py`: no `.garura/project/plans/<intent-issue>.yaml` in the checked-out tree or on main. Runs first, before start-change and before any read of the model | C14 | Stop (exit 3): show its message — the plan exists, and `/update-plan <intent-issue>` changes it — and end the run there. No Standard Play Close runs: its records would be files left behind, so the message is the run's whole report. Nothing was opened or written |
| The product model's spine exists (`<product_base>product-os/_spine.yaml`) | C2 | Hard halt — run `/shape-slice <intent-issue>` first |
| **Clean model tree** — after Step 0: `git status --porcelain -- <product_base>product-os` is empty | C7 | Hard halt (REC7) |
| **Clean plans tree** — after Step 0: `git status --porcelain -- .garura/project/plans` is empty, so the write guard sees only this run's change and a restore never takes back a person's own plan edits | C7, C8 | Hard halt (REC7) |
First the plan-exists check — before anything else:
```
python3 scripts/check_plan_absent.py --plans-dir .garura/project/plans --plan <intent-issue> \
--out "${stm_base}_plans/shape-plan/plan-absent.json"
```
Exit 3: show its `message` as it is, remove `${stm_base}_plans/shape-plan/` so nothing is left
behind, and stop — no start-change, no model read, no write, no task list and no close (C14). Exit 2: the number is not an
intent issue; ask for it. Exit 0: go on; after Step 0, move `plan-absent.json` into `<working>`.
Then resolve the facts with the bundled resolver, not by inference:
```
git branch --show-current
python3 scripts/preflight.py --play shape-plan --config .garura/core/config.yaml \
--branch "<current branch>"
```
`preflight.py` gives `stm_base`, `product_base` and `evidence_record`. The change's own issue
comes from start-change (Step 0), not from the branch name: the intent issue and the change
issue are two issues. After Step 0, with `<issue>` = the change issue:
```
python3 scripts/session_stamp.py --phase start \
--marker "{stm_base}<issue>/status/session-stamp-shape-plan.json" \
--cwd "$(pwd)" --branch "$(git branch --show-current)"
git rev-parse HEAD > {stm_base}<issue>/context/shape-plan/start-sha.txt
shasum -a 256 .garura/project/plans/<intent-issue>.yaml > {stm_base}<issue>/context/shape-plan/plan-before.txt \
|| echo "no plan file" > {stm_base}<issue>/context/shape-plan/plan-before.txt
```
Names used below: `<intent>` = `bi-<intent-issue>`, `<id>` = `<intent-issue>` (the plan's ID),
`<model>` = `<product_base>product-os`, `<plans>` = `.garura/project/plans`,
`<plan-file>` = `<plans>/<id>.yaml`, `<working>` = `{stm_base}<issue>/context/shape-plan/`.
The stop condition reads `{stm_base}<issue>/`.
**Resume check:** if `{stm_base}<issue>/status/shape-plan.json` exists, resume — skip
completed steps, reset an in-progress step to pending, continue from the first incomplete one.
## Task DAG
Create ALL tasks right after pre-flight, before any domain work. The play owns this DAG.
```
[T0] start-change (injected — start) blockedBy: []
[T1] Gather the slices and the clean ICE copies blockedBy: [T0]
[T2] Find the epics and stories that exist blockedBy: [T1]
[T3] Draft the epics and stories blockedBy: [T2]
[T4] Check the drafts blockedBy: [T3]
[T5] The brief and the checkpoint (yes / change / stop) blockedBy: [T4]
[T6] Write the epics and stories in the tracker blockedBy: [T5]
[T7] Write the plan file blockedBy: [T6]
[T8] Check the plan, the tracker and the writes blockedBy: [T7]
[T9] Commit the plan file blockedBy: [T8]
[E1] commit-change (injected — end) blockedBy: [T9]
[E2] propose-change (injected — end) blockedBy: [E1]
[E3] check-change (injected — end) blockedBy: [E2]
[E4] merge-change (injected — end) blockedBy: [E3]
[T10] Scenario Validation blockedBy: [E4]
[T11] Close blockedBy: [T10]
```
Mark each task in progress before its step and completed right after its eval passes. No
runtime reordering. A **change** at the checkpoint sends the run back to T3; T3–T5 run again
in order.
## Workflow
### Phase: Start (injected — D2 position: both, head)
**Step 0 — start-change** · Owner: `start-change` (sub-play) · Depends on: pre-flight
Run the start-of-pipeline member as a sub-play with `parent_run_id`. It resolves or creates
the change issue, cuts its branch (the plan's sub-branch when the intent has a plan file,
`<plans>/<id>.yaml`), and inits short-term memory. Then assert the clean model tree and the
clean plans tree (hard halt if dirty — REC7).
{ "play": "start-change", "parent_run_id": "<this run id>",
"inputs": { "title": "[TASK] shape-plan: make the plan for business intent #<intent-issue>" },
"outputs": { "result": "<working>start-change.json" } }
Step 0 runs only after the plan-exists check (pre-flight) read `exists: false`.
**SE-20 (F13/C14):** `<working>plan-absent.json` reads `exists: false`, and start-change's record
was made after it; on a stop, the check's printed record read `exists: true`, its message named
`/update-plan <intent-issue>` and was shown, and the run made no change issue, no branch, no
short-term folder and no tracker write, and left no file behind.
### Phase: Gather
**Step 1 — Gather the slices and the clean ICE copies** · Owner: play (script) · Depends on: Step 0
```
python3 scripts/gather_plan.py --root <model> --spine <model>/_spine.yaml --intent <intent> \
--plans <plans> --out <working>gather.json
```
It lists every slice whose `intents` lists the intent, in the spine's order (C2), each
slice's features in build order, each feature's capability and the pages of its two ICE. It
runs the ready check (`check_ice_workable.py`) on both ICE of each feature: a feature whose
ICE is not ready is `ready: false` with its `reason`, and gets no clean copy (C9). For each
ready feature it writes `<working>ice/<feature id>.md`: both ICE with every "Fails if" block
taken out — the line and every line under it, nested bullets and wrapped text included (C5).
It also writes `<working>skill-input.json`: each slice's id, name and outcome, and each ready
feature's id, name, one line and clean copy's path — no ICE page path. The skill reads only
`skill-input.json` and the clean copies, never `gather.json` or an ICE page. Exit 2
means no slice serves the intent: tell the person to run `/shape-slice <intent-issue>` first,
and close HALTED.
**SE-1 (F2/C1/C2):** `gather.json` `plan_id` equals `<intent-issue>` and `plan_file` is
`<plans>/<id>.yaml`, the one plan file for the intent; its `slices` are every spine slice whose
`intents` lists `<intent>`, in spine order, none left out and none added.
**SE-2 (F8/C9):** each feature with `ready: false` has a `reason` that is not empty and a null
`clean_ice`; each feature with `ready: true` has a `clean_ice` file on disk; no other file is
in `<working>ice/`.
**SE-3 (F3/C5):** `grep -ril "fails if" <working>ice/` finds nothing; no line of a "Fails if"
block of a feature's two ICE (children included) is in its clean copy; and neither
`skill-input.json` nor a clean copy names an ICE page (`grep -l "feature.md\|capability.md\|ice.md"`
over them finds nothing).
### Phase: Find
**Step 2 — Find the epics and stories that exist** · Owner: play (script) · Depends on: Step 1
```
python3 scripts/find_work_items.py --gather <working>gather.json --plan-file <plan-file> \
--out <working>work-items.json
```
It reads the plan file and, through the code-host adapter, the tracker; it writes nothing.
An epic is `use` when a node of the tree has `represents: <slice id>`, else `make`; a story is
`use`, `add-sections` (it lacks "Scope of work" or "Context guidance"), `make`, or `skip` (its
feature is not ready). A run starts with no plan file (C14), so no node is marked yet: every
epic is `make`, and every story is `make` or `skip`. The script's other actions (`use`,
`add-sections`, `move_out`) and the `misplaced` list need a plan file, so this play does not
reach them. It never compares titles (C3). Exit 1 lists a duplicate (two nodes stand for one
slice, or for one feature in one epic) or a marked issue the tracker does not have: show it to
the person in plain words and stop (REC2); the run goes on only after the person fixes the
plan file. `misplaced` lists each sub-issue of an epic that is not a story of its slice (nor a
moved-out story), with its title (C13): exit 1 too. Show each as "not this slice's work" and
stop before any tracker write (REC12); the person files it in its own place (its own story, bug
or chore, which `/shape-slice` and `/shape-plan` place when they run) and takes it off the epic,
then runs the play again.
**SE-19 (F12/C13):** `work-items.json` `misplaced` names every sub-issue of each used epic that
is neither the issue of a story item of its slice nor a `move_out` story, each with its title;
when `misplaced` is not empty, `ok` reads false and there is no `tracker-record.json`.
**SE-4 (F2/C3):** `work-items.json` reads `ok: true`; its `duplicates` is empty; each `use` or
`add-sections` item's issue is the `issue` of the plan-file node whose `represents` names that
slice or feature; no item with a marked node is `make`.
### Phase: Draft
**Step 3 — Draft the epics and stories** · Owner: `product-os-keeper` · Depends on: Step 2
{ "task": "have the epic of each slice and one story for each ready feature drafted. Hand author-work-items only skill-input.json and the clean ICE copies it names (clean_ice); never open or hand on gather.json or any ICE page. First check that no clean copy holds the words 'Fails if' (grep -il 'fails if' on each); if one does, stop with step_failure and name it. Call author-work-items with skill_input, the clean copies, and, on a re-run, the problems of drafts-check.json (fix mode) or the product owner's change (change mode) with the earlier drafts.json. Pass its drafts.json on as it is. Gather and pass on only; the skill writes every word",
"inputs": { "skill_input": "<working>skill-input.json", "clean_ice": "<working>ice/",
"problems": "<working>drafts-check.json, on a fix", "change": "<the product owner's change, on a re-run>",
"drafts": "<working>drafts.json, on a re-run" },
"outputs": { "drafts": "<working>drafts.json" },
"skills": ["author-work-items"],
"task_id": "shape-plan-draft" }
**Step 4 — Check the drafts** · Owner: play (script) · Depends on: Step 3
```
python3 scripts/check_drafts.py --drafts <working>drafts.json --gather <working>gather.json \
--out <working>drafts-check.json
```
It reads the original ICE pages itself, so the "Fails if" lines reach no skill and no person,
and compares the drafts with every line of each "Fails if" block, children included.
Exit 1 lists each problem by slice and feature: send `drafts-check.json` back to the
`shape-plan-draft` contract (fix mode), then run Step 4 again (REC3). The run does not go on
until it reads ok.
**SE-6 (F3/C5/C6):** `drafts-check.json` reads `ok: true`: each slice has an epic title; no
text of an epic or a story holds a line of a "Fails if" block of its two ICE, the words
"Fails if", or a recovery phrase; each story has goals, limits, expectations and the four Context guidance
lines.
**SE-7 (F1/C9):** in `drafts.json`, each ready feature of each slice has exactly one story, and
no story is for a feature that is not ready or not in its slice.
### Phase: Checkpoint (class: one-way-door, pinned)
**Step 5 — The brief and the checkpoint** · Owner: play (+ `template-manager` for the approval prompt) · Depends on: Step 4 · Checkpoint (class: one-way-door, **pinned** — the product owner decides; never skipped by config, `standards/rules/gate-config.md`)
```
python3 scripts/render_brief.py --gather <working>gather.json \
--work-items <working>work-items.json --drafts <working>drafts.json \
--out <working>brief.md
```
The script builds the whole brief from the run's records, so no write can be left out (C8):
each epic and story the run will make, use, add sections to or move out, with the text a new
item or section gets and each type, label or sub-issue change; the plan-file change; and each
skipped feature with its reason (C9). The brief is never edited by hand: a problem in it goes
back to the step whose record it reads. Frame the brief with the approval prompt from
`template-manager` (`standards/templates/approval-prompt.md`) and show it. Ask the product
owner to type **yes**, **change: <what>**, or **stop**. Save the reply word for word, and
record it with the files the person saw, so the yes stays tied to them (their sha256):
```
python3 scripts/record_approval.py --decision <yes|change|stop> --reply "<their exact words>" \
--at <now, UTC> --brief <working>brief.md --work-items <working>work-items.json \
--drafts <working>drafts.json --out <working>approval.json
```
- **yes** → Step 6.
- **change** → nothing was written; re-run Step 3 with the change (change mode), then Steps 4
and 5; show the brief again.
- **stop** → nothing was written, and nothing is written now. Write
`git status --porcelain -- <plans>` to `<working>stop-check.txt`, and
`shasum -a 256 <plan-file>` (or "no plan file") to `<working>plan-after-stop.txt`; close
HALTED. A stop needs no undo; if any write is found after a stop, undo it with
`scoped_write_guard.py --product-base <product_base> --base-ref HEAD --also-tree .garura/project/plans --restore --out <working>guard-restore.json`
and REC5.
**SE-8 (F5/C8):** `approval.json` holds the product owner's typed words and the sha256 of
`brief.md`, `work-items.json` and `drafts.json` as shown; `brief.md` names every
epic and story in `work-items.json` with its action, and every `move_out`; no
`tracker-record.json` exists before `approval.json` reads `decision: yes`; after a stop,
`stop-check.txt` is empty, `plan-after-stop.txt` equals `plan-before.txt`, and there is no
`tracker-record.json`.
**SE-9 (F8/C9):** each feature with `ready: false` in `gather.json` is in `brief.md`'s
"Skipped features" with its reason.
**SE-10 (F10/C11):** `brief.md`, `drafts.json` and the plan file use personas and plain words
(ASD-STE100) and name no real person — `grep -i "$(git config user.name)"` over them finds
nothing.
### Phase: Write
**Step 6 — Write the epics and stories in the tracker** · Owner: play (scripts) · Depends on: Step 5 (yes)
The gate runs right before the first tracker write; any exit but 0 stops here, and nothing
reaches the tracker (C8):
```
python3 scripts/record_approval.py verify --file <working>approval.json
python3 scripts/write_work_items.py --work-items <working>work-items.json \
--drafts <working>drafts.json --gather <working>gather.json \
--approval <working>approval.json --plan-id <id> --out <working>tracker-record.json
```
`write_work_items.py` refuses again (exit 2, nothing written) unless the approval reads yes,
and when the brief, `work-items.json` or `drafts.json` is no longer the file the person saw
(its sha256 differs): then show the brief again (Step 5). It makes each epic as an issue of
type Epic, and each story as an issue of type Story with its epic as parent, each in one
write and with no label (C13); it adds each story's "Written from" line (the two ICE pages)
from `gather.json`; on a used item it sets the type when it is wrong
and takes off its labels; it writes the epic's "Feature-to-story map" (C6).
Each write goes through the code-host adapter's verbs, and `tracker-record.json` is written
after every write. Exit 1 means the tracker failed part way: the record reads `ok: false`,
names the `failed` step and keeps every issue made before it. Still run Step 7, so the plan
file marks those issues; then show the record to the person and close HALTED (REC5). The plan
file now exists, so a new /shape-plan run stops at pre-flight (C14): the person finishes the
plan with `/update-plan <id>`, or removes the plan file and the issues made and runs
/shape-plan again.
**SE-12 (F5/C8):** `tracker-record.json` `approval_at` equals `approval.json` `at`; the sha256 of
`brief.md`, `work-items.json` and `drafts.json` still equals what `approval.json` recorded; each
issue in `created`, `updated` and `moved_out` matches an item of `work-items.json` that
`brief.md` shows.
**Step 7 — Write the plan file** · Owner: play (script) · Depends on: Step 6
```
python3 scripts/write_plan_file.py --plan-file <plan-file> --gather <working>gather.json \
--record <working>tracker-record.json --out <working>plan-write.json
```
It also takes a record that reads `ok: false`, and marks each issue it lists. It makes the
plan file when it is missing (`id`, `title`, `status: Not Started`, an empty `direction`, `tree`), or
updates it: one epic node for each slice (`represents: <slice id>`) with one story node for
each feature of its map (`represents: <feature id>`), and moved-out story nodes taken out. It
changes the file line by line and keeps every other node, line and comment; with nothing to
change the file stays byte for byte the same. The plan file holds titles, issues and marks
only: never a failure or recovery condition (C5).
**SE-13 (F2/C1):** `plan-write.json` reads `ok: true` and its `path` is `<plan-file>`; there is
one plan file for the intent under `<plans>`, and no slice has two epic nodes in it.
### Phase: Check
**Step 8 — Check the plan, the tracker and the writes** · Owner: play (scripts) · Depends on: Step 7
```
python3 scripts/check_plan.py --plan-file <plan-file> --gather <working>gather.json \
--record <working>tracker-record.json --out <working>plan-check.json
python3 scripts/check_tracker.py --record <working>tracker-record.json \
--out <working>tracker-check.json
python3 scripts/scoped_write_guard.py --product-base <product_base> --base-ref HEAD \
--also-tree .garura/project/plans --allow '.garura/project/plans/<id>.yaml' \
--out <working>guard-report.json
```
`check_plan.py` exit 1: a missing story node is REC1, a tree that disagrees with the map is
REC4 — fix and run Steps 6–8 again. `check_tracker.py` exit 1: a label or a wrong type is
REC11. The guard's allowed set is C7's: the one
plan file. Exit 1: re-run it with `--restore` to undo each write outside the set (REC7), then
run Steps 7–8 again. The run writes nothing in the evals folder and nothing in the product
model, so it makes no `feat(model)` commit.
**SE-14 (F1/F4/C6):** `plan-check.json` reads `ok: true`: each ready feature of each slice has
exactly one story node under its slice's epic node, no story node stands for a feature outside
its slice, each epic's map names the same story for each feature as the tree, and each
skipped feature is in `skipped`.
**SE-15 (F11/C13):** `tracker-check.json` reads `ok: true`: each issue of the record exists, each
epic has type Epic and each story type Story, none has a label, and each epic's sub-issues
are its stories.
**SE-17 (F7/C7):** `guard-report.json` reads `ok: true` — no path in the product model and no
path under `.garura/project/plans/` other than `<plan-file>` changed (the evals folder is
untouched).
### Phase: Commit
**Step 9 — Commit the plan file** · Owner: play · Depends on: Step 8
The commit runs only when the recorded answer is yes and the plan file changed
(`plan-write.json` `changed: true`); the gate runs right before it:
```
python3 scripts/record_approval.py verify --file <working>approval.json
git add -- .garura/project/plans/<id>.yaml
git commit -m "feat(plan): make or update plan <id> for business intent #<id> (#<issue>)"
```
### Phase: End sequence (injected — D2 position: both, tail)
Each member runs as a sub-play with `parent_run_id`; each resolves its own context from the
branch and config.
**Step E1 — commit-change** · blockedBy: Step 9 — commits the run records under `{stm_base}<issue>/`.
{ "play": "commit-change", "parent_run_id": "<this run id>", "inputs": {}, "outputs": { "result": "<working>end/commit-change.json" } }
**Step E2 — propose-change** · blockedBy: E1
{ "play": "propose-change", "parent_run_id": "<this run id>", "inputs": {}, "outputs": { "result": "<working>end/propose-change.json" } }
**Step E3 — check-change** · blockedBy: E2 — a reject stops the chain before merge.
{ "play": "check-change", "parent_run_id": "<this run id>", "inputs": {}, "outputs": { "result": "<working>end/check-change.json" } }
**Step E4 — merge-change** · blockedBy: E3 — lands on the plan's branch when the change sits in a plan.
{ "play": "merge-change", "parent_run_id": "<this run id>", "inputs": {}, "outputs": { "result": "<working>end/merge-change.json" } }
**SE-18 (F9/C10):** `start-change.json` shows the change opened by start-change; the
`<working>end/` results are present in order; the PR's base is the plan's branch when the
intent has a plan (else main), and `merge-change.json` reports it merged.
### Phase: Scenario Validation
**Step 10 — Scenario evals** · Owner: play · Depends on: E4
- **SCE-1 (S1 — product owner, first plan):** with no plan file and one slice of three
features, `plan-write.json` reads `made: true`, `plan-check.json` and `tracker-check.json`
read `ok: true`, `tracker-record.json` `created` holds one epic and three stories, the epic's
map names the three stories, and `drafts-check.json` reads `ok: true`.
- **SCE-6 (S6 — product owner, stop):** `approval.json` reads `decision: stop`, there is no
`tracker-record.json`, `stop-check.txt` is empty, and `plan-after-stop.txt` equals
`plan-before.txt`.
- **SCE-7 (S7 — product owner, an ICE not ready):** `tracker-record.json` `skipped` names the
feature with its reason, `brief.md` names it under "Skipped features", and `plan-check.json`
reads `ok: true` with the feature in `skipped`.
- **SCE-8 (S8 — product owner, misplaced work):** `work-items.json` lists each item that is not a
story of the epic's slice in `misplaced` with its title, there is no `tracker-record.json`, and
the plan file's `shasum -a 256` equals `plan-before.txt`.
- **SCE-9 (S9 — product owner, a plan that exists):** the check's printed record reads
`exists: true` and its message names `/update-plan <intent-issue>`; there is no start-change
record, no change branch, no tracker write and no file left behind, and the plan file is as it
was.
### Phase: Evidence & Close
**Step 11 — Close** · Owner: play · Depends on: Step 10
Run the Standard Play Close. The close is stop-condition gated: the baked
`stop-condition.yaml` (D1 the plan check reads ok; D2 the approval reads yes; D3 the draft
check reads ok; D4 the tracker check reads ok; D5 the write guard reads ok) must read **held**
before a COMPLETED close. A stop, or a run with no slice, closes HALTED.
```bash
# --- Standard Play Close (shared; see ../_shared/play-close.md) ---
# Run the close in ../_shared/play-close.md. Its Step L loads the templates through
# template-manager and sets ts and evidence_dest; then run this play's own lines below;
# then Steps C0-C2.
# evidence_base, slug (project-scoped play):
# evidence_base="${stm_base}${issue}/evidence/shape-plan/" ; slug="#${issue}"
# Session identity stamp (#463) — close phase; start phase ran at pre-flight
session_stamp=$(python3 scripts/session_stamp.py --phase close \
--marker "${stm_base}${issue}/status/session-stamp-shape-plan.json")
# Stop-condition gate (#464) — Step C0: this play carries a baked manifest, so the
# gate is LIVE. Evaluate the Done means against the issue folder.
python3 scripts/check_stop_condition.py \
--manifest "<play-dir>/stop-condition.yaml" \
--base "${stm_base}${issue}/" \
--out "${stm_base}${issue}/status/stop-condition-shape-plan.yaml"
sc_exit=$? # 0 held · 1 unmet · 2 error
```
**Step C0 — bind the verdict.** `sc_exit == 0` (held) permits `status: COMPLETED`. Anything
else closes `HALTED` with `exit_reason: stop_condition_unmet`, and the evidence's Stop
Condition section names every unmet clause. An unevaluable verdict is never a pass.
**Step C1 — Write evidence file.** Gated by the resolved `evidence.record` flag (per-play
`evidence.plays.shape-plan`, else global; absent ⇒ record). When false, skip the write and
record `evidence skipped (record=false)` in the report's pointer line. Otherwise fill the
`evidence-file.md` slots (play `shape-plan`, run_id `shape-plan-${ts}`, issue, the intent
issue, started_at/completed_at, status per C0, exit_reason; artifacts produced: `gather.json`,
`work-items.json`, `drafts.json`, `drafts-check.json`, `brief.md`, `approval.json`,
`plan-absent.json`, `tracker-record.json` with the epics and stories made and used, `plan-write.json`
and the plan file's path, `plan-check.json`, `tracker-check.json`, `guard-report.json`, the
plan commit sha, and the skill the agent contract named; step and scenario eval results
SE-1…SE-4, SE-6…SE-10, SE-12…SE-15, SE-17…SE-20 / SCE-1, SCE-6…SCE-9; the checkpoint answers in the person's words; the session identity
stamp fields from $session_stamp (#463); and stop_condition per C0) and write to
`$evidence_dest`. Do NOT hand-author the body.
**Step C2 — Render delivery report.** Fill the `delivery-report.md` slots and output the
report: `## shape-plan Delivered — #${issue}`, the Run Summary table, the Pipeline Steps table
from the task DAG, the Artifacts Produced table (the plan file, each epic and story made,
each skipped feature with its reason, the PR), and the **Next**
line from the Next-line map (`$next_map`, from `template-manager`), per `play-close.md`; omit it
when the map's command is null. Next Steps: the eval play (#798) reads the epic, its map and
its stories once it is built. End with a pointer to `$evidence_dest`. Always emitted; never
gated.
```bash
# --- end Standard Play Close ---
```
## Scenario Validation
| Scenario | Persona | Eval |
|----------|---------|------|
| S1 — first plan | product owner | SCE-1 |
| S6 — stop at the checkpoint | product owner | SCE-6 |
| S7 — an ICE not ready | product owner | SCE-7 |
| S8 — misplaced work | product owner | SCE-8 |
| S9 — a plan that exists | product owner | SCE-9 |
## Recovery
| For | Trigger | Direction | Handoff |
|-----|---------|-----------|---------|
| F1 | a feature of a slice the plan holds has no story, or a story in the plan stands for no feature of its slice | make the missing story; move a story that stands for no feature of its slice out of the plan | autonomous |
| F2 | a second plan for one intent, a second epic for one slice, or a second story for one feature in one epic | keep the first plan, epic or story, move the copy out of the plan, and tell the person which one was kept | human |
| F3 | a story, the epic or the plan file holds a failure condition or a recovery condition | take the line out and draft the story or the epic again from the two clean ICE copies | autonomous |
| F4 | the plan file's tree and the epic's feature-to-story map disagree | write the plan file's tree and the epic's map again from the run's record, so they agree | autonomous |
| F5 | a tracker write happened before the yes, a write the person did not see in the brief happened, or a stop left a tracker write or a plan-file change behind | undo each tracker write the record lists and put back the plan file as it was (`scoped_write_guard.py --restore`); show the brief again | human |
| F7 | the run wrote outside the plan file, the epics and the stories (the evals folder, the product model, or any other file) | undo the write outside the allowed set (`scoped_write_guard.py --also-tree .garura/project/plans --restore`) | autonomous |
| F8 | a story was built from an ICE that is not ready, or a skipped part is not named in the brief | take back the story built from the part that is not ready, skip the part, and name it in the brief | autonomous |
| F9 | the change landed outside the change plays, or on the wrong branch | land the change through the change plays, on the branch the target script gives | human |
| F10 | a page names a real person, or a person cannot follow it | rewrite with personas and plain words | autonomous |
| F11 | an epic or a story carries a label, or an epic's issue type is not Epic, or a story's is not Story | take the label off, and set the issue type to Epic on an epic and to Story on a story | autonomous |
| F12 | an epic holds a sub-issue that is not a story of its slice, and the run wrote to the tracker or did not name it | stop before any tracker write and name each misplaced item with its title; a person files it in its own place and takes it off the epic, then runs the play again | human |
| F13 | the intent already had a plan file and the run went on past pre-flight (opened a change, read the model, wrote anything), or stopped without naming /update-plan | stop the run, undo what it opened or wrote after pre-flight (the change branch and short-term folder, any tracker write), and show that the plan exists and `/update-plan <id>` changes it | human |
## Pause and Resume
Steps run top to bottom. On entry, resolve the change issue from start-change's result, check
the status marker, skip completed steps, reset an in-progress step to pending, and continue.
Nothing reaches the tracker before Step 6, so a resume before it needs no undo. A resume after
Step 6 started reads `tracker-record.json` first. It is written after every tracker write, so
it lists every issue made. Run Step 7 from it before anything else, so the plan file marks
those issues, then go on in the same run. A run that stopped part way is finished by continuing
it, or by removing what it made and starting again; a new /shape-plan run on an intent whose
plan file exists stops at pre-flight (C14).
A run that closed HALTED at a stop has nothing to resume: it wrote nothing.
## Compilation Metadata
| Field | Value |
|-------|-------|
| fingerprint | sha256:1bd5350cf7d9815047f7593e788d324d0e55bca7ee20307033c3e75e630d05ce (of `reference/ice.md`) |
| compiled_by | create-play (#818); edit-play (#896) |
| pipeline_position | both (start-change head; commit → propose → review → merge close) |
| workflow_structure | A (find before make; one brief at one pinned one-way-door checkpoint; nothing written before the yes; stop-condition gated close) |
| stop_condition | stop-condition.yaml (D1–D5), gate live at Step C0 |
| domain_agents | 1 (product-os-keeper) |
| utility_agents | 1 (template-manager — serves the approval prompt and the close) |
| skills_used | author-work-items (built for this play) |
| skills_missing | none |
| tracker_verbs | view-issue, create-issue-typed (type and parent in one call), set-issue-type, edit-issue-body, remove-label, attach-sub-issue, remove-sub-issue — all through platform_adapter.py |
| scripts | check_plan_absent.py (the plan-exists check), preflight.py, session_stamp.py, check_stop_condition.py, platform_adapter.py, check_ice_workable.py, scoped_write_guard.py, model_names.py (copied unchanged from create-play references; model_names.py is what check_ice_workable.py imports); plan_common.py, gather_plan.py, find_work_items.py, check_drafts.py, render_brief.py, record_approval.py, write_work_items.py, write_plan_file.py, check_plan.py, check_tracker.py (this play) |
| tests | test_check_plan_absent.py, test_support.py, test_gather_plan.py, test_find_work_items.py, test_check_drafts.py, test_render_brief.py, test_record_approval.py, test_write_work_items.py, test_write_plan_file.py, test_check_plan.py, test_check_tracker.py |
| step_evals | 17 (SE-1…SE-4, SE-6…SE-10, SE-12…SE-15, SE-17…SE-20) |
| scenario_evals | 5 (SCE-1, SCE-6…SCE-9) |
| recovery_entries | 12 (one per failure condition; 7 autonomous / 5 human) |