feat(plan-rollout): /plan-rollout MVP skill

Decomposition-as-artifact. Reads the working diff (committed + staged +
unstaged + untracked) plus SYSTEM.md if present, writes decomposition.md
with per-slice file lists, reader-time estimates, dependency edges, and
contract-graph reconciliation flags.

Positioned as the post-decision consumer to /plan-pull-request:
- /plan-pull-request decides shape in conversation (pre-code).
- /plan-rollout analyzes a real diff and writes the artifact (post-code).

Triggers narrowed to "decompose the diff" / "write a decomposition" /
"plan-rollout" to avoid collision with /plan-pull-request's pre-decision
triggers.

MVP boundaries (explicit):
- No rollout.md, no /spill-check, no /ship-/review integrations.
- No SYSTEM.md scaffolder — humans write the schema by hand or copy the
  example.
- Reconciliation is informational, never blocking.
- Step 2 explicitly handles uncommitted working-tree state via
  `git diff <base>` (not `<base>...HEAD`) plus
  `git ls-files --others --exclude-standard` for untracked.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Manas Srivastava 2026-05-11 09:58:57 +05:30
parent cdd2eba564
commit 31c42a5456
2 changed files with 1337 additions and 0 deletions

1027
plan-rollout/SKILL.md Normal file

File diff suppressed because it is too large Load Diff

310
plan-rollout/SKILL.md.tmpl Normal file
View File

