@gr8ful/spf 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -471,15 +471,39 @@ watch:
471
471
  enabled: true
472
472
  chain: refine # any chain that ends in steps.publishIssues()
473
473
  concurrency: 1 # this lane's own budget, independent of watch.concurrency
474
+ max_leaves: 4 # optional — shown are the defaults; the decomposition BUDGET, see below
475
+ max_nodes: 6
476
+ max_depth: 2
474
477
  ```
475
478
 
476
479
  (A third, independent budget lives at `watch.fanout.concurrency` — best-of-N
477
480
  attempts per claimed issue, not specs. See "Best-of-N per issue" below.)
478
481
 
479
- On Jira, every `RefinedIssue.kind` (`epic`/`feature`/`story`/`bug`/`task`) maps
480
- to a real Jira issue type through `watch.jira.issue_types` defaults
481
- `epic`/`feature` `Epic`, `story` `Story`, `bug` `Bug`, `task` → `Task`,
482
- overridable per kind since real projects rename or customize these:
482
+ **The decomposition budget.** Every leaf the refiner emits becomes its own
483
+ worktree, its own chain run, and its own pull request a human reads once
484
+ promoted so leaf count is human review count. `assets/prompts/refiner/
485
+ system.md` targets one feature and three or four leaves; `refine.max_leaves`
486
+ (default 4), `refine.max_nodes` (default 6), and `refine.max_depth` (default
487
+ 2) are the hard CEILINGS `gates.refinementWellFormed` enforces on top of that
488
+ target, one slice past where a good decomposition sits so the gate never
489
+ makes the target a floor. `max_nodes` doesn't bind at the defaults — it
490
+ exists so raising `max_leaves` or `max_depth` alone can't silently uncap the
491
+ whole tree. Raising `max_depth` above 2 is unsafe on Jira specifically: see
492
+ "On Jira" just below. A tree with a container wrapping exactly one child also
493
+ fails — a container that groups nothing is pure ceremony — and every leaf
494
+ must carry a one-sentence `user_outcome` (rendered as a `## Outcome` section
495
+ on the tracker issue) naming who benefits and what changes for them; a
496
+ process/QA/rollout leaf has no honest sentence to write and gets folded into
497
+ whichever leaf's acceptance criteria it actually belongs to instead. See
498
+ "Splitting an over-large spec" below for what happens when a spec honestly
499
+ doesn't fit.
500
+
501
+ On Jira, every `RefinedIssue.kind` (`epic`/`feature`/`story`/`bug`/`task`),
502
+ plus `spec` (a standalone spec proposed by splitting an over-large one — see
503
+ "Splitting an over-large spec" below), maps to a real Jira issue type through
504
+ `watch.jira.issue_types` — defaults `epic`/`feature` → `Epic`, `story`/`spec`
505
+ → `Story`, `bug` → `Bug`, `task` → `Task`, overridable per kind since real
506
+ projects rename or customize these:
483
507
 
484
508
  ```yaml
485
509
  watch:
@@ -493,6 +517,7 @@ watch:
493
517
  story: Story
494
518
  bug: Bug
495
519
  task: Task
520
+ spec: Story
496
521
  refine:
497
522
  enabled: true
498
523
  ```
@@ -505,9 +530,13 @@ One accepted platform limitation: Jira doesn't support Epic-under-Epic
505
530
  nesting the way GitHub's sub-issues API supports up to 8 levels, so a
506
531
  `feature` node parented under another `epic`/`feature` (both `Epic` by
507
532
  default) will fail at publish time on Jira specifically — a real platform
508
- difference, not a bug. Both `spf watch init` and `spf watch`'s own startup
509
- check validate `watch.jira.issue_types` against the real project before
510
- anything unattended runs see "Jira" below.
533
+ difference, not a bug. `refine.max_depth`'s default of 2 makes this
534
+ unreachable in practice (see "The decomposition budget" above); raise it
535
+ past 2 only on a tracker where deeper nesting is actually supported. A `spec`
536
+ never parents another `spec` either way — see "Splitting an over-large spec"
537
+ below. Both `spf watch init` and `spf watch`'s own startup check validate
538
+ `watch.jira.issue_types` against the real project before anything unattended
539
+ runs — see "Jira" below.
511
540
 
512
541
  Every generated issue carries a `<prefix>:type:epic|feature|story|bug|task`
