mirror of https://github.com/garrytan/gstack.git
feat(plan-rollout): compress SKILL.md.tmpl (296 → 204 lines, -31%)
Tighten step prose. All 8 steps + self-check + limits preserved semantically. Behavior unchanged — same bash commands, same priority order in slice ranking, same verdict-first design. Combined with the docs/ compressions, total substantive diff drops 701 → 414 lines (-41%). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
673665d165
commit
cfc14e4ac5
|
|
@ -5,11 +5,10 @@ interactive: true
|
||||||
version: 0.1.0
|
version: 0.1.0
|
||||||
description: |
|
description: |
|
||||||
Decomposition-as-artifact. Given a real working diff (and `SYSTEM.md` if
|
Decomposition-as-artifact. Given a real working diff (and `SYSTEM.md` if
|
||||||
present), produces a written `decomposition.md` with per-slice file lists,
|
present), produces a `decomposition.md` with per-slice file lists,
|
||||||
reader-time estimates, dependency edges, and contract-graph reconciliation
|
reader-time estimates, dependency edges, and contract-graph reconciliation
|
||||||
flags. Runs after a diff exists — it analyzes actual files, not intentions.
|
flags. Runs after a diff exists. Use when asked to "decompose the diff",
|
||||||
Use when asked to "decompose the diff", "write a decomposition.md", or
|
"write a decomposition.md", or "plan-rollout". (gstack)
|
||||||
"plan-rollout". (gstack)
|
|
||||||
Voice triggers (speech-to-text aliases): "decompose the diff", "write a decomposition", "plan-rollout".
|
Voice triggers (speech-to-text aliases): "decompose the diff", "write a decomposition", "plan-rollout".
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Read
|
- Read
|
||||||
|
|
@ -749,54 +748,35 @@ PLAN MODE EXCEPTION — always allowed (it's the plan file).
|
||||||
# /plan-rollout: Decomposition-as-Artifact
|
# /plan-rollout: Decomposition-as-Artifact
|
||||||
|
|
||||||
You read a working diff (plus `SYSTEM.md` if present) and write
|
You read a working diff (plus `SYSTEM.md` if present) and write
|
||||||
`decomposition.md` — a per-slice breakdown a tired reviewer can pick up.
|
`decomposition.md`. You never write code, never split branches, never
|
||||||
You never write code, never split branches, never run `/ship`. The output
|
run `/ship`. The output is the artifact and only the artifact.
|
||||||
is the artifact and only the artifact.
|
|
||||||
|
|
||||||
## When to invoke this skill
|
## When to invoke
|
||||||
|
|
||||||
Run when a diff already exists (committed or working tree) and either:
|
Run when a diff already exists (committed or working tree). Don't run on
|
||||||
- The user explicitly asks for `decomposition.md`.
|
single-component, sub-30-min-reader-time diffs — produce the one-line
|
||||||
- The user has decided the work should ship as a stack and wants an
|
"this is one PR" verdict and stop. False slicing is worse than no slicing.
|
||||||
analysis artifact tied to the actual files.
|
|
||||||
- A reviewer asked to see the change broken down before they read it.
|
|
||||||
|
|
||||||
If invoked before any code has been written, stop and say so: there is
|
If invoked before any code has been written, stop and say so: nothing
|
||||||
nothing to decompose. Suggest writing the smallest end-to-end slice first
|
to decompose, re-run against a real diff.
|
||||||
and re-running the skill against the real diff.
|
|
||||||
|
|
||||||
Don't run on single-component, sub-30-min-reader-time diffs unless the user
|
Out of v1: `rollout.md`, spill-check, `/ship` and `/review` integration,
|
||||||
overrides — produce the one-line "this is one PR" verdict and stop. False
|
SYSTEM.md scaffolding.
|
||||||
slicing is worse than no slicing.
|
|
||||||
|
|
||||||
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
|
## Step 0 — Detect repo state
|
||||||
|
|
||||||
Detect, in this order:
|
1. **Repo root:** `git rev-parse --show-toplevel`. Ask via
|
||||||
|
AskUserQuestion if not in a git repo; stop if unavailable.
|
||||||
|
2. **Base branch:** try `gh pr view --json baseRefName -q .baseRefName`,
|
||||||
|
then `origin/main`, then `origin/master`. Ask if unresolved.
|
||||||
|
3. **Head ref:** `git rev-parse --abbrev-ref HEAD`. If detached or on
|
||||||
|
the base, ask whether to use a description-only input.
|
||||||
|
4. **Plan source:** in order, (a) plan-file path from args; (b)
|
||||||
|
`~/.gstack/projects/<slug>/...-design-*.md` for this branch
|
||||||
|
(mirror `plan-eng-review`'s lookup); (c) AskUserQuestion for
|
||||||
|
paste / path / diff-only.
|
||||||
|
|
||||||
1. **Repo root.** `git rev-parse --show-toplevel`. If not a git repo, ask
|
State each detected value back in one line. Facts, not prose.
|
||||||
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
|
## Step 1 — Read SYSTEM.md if present
|
||||||
|
|
||||||
|
|
@ -804,208 +784,138 @@ narration — just facts.
|
||||||
test -f SYSTEM.md && cat SYSTEM.md || echo "(no SYSTEM.md — using path heuristics)"
|
test -f SYSTEM.md && cat SYSTEM.md || echo "(no SYSTEM.md — using path heuristics)"
|
||||||
```
|
```
|
||||||
|
|
||||||
If SYSTEM.md exists:
|
If present: parse YAML frontmatter, build a path→component map
|
||||||
- Parse the YAML frontmatter. Components without a `path` field are skipped
|
(longest-path-wins), build the contract graph (`rollout-edge: hard`
|
||||||
with a one-line warning.
|
edges drive coordinated-deploy warnings).
|
||||||
- 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:
|
If absent: fall back to one slice per top-level directory touched.
|
||||||
- Fall back to "top-level dir of change = component."
|
Never invent a SYSTEM.md from heuristics.
|
||||||
- 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)
|
## Step 2 — Enumerate the diff
|
||||||
|
|
||||||
The "diff to decompose" usually includes a mix of committed work and
|
Capture committed + staged + unstaged + untracked work:
|
||||||
uncommitted work. Capture all of it. Use these commands (substitute the
|
|
||||||
base branch detected in Step 0):
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Tracked changes (committed + staged + unstaged) vs base.
|
# Tracked changes vs base. NO triple-dot — `git diff <base>` compares
|
||||||
# Note: NO triple-dot. `git diff <base>` compares working tree to <base>
|
# the working tree to <base> and includes everything not yet pushed.
|
||||||
# and includes everything that's not yet pushed.
|
|
||||||
git diff --name-status "<base>"
|
git diff --name-status "<base>"
|
||||||
git diff --numstat "<base>"
|
git diff --numstat "<base>"
|
||||||
|
git ls-files --others --exclude-standard # untracked, treat as fully added
|
||||||
# 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
|
Union and deduplicate by path. If empty, print "No changes between
|
||||||
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.
|
<base> and the current working state. Nothing to decompose." and stop.
|
||||||
|
For >200 files, warn and ask before proceeding.
|
||||||
|
|
||||||
For very large diffs (>200 files), warn the user and ask whether to
|
Bucket each file: with SYSTEM.md, via the path map (unmatched → `(unmapped)`
|
||||||
proceed or abort — decomposing thousand-file diffs is not what this skill
|
bucket, flagged in output). Without, by top-level directory.
|
||||||
is for.
|
|
||||||
|
|
||||||
Bucket each file:
|
## Step 3 — Light-touch import discovery
|
||||||
- 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
|
```bash
|
||||||
# TypeScript / JavaScript
|
grep -E "^import .* from |^const .* = require\(" <file> # TS/JS
|
||||||
grep -E "^import .* from |^const .* = require\(" <file>
|
grep -E "^(from |import )" <file> # Python
|
||||||
# Python
|
grep -E "^import " <file> # Go
|
||||||
grep -E "^(from |import )" <file>
|
|
||||||
# Go
|
|
||||||
grep -E "^import " <file>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Resolve each import to a component (or top-level dir) using the same
|
Resolve each import to a component (or top-level dir). Record directed
|
||||||
path map. Record the directed edge `<file's component> → <imported component>`.
|
edges `<file's component> → <imported component>`. Unresolvable imports
|
||||||
|
(external packages, ambiguous paths) are no-ops — log, don't fail.
|
||||||
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
|
## Step 4 — Propose the slice stack
|
||||||
|
|
||||||
Generate the stack with these rules, in priority order:
|
Rules in priority order:
|
||||||
|
|
||||||
1. **Honor `rollout-edge: hard`** (if SYSTEM.md): if a contract with
|
1. **`rollout-edge: hard`** (SYSTEM.md): if BOTH sides of a hard contract
|
||||||
`rollout-edge: hard` connects two components and BOTH have changed
|
have changed files, those files merge into one slice tagged
|
||||||
files, those files merge into a single slice with the annotation
|
"coordinated deploy required — <breaks-if reason>".
|
||||||
"coordinated deploy required — <breaks-if reason>."
|
2. **Topological by import edges:** a slice ships after every slice it
|
||||||
2. **Topological by import edges:** a slice must come after every slice
|
imports from. Cycles flagged + merged.
|
||||||
it imports from. Cycles get flagged and merged.
|
3. **`rollout-order`** breaks ties (lower first).
|
||||||
3. **`rollout-order` from SYSTEM.md** breaks ties: lower numbers ship first.
|
4. **`leaf-util` / `types-only`** float to slice 0.
|
||||||
4. **`leaf-util` / `types-only` components** float to slice 0 (foundational,
|
5. **Alphabetical** on remaining ties. Predictable > clever.
|
||||||
ship first, no contract dependents).
|
|
||||||
5. **Fall-through tie-break:** alphabetical by component name. Predictable
|
|
||||||
beats clever.
|
|
||||||
|
|
||||||
For each slice, compute:
|
Per slice, compute: file list, lines +/-, dependencies, reader-time
|
||||||
- **Files included** (full list).
|
(`ceil(lines/80) + ceil(files/5)` min; cap each slice at 30 min — split
|
||||||
- **Lines added / removed.**
|
or flag if exceeded), reader guide (2-4 sentences, tired-reviewer voice).
|
||||||
- **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,
|
**One-PR escape:** if the diff is ≤1 component, ≤30 min reader time,
|
||||||
<= 30 min reader time, no hard edges), say so plainly: "This is one PR.
|
no hard edges, write a one-line decomposition.md ("This is one PR. No
|
||||||
No decomposition needed." Output a one-line decomposition.md confirming
|
decomposition needed.") and exit.
|
||||||
that and exit.
|
|
||||||
|
|
||||||
## Step 5 — Reconciliation flags (informational)
|
## Step 5 — Reconciliation flags (informational)
|
||||||
|
|
||||||
If SYSTEM.md was present, compute:
|
With SYSTEM.md present, compute and print (never blocking):
|
||||||
- `import-without-contract`: components A and B have an import edge but
|
|
||||||
no declared contract.
|
- `import-without-contract`: A imports B but no contract declared.
|
||||||
- `contract-without-imports`: a contract is declared with no supporting
|
- `contract-without-imports`: contract declared with no supporting
|
||||||
import edge AND no `note: runtime-only`.
|
import edge AND no `note: runtime-only`.
|
||||||
- `rollout-order-inversion`: a slice with lower rollout-order imports
|
- `rollout-order-inversion`: declared order disagrees with discovered.
|
||||||
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 — Artifact location
|
||||||
|
|
||||||
## Step 6 — Choose artifact location
|
AskUserQuestion:
|
||||||
|
|
||||||
Use AskUserQuestion to ask where `decomposition.md` should be written:
|
|
||||||
|
|
||||||
| Option | Path |
|
| Option | Path |
|
||||||
|--------|------|
|
|--------|------|
|
||||||
| In-repo (committed to branch) | `.gstack/plan-rollout/<branch-slug>-decomposition.md` |
|
| In-repo | `.gstack/plan-rollout/<branch-slug>-decomposition.md` |
|
||||||
| User scope (uncommitted) | `~/.gstack/projects/<repo-slug>/<branch-slug>-decomposition.md` |
|
| User scope | `~/.gstack/projects/<repo-slug>/<branch-slug>-decomposition.md` |
|
||||||
|
|
||||||
Default recommendation: in-repo when the branch already has other planning
|
Recommend in-repo when other `.gstack/` planning artifacts exist on the
|
||||||
artifacts under `.gstack/`, user scope otherwise. State your recommendation
|
branch; user scope otherwise.
|
||||||
and let the user pick.
|
|
||||||
|
|
||||||
## Step 7 — Write decomposition.md
|
## Step 7 — Write decomposition.md
|
||||||
|
|
||||||
Layout:
|
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
# Decomposition: <branch-name>
|
# Decomposition: <branch-name>
|
||||||
|
|
||||||
**Base:** <base-branch> **Head:** <head-ref> **Diff:** <N files, +A / -D lines>
|
**Base:** <base> **Head:** <head> **Diff:** <N files, +A / -D>
|
||||||
**SYSTEM.md:** <present | absent — heuristics used>
|
**SYSTEM.md:** <present | absent — heuristics used>
|
||||||
**Generated:** <ISO timestamp> **By:** /plan-rollout vX.Y.Z
|
**Generated:** <ISO timestamp> **By:** /plan-rollout vX.Y.Z
|
||||||
|
|
||||||
## Verdict
|
## Verdict
|
||||||
|
|
||||||
<One paragraph. Either: "This is one PR — no decomposition needed."
|
<One paragraph. "This is one PR — no decomposition needed." | "Ship as
|
||||||
Or: "Ship as N PRs in this order. Total reviewer time: ~M minutes."
|
N PRs in this order. Total reviewer time: ~M min." | "Stop. <issue>. Do
|
||||||
Or: "Stop. This diff has <unresolvable issue>. Do <X> first.">
|
<X> first.">
|
||||||
|
|
||||||
## Slices
|
## Slices
|
||||||
|
|
||||||
### Slice 1: <component-or-dir name>
|
### Slice 1: <name>
|
||||||
|
**Files (<n>):** <list>
|
||||||
**Files (<n>):** <bulleted, full paths>
|
**Diff:** +A / -D **Reader time:** ~M min **Depends on:** none | Slice K
|
||||||
**Diff:** +A / -D lines
|
**Coordinated deploy:** <only if hard-edge applies>
|
||||||
**Reader time:** ~M min
|
|
||||||
**Depends on:** none | Slice K
|
|
||||||
**Coordinated deploy:** <only if a hard edge applies, else omit>
|
|
||||||
|
|
||||||
**Reader guide.** <2-4 sentences>
|
**Reader guide.** <2-4 sentences>
|
||||||
|
|
||||||
### Slice 2: ...
|
### Slice 2: ...
|
||||||
|
|
||||||
...
|
|
||||||
|
|
||||||
## Reconciliation flags (informational)
|
## Reconciliation flags (informational)
|
||||||
|
|
||||||
- `import-without-contract` between auth and middleware (auth imports
|
|
||||||
middleware/types but no contract declared)
|
|
||||||
- ...
|
- ...
|
||||||
|
(Emit only if SYSTEM.md present and ≥1 flag fired.)
|
||||||
(Only emit this section if SYSTEM.md was present and at least one flag fired.)
|
|
||||||
|
|
||||||
## What's NOT in this decomposition
|
## What's NOT in this decomposition
|
||||||
|
<Excluded on purpose. "All changed files allocated." if none.>
|
||||||
<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
|
Write the file, print its path, stop. Do not implement, do not split
|
||||||
the branch, do not run `/ship`. The user reviews the decomposition and
|
the branch, do not run `/ship`.
|
||||||
decides whether to act on it.
|
|
||||||
|
|
||||||
## Self-check before exit
|
## Self-check before exit
|
||||||
|
|
||||||
Before you write the final file, verify:
|
1. Every changed file in exactly one slice (or flagged `(unmapped)`).
|
||||||
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).
|
2. No slice depends on a later slice (no cycles).
|
||||||
3. The verdict line is honest — if reader time totals 4 minutes and the
|
3. Verdict matches the math — if reader time is 4 min on 2 files, the
|
||||||
diff touched 2 files, the verdict is "this is one PR," not a 3-slice stack.
|
verdict is "one PR," not a stack.
|
||||||
4. If SYSTEM.md was used, every slice maps to a real component name.
|
4. With SYSTEM.md, every slice maps to a real component name.
|
||||||
|
|
||||||
If any check fails, fix the decomposition before writing. If you can't
|
If a check fails and you can't fix the decomposition, write the file
|
||||||
fix it, write the file with the failure flagged in the verdict and tell
|
with the failure flagged in the verdict.
|
||||||
the user what you couldn't resolve.
|
|
||||||
|
|
||||||
## Limits
|
## Limits
|
||||||
|
|
||||||
- This skill does NOT enforce or split branches. It produces a doc.
|
- Does NOT enforce or split branches — produces a doc.
|
||||||
- It does NOT validate `breaks-if` claims in SYSTEM.md — that's a human
|
- Does NOT validate `breaks-if` claims — human judgment.
|
||||||
judgment.
|
- Does NOT scaffold SYSTEM.md (deferred to v2).
|
||||||
- It does NOT scaffold SYSTEM.md (deferred to v2).
|
- Reader-time estimates are heuristic; v1 has no calibration data.
|
||||||
- Reader-time estimates are heuristic. Calibrate against real reviewer
|
- Mostly-one-component diffs with stray files: call them one PR with a
|
||||||
feedback over time; v1 has no calibration data yet.
|
"stray files" flag. Don't force a 2-slice stack.
|
||||||
- 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.
|
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,10 @@ interactive: true
|
||||||
version: 0.1.0
|
version: 0.1.0
|
||||||
description: |
|
description: |
|
||||||
Decomposition-as-artifact. Given a real working diff (and `SYSTEM.md` if
|
Decomposition-as-artifact. Given a real working diff (and `SYSTEM.md` if
|
||||||
present), produces a written `decomposition.md` with per-slice file lists,
|
present), produces a `decomposition.md` with per-slice file lists,
|
||||||
reader-time estimates, dependency edges, and contract-graph reconciliation
|
reader-time estimates, dependency edges, and contract-graph reconciliation
|
||||||
flags. Runs after a diff exists — it analyzes actual files, not intentions.
|
flags. Runs after a diff exists. Use when asked to "decompose the diff",
|
||||||
Use when asked to "decompose the diff", "write a decomposition.md", or
|
"write a decomposition.md", or "plan-rollout". (gstack)
|
||||||
"plan-rollout". (gstack)
|
|
||||||
voice-triggers:
|
voice-triggers:
|
||||||
- "decompose the diff"
|
- "decompose the diff"
|
||||||
- "write a decomposition"
|
- "write a decomposition"
|
||||||
|
|
@ -32,54 +31,35 @@ triggers:
|
||||||
# /plan-rollout: Decomposition-as-Artifact
|
# /plan-rollout: Decomposition-as-Artifact
|
||||||
|
|
||||||
You read a working diff (plus `SYSTEM.md` if present) and write
|
You read a working diff (plus `SYSTEM.md` if present) and write
|
||||||
`decomposition.md` — a per-slice breakdown a tired reviewer can pick up.
|
`decomposition.md`. You never write code, never split branches, never
|
||||||
You never write code, never split branches, never run `/ship`. The output
|
run `/ship`. The output is the artifact and only the artifact.
|
||||||
is the artifact and only the artifact.
|
|
||||||
|
|
||||||
## When to invoke this skill
|
## When to invoke
|
||||||
|
|
||||||
Run when a diff already exists (committed or working tree) and either:
|
Run when a diff already exists (committed or working tree). Don't run on
|
||||||
- The user explicitly asks for `decomposition.md`.
|
single-component, sub-30-min-reader-time diffs — produce the one-line
|
||||||
- The user has decided the work should ship as a stack and wants an
|
"this is one PR" verdict and stop. False slicing is worse than no slicing.
|
||||||
analysis artifact tied to the actual files.
|
|
||||||
- A reviewer asked to see the change broken down before they read it.
|
|
||||||
|
|
||||||
If invoked before any code has been written, stop and say so: there is
|
If invoked before any code has been written, stop and say so: nothing
|
||||||
nothing to decompose. Suggest writing the smallest end-to-end slice first
|
to decompose, re-run against a real diff.
|
||||||
and re-running the skill against the real diff.
|
|
||||||
|
|
||||||
Don't run on single-component, sub-30-min-reader-time diffs unless the user
|
Out of v1: `rollout.md`, spill-check, `/ship` and `/review` integration,
|
||||||
overrides — produce the one-line "this is one PR" verdict and stop. False
|
SYSTEM.md scaffolding.
|
||||||
slicing is worse than no slicing.
|
|
||||||
|
|
||||||
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
|
## Step 0 — Detect repo state
|
||||||
|
|
||||||
Detect, in this order:
|
1. **Repo root:** `git rev-parse --show-toplevel`. Ask via
|
||||||
|
AskUserQuestion if not in a git repo; stop if unavailable.
|
||||||
|
2. **Base branch:** try `gh pr view --json baseRefName -q .baseRefName`,
|
||||||
|
then `origin/main`, then `origin/master`. Ask if unresolved.
|
||||||
|
3. **Head ref:** `git rev-parse --abbrev-ref HEAD`. If detached or on
|
||||||
|
the base, ask whether to use a description-only input.
|
||||||
|
4. **Plan source:** in order, (a) plan-file path from args; (b)
|
||||||
|
`~/.gstack/projects/<slug>/...-design-*.md` for this branch
|
||||||
|
(mirror `plan-eng-review`'s lookup); (c) AskUserQuestion for
|
||||||
|
paste / path / diff-only.
|
||||||
|
|
||||||
1. **Repo root.** `git rev-parse --show-toplevel`. If not a git repo, ask
|
State each detected value back in one line. Facts, not prose.
|
||||||
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
|
## Step 1 — Read SYSTEM.md if present
|
||||||
|
|
||||||
|
|
@ -87,208 +67,138 @@ narration — just facts.
|
||||||
test -f SYSTEM.md && cat SYSTEM.md || echo "(no SYSTEM.md — using path heuristics)"
|
test -f SYSTEM.md && cat SYSTEM.md || echo "(no SYSTEM.md — using path heuristics)"
|
||||||
```
|
```
|
||||||
|
|
||||||
If SYSTEM.md exists:
|
If present: parse YAML frontmatter, build a path→component map
|
||||||
- Parse the YAML frontmatter. Components without a `path` field are skipped
|
(longest-path-wins), build the contract graph (`rollout-edge: hard`
|
||||||
with a one-line warning.
|
edges drive coordinated-deploy warnings).
|
||||||
- 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:
|
If absent: fall back to one slice per top-level directory touched.
|
||||||
- Fall back to "top-level dir of change = component."
|
Never invent a SYSTEM.md from heuristics.
|
||||||
- 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)
|
## Step 2 — Enumerate the diff
|
||||||
|
|
||||||
The "diff to decompose" usually includes a mix of committed work and
|
Capture committed + staged + unstaged + untracked work:
|
||||||
uncommitted work. Capture all of it. Use these commands (substitute the
|
|
||||||
base branch detected in Step 0):
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Tracked changes (committed + staged + unstaged) vs base.
|
# Tracked changes vs base. NO triple-dot — `git diff <base>` compares
|
||||||
# Note: NO triple-dot. `git diff <base>` compares working tree to <base>
|
# the working tree to <base> and includes everything not yet pushed.
|
||||||
# and includes everything that's not yet pushed.
|
|
||||||
git diff --name-status "<base>"
|
git diff --name-status "<base>"
|
||||||
git diff --numstat "<base>"
|
git diff --numstat "<base>"
|
||||||
|
git ls-files --others --exclude-standard # untracked, treat as fully added
|
||||||
# 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
|
Union and deduplicate by path. If empty, print "No changes between
|
||||||
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.
|
<base> and the current working state. Nothing to decompose." and stop.
|
||||||
|
For >200 files, warn and ask before proceeding.
|
||||||
|
|
||||||
For very large diffs (>200 files), warn the user and ask whether to
|
Bucket each file: with SYSTEM.md, via the path map (unmatched → `(unmapped)`
|
||||||
proceed or abort — decomposing thousand-file diffs is not what this skill
|
bucket, flagged in output). Without, by top-level directory.
|
||||||
is for.
|
|
||||||
|
|
||||||
Bucket each file:
|
## Step 3 — Light-touch import discovery
|
||||||
- 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
|
```bash
|
||||||
# TypeScript / JavaScript
|
grep -E "^import .* from |^const .* = require\(" <file> # TS/JS
|
||||||
grep -E "^import .* from |^const .* = require\(" <file>
|
grep -E "^(from |import )" <file> # Python
|
||||||
# Python
|
grep -E "^import " <file> # Go
|
||||||
grep -E "^(from |import )" <file>
|
|
||||||
# Go
|
|
||||||
grep -E "^import " <file>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Resolve each import to a component (or top-level dir) using the same
|
Resolve each import to a component (or top-level dir). Record directed
|
||||||
path map. Record the directed edge `<file's component> → <imported component>`.
|
edges `<file's component> → <imported component>`. Unresolvable imports
|
||||||
|
(external packages, ambiguous paths) are no-ops — log, don't fail.
|
||||||
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
|
## Step 4 — Propose the slice stack
|
||||||
|
|
||||||
Generate the stack with these rules, in priority order:
|
Rules in priority order:
|
||||||
|
|
||||||
1. **Honor `rollout-edge: hard`** (if SYSTEM.md): if a contract with
|
1. **`rollout-edge: hard`** (SYSTEM.md): if BOTH sides of a hard contract
|
||||||
`rollout-edge: hard` connects two components and BOTH have changed
|
have changed files, those files merge into one slice tagged
|
||||||
files, those files merge into a single slice with the annotation
|
"coordinated deploy required — <breaks-if reason>".
|
||||||
"coordinated deploy required — <breaks-if reason>."
|
2. **Topological by import edges:** a slice ships after every slice it
|
||||||
2. **Topological by import edges:** a slice must come after every slice
|
imports from. Cycles flagged + merged.
|
||||||
it imports from. Cycles get flagged and merged.
|
3. **`rollout-order`** breaks ties (lower first).
|
||||||
3. **`rollout-order` from SYSTEM.md** breaks ties: lower numbers ship first.
|
4. **`leaf-util` / `types-only`** float to slice 0.
|
||||||
4. **`leaf-util` / `types-only` components** float to slice 0 (foundational,
|
5. **Alphabetical** on remaining ties. Predictable > clever.
|
||||||
ship first, no contract dependents).
|
|
||||||
5. **Fall-through tie-break:** alphabetical by component name. Predictable
|
|
||||||
beats clever.
|
|
||||||
|
|
||||||
For each slice, compute:
|
Per slice, compute: file list, lines +/-, dependencies, reader-time
|
||||||
- **Files included** (full list).
|
(`ceil(lines/80) + ceil(files/5)` min; cap each slice at 30 min — split
|
||||||
- **Lines added / removed.**
|
or flag if exceeded), reader guide (2-4 sentences, tired-reviewer voice).
|
||||||
- **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,
|
**One-PR escape:** if the diff is ≤1 component, ≤30 min reader time,
|
||||||
<= 30 min reader time, no hard edges), say so plainly: "This is one PR.
|
no hard edges, write a one-line decomposition.md ("This is one PR. No
|
||||||
No decomposition needed." Output a one-line decomposition.md confirming
|
decomposition needed.") and exit.
|
||||||
that and exit.
|
|
||||||
|
|
||||||
## Step 5 — Reconciliation flags (informational)
|
## Step 5 — Reconciliation flags (informational)
|
||||||
|
|
||||||
If SYSTEM.md was present, compute:
|
With SYSTEM.md present, compute and print (never blocking):
|
||||||
- `import-without-contract`: components A and B have an import edge but
|
|
||||||
no declared contract.
|
- `import-without-contract`: A imports B but no contract declared.
|
||||||
- `contract-without-imports`: a contract is declared with no supporting
|
- `contract-without-imports`: contract declared with no supporting
|
||||||
import edge AND no `note: runtime-only`.
|
import edge AND no `note: runtime-only`.
|
||||||
- `rollout-order-inversion`: a slice with lower rollout-order imports
|
- `rollout-order-inversion`: declared order disagrees with discovered.
|
||||||
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 — Artifact location
|
||||||
|
|
||||||
## Step 6 — Choose artifact location
|
AskUserQuestion:
|
||||||
|
|
||||||
Use AskUserQuestion to ask where `decomposition.md` should be written:
|
|
||||||
|
|
||||||
| Option | Path |
|
| Option | Path |
|
||||||
|--------|------|
|
|--------|------|
|
||||||
| In-repo (committed to branch) | `.gstack/plan-rollout/<branch-slug>-decomposition.md` |
|
| In-repo | `.gstack/plan-rollout/<branch-slug>-decomposition.md` |
|
||||||
| User scope (uncommitted) | `~/.gstack/projects/<repo-slug>/<branch-slug>-decomposition.md` |
|
| User scope | `~/.gstack/projects/<repo-slug>/<branch-slug>-decomposition.md` |
|
||||||
|
|
||||||
Default recommendation: in-repo when the branch already has other planning
|
Recommend in-repo when other `.gstack/` planning artifacts exist on the
|
||||||
artifacts under `.gstack/`, user scope otherwise. State your recommendation
|
branch; user scope otherwise.
|
||||||
and let the user pick.
|
|
||||||
|
|
||||||
## Step 7 — Write decomposition.md
|
## Step 7 — Write decomposition.md
|
||||||
|
|
||||||
Layout:
|
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
# Decomposition: <branch-name>
|
# Decomposition: <branch-name>
|
||||||
|
|
||||||
**Base:** <base-branch> **Head:** <head-ref> **Diff:** <N files, +A / -D lines>
|
**Base:** <base> **Head:** <head> **Diff:** <N files, +A / -D>
|
||||||
**SYSTEM.md:** <present | absent — heuristics used>
|
**SYSTEM.md:** <present | absent — heuristics used>
|
||||||
**Generated:** <ISO timestamp> **By:** /plan-rollout vX.Y.Z
|
**Generated:** <ISO timestamp> **By:** /plan-rollout vX.Y.Z
|
||||||
|
|
||||||
## Verdict
|
## Verdict
|
||||||
|
|
||||||
<One paragraph. Either: "This is one PR — no decomposition needed."
|
<One paragraph. "This is one PR — no decomposition needed." | "Ship as
|
||||||
Or: "Ship as N PRs in this order. Total reviewer time: ~M minutes."
|
N PRs in this order. Total reviewer time: ~M min." | "Stop. <issue>. Do
|
||||||
Or: "Stop. This diff has <unresolvable issue>. Do <X> first.">
|
<X> first.">
|
||||||
|
|
||||||
## Slices
|
## Slices
|
||||||
|
|
||||||
### Slice 1: <component-or-dir name>
|
### Slice 1: <name>
|
||||||
|
**Files (<n>):** <list>
|
||||||
**Files (<n>):** <bulleted, full paths>
|
**Diff:** +A / -D **Reader time:** ~M min **Depends on:** none | Slice K
|
||||||
**Diff:** +A / -D lines
|
**Coordinated deploy:** <only if hard-edge applies>
|
||||||
**Reader time:** ~M min
|
|
||||||
**Depends on:** none | Slice K
|
|
||||||
**Coordinated deploy:** <only if a hard edge applies, else omit>
|
|
||||||
|
|
||||||
**Reader guide.** <2-4 sentences>
|
**Reader guide.** <2-4 sentences>
|
||||||
|
|
||||||
### Slice 2: ...
|
### Slice 2: ...
|
||||||
|
|
||||||
...
|
|
||||||
|
|
||||||
## Reconciliation flags (informational)
|
## Reconciliation flags (informational)
|
||||||
|
|
||||||
- `import-without-contract` between auth and middleware (auth imports
|
|
||||||
middleware/types but no contract declared)
|
|
||||||
- ...
|
- ...
|
||||||
|
(Emit only if SYSTEM.md present and ≥1 flag fired.)
|
||||||
(Only emit this section if SYSTEM.md was present and at least one flag fired.)
|
|
||||||
|
|
||||||
## What's NOT in this decomposition
|
## What's NOT in this decomposition
|
||||||
|
<Excluded on purpose. "All changed files allocated." if none.>
|
||||||
<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
|
Write the file, print its path, stop. Do not implement, do not split
|
||||||
the branch, do not run `/ship`. The user reviews the decomposition and
|
the branch, do not run `/ship`.
|
||||||
decides whether to act on it.
|
|
||||||
|
|
||||||
## Self-check before exit
|
## Self-check before exit
|
||||||
|
|
||||||
Before you write the final file, verify:
|
1. Every changed file in exactly one slice (or flagged `(unmapped)`).
|
||||||
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).
|
2. No slice depends on a later slice (no cycles).
|
||||||
3. The verdict line is honest — if reader time totals 4 minutes and the
|
3. Verdict matches the math — if reader time is 4 min on 2 files, the
|
||||||
diff touched 2 files, the verdict is "this is one PR," not a 3-slice stack.
|
verdict is "one PR," not a stack.
|
||||||
4. If SYSTEM.md was used, every slice maps to a real component name.
|
4. With SYSTEM.md, every slice maps to a real component name.
|
||||||
|
|
||||||
If any check fails, fix the decomposition before writing. If you can't
|
If a check fails and you can't fix the decomposition, write the file
|
||||||
fix it, write the file with the failure flagged in the verdict and tell
|
with the failure flagged in the verdict.
|
||||||
the user what you couldn't resolve.
|
|
||||||
|
|
||||||
## Limits
|
## Limits
|
||||||
|
|
||||||
- This skill does NOT enforce or split branches. It produces a doc.
|
- Does NOT enforce or split branches — produces a doc.
|
||||||
- It does NOT validate `breaks-if` claims in SYSTEM.md — that's a human
|
- Does NOT validate `breaks-if` claims — human judgment.
|
||||||
judgment.
|
- Does NOT scaffold SYSTEM.md (deferred to v2).
|
||||||
- It does NOT scaffold SYSTEM.md (deferred to v2).
|
- Reader-time estimates are heuristic; v1 has no calibration data.
|
||||||
- Reader-time estimates are heuristic. Calibrate against real reviewer
|
- Mostly-one-component diffs with stray files: call them one PR with a
|
||||||
feedback over time; v1 has no calibration data yet.
|
"stray files" flag. Don't force a 2-slice stack.
|
||||||
- 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.
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue