Skip to content
Back to skills

Update Plan

ASecurity

Manage one plan in a working session — /update-plan <id> or /update-plan <a description>. The play finds the plan (a description''s match is shown and waits for a yes; it never guesses and never makes a plan), moves onto the plan''s own branch (it stops on uncommitted work on any branch, the plan''s own included, and names the files; for a plan with no branch it sets main''s plan file to In Progress first, then cuts the branch), and takes instructions in the person''s own words — finish, drop...

  • 3 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added November 7, 2026
toolspythongobashgit

Security analysis

A100/100

Pro scans all 20 files and shows the line behind each finding

Scanned November 7, 2026

npx -y skills add intent-driven-ai/garura --skill update-plan --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Update Plan?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Update Plan
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/intent-driven-ai-update-plan/badge)](https://www.skillsdirectory.com/skills/intent-driven-ai-update-plan)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: update-plan
position: none
description: 'Manage one plan in a working session — /update-plan <id> or /update-plan <a description>. The play finds the plan (a description''s match is shown and waits for a yes; it never guesses and never makes a plan), moves onto the plan''s own branch (it stops on uncommitted work on any branch, the plan''s own included, and names the files; for a plan with no branch it sets main''s plan file to In Progress first, then cuts the branch), and takes instructions in the person''s own words — finish, drop, start, add, move, rename, change the text, change the kind, set the plan''s Status and Direction, or a new feature found while the plan is in progress. Each instruction becomes list entries, checked against the plan''s rules before they join the running list; a break is shown with what works instead, and the person may override it (recorded); big changes are confirmed first. Only a plain "approved" lands the list, all at once: the product model (a new feature''s thin page and its model entry; a new feature may join a slice in progress, named as such, ADR 062), then GitHub, then the plan file, then the scoped-write guard and a check that the three agree and nothing else changed, then one commit on the plan''s branch, pushed, whose message names the instructions, the overrides and what is owed; then back to the starting branch. "Rejected" lands nothing and the session goes on until the list is ready; "abort" drops the list, undoes a plan branch the session cut, and goes back where it started. A person sets every status and the direction; no roll-ups — except that an item whose own issue is closed on GitHub goes on the list as "closed on GitHub" when the session starts. Never writes the evals folder; names what is owed. Use when someone wants to change a plan: "#730 is done", "drop #909", "set plan 717 to Paused", "we found a new feature".'
user-invocable: true
---

# update-plan

A working session on one plan. The person gives instructions in their own words; the play works
out what each one changes, checks it against the plan's rules, and keeps a running list. On one
plain "approved", the whole list lands at once — the product model, GitHub and the plan file —
on the plan's own branch, and a check proves they agree and nothing else changed.

## Compiled From

This play was compiled from the update-plan ICE (`reference/ice.md`) by create-play (#730, with
#931's model part; feature #720, plan 717), and recompiled by edit-play for the PR #998 review
(ADR 062). Intent defines constraints (C1–C14) and failure
conditions (F1–F16); the expectation defines success scenarios (S1–S11), a Done means (D1–D3,
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 session, the step order and the one question at the end.
You do no domain work: what an instruction means is `author-plan-changes`' judgment (through
`project-orchestrator`); where a new feature belongs and its page are the model's (through
`product-os-keeper`); every fixed rule — the plan's rules, the list, the answer, the writes'
order, the plan file's lines, the guard, the agreement, the commit, the screens — is a script. You show
each screen exactly as `render_screen.py` prints it.

**Forbidden:** guessing a plan or making one (C1/F1); switching away from uncommitted work, going on
over it on the plan's own branch, or writing on any branch but the plan's beyond the plan-branch
step (C2/F2); any write to GitHub, the
model or a plan file before `record_answer.py verify` exits 0, other than the plan-branch step
(C5/F3); adding to the list an entry the person did not ask for, or a status or direction no
instruction named — other than an item whose own issue is closed on GitHub (C8/F7); a tracker or model write outside the two sub-agents' skills (C9/F8);
wording a screen or a question yourself (C10/F9); writing in any plan's `evals/` folder
(C11/F10); committing when the scoped-write guard put a model path back (C7/F13); running a
change play, merging a branch, or touching code (C7).

**Agent boundaries:**

| Agent | Domain | Skills it invokes | Phases |
|-------|--------|-------------------|--------|
| `template-manager` | Load the plan-update, plan-view and close templates | — | Prepare, Close |
| `project-orchestrator` | Read the plan's issues; turn an instruction into list entries; make the approved tracker writes | `track-plan-changes`, `author-plan-changes` | Prepare, Session, Landing |
| `product-os-keeper` | Suggest a capability for a new feature when no rule settles it (the placement itself is `place_feature.py`); on approval write its page with its thin ICE from the person's words, its model entry with both thin marks and its place in the slice, then name its story on the entry; draft the story for a feature the person's words name, from its clean ICE copy (story mode) | `find-work-placement`, `enrich-grounding`, `link-model-part`, `author-work-items` | Session, Landing |

`template-manager` and `project-orchestrator` are **utility agents**. `product-os-keeper` is the
one **domain agent**, dispatched only for a new feature.

## Pre-flight

| Check | Constraint | Action on Failure |
|-------|-----------|-------------------|
| Resolve config (`.garura/core/config.yaml`) via `scripts/preflight.py` | — | Hard halt |
| One plan, via `scripts/find_plan.py find` | C1 | Exit 1: show the `not_found` screen and stop — never guess |
| The plan tool's plan-branch script is installed (`.garura/core/tools/plan/plan_branch.py`) | C2 | Hard halt |

```
git branch --show-current
git status --porcelain > <tmp>/porcelain.txt
python3 scripts/preflight.py --play update-plan --config .garura/core/config.yaml \
    --branch "$(git branch --show-current)" --porcelain-file <tmp>/porcelain.txt
test -f .garura/core/tools/plan/plan_branch.py || { echo "the plan tool is not installed — halt"; exit 1; }
```

`<plans>` is `plans.base-path` from config, else `.garura/project/plans/`. Everything the session
makes lives in one session folder (C13): `<start> = ${stm_base}_plans/update-plan/_start/` until the
plan is known, then `<working> = ${stm_base}_plans/update-plan/<id>/` (Step 2 moves `<start>` into
it). `<session>` names whichever of the two the session is in. Right after, record the session
identity stamp's start marker in `<start>` (#463 — soft-fail, never a halt):

```
python3 scripts/session_stamp.py --phase start \
    --marker "<start>/session-stamp-update-plan.json" \
    --cwd "$(pwd)" --branch "$(git branch --show-current)"
```

## Task DAG

Create ALL tasks immediately after pre-flight — before any domain work.

```
[T1]  Load the templates                    blockedBy: []
[T2]  Find the plan                         blockedBy: [T1]
[T3]  Get onto the plan's branch            blockedBy: [T2]
[T4]  Read the plan's issues; the list      blockedBy: [T3]
[T5]  Take instructions                     blockedBy: [T4]
[T6]  Ask: approved or rejected             blockedBy: [T5]
[T7]  Gate and plan the writes              blockedBy: [T6]
[T8]  Land: the model                       blockedBy: [T7]
[T9]  Land: GitHub                          blockedBy: [T8]
[T10] Land: the plan file                   blockedBy: [T9]
[T11] Check the agreement                   blockedBy: [T10]
[T12] One commit on the plan's branch       blockedBy: [T11]
[T13] Back to the starting branch           blockedBy: [T12]
[T14] Scenario Validation                   blockedBy: [T13]
[T15] Close: the stop condition           blockedBy: [T14]
[T16] Self-clean                            blockedBy: [T15]
[T17] Close: evidence and report           blockedBy: [T16]
```

Mark each task in-progress before its step and completed right after its eval passes. T5 and T6
repeat until the person answers approved or "abort" ("rejected" goes back to T5). An aborted
session skips T7–T12 (marked skipped) and goes to T13; a session stopped on uncommitted work
(Step 3) goes to T14. Every session ends T15, T16, T17 in that order: the stop condition reads the
session's folder, the self-clean removes it, and the evidence and the report are written outside it.

## Workflow

### Phase: Prepare

**Step 1 — Load the templates** · Owner: `template-manager` · Depends on: pre-flight

    { "task":    "load these templates into the run's working folder",
      "inputs":  { "issue": null,
                   "templates": ["standards/templates/plan-update.md", "standards/templates/plan-view.md",
                                 "standards/templates/delivery-report.md", "standards/rules/pipeline-next.md"] },
      "outputs": { "out_dir":  "<start>/templates/",
                   "manifest": "<start>/templates/manifest-update-plan.json" },
      "task_id": "load-templates" }

`<update>` and `<view>` are the copies of `plan-update.md` and `plan-view.md` in
`<session>/templates/` — the session's own copies, so the screens do not change mid-session. Every
`render_screen.py` call below passes `--template <update> --view-template <view> --work <session>`.
**SE-1 (F9/C10):** the manifest names both templates with a source tier; a `template_missing` for
either is a hard halt — the play never words a screen of its own.

**Step 2 — Find the plan** · Owner: play (script) · Depends on: Step 1

```
python3 scripts/find_plan.py find --args="<the play's arguments>" --plans-dir <plans> --out <start>/plan.json
```

Exit 1: render `not_found`, show it, and stop (close HALTED, `exit_reason: no_plan`). Exit 0 with
`by: number`: go on. Exit 0 with `by: description`: render `found` and wait for the person's typed
answer. A yes runs `find_plan.py confirm --out <start>/plan.json`; any other plan number starts
Step 2 again with it; anything else stops. Once the plan is known, move the start folder into the
plan's session folder (C13); an earlier session's list there is kept:

```
python3 scripts/session_files.py adopt --start <start> --work <working> --out <working>/adopt.json
```
**SE-2 (F1/C1):** `plan.json` reads `confirmed: true` before any later record exists, and its
`plan` is a plan file in `<plans>`; no plan file was made.

**Step 3 — Get onto the plan's branch** · Owner: play (script) · Depends on: Step 2

```
python3 .garura/core/tools/plan/plan_branch.py go --plan <id> --plans-dir <plans> \
    --ignore "${stm_base}_plans/" --out <working>/branch.json
```

Exit 3: render `stopped`, show it, and go to Step 14 — nothing was written; the person cleans up and
runs the play again. Exit 2: show its `error` and stop. Exit 0: render `on_branch` and show it.
**SE-3 (F2/C2):** `branch.json` reads `stay`, `switch` or `cut` and the branch checked out is
`plan/<id>/base`, with nothing uncommitted outside the session's own folder; or it reads `stop`
with each uncommitted file named (on any branch, the plan's own included), and the branch checked
out and its commit are unchanged. A `cut` names its `main_commit`, and main's plan file reads
`status: In Progress` and `branch: plan/<id>/base` at that commit.

**Step 4 — Read the plan's issues; the list** · Owner: `project-orchestrator` · Depends on: Step 3
Collect the plan's issue numbers with the script (the plan's own number first, then its tree):

```
python3 scripts/session_files.py numbers --plan-file <plans>/<id>.yaml --out <working>/numbers.json
```

`<numbers>` is its `csv` (the numbers, comma separated), then:

    { "task":    "read the plan's issues (track-plan-changes, mode read)",
      "inputs":  { "issues": "<numbers>" },
      "outputs": { "snapshot": "<working>/snapshot.json" },
      "task_id": "read-issues" }

Then the list. When `<working>/changes.yaml` has entries from an earlier session, render `resume`
and wait: "carry on" keeps it; "start fresh" moves it to `changes-<ts>.yaml`. Then, when there is
no list:

```
python3 scripts/add_entries.py start --changes <working>/changes.yaml --plan <id> \
    --title "<the plan's title>" --plan-branch plan/<id>/base --at <UTC time>
```

Then the items closed on GitHub (C8, #1007): an item's status follows its own issue. The
script lists each item whose issue is closed on GitHub but open in the plan file, as status
entries (closed as completed: Done; as not planned: Dropped), and never a parent from its
children:

```
python3 scripts/closed_on_github.py --plan-file <plans>/<id>.yaml --snapshot <working>/snapshot.json \
    --out <working>/draft-closed.json
```

When it names any, they go through Step 5's check and take, as the instruction "closed on
GitHub": `check_entries.py --draft <working>/draft-closed.json ...`, then `add_entries.py add`,
then render `taken` and show it. They land only with the person's "approved", like any entry.
**SE-19 (F14/C8):** `draft-closed.json` exists before the first instruction is taken; every item
whose own issue reads CLOSED in `snapshot.json` and whose status is open in the plan file is in
it, with Done for COMPLETED and Dropped for NOT_PLANNED, and no other item is; each is in
`changes.yaml` under the instruction "closed on GitHub" (or carries `already`).

### Phase: Session

**Step 5 — Take instructions** · Owner: play + `project-orchestrator` (+ `product-os-keeper` for a
new feature) · Depends on: Step 4 · repeats
The person types. A request about the list itself is handled here, never turned into a plan
change: "show the list" renders `list`; "take out 3" runs `add_entries.py remove --n 3`. Any other
words are an instruction: write them to `<working>/instruction.txt` and dispatch:

    { "task":    "turn this instruction into plan list entries (author-plan-changes)",
      "inputs":  { "instruction": "<working>/instruction.txt", "plan_file": "<plans>/<id>.yaml",
                   "snapshot": "<working>/snapshot.json", "changes": "<working>/changes.yaml" },
      "outputs": { "draft": "<working>/draft-entries.json" },
      "task_id": "draft-entries" }

**The product model (C14, #1002).** `<model>` is `<product_base>product-os`. For each ask in the
draft's `model_lookups` ("find #N in the model"), look it up and collect the answer, then render
`model` and show it; a lookup makes no list entry:

```
python3 scripts/feature_ice.py find --root <model> --spine <model>/_spine.yaml --plans-dir <plans> \
    (--story <n> | --ref "<their words>") --out <working>/found.json --collect <working>/model-found.json
```

For each `add` entry at index `<i>` that carries a `feature_ref`, find the feature and name it on
the entry; words that fit no feature, or more than one, become a question in its four parts
(nothing is guessed):

```
python3 scripts/feature_ice.py find --root <model> --spine <model>/_spine.yaml --ref "<feature_ref>" \
    --out <working>/found-<i>.json
python3 scripts/feature_ice.py mark --draft <working>/draft-entries.json --at <i> --found <working>/found-<i>.json
```

(Render the lookup answer with `render_screen.py --screen model --out <working>/screens/model-<n>.txt`.)

When it was found, make the clean copy of its two ICE (no "Fails if" line); a feature whose ICE is
not ready keeps the person's words and the entry says why:

```
python3 scripts/feature_ice.py copy --root <model> --spine <model>/_spine.yaml --intents-dir <model>/intents \
    --feature <feature> --working <working> --out <working>/feature-ice-<i>.json
python3 scripts/feature_ice.py mark --draft <working>/draft-entries.json --at <i> --record <working>/feature-ice-<i>.json
```

When it is ready, the story writer drafts the story from the clean copy, through the model
sub-agent:

    { "task":    "draft the story for this feature (author-work-items, story mode); write nothing in the model",
      "inputs":  { "skill_input": "<working>/story-input.json", "clean_ice": "<working>/ice/",
                   "problems": "<the body check's problems, on a fix>" },
      "outputs": { "drafts": "<working>/story-draft-<i>.json" },
      "task_id": "draft-story" }

Then check it and write its body in /shape-plan's layout; exit 1 sends the problems back to the
contract above (fix mode) and checks again:

```
python3 scripts/feature_ice.py body --draft <working>/story-draft-<i>.json --record <working>/feature-ice-<i>.json \
    --plan <id> --root <model> --out <working>/story-body-<i>.md
python3 scripts/feature_ice.py mark --draft <working>/draft-entries.json --at <i> \
    --body <working>/story-body-<i>.md --story-draft <working>/story-draft-<i>.json
```

The entry now reads `feature`, and `from_ice: true` (or `no_ice` with its reason); the list shows
which under its row, and its item records `represents: <feature>` when it lands.
**SE-20 (F16/C14):** for each `add` entry with `feature`, `found-<i>.json` reads `found: true` and
names it; each with `from_ice` has `feature_ice.py body` exit 0 on its draft and its `body` is that
file; each `feature_ref` that `found-<i>.json` did not settle became a question and the entry has no
`feature`; `changes.yaml` holds no entry for a `model_lookups` ask, and each ask is in
`model-found.json` with a saved `screens/model-<n>.txt`.

When the draft holds a `new-feature` entry, place it by fixed rules before the check — the
capability the person named, else the one that shares an issue number with the item's parent (or
an item above it); the slice the plan holds when exactly one serves `bi-<id>`, none when none
does; a feature id; the person's title as its one-line. What no rule settles becomes a question
for the person, in the four parts of the asking rule, in the draft's `placement_questions` (the
script replaces its own questions on every run). It also marks a slice in progress (its epic past
ready), with each such epic and its status. No ICE is drafted in the session: the page skill
writes the thin ICE at the landing (Step 8), from the person's words.

```
python3 scripts/place_feature.py --draft <working>/draft-entries.json --plan-file <plans>/<id>.yaml \
    --spine <product_base>product-os/_spine.yaml --plan <id>
```

When it leaves a question about the capability, dispatch `product-os-keeper` for a suggestion (the
`find-work-placement` skill; it writes only the routing result), run the script again with
`--suggestion <working>/routing-result.json`; the question's answer then carries the suggestion:

    { "task":    "suggest a capability for this new feature (find-work-placement); write nothing in the model",
      "inputs":  { "draft": "<working>/draft-entries.json", "plan_id": "<id>", "product_base": "<product_base>" },
      "outputs": { "route": "<working>/routing-result.json" },
      "task_id": "suggest-capability" }

Then check it:

```
python3 scripts/check_entries.py --draft <working>/draft-entries.json --plan-file <plans>/<id>.yaml \
    --snapshot <working>/snapshot.json --changes <working>/changes.yaml --view-template <view> \
    --out <working>/checked.json
```

- `unknown_issues` not empty: dispatch the read again (Step 4's contract) for those numbers into
  `snapshot-more.json`, merge it into `snapshot.json`, and check again.
- `questions` not empty (the skill's and the placement script's): render `question --out
  <working>/screens/question-<n>.txt` (`<n>` counts up through the session) and show it —
  each in its four parts, from the template — and take the person's answer as a new instruction.
  No entry is taken from a part with a question.
- Take every entry that may join: `add_entries.py add --changes <working>/changes.yaml --checked
  <working>/checked.json --out <working>/taken.json`, then render `taken` and show it.
- For each entry left as big: render `confirm --at <i> --out <working>/screens/confirm-<n>.txt` and
  wait; a yes runs `add_entries.py add
  ... --pick <i> --confirmed <i>`. For a new feature the screen always carries the thin warning,
  and, when its slice is in progress, says so and names each epic past ready, with its status,
  as out of date (ADR 058, ADR 062).
- For each entry left with a problem: render `break --at <i>` and wait; "do it anyway" runs
  `add_entries.py add ... --pick <i> --override <i>`; other words are a new instruction.
**SE-4 (F6/C3, C4):** every entry of `changes.yaml` with a problem in `checked.json` carries
`override`, and every big one was confirmed (`add_entries.py` refuses either otherwise); nothing
is written outside the working folder during the session.
**SE-5 (F7/C8):** every entry of `changes.yaml` came from a draft of the person's instruction
(`asked` holds that instruction) or from `draft-closed.json` (`asked` holds "closed on
GitHub"); no status or direction entry exists that no draft named.
**SE-17 (F10/C11, F9/C10):** for each `new-feature` entry of `changes.yaml`, a saved
`screens/confirm-<n>.txt` names it and holds the template's `thin_warning` text, and, when its
`slice_in_progress` names an epic, the template's `slice_in_progress` line with each epic and its
status; each question shown has a saved `screens/question-<n>.txt` with its four parts.

**Step 6 — Ask: approved or rejected** · Owner: play (script) · Depends on: Step 5
When the person says they are done (or asks to see it), render `list` and show it. Record their
typed reply:

```
python3 scripts/record_answer.py record --reply "<the person's words>" --changes <working>/changes.yaml \
    --at <UTC time> --out <working>/answer.json
```

`approved` → Step 7. `rejected` → render `rejected` (nothing lands; it asks what to change) and go
back to Step 5. `abort` → put back as they were the files the session's sub-agents wrote in
the plan's own short-term folder, with the script, which records what it put back:

```
python3 scripts/session_files.py restore --folder "${stm_base}<id>/" --out <working>/restore.json
```

then render `aborted`, show it, and go to Step 13, which undoes the branch step; the list is
dropped at the close. `other` → answer the question or follow the instruction (back to Step 5), show the
list again and ask again.
**SE-6 (F3/C5):** `answer.json` is written by the script, and only the plain word "approved" reads
`approved`; on "abort", `restore.json` reads `clean: true`.

### Phase: Landing

**Step 7 — Gate and plan the writes** · Owner: play (script) · Depends on: Step 6

```
python3 scripts/record_answer.py verify --answer <working>/answer.json --changes <working>/changes.yaml \
    --out <working>/verify.json
cp <plans>/<id>.yaml <working>/plan-before.yaml
python3 scripts/check_agreement.py hash-evals --plans-dir <plans> --out <working>/evals-before.json
python3 scripts/plan_ops.py --changes <working>/changes.yaml --plan-file <plans>/<id>.yaml \
    --snapshot <working>/snapshot.json --out-github <working>/github-ops.json --out-model <working>/model-ops.json
```

A non-zero `verify` (its record reads `ok: false` and why) writes nothing more: show the list again
and ask again (REC4).
**SE-7 (F3, F4/C5, C6):** `verify.json`, written by the script, reads `ok: true` and exists before
`github-record.json`, `model-record.json` and `plan-write.json`, and its `list_sha` equals the
list's sha256 and `answer.json`'s `list_sha`.

**Step 8 — Land: the model** · Owner: `product-os-keeper` · Depends on: Step 7
*Skip when `model-ops.json` has no parts.* For each part: the page skill (`enrich-grounding`,
new-feature and thin modes) writes the feature's page under its capability, and its thin ICE from
the person's words (the part's title and description), nothing made up; then `link-model-part` adds its model entry (`mode: new`, thin, its intent `bi-<id>`,
its `issue` — the story, or the number the tracker step makes, filled in after Step 9 with
`mode: update` — and `identified_by`) and, when the entry names a slice, the slice's `add_features`.

    { "task":    "write each new feature of model-ops.json: page, model entry with both thin marks, its place in the slice",
      "inputs":  { "model_ops": "<working>/model-ops.json", "product_base": "<product_base>" },
      "outputs": { "record": "<working>/model-record.json" },
      "task_id": "land-model" }

`model-record.json`: `{parts: [{feature, doc, linked}], files: [every model file written, each path relative to the repository root]}`.
**SE-8 (F8, F10/C9, C11):** the model files were written by `enrich-grounding` and
`link-model-part` (their reports are in the record), and no path under any plan's `evals/` is in
`files`.

**Step 9 — Land: GitHub** · Owner: `project-orchestrator` · Depends on: Step 8

    { "task":    "make the approved tracker writes (track-plan-changes, mode apply)",
      "inputs":  { "ops": "<working>/github-ops.json", "changes": "<working>/changes.yaml",
                   "approval": "<working>/answer.json", "read": "<numbers>" },
      "outputs": { "record": "<working>/github-record.json",
                   "before": "<working>/snapshot-before.json", "after": "<working>/snapshot-after.json" },
      "task_id": "land-github" }

Exit 1: dispatch it again once (it goes on from its record); a second failure stops the landing and
names the failed op for the person (REC5). Exit 2: nothing was written; back to Step 6.
When a new feature's story was made in this step (an op `<n>-create` of a `new-feature` entry),
dispatch `product-os-keeper` once more to name it on the feature's model entry:

    { "task":    "name each new feature's story on its model entry (link-model-part, mode update, issue)",
      "inputs":  { "model_ops": "<working>/model-ops.json", "github_record": "<working>/github-record.json",
                   "product_base": "<product_base>" },
      "outputs": { "record": "<working>/model-record-story.json" },
      "task_id": "name-story" }

This dispatch writes its own record and never changes `model-record.json`; the guard, the
agreement check and the commit read the two as one (`plan_session.model_records`), so the story
step cannot drop a file Step 8 wrote (HF-12). The story record has the same shape as
`model-record.json`: `{parts: [{feature, issue}], files: [the model files it changed, each relative
to the repository root]}`. `<records>` below is empty when Step 8 was skipped (`model-ops.json`
has no parts); otherwise `--model-record <working>/model-record.json`, and also `--model-record
<working>/model-record-story.json` when the story step ran.
**SE-9 (F8/C9):** `github-record.json` reads `ok: true`, and every op of `github-ops.json` is in its
`done`.

**Step 10 — Land: the plan file** · Owner: play (script) · Depends on: Step 9

```
python3 scripts/write_plan_changes.py --plan-file <plans>/<id>.yaml --changes <working>/changes.yaml \
    --github-record <working>/github-record.json --at <UTC time> --out <working>/plan-write.json
```

**SE-10 (F4/C6, C7):** `plan-write.json` reads `ok: true`; the file read back as the approved list
(the script compares before it writes), and it holds no `history`.

**Step 11 — The guard, then the agreement** · Owner: play (script) · Depends on: Step 10
First the shared scoped-write guard, once, after the last model write (Step 9's) and before the
commit (ADR 062): any changed model path that is not a file of the model record, or the model
index, is put back.

```
python3 scripts/session_files.py guard --model-ops <working>/model-ops.json <records> \
    --product-base <product_base> --out <working>/guard-report.json
```

Exit 1: the guard put a path back; show each path, commit nothing, and stop for the person
(REC13). Exit 2: a model step was planned but its record is not there; nothing is put back and
nothing is committed — stop for the person.

```
python3 scripts/check_agreement.py hash-evals --plans-dir <plans> --out <working>/evals-after.json
python3 scripts/check_agreement.py --changes <working>/changes.yaml --plan-before <working>/plan-before.yaml \
    --plan-file <plans>/<id>.yaml --snapshot-before <working>/snapshot-before.json \
    --snapshot-after <working>/snapshot-after.json --github-record <working>/github-record.json \
    <records> --spine <product_base>product-os/_spine.yaml \
    --evals-before <working>/evals-before.json --evals-after <working>/evals-after.json \
    --out <working>/agreement.json
```

Exit 1: show each problem; nothing is committed (REC5).
**SE-11 (F5/C7):** `agreement.json` reads `ok: true`.
**SE-18 (F13/C7):** `guard-report.json` exists before `commit.json` and lists no violation; every
model file of `commit.json` is in the guard report's `allow`, which holds every file of both model
records; `model-record.json` still names each page Step 8 wrote after the story step ran.
**SE-13 (F10/C11):** `agreement.json` reads `features_ok: true` and `evals_unchanged: true`.

**Step 12 — One commit on the plan's branch** · Owner: play (script) · Depends on: Step 11

```
python3 scripts/commit_plan.py --plan <id> --plan-branch plan/<id>/base --plan-file <plans>/<id>.yaml \
    --changes <working>/changes.yaml <records> \
    --records-dir "${stm_base}<id>/" --out <working>/commit.json
```

The commit message names each instruction as the person typed it (`Asked:`), each override with
its rule (`Override:`) and what a new feature still owes (`Owed:`). The plan file keeps no history;
git does.

Then render `landed` and show it.
**SE-14 (F2/C7):** `commit.json` reads `ok: true` and `pushed: true`, its files are only the plan
file, the model record's files and the decision records in the plan's own short-term folder, and it
was made on `plan/<id>/base`.
**SE-12 (F6/C3):** `commit.json` reads `overrides_recorded: true`: every override of the list is an
`Override:` line of the commit message.

### Phase: Back

**Step 13 — Back to the starting branch** · Owner: play (script) · Depends on: Step 12 (or Step 6
on "abort")

```
python3 .garura/core/tools/plan/plan_branch.py back --record <working>/branch.json --ignore "${stm_base}_plans/"
```

On "abort", run `abort` in its place: it undoes a branch the session cut (the branch deleted, main's
plan file put back by a revert commit) when nothing else has landed on it — else it stops and names
what is there, for the person — and then goes back:

```
python3 .garura/core/tools/plan/plan_branch.py abort --record <working>/branch.json --ignore "${stm_base}_plans/"
```

**SE-15 (F12):** the branch checked out is `branch.json`'s `back_to` (or the plan's branch, when the
session started there).

### Phase: Scenario Validation

**Step 14 — Scenario evals** · Owner: play · Depends on: Step 13

```
python3 scripts/close_session.py --work <working> --out <working>/session.json
```

- **SCE-1 (S1 — product owner, several instructions, one approval):** `session.json` reads
  `outcome: landed` and `ok: true`, and `agreement.json` reads `ok: true`.
- **SCE-2 (S2 — developer, uncommitted work):** `branch.json` reads `action: stop` with the file
  named, and `session.json` reads `outcome: stopped` and `written: false`.
- **SCE-3 (S3 — lead, a plan with no branch):** `branch.json` reads `action: cut` with a
  `main_commit`.
- **SCE-4 (S4 — product owner, rejected, then abort):** `answer.json` reads `decision: abort` with
  `rejected` in its history, and `session.json` reads `outcome: aborted`, `written: false` and
  `back: true`.
- **SCE-5 (S5 — product owner, an override):** `commit.json` reads `overrides_recorded: true`.
- **SCE-6 (S6 — product owner, a new feature in a plan with no slice):** `agreement.json` reads
  `features_ok: true` and `evals_unchanged: true`.
- **SCE-7 (S7 — anyone, a plan found by description):** `plan.json` reads `by: description` and
  `confirmed: true` before any other record.
- **SCE-8 (S8 — product owner, an issue closed on GitHub):** `draft-closed.json` names the item
  with `to: Done` and no other item (its parent is not in it), `changes.yaml` holds it under
  the instruction "closed on GitHub", and after "approved" `plan-write.json` and
  `agreement.json` read `ok: true` with the item Done in the plan file.
- **SCE-9 (S9 — product owner, nothing left behind):** evaluated at Step 16, on the record
  `clear` prints (`$cleared`), since the folder is gone after it: it lists the session's folder
  and its start folder as removed and `left` names no file of this session (or `kept` names a
  landing that stopped part way). Its result goes into the evidence and the report (C1, C2).
- **SCE-10 (S10 — product owner, a story for a feature in the model):** the entry reads
  `feature: X` and `from_ice: true`, `feature_ice.py body` exited 0 on its draft, and after
  "approved" the plan file's new item reads `represents: X`.
- **SCE-11 (S11 — product owner, find a story in the model):** `model-found.json` holds the
  feature and its pages, `screens/model-<n>.txt` is saved, and `changes.yaml` gained no entry.

### Phase: Evidence & Close

**Step 15 — Close** · Owner: play · Depends on: Step 14
Run the Standard Play Close. `/update-plan` has no issue of its own: its run's short-term folder is
`<working>`, and the close finds the templates Step 1 loaded.
**SE-16 (F11/C12):** the stop condition (D1 the person answered; D2 the session closed as it
should; D3 it ended on the starting branch) reads **held** before the run closes COMPLETED; it is
evaluated before the self-clean below. The close runs in this order: the session stamp and the stop
condition read the session's folder; then Step 16 removes it; then C1 and C2 write the evidence and the
report, both outside it.

**Step 16 — Self-clean (C13)** · Owner: play (script) · Depends on: Step 15's stop condition · Runs before
C1 and C2. Remove the session's folder and its start folder, so nothing of the session is left.
Pass `--keep` only for a landing that stopped part way (`answer.json` reads `approved` and
`commit.json` does not read `ok: true`): its records let the next session carry on. A session
that found no plan has no `<working>`: pass `--start` only.

```bash
cleared=$(python3 scripts/session_files.py clear --start <start> [--work <working>] [--keep])
```

`clear` writes no file (it would be left behind); `$cleared` holds its record for SCE-9, C1 and C2.

**SE-21 (F15/C13):** the `clear` record lists `<start>` and (unless `--keep`) `<working>` as removed,
and its `left` names no file of this session; with `--keep`, `kept` names `<working>` and
`answer.json` there reads `approved` with no `commit.json` reading `ok: true`.

```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 (no issue — the session's own folder):
#     evidence_base="${stm_base}_plans/evidence/update-plan/" ; slug="plan ${id}"
# Session identity stamp (#463) — close phase; start phase ran at pre-flight
session_stamp=$(python3 scripts/session_stamp.py --phase close \
    --marker "<session>/session-stamp-update-plan.json")
# Stop-condition gate (#464) — Step C0: evaluate the Done means BEFORE the self-clean.
python3 scripts/check_stop_condition.py \
    --manifest "<play-dir>/stop-condition.yaml" \
    --base "<session>/" \
    --out "<session>/stop-condition-update-plan.yaml"
sc_exit=$?   # 0 held · 1 unmet · 2 error
# Then Step 16 (self-clean, C13) removes the session's folder; then Steps C1-C2.
```

**Step C0 — bind the verdict.** `sc_exit == 0` (held) permits `status: COMPLETED`. Anything else
closes `HALTED` with `exit_reason: stop_condition_unmet` (or `stopped_uncommitted`, `no_plan`) and
the unmet clauses named. An unevaluable verdict is never a pass.

**Step C1 — Write evidence file.** Gated by the resolved `evidence.record` flag (global + per-play
`evidence.plays.update-plan`; first match wins, absent ⇒ record). When false, skip and record
`evidence skipped (record=false)` in the report. Otherwise fill the `evidence-file.md` slots (play
`update-plan`, run_id `update-plan-${ts}`, status per C0; artifacts: plan.json, branch.json,
changes.yaml, answer.json, github-record.json, model-record.json, plan-write.json, agreement.json,
commit.json, session.json, the stop-condition verdict, the clear record ($cleared); step and scenario eval results SE-1…SE-21 /
SCE-1…SCE-11; checkpoint decisions: the person's answers; the session identity stamp fields from
$session_stamp) and write to `$evidence_dest`. Do NOT hand-author the body.

**Step C2 — Render delivery report.** Resolve this play in the Next-line map and emit the **Next**
line only when it names a command. Fill the `delivery-report.md` slots and output:
`## update-plan Delivered — plan ${id}`, the Run Summary table (outcome, entries, overrides, owed),
the Pipeline Steps table from the task DAG, the Artifacts Produced table (the commit, the issues
written, the model files), and a pointer to `$evidence_dest`. Always emitted; never gated.

```bash
# --- end Standard Play Close ---
```

## Scenario Validation

| Scenario | Persona | Eval |
|----------|---------|------|
| S1 — several instructions, one approval | product owner | SCE-1 |
| S2 — uncommitted work | developer | SCE-2 |
| S3 — a plan with no branch | lead | SCE-3 |
| S4 — rejected, then abort | product owner | SCE-4 |
| S5 — an override | product owner | SCE-5 |
| S6 — a new feature in a plan with no slice | product owner | SCE-6 |
| S7 — a plan found by description | anyone | SCE-7 |
| S8 — an issue closed on GitHub | product owner | SCE-8 |
| S9 — nothing left behind | product owner | SCE-9 |
| S10 — a story for a feature in the model | product owner | SCE-10 |
| S11 — find a story in the model | product owner | SCE-11 |

## Recovery

| For | Trigger | Direction | Handoff |
|-----|---------|-----------|---------|
| F1 | a plan was picked without a number or a confirmed match, or a new plan was made | stop, show the candidates, and ask for the number | human |
| F2 | a switch away from uncommitted work, or a write on another branch | stop, name the branch and the files, and leave them for the person | human |
| F3 | a record of a write exists before the approval | stop, name the write, and leave it for the person to keep or undo | human |
| F4 | the list's fingerprint differs from the one the approval recorded | write nothing more, show the list again and ask again | autonomous |
| F5 | the agreement check names a difference | name each difference; re-run the landing for the ops the record does not have; a difference that remains goes to the person | human |
| F6 | a breaking entry is in the list without its override mark, or an override is missing from the commit message | take the entry out of the list, show the break, and ask again | autonomous |
| F7 | an entry changes a status or the direction that no instruction named, and its item's own issue is not closed on GitHub | take the entry out and show the list again | autonomous |
| F8 | a tracker or model write was made outside the sub-agents' skills | stop and name it for the person | human |
| F9 | a screen line does not match the template | render it again from the template | autonomous |
| F10 | a new feature's page, entry, thin marks or story is missing, the evals folder changed, or what is owed is not named | re-run the model or tracker step for that feature; put back the evals folder; name what is owed in the commit message | autonomous |
| F11 | the close would report COMPLETED without the Done means held | surface the unmet clauses and close HALTED | autonomous |
| F12 | the session ended on another branch | switch back to the starting branch; if that is stopped by uncommitted work, name it for the person | autonomous |
| F13 | the guard report names a path outside the session's parts | the guard has put it back; commit nothing, name each path for the person, who decides | human |
| F14 | an item closed on GitHub is open in the plan file and not on the list, or a status was set from the items under it | run `closed_on_github.py` again and take its entries into the list; take out any entry no item's own issue or instruction named | autonomous |
| F15 | a session's file is left after the close | run `session_files.py clear` for the session's folder and its start folder; keep only a landing that stopped part way | autonomous |
| F16 | a story for a feature with a ready ICE is not drafted from it, holds a "Fails if" line, a feature is guessed, or a lookup made an entry | run `feature_ice.py` find, copy and body again and send its problems to the story writer; turn a guessed feature into a question; take a lookup's entry out of the list | autonomous |

## Pause and Resume

A session's list is kept in `<working>/changes.yaml` until it lands. A new `/update-plan` on the
same plan finds it and asks: carry on, or start fresh (Step 4). A landing that stopped part way
goes on from its records: the tracker step skips every op its record holds, and the plan file step
writes only when the tracker record reads `ok: true`. Nothing is resumed past a failed agreement
check: the person decides.

## Compilation Metadata

| Field | Value |
|-------|-------|
| fingerprint | sha256:93a2357c773177200f97775974c13f36df0a39cff8866babbcc2a8321c58858e (of `reference/ice.md`) |
| compiled_by | create-play (#730, with #931; feature #720); edit-play (PR #998 review, ADR 062; #1007 closed on GitHub; #1001 one session folder; #1002 the product model) |
| pipeline_position | none |
| position_exception | model-writing session play — writes the model, GitHub and the plan file on the plan's own branch after one "approved" and commits them as one commit there (C7); the plan's branch reaches main later through the change plays, with the plan's other work (#720 design) |
| workflow_structure | A (full checkpoint: one approval gates every write) with a repeating session step |
| stop_condition | stop-condition.yaml (D1–D3), gate live at Step C0 — evaluated before the self-clean |
| domain_agents | 1 (product-os-keeper — a new feature: a capability suggestion, and the landing; a story for a feature in the model: its draft, author-work-items story mode) |
| utility_agents | 2 (template-manager, project-orchestrator) |
| skills_reused | author-plan-changes, track-plan-changes, find-work-placement, enrich-grounding, link-model-part, author-work-items |
| tools | the plan tool's plan-branch script (`.garura/core/tools/plan/plan_branch.py`) |
| scripts | 22 (feature_ice.py, plan_common.py, check_ice_workable.py, model_names.py, closed_on_github.py, preflight.py, session_stamp.py, check_stop_condition.py, plan_session.py, find_plan.py, check_entries.py, add_entries.py, record_answer.py, render_screen.py, plan_ops.py, write_plan_changes.py, check_agreement.py, commit_plan.py, close_session.py, place_feature.py, session_files.py, scoped_write_guard.py — stamped from create-play) |
| step_evals | 21 (SE-1…SE-21) |
| scenario_evals | 11 (SCE-1…SCE-11) |
| recovery_entries | 16 (one per failure condition; 10 autonomous / 6 human) |

Files in this skill

  • SKILL.md42.1 KB
  • reference/ice.md18.4 KB
  • scripts/add_entries.py5 KB
  • scripts/check_agreement.py9.4 KB
  • scripts/check_entries.py13.8 KB
  • scripts/check_ice_workable.py14.7 KB
  • scripts/check_stop_condition.py5 KB
  • scripts/close_session.py3.1 KB
  • scripts/closed_on_github.py2.3 KB
  • scripts/commit_plan.py5.7 KB
  • scripts/feature_ice.py14.2 KB
  • scripts/find_plan.py4.6 KB
  • scripts/model_names.py4.1 KB
  • scripts/place_feature.py7.5 KB
  • scripts/plan_common.py21.3 KB
  • scripts/plan_ops.py6 KB
  • scripts/plan_session.py6.2 KB
  • scripts/preflight.py7 KB
  • scripts/record_answer.py3.8 KB
  • scripts/render_screen.py18.8 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.