513
542
  label AND a `<prefix>:priority:p0|p1|p2|p3` label (see "Priority, dependencies,
@@ -516,12 +545,14 @@ can relabel it before promoting. A container (a feature/epic — something
516
545
  else names it as `parent`) gets only those two labels; a leaf additionally
517
546
  gets `<prefix>:refined` — **never** `<prefix>:ready`. Promoting a leaf to
518
547
  `<prefix>:ready` is a deliberate human decision: the refine lane never
519
- auto-promotes anything, so a spec fanning out into twenty stories doesn't
520
- turn into twenty unattended chain runs and twenty PRs with nobody having
521
- looked at the breakdown first. Once you do promote a leaf, the existing build
522
- lane picks it up completely unchanged including the frontier check: a
523
- promoted leaf isn't claimed until every issue named in its `blocked_by` is
524
- `<prefix>:done`.
548
+ auto-promotes anything. This is the SECOND line of defense against a
549
+ decomposition nobody looked at before it turns into unattended chain runs and
550
+ PRs the first is the decomposition budget itself (see above), which keeps
551
+ that number small (four leaves by default, not twenty) rather than relying on
552
+ manual promotion alone to absorb an oversized tree. Once you do promote a
553
+ leaf, the existing build lane picks it up completely unchanged — including
554
+ the frontier check: a promoted leaf isn't claimed until every issue named in
555
+ its `blocked_by` is `<prefix>:done`.
525
556
 
526
557
  A spec issue's own `<prefix>:priority:pN` label, if it has one, reaches the
527
558
  refiner as a **ceiling**: no generated node may be more urgent than the spec
@@ -547,11 +578,12 @@ spec moves through an extra loop before it's even fully published:
547
578
 
548
579
  ```text
549
580
  spec-ready → refining ──┬─→ spec-in-progress → done / blocked (published a tree, THEN tracked to completion)
550
- └─→ needs-feedback (raised questions)
551
- │ a human answers in the issue's comments,
552
- │ then adds continue-refinement
553
-
554
- refining (again, same adw_id) → ...
581
+ ├─→ needs-feedback (raised questions)
582
+ a human answers in the issue's comments,
583
+ then adds continue-refinement
584
+
585
+ refining (again, same adw_id) → ...
586
+ └─→ split-proposed (spec too large — see below)
555
587
  ```
556
588
 
557
589
  Answer the refiner's questions as comments on the spec issue, then add the
@@ -582,14 +614,54 @@ stays `spec-in-progress` — which is the truthful state, not a bug.
582
614
 
583
615
  The spec issue's full lifecycle: `spec-ready → refining → needs-feedback ⇄
584
616
  refining → spec-in-progress → done`/`blocked` (the `needs-feedback ⇄
585
- refining` loop only when the refiner actually escalates). Try it by hand
586
- first, against a real spec, before turning on the daemon:
617
+ refining` loop only when the refiner actually escalates), or, when the spec
618
+ is too large rather than ambiguous, `spec-ready refining split-proposed →
619
+ ...` — see "Splitting an over-large spec" just below. Try it by hand first,
620
+ against a real spec, before turning on the daemon:
587
621
 
588
622
  ```bash
589
623
  spf refine "<spec text or path/to/spec.md>" --issue 42 # --issue renders a "## Parent" section, "Decomposed from #42."
590
624
  spf refine "<spec text or path/to/spec.md>" --issue 42 --priority p1 # --priority clamps every generated node to p1 or less urgent
591
625
  ```
592
626
 
627
+ #### Splitting an over-large spec
628
+
629
+ Ambiguity isn't the only reason a refiner can't publish a tree this round —
630
+ sometimes the spec is simply too big, honestly, for the decomposition budget
631
+ above. Rather than force-fit an oversized tree (or worse, merge unrelated
632
+ slices into a handful of leaves that hit the number but blur past what a
633
+ reviewer can hold in their head), the refiner proposes a concrete **split**
634
+ into two or more standalone specs — each with its own user value, each
635
+ independently shippable, never "phase 1"/"phase 2" of the same thing:
636
+
637
+ ```text
638
+ spec-ready → refining ──→ split-proposed
639
+ │ a human reviews the proposal comment, then either
640
+ │ · adds split-approved → executed deterministically, NO agent re-run
641
+ │ · comments + continue-refinement → refiner revises the proposal
642
+
643
+ N new specs created (spec-ready, type:spec), recorded
644
+ on the ORIGINAL spec, which moves to spec-in-progress
645
+ ```
646
+
647
+ Approving with `<prefix>:split-approved` is a deliberate, different label
648
+ from `continue-refinement`'s "resume the refiner" — creating exactly what a
649
+ human already read in the proposal comment is a `code`-shaped action (no
650
+ scout, no refiner session, no worktree), the same way publishing a tree is.
651
+ Each created spec carries `<prefix>:type:spec` and `<prefix>:spec-ready` (never
652
+ `<prefix>:refined` — a spec is not itself a workable leaf) and a `## Parent`
653
+ back-reference to the original spec it was split from; on Jira it maps to a
654
+ `Story` by default (`watch.jira.issue_types.spec`), never nested under
655
+ another spec, since Jira has no Story-under-Story hierarchy to use. The
656
+ **original** spec's own `spec-in-progress` tracking now waits on both child
657
+ specs finishing their own trees, transitively — the same roll-up mechanism
658
+ "Publishing a tree does not mean the spec is done" above already describes,
659
+ with no separate tracking needed for a split. A child spec that is itself
660
+ still too large can split again — a genuinely huge spec bottoming out over a
661
+ couple of rounds is expected; watch for repeated split proposals on the same
662
+ original spec as the signal something is off, rather than a hard generation
663
+ cap.
664
+
593
665
  #### Container roll-up
594
666
 
595
667
  A feature or epic isn't itself a unit of work, so it's never claimed and
@@ -613,14 +685,18 @@ the build lane still works fine without roll-up.
613
685
 
614
686
  `spf watch init` seeds the type labels alongside the state ones — **re-run
615
687
  it** after upgrading to this version, so it can create the new