@ -0,0 +1,310 @@
---
name: plan-rollout
preamble-tier: 3
interactive: true
version: 0.1.0
description: |
Decomposition-as-artifact. Given a real working diff (and `SYSTEM.md` if
present), produces a written `decomposition.md` with per-slice file lists,
reader-time estimates, dependency edges, and contract-graph reconciliation
flags. This is the post-decision consumer: it analyzes a diff that already
exists. For the pre-decision conversation ("should this be one PR or
many?"), use `/plan-pull-request` first. Use when asked to "decompose
the diff", "write a decomposition.md", "plan-rollout", or after
`/plan-pull-request` has already concluded the work should slice. (gstack)
voice-triggers:
- "decompose the diff"
- "write a decomposition"
- "plan-rollout"
allowed-tools:
- Read
- Write
- Grep
- Glob
- AskUserQuestion
- Bash
triggers:
- decompose the diff
- write decomposition.md
- plan rollout
---
{{PREAMBLE}}
# /plan-rollout: Decomposition-as-Artifact
You read a working diff (plus `SYSTEM.md` if present) and write
`decomposition.md` — a per-slice breakdown a tired reviewer can pick up.
You never write code, never split branches, never run `/ship`. The output
is the artifact and only the artifact.
## Relationship to /plan-pull-request
These are two halves of one workflow, not competitors. Run them in order:
| Skill | When | Mode | Output |
|-------|------|------|--------|
| `/plan-pull-request` | Pre-code or early-code | Conversation | Decision: boil vs slice + a sketch |
| `/plan-rollout` | After a real diff exists | Analysis | `decomposition.md` artifact tied to actual files |
If the user invokes `/plan-rollout` before any code has been written, stop
and tell them: "`/plan-pull-request` is the right skill for pre-code shape
decisions. Run that first; come back here when you have a real diff to
decompose."
If the user invokes `/plan-pull-request`'s decision already concluded "boil
it — one PR," and they invoke `/plan-rollout` anyway, do the analysis
honestly — if the diff really is one PR's worth, the output's verdict
says so in one line and you stop. Don't manufacture slices.
## When to invoke this skill
Run when a diff already exists (committed or working tree) and either:
- The user explicitly asks for `decomposition.md`.
- `/plan-pull-request` concluded "slice it" and the user wants an analysis
artifact tied to the actual files.
- A reviewer asked to see the change broken down before they read it.
Don't run on single-component, sub-30-min-reader-time diffs unless the user
overrides — produce the one-line "this is one PR" verdict and stop.
Out of scope for v1: writing `rollout.md` (rollout/rollback strategy),
running `/spill-check` against in-progress diffs, integrating with `/ship`
or `/review`, scaffolding `SYSTEM.md`. This skill produces decomposition.md
and stops.
## Step 0 — Detect repo state
Detect, in this order:
1. **Repo root.** `git rev-parse --show-toplevel`. If not a git repo, ask
the user for the project root path via AskUserQuestion and stop if they
can't provide one.
2. **Base branch.** Try in order: `gh pr view --json baseRefName -q .baseRefName`,
then `git rev-parse --verify origin/main`, then `origin/master`. If none
resolve, ask via AskUserQuestion.
3. **Head ref.** Current branch via `git rev-parse --abbrev-ref HEAD`. If
detached or on the base branch itself, you have no diff to decompose —
ask the user whether they want to plan from a description-only input
instead.
4. **Plan source.** Three possibilities:
- User passed a plan-file path as an argument → read it.
- A `~/.gstack/projects/<slug>/...-design-*.md` exists for this branch
(mirror the lookup pattern in `plan-eng-review`) → read it.
- Neither → AskUserQuestion: paste the plan text, point to a file, or
proceed with diff-only.
State each detected value back to the user in one line each. No prose
narration — just facts.
## Step 1 — Read SYSTEM.md if present
```bash
test -f SYSTEM.md && cat SYSTEM.md || echo "(no SYSTEM.md — using path heuristics)"
```
If SYSTEM.md exists:
- Parse the YAML frontmatter. Components without a `path` field are skipped
with a one-line warning.
- Build a path → component map. Longest path wins (so `src/auth/session/` resolves
to the more-specific component if both `src/auth` and `src/auth/session` are declared).
- Build the contract graph. `rollout-edge: hard` edges drive coordinated-deploy
warnings later.
If SYSTEM.md does not exist:
- Fall back to "top-level dir of change = component."
- One slice per top-level directory touched by the diff. Never invent
a SYSTEM.md from heuristics — leave that to a future scaffolder.
## Step 2 — Enumerate the diff (committed + working tree + untracked)
The "diff to decompose" usually includes a mix of committed work and
uncommitted work. Capture all of it. Use these commands (substitute the
base branch detected in Step 0):
```bash
# Tracked changes (committed + staged + unstaged) vs base.
# Note: NO triple-dot. `git diff <base>` compares working tree to <base>
# and includes everything that's not yet pushed.
git diff --name-status "<base>"
git diff --numstat "<base>"
# Untracked files (new files not yet `git add`-ed).
git ls-files --others --exclude-standard
```
Union the two lists; treat each untracked file as fully added (count its
line count via `wc -l`). Deduplicate by path. If you also want the
committed-only delta for context, run `git diff --name-status <base>...HEAD`
separately — but the working-tree view is what `decomposition.md` should
describe, because that's what the eventual PR will contain.
If the union is empty, exit early: print "No changes detected between
<base> and the current working state. Nothing to decompose." and stop.
For very large diffs (>200 files), warn the user and ask whether to
proceed or abort — decomposing thousand-file diffs is not what this skill
is for.
Bucket each file:
- If SYSTEM.md exists: file → component via the path map (Step 1). Files
outside any declared component path go to a `(unmapped)` bucket and
surface in the output as a flag.
- If no SYSTEM.md: file → top-level directory.
## Step 3 — Discover import edges (light-touch)
For each file in the diff, grep for imports/requires:
```bash
# TypeScript / JavaScript
grep -E "^import .* from |^const .* = require\(" <file>
# Python
grep -E "^(from |import )" <file>
# Go
grep -E "^import " <file>
```
Resolve each import to a component (or top-level dir) using the same
path map. Record the directed edge `<file's component> → <imported component>`.
These edges are how you order slices: a slice that imports another slice
must ship after it. For the MVP, treat unresolvable imports (external
packages, ambiguous paths) as no-ops — log them, don't fail.
## Step 4 — Propose the slice stack
Generate the stack with these rules, in priority order:
1. **Honor `rollout-edge: hard`** (if SYSTEM.md): if a contract with
`rollout-edge: hard` connects two components and BOTH have changed
files, those files merge into a single slice with the annotation
"coordinated deploy required — <breaks-if reason>."
2. **Topological by import edges:** a slice must come after every slice
it imports from. Cycles get flagged and merged.
3. **`rollout-order` from SYSTEM.md** breaks ties: lower numbers ship first.
4. **`leaf-util` / `types-only` components** float to slice 0 (foundational,
ship first, no contract dependents).
5. **Fall-through tie-break:** alphabetical by component name. Predictable
beats clever.
For each slice, compute:
- **Files included** (full list).
- **Lines added / removed.**
- **Import dependencies on earlier slices.**
- **Reader-time estimate.** Heuristic: `ceil(lines_added / 80) + ceil(files / 5)` minutes,
doubled for files matching `*.test.*` or `test/*` (test bodies skim faster but
reviewers verify they cover the right surface). Cap a single slice at 30 min
reviewer-time; if it exceeds, the slice is too big and you must propose a
further split or flag it explicitly.
- **Reader guide.** Two to four sentences answering: what's in this slice,
why it ships first/middle/last, what to look for. No marketing voice —
it's a working note for a tired reviewer.
If the entire diff fits comfortably in one slice (<= 1 component touched,
<= 30 min reader time, no hard edges), say so plainly: "This is one PR.
No decomposition needed." Output a one-line decomposition.md confirming
that and exit.
## Step 5 — Reconciliation flags (informational)
If SYSTEM.md was present, compute:
- `import-without-contract`: components A and B have an import edge but
no declared contract.
- `contract-without-imports`: a contract is declared with no supporting
import edge AND no `note: runtime-only`.
- `rollout-order-inversion`: a slice with lower rollout-order imports
from a slice with higher rollout-order (i.e., declared order disagrees
with discovered order).
These are PRINTED in the output, never blocking. Resolve in a follow-up.
## Step 6 — Choose artifact location
Use AskUserQuestion to ask where `decomposition.md` should be written:
| Option | Path |
|--------|------|
| In-repo (committed to branch) | `.gstack/plan-rollout/<branch-slug>-decomposition.md` |
| User scope (uncommitted) | `~/.gstack/projects/<repo-slug>/<branch-slug>-decomposition.md` |
Default recommendation: in-repo when the branch already has other planning
artifacts under `.gstack/`, user scope otherwise. State your recommendation
and let the user pick.
## Step 7 — Write decomposition.md
Layout:
```markdown
# Decomposition: <branch-name>
**Base:** <base-branch> **Head:** <head-ref> **Diff:** <N files, +A / -D lines>
**SYSTEM.md:** <present | absent — heuristics used>
**Generated:** <ISO timestamp> **By:** /plan-rollout vX.Y.Z
## Verdict
<One paragraph. Either: "This is one PR — no decomposition needed."
Or: "Ship as N PRs in this order. Total reviewer time: ~M minutes."
Or: "Stop. This diff has <unresolvable issue>. Do <X> first.">
## Slices
### Slice 1: <component-or-dir name>
**Files (<n>):** <bulleted, full paths>
**Diff:** +A / -D lines
**Reader time:** ~M min
**Depends on:** none | Slice K
**Coordinated deploy:** <only if a hard edge applies, else omit>
**Reader guide.** <2-4 sentences>
### Slice 2: ...
...
## Reconciliation flags (informational)
- `import-without-contract` between auth and middleware (auth imports
middleware/types but no contract declared)
- ...
(Only emit this section if SYSTEM.md was present and at least one flag fired.)
## What's NOT in this decomposition
<Anything excluded on purpose: out-of-scope files, deferred slices, etc.
If everything in the diff is covered, say "All changed files allocated.">
```
Write the file, print its path, and stop. Do not implement, do not split
the branch, do not run `/ship`. The user reviews the decomposition and
decides whether to act on it.
## Self-check before exit
Before you write the final file, verify:
1. Every changed file appears in exactly one slice (or in the `(unmapped)`
bucket with an explicit flag).
2. No slice depends on a later slice (no cycles).
3. The verdict line is honest — if reader time totals 4 minutes and the
diff touched 2 files, the verdict is "this is one PR," not a 3-slice stack.
4. If SYSTEM.md was used, every slice maps to a real component name.
If any check fails, fix the decomposition before writing. If you can't
fix it, write the file with the failure flagged in the verdict and tell
the user what you couldn't resolve.
## Limits
- This skill does NOT enforce or split branches. It produces a doc.
- It does NOT validate `breaks-if` claims in SYSTEM.md — that's a human
judgment.
- It does NOT scaffold SYSTEM.md (deferred to v2).
- Reader-time estimates are heuristic. Calibrate against real reviewer
feedback over time; v1 has no calibration data yet.
- For diffs that are mostly in one component but with a few stray files
in another, do NOT force a 2-slice stack — call it one PR with a
"stray files" flag instead. False slicing is worse than no slicing.