@gr8ful/spf 0.12.0 → 0.13.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 +104 -28
- package/assets/prompts/refiner/system.md +105 -25
- package/assets/prompts/refiner/user.md +50 -15
- package/assets/prompts/scout/system.md +2 -2
- package/assets/prompts/scout/user.md +1 -1
- package/assets/skill/references/config.md +27 -8
- package/assets/templates/ts.spf.config.yaml +11 -4
- package/dist/chains/steps.d.ts +14 -14
- package/dist/chains/steps.js +44 -25
- package/dist/cli/commands/doctor.js +6 -0
- package/dist/cli/commands/watch.js +33 -11
- package/dist/core/data_types.d.ts +199 -1
- package/dist/core/data_types.js +127 -1
- package/dist/core/gates.d.ts +24 -8
- package/dist/core/gates.js +175 -20
- package/dist/core/issues/github_provider.d.ts +10 -5
- package/dist/core/issues/github_provider.js +13 -2
- package/dist/core/issues/jira_provider.d.ts +3 -3
- package/dist/core/issues/jira_provider.js +2 -0
- package/dist/core/issues/provider.d.ts +48 -4
- package/dist/core/notify/channel.d.ts +1 -1
- package/dist/core/refine.d.ts +35 -1
- package/dist/core/refine.js +56 -1
- package/dist/core/watch.d.ts +65 -9
- package/dist/core/watch.js +157 -11
- package/package.json +1 -1
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
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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.
|
|
509
|
-
|
|
510
|
-
|
|
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
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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)
|
|
586
|
-
|
|
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>:
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
—
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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
|
|
|
@@ -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
|
-
|
|
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
|
-
|
|
43
|
+
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).
|
|
38
44
|
|
|
39
|
-
- `why_it_matters` —
|
|
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
|
-
##
|
|
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
|
-
-
|
|
57
|
-
-
|
|
58
|
-
-
|
|
59
|
-
|
|
60
|
-
|
|
92
|
+
- it would otherwise be genuinely unreviewable in one sitting (roughly >400 lines of hand-written diff — mechanical 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` (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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`
|
|
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 —
|
|
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,
|
|
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
|
|
29
|
-
3.
|
|
30
|
-
4.
|
|
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;
|
|
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
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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 `
|
|
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
|
-
- `
|
|
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
|
|
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
|
],
|