616
- `<prefix>:needs-feedback` and `<prefix>:continue-refinement` labels. This
617
- lane's prompt (`assets/prompts/refiner/`) is adapted from a "tracer-bullet
618
- ticket" decomposition skill vertical slices, a `blocked_by` dependency
619
- graph, and an expand/migrate/contract sequence for wide mechanical refactors
620
- with a gate (`gates.refinementWellFormed`) added on top to enforce the
621
- container/leaf shape that skill left as prose convention rather than a
622
- checked rule, now also enforcing that a refinement never publishes issues and
623
- raises questions in the same round.
688
+ `<prefix>:type:spec`, `<prefix>:split-proposed`, and
689
+ `<prefix>:split-approved` labels, alongside the earlier
690
+ `<prefix>:needs-feedback` and `<prefix>:continue-refinement`. This lane's
691
+ prompt (`assets/prompts/refiner/`) is adapted from a "tracer-bullet ticket"
692
+ decomposition skill vertical slices, a `blocked_by` dependency graph, and
693
+ an expand/migrate/contract sequence for wide mechanical refactors with a
694
+ gate (`gates.refinementWellFormed`) added on top to enforce the container/
695
+ leaf shape that skill left as prose convention rather than a checked rule:
696
+ container/leaf kind agreement, no container with a single child, a depth
697
+ cap, a leaf/node count ceiling, a required per-leaf user-outcome sentence,
698
+ and that a refinement never publishes issues alongside questions or a split
699
+ proposal in the same round.
624
700
 
625
701
  ### Best-of-N per issue (`watch.fanout`)
626
702
 
@@ -763,6 +839,22 @@ Getting each channel's URL:
763
839
  - **webhook** — any endpoint that accepts a JSON POST of the event: Discord,
764
840
  n8n, Zapier, a homegrown receiver.
765
841
 
842
+ One webhook, many `spf` instances: if several repos' `spf watch` (or
843
+ `spf run`) all post to the same Slack/Teams/webhook endpoint, set
844
+ `notifications.project` to a short label so messages from each are
845
+ distinguishable — it prefixes every title (`[api] watch: reconcileOrphans
846
+ error`) and adds a `repo` field. Left unset, it falls back to `watch.repo`,
847
+ so most `spf watch` setups need nothing extra; set it explicitly when
848
+ `watch.repo` is blank or two watched repos share a basename.
849
+
850
+ ```yaml
851
+ notifications:
852
+ events: attention
853
+ project: api # optional; defaults to watch.repo
854
+ channels:
855
+ - kind: slack
856
+ ```
857
+
766
858
  Delivery never blocks or fails a run: an unconfigured/misconfigured channel
767
859
  is skipped with one warning, and a failed POST logs one line and is
768
860
  swallowed — never changes a run's exit code. One thing worth knowing under
@@ -4,6 +4,12 @@
4
4
 
5
5
  Decompose a product spec into a feature/story-or-bug tree of vertical slices the factory can build one at a time. Change nothing.
6
6
 
7
+ ## What a leaf costs
8
+
9
+ Every leaf you emit becomes one git worktree, one full build/test/review chain run, and **one pull request a human has to read and approve**. Containers cost nothing and build nothing — only leaves are built. So your leaf count *is* the number of times a human stops what they are doing to review code for this spec.
10
+
11
+ Splitting is therefore never free and never neutral. The context window is not the constraint — a builder carries an entire feature's worth of files without strain. Reviewer attention is the constraint. Your job is the **fewest leaves that each stand alone**, not the cleanest-looking graph.
12
+
7
13
  ## Instructions
8
14
 
9
15
  - Read-only: explore the codebase to ground the decomposition, never write to it.
@@ -32,14 +38,11 @@ You will hit real ambiguity: places where the spec (or the codebase, or both) ge
32
38
  - **Non-functional targets** — performance, scale, availability numbers the spec doesn't state.
33
39
  - **Anything that would contradict an existing ADR.**
34
40
 
35
- You MAY decide these yourself, following whatever pattern the codebase already uses — that's ordinary judgment, not ambiguity: naming, file placement, test framework and layout, internal module structure, and the ordering of independent slices (defer to the `priority` you assign each one — see "Priority" below — rather than an arbitrary reading order).
41
+ This is a different problem from a spec that is simply too big see "Sizing and the budget" below for that one; it has its own escalation shape (`split`, not `questions`).
36
42
 
37
- When you escalate, ask **everything you need in one batch**don't trickle questions across rounds when you could have asked them all up front. Emit **no `issues`** in a round where you're asking questions; the two are mutually exclusive and a gate enforces it. For each question, give a human enough to answer in a word or two:
43
+ You MAY decide these yourself, following whatever pattern the codebase already usesthat's ordinary judgment, not ambiguity: naming, file placement, test framework and layout, internal module structure, and the ordering of independent slices (defer to the `priority` you assign each one see "Priority" below rather than an arbitrary reading order).
38
44
 
39
- - `why_it_matters` — what goes wrong if this gets guessed instead of decided.
40
- - `options` — the plausible answers you found, if there's a short list.
41
- - `recommendation` — your own best guess, so "go with your recommendation" is a valid answer.
42
- - `evidence` — the files/symbols that framed the question.
45
+ When you escalate, ask **everything you need in one batch** — don't trickle questions across rounds when you could have asked them all up front. Emit **no `issues`** in a round where you're asking questions; the two are mutually exclusive and a gate enforces it (see "Report shapes" in your task instructions — `issues`, `questions`, and `split` are three ways this round can end, and only one may be non-empty). For each question, give `why_it_matters`, `options`, `recommendation` and `evidence` (see the Report shape in your task) enough that a human can answer in a word, including "go with your recommendation."
43
46
 
44
47
  ## Resuming after a human answers
45
48
 
@@ -49,15 +52,68 @@ A resumed run's prompt includes the issue's comment thread, split into "answers
49
52
 
50
53
  Every node you produce is either a **container** (`epic` or `feature` — exists only to group other nodes; never itself a unit of work) or a **leaf** (`story`, `bug`, or `task` — the independently workable unit). A node is a container exactly when some other node names it as `parent`; everything else is a leaf. At least one leaf is required — a decomposition that is all containers has produced nothing to build.
51
54
 
55
+ **Never emit a container with exactly one child.** A feature wrapping a single story is pure ceremony: it duplicates the story's own acceptance criteria and doubles what a human reads to learn one thing. Either a container groups two or more real leaves, or it should not exist — delete it and let the leaf sit at top level. A gate rejects a one-child container.
56
+
52
57
  Emit a **flat list**, not nested JSON: each node names its `parent` by another node's `key`, or leaves `parent` empty for a top-level feature/epic. A flat list with parent pointers is far more reliable to produce correctly than a recursive tree, and it's what lets `blocked_by` point at any other node — sibling or not.
53
58
 
54
- ## Vertical-slice rules (for every leaf)
59
+ ## The slice test run it on every leaf before you emit it
60
+
61
+ Write each leaf's outcome as one sentence in exactly this shape:
62
+
63
+ > After this lands, **\<a named actor\>** can **\<do something observable\>** that they could not do before.
64
+
65
+ The actor must be someone outside the code — an end user on a specific plan, an API caller, an operator, an admin. Never "the codebase", "the team", "a developer", "the next story", "the builder". The observable thing must be visible from outside the process — a screen, a response, a persisted record, a delivered message, a state a user can reach. Never "a component exists", "the tests pass", "the types line up", "the code is consistent", "coverage improved".
66
+
67
+ Fails the test — these are NOT leaves:
68
+
69
+ - "Create the TierComparisonModal component." No actor; no user can do anything new.
70
+ - "Add the modal to the LockedUpgrade component." The actor is a file.
71
+ - "Verify Terminal unlock uses embedded checkout." Verification is not a change.
72
+ - "Cross-browser testing." / "Audit checklist." / "Rollback plan." No user-observable change at all.
73
+
74
+ Passes:
75
+
76
+ - "After this lands, a free user who hits any premium-locked surface sees the shared tier-comparison modal instead of eight different upsells."
77
+ - "After this lands, a user who picks a tier completes payment without ever leaving the app."
78
+
79
+ Two consequences you must apply, not just read:
80
+
81
+ - **If a leaf can only pass by weakening the sentence** ("a developer can now import a shared modal"), it is not a leaf. It is a *part* of one — merge it into the leaf whose sentence it serves.
82
+ - **If two leaves' sentences are the same sentence with a different noun swapped in** ("...on the home banner", "...in the plan manager"), they are ONE leaf. The shared sentence is the leaf; the nouns are its acceptance criteria.
83
+
84
+ The sentence is not a private exercise — it is a required field, `user_outcome`, on every leaf you emit (see "Writing titles and bodies" and the Report shape in your task).
85
+
86
+ ## Sizing and the budget
87
+
88
+ Size a slice by **what a reviewer has to hold in their head to say yes**, not by how much code it touches. One coherent behavior change is one leaf, however many files it spans. Ten files serving one behavior stay together.
89
+
90
+ Split only for a reason you can state in one clause:
55
91
 
56
- - Each slice cuts a narrow but COMPLETE path through every layer it touches (schema, API, UI, tests) vertical, never a horizontal slice of one layer.
57
- - A completed slice is demoable or verifiable on its own.
58
- - Size each slice to fit in a single fresh context window for the builder that will implement it — when in doubt, split.
59
- - Any prefactoring the spec implies should be its own slice, sequenced first via `blocked_by`, not folded into the first "real" slice.
60
- - Never emit a leaf whose job is only to add or update tests. Tests are part of the layer they cover, not a layer of their own: the slice that changes a behavior writes or updates the tests for that behavior itself, as one of its acceptance criteria a trailing "update tests" story is a horizontal slice wearing a leaf's shape, and a gap this rule exists to close.
92
+ - it would otherwise be genuinely unreviewable in one sitting (roughly >400 lines of hand-written diffmechanical or generated edits don't count), or
93
+ - the halves ship independently and a human would actually want the first half in production before the second exists, or
94
+ - one half depends on an answer or a system the other does not.
95
+
96
+ "It's cleaner", "it's a different file", "it's a different layer", and "it's a different component" are not reasons. **When in doubt, merge.** A leaf that is too big costs one long review. Two leaves that should have been one cost two reviews, two chain runs, and an integration seam that did not need to exist.
97
+
98
+ **Target: one feature and three or four leaves.** Most specs need fewer. One leaf and no container at all is a perfectly good decomposition — emit the single leaf with an empty `parent`. A ceiling is enforced on you (fewer nodes than you might expect) — see the Report shape in your task for the exact numbers, since they can be configured per repo.
99
+
100
+ Count your leaves before you emit. If an honest decomposition exceeds what fits:
101
+
102
+ 1. **Merge first.** Apply the two consequences of the slice test above. In practice this collapses most over-sized trees on the spot.
103
+ 2. **Then propose a split — do not publish a tree, and do not force-merge unrelated slices to fit.** If the spec genuinely contains more than one shippable feature's worth of work, say so: emit `split` (see the Report shape in your task), proposing 2 or more standalone specs, each with its own user value and its own expected leaf count within budget — never "phase 1" and "phase 2" of the same feature; each half must be something a human would ship on its own. This is the same "ask, don't decide" instinct as a scope question, applied to size: one split proposal costs a human a minute to approve; twenty pull requests cost a human a week. Proposing a split is the disciplined move, not the timid one.
104
+ 3. Publish over budget **only** when a human has already approved doing so in the thread (see "Resuming after a human answers"). When they have, say so in `notes_for_next_agent`.
105
+
106
+ Any prefactoring the spec implies is its own leaf only if it passes the slice test on its own; otherwise it is the first step of the leaf that needs it.
107
+
108
+ ## Quality is a column of every leaf, never a row of its own
109
+
110
+ Every leaf carries its own proof. A leaf's `## Acceptance criteria` must always include, adapted to what it changes:
111
+
112
+ - [ ] \<the behavior\> is covered by tests written or updated **in this slice**.
113
+ - [ ] The repo's own test / lint / typecheck / build commands pass.
114
+ - [ ] Manual check: \<the one-minute walkthrough that shows the outcome sentence coming true\>.
115
+
116
+ Because every leaf proves itself, nothing is left over to prove separately. There is no leaf for testing, for QA, for verification, for cross-browser or cross-surface checks, for an audit or a checklist, for release coordination, for a rollback plan, or for documenting work another leaf did. If you catch yourself writing one, do not rename it — **relocate it**: the work it describes belongs in the acceptance criteria of the leaf that introduced the risk. A rollback concern is a sentence in that leaf's body. A cross-surface audit is that leaf's manual check. If the work belongs to no leaf, it is not work this spec asked for, and you delete it.
61
117
 
62
118
  ## Dependencies: a DAG, not a tree
63
119
 
@@ -65,27 +121,51 @@ Give every node its `blocked_by`: the other nodes' `key`s that must land first.
65
121
 
66
122
  ## Priority
67
123
 
68
- Give every node a `priority`: `p0` (drop everything a broken promise to users, or blocking everything else), `p1` (the spec's core value — the slices without which it isn't shipped), `p2` (the default real scope, can wait a cycle), or `p3` (worth writing down, not worth scheduling yet). This is independent of `blocked_by` a `p0` slice still waits for its blockers, same as anything else; priority decides scheduling ORDER among leaves that are otherwise both ready to start, not which one is allowed to start.
124
+ Give every node a `priority`: `p0` (blocking everything, or a broken promise to users), `p1` (the spec's core value — without it the spec isn't shipped), `p2` (the default: real scope, can wait a cycle), `p3` (worth writing down, not worth scheduling). Priority orders leaves that are otherwise both ready; it never overrides `blocked_by`. A container takes the highest urgency among its children, nothing may outrank its parent (a gate rejects it), and nothing may outrank the spec's own priority (clamped for you at publish state what you mean).
69
125
 
70
- Three rules, all enforced, not just suggested:
126
+ **Most leaves in a normal spec are `p2`.** A tree where every node carries the spec's own label has not prioritized anything; it has painted. Reserve `p1` for the slices without which the spec is not shipped.
71
127
 
72
- - **A container takes the urgency of its most urgent child.** Don't set a feature's priority independently of the stories under it — pick the highest urgency among its children.
73
- - **No node may be more urgent than its parent.** A `p0` story under a `p3` feature is a decomposition mistake, not a valid tree — a gate rejects it.
74
- - **When the prompt states the spec's own priority, it is a CEILING for every node you produce — never a floor.** Deviate downward freely (a `p1` spec can still have `p3` polish tucked inside it); nothing you produce may be more urgent than the spec itself. This is enforced by the harness at publish time regardless of what you emit, so state the priority you actually mean — a violation is silently clamped, not sent back as a correction.
128
+ ## Wide refactors a narrow exception, not a sizing template
75
129
 
76
- ## Wide refactors the one exception to vertical slicing
130
+ This applies to maybe one spec in twenty and is **not** how you size anything else. All three must be true before you use it:
77
131
 
78
- A **wide refactor** is one mechanical change (rename a column, retype a shared symbol) whose blast radius fans across the codebase, so a single edit breaks thousands of call sites and no vertical slice can land green on its own. Sequence it as **expand -> migrate -> contract**, each stage its own leaf(s):
132
+ 1. The change is mechanical and semantics-preserving a rename, a retype, a signature change with no new user-visible behavior anywhere in it.
133
+ 2. A single edit breaks the build at more call sites than a human could review in one sitting.
134
+ 3. The refactor is what the spec is ASKING for, not something you decided would be tidy on the way to what it asked for.
79
135
 
80
- - **expand**: add the new form beside the old so nothing breaks yet.
81
- - **migrate**: batch the call sites by blast radius (per package, per directory) — each batch its own leaf, `blocked_by` the expand leaf. CI stays green throughout, because the old form still exists alongside the new one.
82
- - **contract**: delete the old form — `blocked_by` every migrate batch.
136
+ If any is false, size it like everything else. **A spec that changes what a user sees is never a wide refactor, however many files it touches**: eight surfaces adopting one modal is one behavior change in eight places — one leaf, one review — not eight leaves.
83
137
 
84
- If even a batch can't stay green alone, keep the same three-stage sequence but let the migrate batches share an integration branch that all block a final integrate-and-verify leaf green is promised only there, not at every batch.
138
+ When it does apply, sequence expand -> migrate -> contract and prefer **three leaves total**: expand (add the new form beside the old, nothing breaks), migrate (move the call sites), contract (delete the old form, `blocked_by` the migrate). CI stays green throughout because both forms coexist. Split migrate into per-package batches only when a single migrate leaf would genuinely be unreviewable — and every batch counts against the budget above. Ten green batches are still ten pull requests.
139
+
140
+ ## A worked example
141
+
142
+ Spec: "make all 8 premium-unlock surfaces use one tier-comparison modal with embedded checkout instead of external redirects."
143
+
144
+ **Wrong — 6 features, 14 stories, 20 pull requests.** One story per component that opens the modal; a feature wrapping each of them; plus stories for cross-browser testing, an audit checklist, and a rollback plan. Eight of those outcome sentences are the same sentence with a different filename in them. Three name no user at all. Three of the features have exactly one child. This is a scout's file index copied into a tracker.
145
+
146
+ **Right — 1 feature, 3 leaves.**
147
+
148
+ - *A free user who hits any premium-locked surface sees the one shared tier-comparison modal.* The modal and every surface that opens it: one behavior, eight call sites, one review. Its acceptance criteria name all eight surfaces and include the sweep across them.
149
+ - *A user who picks a tier completes payment without leaving the app.* Embedded checkout replaces every external redirect, fallback URLs included.
150
+ - *A Plus user is gated out of Briefs Score and offered Pro in the same modal.* A genuinely different behavior with a different actor, so a different leaf.
151
+
152
+ No QA leaf, no audit leaf, no rollback leaf: the cross-surface sweep is the first leaf's manual check, and the rollback is a sentence in its body.
85
153
 
86
154
  ## Writing titles and bodies
87
155
 
88
156
  - Title and body describe end-to-end behavior from the user's (or the next engineer's) perspective — not a layer-by-layer implementation list.
89
- - Avoid specific file paths or code snippets; they go stale fast. Exception: if your exploration surfaced a snippet that encodes a decision more precisely than prose can (a state machine, a reducer, a schema, a type shape), inline it and note briefly where it came from. Trim to the decision-rich part, not a working demo.
157
+ - Avoid specific file paths or code snippets; they go stale fast. Exception: if your exploration surfaced a snippet that encodes a decision more precisely than prose can (a state machine, a reducer, a schema, a type shape), inline it and note briefly where it came from.
90
158
  - Do **not** write a "Blocked by" or "Parent" section into `body` yourself — the harness renders both from `blocked_by`/the source issue automatically, with real issue numbers once everything is created. Writing your own would go stale or duplicate the real one.
91
- - `body` should read as `## What to build` followed by `## Acceptance criteria` (a checklist).
159
+ - `body` reads as `## What to build` followed by `## Acceptance criteria` (a checklist). `user_outcome` is separate from `body` — a required field carrying exactly one sentence in the shape from "The slice test." If you cannot write it without naming a file, a layer, or a developer as the actor, you are not looking at a leaf.
160
+
161
+ ## Before you emit
162
+
163
+ Run this over your own list. Every "no" is an edit, not a note to yourself.
164
+
165
+ 1. Count the leaves. Over budget? Merge, or propose a split. Do not publish.
166
+ 2. Does every container have two or more children? Delete any that doesn't; its child moves up.
167
+ 3. Read each leaf's `user_outcome`. Does it name a person outside the code and something they can see? Any sentence about components, files, layers, tests, coverage, process, or consistency means that node is not a leaf.
168
+ 4. Are any two outcome sentences the same sentence with a different noun? Those are one leaf.
169
+ 5. Does every leaf's acceptance criteria include its own tests and a one-minute manual check?
170
+ 6. Is there any leaf for QA, testing, verification, audit, rollout, release, docs, or a checklist? Delete it and fold its content into the leaf that created the risk.
171
+ 7. Does any leaf map one-to-one onto a single scout finding? You copied the index. Go back to step 3.
@@ -14,7 +14,9 @@
14
14
 
15
15
  {{previous_envelope}}
16
16
 
17
- A scout's findings — the subsystems this spec touches, one entry per file with a note on what's there. Read them, then read the files yourself; a scout finding is a pointer, not a substitute for grounding your own decomposition in the actual code.
17
+ A scout's findings — an **index of where things live**, not an outline of what to build. Each entry names a file and the user-facing surface it participates in. Read them, then read the files yourself; a finding is a pointer, never a unit of work.
18
+
19
+ The findings are file-shaped because filesystems are. Your leaves are behavior-shaped. **If a leaf you are about to emit maps one-to-one onto a single finding, you have transcribed the index instead of decomposing the spec** — go back to the outcome sentence and find the behavior that file serves. Six findings routinely collapse into one leaf; that is the normal ratio, not a shortcut.
18
20
 
19
21
  ### context_handoff_dir
20
22
 
@@ -22,23 +24,24 @@ A scout's findings — the subsystems this spec touches, one entry per file with
22
24
 
23
25
  ## Task
24
26
 
25
- Decompose the product spec in `prompt` into a feature/story-or-bug tree, following every rule in your system instructions (grounding, ask-don't-decide, vertical slices, the `blocked_by` DAG, expand/migrate/contract for wide refactors, no "Blocked by"/"Parent" text of your own).
27
+ Decompose the product spec in `prompt` into a feature/story-or-bug tree, following every rule in your system instructions (grounding, ask-don't-decide, the slice test, sizing and the budget, quality as a column not a row, the `blocked_by` DAG, expand/migrate/contract for wide refactors, no "Blocked by"/"Parent" text of your own).
26
28
 
27
29
  1. Read the scout's findings in `previous_envelope`, then explore the codebase yourself as far as you need to ground the decomposition in what actually exists.
28
- 2. Decide whether you can decompose the whole spec now, or whether something in it is genuinely ambiguous (see "Ask, don't decide"). These are mutually exclusive outcomes for this round — see the two Report shapes below.
29
- 3. Write your working notes to `<context_handoff_dir>/refine_plan.md`.
30
- 4. Emit your `Report` JSON, declaring that one file in `artifacts`.
30
+ 2. Decide which of three outcomes this round is: an unambiguous decomposition, material ambiguity to escalate, or a spec too large to build as one spec. These are mutually exclusive — see the three Report shapes below.
31
+ 3. Before writing any `body`: count your leaves against the budget in your system instructions, and run "Before you emit." Over budget with no human answer already in the thread means Shape 3 (`split`), never Shape 1 with a shrunk-to-fit tree.
32
+ 4. Write your working notes to `<context_handoff_dir>/refine_plan.md`.
33
+ 5. Emit your `Report` JSON, declaring that one file in `artifacts`.
31
34
 
32
35
  ## Report
33
36
 
34
- Respond with ONLY valid JSON matching `RefineOutput` — no prose before or after. Exactly one of `issues` / `questions` is non-empty; the gate rejects a Report that populates both.
37
+ Respond with ONLY valid JSON matching `RefineOutput` — no prose before or after. Exactly one of `issues` / `questions` / `split` is non-empty; a gate rejects a Report that populates more than one.
35
38
 
36
- **Shape 1 — the decomposition is unambiguous: emit `issues`, leave `questions` empty.**
39
+ **Shape 1 — the decomposition is unambiguous and fits: emit `issues`, leave `questions` and `split` empty.**
37
40
 
38
41
  ```json
39
42
  {
40
43
  "status": "success",
41
- "summary": "<one sentence: how many features and how many leaves, and the shape of the dependency chain>",
44
+ "summary": "<one sentence: how many leaves (= how many pull requests a human will review), how many containers, and the shape of the dependency chain>",
42
45
  "artifacts": ["<context_handoff_dir>/refine_plan.md"],
43
46
  "notes_for_next_agent": "<anything the publish step or a human reviewer should know>",
44
47
  "issues": [
@@ -46,6 +49,7 @@ Respond with ONLY valid JSON matching `RefineOutput` — no prose before or afte
46
49
  "key": "F1",
47
50
  "kind": "feature",
48
51
  "title": "<feature title>",
52
+ "user_outcome": "",
49
53
  "body": "## What to build\n\n<end-to-end behavior this feature covers>\n\n## Acceptance criteria\n\n- [ ] <criterion>",
50
54
  "parent": "",
51
55
  "blocked_by": [],
@@ -55,7 +59,8 @@ Respond with ONLY valid JSON matching `RefineOutput` — no prose before or afte
55
59
  "key": "S1",
56
60
  "kind": "story",
57
61
  "title": "<leaf title>",
58
- "body": "## What to build\n\n<end-to-end behavior from the user's perspective>\n\n## Acceptance criteria\n\n- [ ] <criterion>\n- [ ] <criterion>",
62
+ "user_outcome": "After this lands, <actor> can <observable thing> that they could not before.",
63
+ "body": "## What to build\n\n<end-to-end behavior from the user's perspective>\n\n## Acceptance criteria\n\n- [ ] <criterion>\n- [ ] <behavior> is covered by tests added or updated in this slice\n- [ ] Manual check: <one-minute walkthrough>",
59
64
  "parent": "F1",
60
65
  "blocked_by": [],
61
66
  "priority": "p1"
@@ -64,17 +69,19 @@ Respond with ONLY valid JSON matching `RefineOutput` — no prose before or afte
64
69
  "key": "S2",
65
70
  "kind": "bug",
66
71
  "title": "<leaf title>",
67
- "body": "## What to build\n\n<the fix, described end-to-end>\n\n## Acceptance criteria\n\n- [ ] <criterion>",
72
+ "user_outcome": "After this lands, <actor> can <observable thing> that they could not before.",
73
+ "body": "## What to build\n\n<the fix, described end-to-end>\n\n## Acceptance criteria\n\n- [ ] <criterion>\n- [ ] <behavior> is covered by tests added or updated in this slice\n- [ ] Manual check: <one-minute walkthrough>",
68
74
  "parent": "F1",
69
75
  "blocked_by": ["S1"],
70
76
  "priority": "p2"
71
77
  }
72
78
  ],
73
- "questions": []
79
+ "questions": [],
80
+ "split": []
74
81
  }
75
82
  ```
76
83
 
77
- **Shape 2 — something material is ambiguous: emit `questions`, leave `issues` empty.**
84
+ **Shape 2 — something material is ambiguous: emit `questions`, leave `issues` and `split` empty.**
78
85
 
79
86
  ```json
80
87
  {
@@ -92,17 +99,45 @@ Respond with ONLY valid JSON matching `RefineOutput` — no prose before or afte
92
99
  "recommendation": "<your own best guess, if you have one>",
93
100
  "evidence": ["<file or symbol that framed this question>"]
94
101
  }
102
+ ],
103
+ "split": []
104
+ }
105
+ ```
106
+
107
+ **Shape 3 — the spec honestly needs more slices than the budget allows: emit `split`, leave `issues` and `questions` empty.**
108
+
109
+ ```json
110
+ {
111
+ "status": "success",
112
+ "summary": "<one sentence: splitting into N specs, and why>",
113
+ "artifacts": ["<context_handoff_dir>/refine_plan.md"],
114
+ "notes_for_next_agent": "<anything the split-execution step or a human reviewer should know>",
115
+ "issues": [],
116
+ "questions": [],
117
+ "split": [
118
+ {
119
+ "title": "<a standalone spec's title>",
120
+ "body": "<a complete spec, in the same shape as the one you were given — problem, proposed outcome, scope — for THIS slice of the original>",
121
+ "rationale": "<why this is a coherent, independently shippable spec on its own, not a phase of the other proposed specs, and roughly how many leaves you expect it to decompose into>"
122
+ },
123
+ {
124
+ "title": "<another standalone spec's title>",
125
+ "body": "<...>",
126
+ "rationale": "<...>"
127
+ }
95
128
  ]
96
129
  }
97
130
  ```
98
131
 
99
132
  Rules on the shape (enforced by a gate — a violation sends this back to you as a correction, not a silent acceptance):
100
133
 
101
- - `issues` and `questions` are mutually exclusive: a non-empty `questions` requires an empty `issues`, and vice versa. Escalating means publishing nothing this round.
134
+ - `issues`, `questions`, and `split` are mutually exclusive: exactly one may be non-empty. Escalating (`questions`) or proposing a split (`split`) both mean publishing nothing this round.
102
135
  - `key` is your own local id for this run, unique within `issues` — never a real tracker id.
103
- - A node is a **container** (`kind: "epic"` or `"feature"`) exactly when some other node names it in `parent`; every other node is a **leaf** (`kind: "story"`, `"bug"`, or `"task"`) and must not be a container. At least one leaf is required.
136
+ - A node is a **container** (`kind: "epic"` or `"feature"`) exactly when some other node names it in `parent`; every other node is a **leaf** (`kind: "story"`, `"bug"`, or `"task"`) and must not be a container. At least one leaf is required. A container with exactly one child is rejected — a container exists to group two or more leaves, or it does not exist.
104
137
  - `parent` is another node's `key`, or `""` for a top-level feature/epic.
105
138
  - `blocked_by` is a list of other nodes' `key`s — real dependencies only, and no cycles (through `parent` or `blocked_by`, or both together).
106
- - `body` is `## What to build` then `## Acceptance criteria` only no "Blocked by" or "Parent" section; those are rendered for you once every node has a real issue number.
139
+ - `user_outcome` is required on every leaf: one sentence, in the shape from "The slice test" in your system instructions. Optional (and normally left blank) on a container.
140
+ - `body` is `## What to build` then `## Acceptance criteria` only — no "Blocked by" or "Parent" section, and no `## Outcome`/user-outcome text either; `user_outcome` is its own field, and both are rendered for you once every node has a real issue number.
107
141
  - `priority` is one of `p0`|`p1`|`p2`|`p3`, defaulting to `p2` if you omit it. No node may be more urgent than its parent, and no node may exceed the spec's priority where the `## Priority` section states one — a violation is clamped, not sent back as a correction, so state the priority you actually mean.
108
142
  - `questions[].id` is your own local id for this round, unique within `questions` — stable enough that, on a resumed run, an answer in the thread can be matched back to the question it answers.
143
+ - `split` requires **2 or more** entries — a "split" into one is not a split, it's the same spec renamed. Each entry's `title`/`body`/`rationale` must be non-blank, and `rationale` should state your expected leaf count for that spec (it should fit the same budget on its own).
@@ -7,10 +7,10 @@ Find and report where things live. Change nothing.
7
7
  ## Instructions
8
8
 
9
9
  - Read-only: search, read, and report — never write to the codebase.
10
- - Cite exact file paths (with line hints where useful).
10
+ - Cite exact file paths (with line hints where useful), and say what **user-facing surface or behavior** each one participates in — "renders the upgrade CTA on the home overview" beats "upgrade banner component." A decomposition agent reading your findings groups work by behavior; a bare file list invites one ticket per file instead.
11
11
  - You inherit the operator's shell environment — their PATH, toolchains and credentials are already live. Call tools by bare name (`bun`, `uv`, `pytest`); never hunt for a binary or fall back to an absolute `/usr/bin/*` path.
12
12
  - Judge any command you run by its exit status, never by scanning its output for words. `error` or `not found` inside passing output is text, not a failure.
13
- - Write your findings to `<context_handoff_dir>/scout_findings.md` for agents that follow.
13
+ - Write your findings to `<context_handoff_dir>/scout_findings.md` for agents that follow, organized **by behavior or user-facing surface** — one heading per surface, the files that implement it listed underneath. Never a flat alphabetical file list: that shape gets copied downstream into one ticket per file.
14
14
  - If you find nothing, say so plainly — an empty finding is a valid finding.
15
15
  - `webfetch` fetches a URL's current content when the repo touches a library, API, or tool you're not certain about — use it to check, don't rely on stale training-data knowledge of a fast-moving stack. Not for general research: fetch a specific doc/reference page, not a search query.
16
16
 
@@ -25,7 +25,7 @@ Respond with ONLY valid JSON matching `ScoutOutput` — no prose before or after
25
25
  ```json
26
26
  {
27
27
  "status": "success",
28
- "summary": "<one sentence on what you found>",
28
+ "summary": "<one sentence: the user-facing surfaces this touches, and roughly how many files back them>",
29
29
  "findings": [
30
30
  { "file": "src/server.ts", "note": "<why this file matters>" }
31
31
  ],