@gr8ful/spf 0.5.1 → 0.7.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.
Files changed (101) hide show
  1. package/README.md +168 -29
  2. package/assets/defaults/spf.config.yaml +68 -0
  3. package/assets/prompts/refiner/system.md +42 -6
  4. package/assets/prompts/refiner/user.md +46 -8
  5. package/assets/skill/SKILL.md +1 -0
  6. package/assets/skill/references/config.md +192 -5
  7. package/assets/templates/ts-flue-ollama.spf.config.yaml +25 -0
  8. package/assets/templates/ts.spf.config.yaml +12 -2
  9. package/dist/chains/index.d.ts +11 -0
  10. package/dist/chains/index.js +38 -3
  11. package/dist/chains/repo_chains.js +1 -0
  12. package/dist/chains/simple_sdlc.js +1 -1
  13. package/dist/chains/steps.d.ts +8 -17
  14. package/dist/chains/steps.js +102 -14
  15. package/dist/cli/commands/doctor.js +112 -3
  16. package/dist/cli/commands/estimate.d.ts +82 -0
  17. package/dist/cli/commands/estimate.js +317 -0
  18. package/dist/cli/commands/fanout.d.ts +40 -0
  19. package/dist/cli/commands/fanout.js +401 -0
  20. package/dist/cli/commands/init.js +23 -1
  21. package/dist/cli/commands/run.js +9 -2
  22. package/dist/cli/commands/trace.d.ts +18 -0
  23. package/dist/cli/commands/trace.js +22 -3
  24. package/dist/cli/commands/watch.d.ts +8 -0
  25. package/dist/cli/commands/watch.js +95 -33
  26. package/dist/cli/index.js +13 -1
  27. package/dist/cli/interview.js +9 -5
  28. package/dist/core/agents.d.ts +56 -0
  29. package/dist/core/agents.js +152 -1
  30. package/dist/core/data_types.d.ts +340 -5
  31. package/dist/core/data_types.js +185 -5
  32. package/dist/core/fanout.d.ts +229 -0
  33. package/dist/core/fanout.js +313 -0
  34. package/dist/core/gates.d.ts +8 -0
  35. package/dist/core/gates.js +48 -2
  36. package/dist/core/git_helper.d.ts +98 -0
  37. package/dist/core/git_helper.js +127 -0
  38. package/dist/core/issues/github_provider.d.ts +46 -6
  39. package/dist/core/issues/github_provider.js +120 -5
  40. package/dist/core/issues/jira_provider.d.ts +83 -12
  41. package/dist/core/issues/jira_provider.js +110 -4
  42. package/dist/core/issues/provider.d.ts +124 -19
  43. package/dist/core/issues/provider.js +24 -7
  44. package/dist/core/notify/channel.d.ts +1 -1
  45. package/dist/core/notify/notifier.d.ts +16 -0
  46. package/dist/core/notify/notifier.js +36 -0
  47. package/dist/core/otel.d.ts +64 -9
  48. package/dist/core/otel.js +78 -14
  49. package/dist/core/refine.d.ts +45 -8
  50. package/dist/core/refine.js +98 -24
  51. package/dist/core/runner.d.ts +8 -0
  52. package/dist/core/runner.js +7 -0
  53. package/dist/core/session.d.ts +25 -0
  54. package/dist/core/session.js +97 -28
  55. package/dist/core/tiering.d.ts +145 -0
  56. package/dist/core/tiering.js +235 -0
  57. package/dist/core/tracer.d.ts +10 -0
  58. package/dist/core/tracer.js +12 -0
  59. package/dist/core/watch.d.ts +174 -11
  60. package/dist/core/watch.js +531 -33
  61. package/dist/ui/server/db.d.ts +39 -0
  62. package/dist/ui/server/db.js +61 -0
  63. package/package.json +2 -1
  64. package/dist/test/agent_cc.test.d.ts +0 -1
  65. package/dist/test/agent_cc.test.js +0 -95
  66. package/dist/test/agent_flue.test.d.ts +0 -1
  67. package/dist/test/agent_flue.test.js +0 -83
  68. package/dist/test/chains.test.d.ts +0 -12
  69. package/dist/test/chains.test.js +0 -92
  70. package/dist/test/data_types.test.d.ts +0 -10
  71. package/dist/test/data_types.test.js +0 -220
  72. package/dist/test/env_file.test.d.ts +0 -1
  73. package/dist/test/env_file.test.js +0 -74
  74. package/dist/test/fake_asker.d.ts +0 -23
  75. package/dist/test/fake_asker.js +0 -30
  76. package/dist/test/git_helper.test.d.ts +0 -1
  77. package/dist/test/git_helper.test.js +0 -59
  78. package/dist/test/hermetic_git.d.ts +0 -1
  79. package/dist/test/hermetic_git.js +0 -22
  80. package/dist/test/init_command.test.d.ts +0 -14
  81. package/dist/test/init_command.test.js +0 -136
  82. package/dist/test/interview.test.d.ts +0 -15
  83. package/dist/test/interview.test.js +0 -425
  84. package/dist/test/notify.test.d.ts +0 -1
  85. package/dist/test/notify.test.js +0 -174
  86. package/dist/test/ollama_provider.test.d.ts +0 -1
  87. package/dist/test/ollama_provider.test.js +0 -103
  88. package/dist/test/otel.test.d.ts +0 -26
  89. package/dist/test/otel.test.js +0 -512
  90. package/dist/test/paths.test.d.ts +0 -1
  91. package/dist/test/paths.test.js +0 -68
  92. package/dist/test/refine.test.d.ts +0 -1
  93. package/dist/test/refine.test.js +0 -189
  94. package/dist/test/repo_chains.test.d.ts +0 -21
  95. package/dist/test/repo_chains.test.js +0 -416
  96. package/dist/test/signoff.test.d.ts +0 -1
  97. package/dist/test/signoff.test.js +0 -329
  98. package/dist/test/ui_server.test.d.ts +0 -7
  99. package/dist/test/ui_server.test.js +0 -120
  100. package/dist/test/watch.test.d.ts +0 -1
  101. package/dist/test/watch.test.js +0 -687
package/README.md CHANGED
@@ -369,7 +369,7 @@ the Bitbucket identifier — finds no matching issues and looks exactly like
369
369
  nothing being configured at all.
370
370
 
371
371
  ```bash
372
- spf watch init # idempotently seed tracker state (no-op for Jira — see below); run this first
372
+ spf watch init # idempotently seed tracker state (labels are a no-op report on Jira — see below); run this first
373
373
  spf watch # foreground daemon; Ctrl-C drains in-flight claims first
374
374
  spf watch --once # one poll tick, then exit — good for cron
375
375
  spf watch --dry-run # log intended claims/transitions, mutate nothing
@@ -377,19 +377,32 @@ spf watch --dry-run # log intended claims/transitions, mutate nothing
377
377
 
378
378
  No GitHub App, no webhook, no Jira/Bitbucket app install — it's a plain REST poll against whichever combination is configured, same philosophy as the trace db's own polling contract. See [`assets/templates/`](assets/templates/) for full worked configs (also usable directly via `spf init --template <name>`), and `spf install-skill`'s installed skill (`roster.md`, `references/config.md`) for the field-by-field reference.
379
379
 
380
+ ### Priority, dependencies, and picking what's next
381
+
382
+ Among every `<prefix>:ready` issue, `spf watch` claims in this order:
383
+
384
+ 1. **Priority** — the `<prefix>:priority:p0|p1|p2|p3` label (`p0` drop-everything, `p2` the default, `p3` someday). `spf watch init` seeds all four. A human relabeling an issue is the whole override mechanism — there's no separate priority command.
385
+ 2. **Sibling affinity** — among issues at the same priority, one whose parent feature already has a sibling in flight goes first. This is what tends to finish one feature before starting the next without giving up the one-PR-per-story design (see "Refining specs" below): each story is still its own claim, its own worktree, its own PR — affinity only orders which claim happens next.
386
+ 3. **Creation order** (oldest first) — the final tiebreaker.
387
+
388
+ Before claiming anything, `spf watch` also checks the **frontier**: an issue's `blocked_by` dependencies (set by the refine lane, or by hand) must all carry `<prefix>:done` first. A leaf whose blockers aren't done yet is skipped, not blocked — it's simply reconsidered next tick, once the log line naming what it's waiting on stops applying.
389
+
390
+ This is a **label**, not this repo's own GitHub Projects v2 "Priority" field (if your board has one — Urgent/High/Medium/Low, say). `spf watch` never reads or writes Projects v2: no GraphQL, no `project` token scope, no Jira equivalent. If you use both, they're independent — nothing reconciles them, and `spf watch` obeys only the label. Keep them aligned yourself, or don't use the board field for this repo's issues.
391
+
380
392
  ### Refining specs (`watch.refine`)
381
393
 
382
394
  A product spec isn't individually workable — it needs to become a feature,
383
395
  broken down into user stories and bugs, before the build lane above has
384
396
  anything to claim. `watch.refine` is a second lane over the same poll loop
385
397
  that does exactly that: it polls `<prefix>:spec-ready`, runs a decomposition
386
- chain (`refine` by default) against the spec in its own worktree, and
387
- publishes what it produces as real tracker issues — a feature/epic container
388
- plus story/bug/task leaves, linked via GitHub's native sub-issue hierarchy.
398
+ chain (`refine` by default `request scout refiner publish`) against
399
+ the spec in its own worktree, and publishes what it produces as real tracker
400
+ issues — a feature/epic container plus story/bug/task leaves, linked into a
401
+ real hierarchy: GitHub's native sub-issues API, or Jira's `parent` field.
389
402
 
390
403
  ```yaml
391
404
  watch:
392
- issue_provider: github # required — issue authoring isn't implemented for Jira yet
405
+ issue_provider: github # or jira both implement issue authoring
393
406
  repo: owner/name
394
407
  refine:
395
408
  enabled: true
@@ -397,31 +410,151 @@ watch:
397
410
  concurrency: 1 # this lane's own budget, independent of watch.concurrency
398
411
  ```
399
412
 
413
+ On Jira, every `RefinedIssue.kind` (`epic`/`feature`/`story`/`bug`/`task`) maps
414
+ to a real Jira issue type through `watch.jira.issue_types` — defaults
415
+ `epic`/`feature` → `Epic`, `story` → `Story`, `bug` → `Bug`, `task` → `Task`,
416
+ overridable per kind since real projects rename or customize these:
417
+
418
+ ```yaml
419
+ watch:
420
+ issue_provider: jira
421
+ jira:
422
+ base_url: https://your-domain.atlassian.net
423
+ project_key: PROJ
424
+ issue_types: # optional — shown are the defaults
425
+ epic: Epic
426
+ feature: Epic
427
+ story: Story
428
+ bug: Bug
429
+ task: Task
430
+ refine:
431
+ enabled: true
432
+ ```
433
+
434
+ Hierarchy uses Jira's modern `parent` field only (no legacy "Epic Link"
435
+ custom-field support) — this works on team-managed projects and on
436
+ company-managed projects with Jira's current issue-hierarchy setting; a
437
+ project not configured for it surfaces Jira's own API error, unmodified.
438
+ One accepted platform limitation: Jira doesn't support Epic-under-Epic
439
+ nesting the way GitHub's sub-issues API supports up to 8 levels, so a
440
+ `feature` node parented under another `epic`/`feature` (both `Epic` by
441
+ default) will fail at publish time on Jira specifically — a real platform
442
+ difference, not a bug. Both `spf watch init` and `spf watch`'s own startup
443
+ check validate `watch.jira.issue_types` against the real project before
444
+ anything unattended runs — see "Jira" below.
445
+
400
446
  Every generated issue carries a `<prefix>:type:epic|feature|story|bug|task`
401
- label. A container (a feature/epic something else names it as `parent`)
402
- gets only that label; a leaf additionally gets `<prefix>:refined` **never**
403
- `<prefix>:ready`. Promoting a leaf to `<prefix>:ready` is a deliberate human
404
- decision: the refine lane never auto-promotes anything, so a spec fanning out
405
- into twenty stories doesn't turn into twenty unattended chain runs and twenty
406
- PRs with nobody having looked at the breakdown first. Once you do promote a
407
- leaf, the existing build lane picks it up completely unchanged.
408
-
409
- The spec issue itself gets a `spec-ready refining → done`/`blocked`
410
- lifecycle, same shape as the build lane, and a summary comment listing every
411
- issue it created. Try it by hand first, against a real spec, before turning
412
- on the daemon:
447
+ label AND a `<prefix>:priority:p0|p1|p2|p3` label (see "Priority, dependencies,
448
+ and picking what's next" above) the refiner proposes the priority, a human
449
+ can relabel it before promoting. A container (a feature/epic — something
450
+ else names it as `parent`) gets only those two labels; a leaf additionally
451
+ gets `<prefix>:refined` **never** `<prefix>:ready`. Promoting a leaf to
452
+ `<prefix>:ready` is a deliberate human decision: the refine lane never
453
+ auto-promotes anything, so a spec fanning out into twenty stories doesn't
454
+ turn into twenty unattended chain runs and twenty PRs with nobody having
455
+ looked at the breakdown first. Once you do promote a leaf, the existing build
456
+ lane picks it up completely unchanged including the frontier check: a
457
+ promoted leaf isn't claimed until every issue named in its `blocked_by` is
458
+ `<prefix>:done`.
459
+
460
+ A spec issue's own `<prefix>:priority:pN` label, if it has one, reaches the
461
+ refiner as a **ceiling**: no generated node may be more urgent than the spec
462
+ itself (enforced regardless of what the refiner emits — see
463
+ `core/refine.ts`'s `publish()`). `spf refine ... --priority p1` sets the same
464
+ ceiling for a bare manual run with no spec issue to read a label from.
465
+
466
+ Grounding the decomposition in real code is structural, not just prose: a
467
+ `scout` phase maps the subsystems the spec touches before the refiner ever
468
+ runs, and its findings flow straight into the refiner as context. This makes
469
+ `scout` a required agent for the `refine` chain — a roster that pruned it
470
+ fails `spf watch` startup by name, the same as any other missing required
471
+ agent.
472
+
473
+ #### Human-in-the-loop escalation
474
+
475
+ The refiner is instructed not to guess on anything material (scope, data
476
+ model, an external dependency choice, a UX contract, a breaking change, or
477
+ anything that would contradict an existing ADR — see
478
+ `assets/prompts/refiner/system.md`'s "Ask, don't decide"). When it hits real
479
+ ambiguity it raises questions instead of publishing a partial tree, and the
480
+ spec moves through an extra loop before it's even fully published:
481
+
482
+ ```text
483
+ spec-ready → refining ──┬─→ spec-in-progress → done / blocked (published a tree, THEN tracked to completion)
484
+ └─→ needs-feedback (raised questions)
485
+ │ a human answers in the issue's comments,
486
+ │ then adds continue-refinement
487
+
488
+ refining (again, same adw_id) → ...
489
+ ```
490
+
491
+ Answer the refiner's questions as comments on the spec issue, then add the
492
+ `<prefix>:continue-refinement` label. `spf watch` resumes the **same**
493
+ `adw_id` — the refiner's own coding-agent session continues, with the
494
+ comment thread folded into its prompt — so it never re-derives context it
495
+ already had, and it can loop through as many rounds as it takes; there's no
496
+ cap. If channels are configured (see "Notifications" below), a
497
+ `spec_needs_feedback` event fires every round.
498
+
499
+ **Publishing a tree does not mean the spec is done.** A product manager
500
+ watching a spec's status needs "done" to mean the work is actually finished,
501
+ not merely that a decomposition happened — so once the refiner publishes,
502
+ the spec moves to `<prefix>:spec-in-progress`, not `<prefix>:done`. It stays
503
+ there — with a summary comment listing every issue it created, noting how
504
+ many rounds of feedback it took, if any — until **every one of those
505
+ issues** reaches `<prefix>:done` (a leaf that lands, or a feature/epic
506
+ container once `rollUp` has already finished it — see "Container roll-up"
507
+ below). Only then does `spf watch` transition the spec to `<prefix>:done`,
508
+ post a closing comment, and close it on the tracker where the tracker
509
+ supports closing at all — GitHub does (best-effort: a failed close never
510
+ turns completed work back into `blocked`); Jira doesn't have this wired up,
511
+ so a Jira spec still relabels and comments correctly, just stays open. A
512
+ `spec_done` event fires either way, distinct from the earlier
513
+ `spec_refined`. A spec whose
514
+ generated leaf never gets promoted, or whose work stalls `blocked`, simply
515
+ stays `spec-in-progress` — which is the truthful state, not a bug.
516
+
517
+ The spec issue's full lifecycle: `spec-ready → refining → needs-feedback ⇄
518
+ refining → spec-in-progress → done`/`blocked` (the `needs-feedback ⇄
519
+ refining` loop only when the refiner actually escalates). Try it by hand
520
+ first, against a real spec, before turning on the daemon:
413
521
 
414
522
  ```bash
415
- spf refine "<spec text or path/to/spec.md>" --issue 42 # --issue renders a "## Parent: #42" back-reference
523
+ spf refine "<spec text or path/to/spec.md>" --issue 42 # --issue renders a "## Parent" section, "Decomposed from #42."
524
+ spf refine "<spec text or path/to/spec.md>" --issue 42 --priority p1 # --priority clamps every generated node to p1 or less urgent
416
525
  ```
417
526
 
418
- `spf watch init` seeds the type labels alongside the state ones. This lane's
419
- prompt (`assets/prompts/refiner/`) is adapted from a "tracer-bullet ticket"
420
- decomposition skill vertical slices, a `blocked_by` dependency graph, and
421
- an expand/migrate/contract sequence for wide mechanical refactors with a
422
- gate (`gates.refinementWellFormed`) added on top to enforce the
527
+ #### Container roll-up
528
+
529
+ A feature or epic isn't itself a unit of work, so it's never claimed and
530
+ never opens a PR but it isn't abandoned once its children exist, either.
531
+ Every time a leaf's PR merges, `spf watch` checks that leaf's parent: once
532
+ **every** child under a container carries `<prefix>:done`, the container
533
+ gets a summary comment (every child it rolled up, by id), transitions to
534
+ `<prefix>:done`, and closes on the tracker if the tracker supports closing
535
+ at all (GitHub does; Jira doesn't have this wired up, so a Jira container
536
+ still relabels and comments, just stays open) — then the same check runs on
537
+ *its* parent, so an epic of features rolls up once its last feature does.
538
+ A container with even one unfinished child (including one still sitting at
539
+ `<prefix>:refined`, never promoted) is left exactly as it is; nothing times
540
+ out or force-closes it.
541
+
542
+ Roll-up needs to read back the hierarchy `publish()` created — GitHub's
543
+ native sub-issues API, or a Jira `parent = "<id>"` JQL search — so it works
544
+ on both providers today, same as authoring itself. A tracker that isn't
545
+ authoring-capable at all gets a logged no-op, not a startup failure, since
546
+ the build lane still works fine without roll-up.
547
+
548
+ `spf watch init` seeds the type labels alongside the state ones — **re-run
549
+ it** after upgrading to this version, so it can create the new
550
+ `<prefix>:needs-feedback` and `<prefix>:continue-refinement` labels. This
551
+ lane's prompt (`assets/prompts/refiner/`) is adapted from a "tracer-bullet
552
+ ticket" decomposition skill — vertical slices, a `blocked_by` dependency
553
+ graph, and an expand/migrate/contract sequence for wide mechanical refactors
554
+ — with a gate (`gates.refinementWellFormed`) added on top to enforce the
423
555
  container/leaf shape that skill left as prose convention rather than a
424
- checked rule.
556
+ checked rule, now also enforcing that a refinement never publishes issues and
557
+ raises questions in the same round.
425
558
 
426
559
  ### GitHub (`issue_provider: github` and/or `code_host: github`)
427
560
 
@@ -451,7 +584,7 @@ export JIRA_API_TOKEN=... # id.atlassian.com -> Security -> API tokens
451
584
 
452
585
  State is modeled as Jira **labels** (`<prefix>:ready`, etc.), mirroring GitHub exactly, rather than native workflow status transitions — the latter would need per-project transition-id mapping, since workflows vary by project/scheme; labels work identically everywhere with zero per-project setup. One caveat: colons are a legal Jira label character and JQL matches on them fine, but they won't show up in Jira's own label autocomplete UI — cosmetic only.
453
586
 
454
- `spf watch init` is a no-op here (Jira labels are freeform strings with no color/description registry to seed, unlike GitHub's) — it just reports the labels this run will use.
587
+ `spf watch init` still doesn't create any labels here (Jira labels are freeform strings with no color/description registry to seed, unlike GitHub's) — it reports the labels this run will use. But with `watch.refine.enabled`, it now also validates `watch.jira.issue_types` against the real project's issue types, read-only, and exits non-zero on a mismatch — see "Refining specs" above.
455
588
 
456
589
  ### Bitbucket (`code_host: bitbucket`)
457
590
 
@@ -482,10 +615,16 @@ notifications:
482
615
  ```
483
616
 
484
617
  `events` is the whole filter: `errors` sends only failed runs/phases, blocked
485
- issues, and watch errors; `all` adds every milestone run started/finished,
486
- issue claimed, PR opened, issue done. A channel's own `events` overrides the
487
- top-level scope for just that channel. `spf doctor` reports whether each
488
- configured channel's env var is set.
618
+ issues, watch errors, and a spec needing feedback (`spec_needs_feedback`
619
+ see "Human-in-the-loop escalation" above; it's `error`-level on purpose, the
620
+ same class of event as a blocked issue, so an `errors`-scope channel sees it
621
+ too); `all` adds every milestone — run started/finished, issue claimed, PR
622
+ opened, issue done, a container's roll-up (`feature_done` — see "Container
623
+ roll-up" above), and a spec reaching actual completion (`spec_done` —
624
+ distinct from `spec_refined`, which fires the moment a tree is published;
625
+ see "Human-in-the-loop escalation" above). A channel's own `events`
626
+ overrides the top-level scope for just that channel. `spf doctor` reports
627
+ whether each configured channel's env var is set.
489
628
 
490
629
  The webhook URL is a secret and lives only in `.env` — `webhook_url_env`
491
630
  names the key, never the URL itself, matching `GITHUB_TOKEN`/
@@ -52,6 +52,74 @@ observability:
52
52
  # test: [test]
53
53
  # all: [test, lint]
54
54
 
55
+ # OFF by default, and absence/false is a TOTAL no-op: every agent dispatches
56
+ # at exactly the model its roster entry names, byte-identically to before
57
+ # this key existed. Same discipline as max_run_cost/max_run_tokens
58
+ # (data_types.ts:463-491): a repo that upgrades must not silently start
59
+ # routing its phases to different models.
60
+ #
61
+ # Turning it on is a deliberate edit, or `spf init --template ts-flue-ollama`
62
+ # (the one packaged template whose two rungs are MEASURED). `spf init`'s
63
+ # interview does NOT write this block — see config.md's "tiering" section
64
+ # for why.
65
+ tiering:
66
+ enabled: false
67
+
68
+ # The ladder, WEAKEST FIRST. A SEQUENCE, not a mapping: order is the whole
69
+ # semantics here (a risk level shifts every routed role UP or DOWN this
70
+ # list), and a YAML sequence states order unambiguously where a mapping
71
+ # would make it depend on the parser preserving key order.
72
+ #
73
+ # `coding_agent` (default "flue", the same default an agent's own field
74
+ # uses) declares WHICH BACKEND'S VOCABULARY this rung's `model` speaks. It
75
+ # is not optional in spirit: a tier changes an agent's `model` and NOTHING
76
+ # else — `coding_agent` is a per-agent field and always stays the agent's
77
+ # own — so a rung can only route roles whose backend matches. See
78
+ # config.md's "tiering" section for rule T, and note that this is what
79
+ # makes "validated by the same backend branch agents.validate() uses" an
80
+ # answerable question: the branch is the TIER's declared backend.
81
+ #
82
+ # `model` then speaks EXACTLY the same vocabulary as an agent's own `model:`
83
+ # for that backend — provider/model-id for flue, Claude Code's bare alias for
84
+ # claude_code. There is deliberately no per-provider table: for flue the
85
+ # provider is already the first segment of the value.
86
+ #
87
+ # WHEN tiering.enabled, `spf doctor` runs the SAME provider-key and
88
+ # model-shape checks over these rungs that it already runs over
89
+ # cfg.agents[].model — a rung naming a provider whose key is unset is a
90
+ # green doctor and a first-dispatch failure otherwise. When tiering is
91
+ # DISABLED (which is this file's shipped state) doctor does not look at
92
+ # `tiers` at all, so shipping this populated ladder cannot fail `spf
93
+ # doctor` in a repo that never opted in.
94
+ tiers:
95
+ - { name: scout, coding_agent: flue, model: google/gemini-3.6-flash }
96
+ - { name: builder, coding_agent: flue, model: fireworks/accounts/fireworks/models/kimi-k3 }
97
+ - { name: deep, coding_agent: flue, model: openai/gpt-5.6-terra }
98
+
99
+ # Baseline tier per ROLE (an agent name). Naming an agent here IS the
100
+ # operator's statement "route this one by tier" — so the resolved tier wins
101
+ # over that agent's own `model:`. An agent NOT named here is never retiered:
102
+ # its `model:` stands, untouched. That is the whole precedence rule.
103
+ #
104
+ # A key naming an agent this repo's roster does not define is NOT a global
105
+ # error — it is only reported for a chain that actually requires that
106
+ # agent. A pruned or renamed roster must not fail every chain.
107
+ #
108
+ # NOTE for the two packaged templates that set `defaults.coding_agent:
109
+ # claude_code` (ts, ts-cc): this `roles` map and the flue-shaped `tiers`
110
+ # above survive into their effective config through key-by-key merge. That
111
+ # is harmless while `enabled: false`. Flip `enabled: true` there without
112
+ # also declaring your own ladder and validate() fails LOUDLY by name on the
113
+ # backend mismatch (rule T) rather than dispatching a `fireworks/...` id
114
+ # through Claude Code.
115
+ roles:
116
+ scout: scout
117
+ documenter: scout
118
+ planner: builder
119
+ builder: builder
120
+ refiner: builder
121
+ reviewer: deep
122
+
55
123
  agents:
56
124
  - name: planner
57
125
  model: fireworks/accounts/fireworks/models/kimi-k3
@@ -12,6 +12,38 @@ Decompose a product spec into a feature/story-or-bug tree of vertical slices the
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
13
  - Write your working notes to `<context_handoff_dir>/refine_plan.md` before emitting your Report JSON.
14
14
 
15
+ ## Grounding: tie every slice to real code
16
+
17
+ A scout has already mapped the subsystems this spec touches — its findings arrive as `previous_envelope`. Read them, then read the files they name yourself: a scout finding is a pointer, not a substitute for looking. Before writing a single slice, be able to name the concrete modules, functions, or types each slice will touch. Record what you read and what you concluded in `refine_plan.md`.
18
+
19
+ A slice you cannot tie to code that actually exists is not a slice — it is a guess wearing a slice's shape. Turn it into a question instead (see below).
20
+
21
+ ## Ask, don't decide
22
+
23
+ You will hit real ambiguity: places where the spec (or the codebase, or both) genuinely supports more than one answer, and picking wrong wastes real engineering time on the wrong tree. Escalate those to a human rather than deciding for them. You MUST escalate, rather than guess, anything touching:
24
+
25
+ - **Scope boundaries** — is X in or out of this decomposition.
26
+ - **Data model or schema shape** — a new field, table, or type whose shape isn't implied by what already exists.
27
+ - **Choice of external service or dependency** — which library, API, or vendor.
28
+ - **Auth or permission semantics** — who can do what.
29
+ - **User-facing contract or UX behavior** — what the feature actually does from the outside, when the spec doesn't pin it down.
30
+ - **Breaking changes and migration strategy** — whether existing behavior/data changes shape, and how the transition is sequenced.
31
+ - **Non-functional targets** — performance, scale, availability numbers the spec doesn't state.
32
+ - **Anything that would contradict an existing ADR.**
33
+
34
+ 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).
35
+
36
+ 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:
37
+
38
+ - `why_it_matters` — what goes wrong if this gets guessed instead of decided.
39
+ - `options` — the plausible answers you found, if there's a short list.
40
+ - `recommendation` — your own best guess, so "go with your recommendation" is a valid answer.
41
+ - `evidence` — the files/symbols that framed the question.
42
+
43
+ ## Resuming after a human answers
44
+
45
+ A resumed run's prompt includes the issue's comment thread, split into "answers to your open questions" and "earlier discussion." Treat the answers as **authoritative** — never re-ask a question that's been answered. If an answer is itself ambiguous or incomplete, ask one narrower follow-up rather than repeating the original question.
46
+
15
47
  ## The tree
16
48
 
17
49
  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.
@@ -29,6 +61,16 @@ Emit a **flat list**, not nested JSON: each node names its `parent` by another n
29
61
 
30
62
  Give every node its `blocked_by`: the other nodes' `key`s that must land first. A node with no blockers can start immediately — leave `blocked_by` empty rather than inventing an order where none is required. The factory works the **frontier**: any leaf whose blockers are all done. For a purely linear chain that means top to bottom; for anything wider, only real dependencies belong in `blocked_by` — an artificial one just stalls the frontier.
31
63
 
64
+ ## Priority
65
+
66
+ 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.
67
+
68
+ Three rules, all enforced, not just suggested:
69
+
70
+ - **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.
71
+ - **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.
72
+ - **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.
73
+
32
74
  ## Wide refactors — the one exception to vertical slicing
33
75
 
34
76
  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):
@@ -45,9 +87,3 @@ If even a batch can't stay green alone, keep the same three-stage sequence but l
45
87
  - 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.
46
88
  - 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.
47
89
  - `body` should read as `## What to build` followed by `## Acceptance criteria` (a checklist).
48
-
49
- ## Subagents
50
-
51
- `subagent_create` / `_continue` / `_list` / `_remove` fan out exploration — one per subsystem or open question — when the spec spans more than you can read cheaply. Give each a self-contained task, hold it to read-only work, and omit `model`.
52
-
53
- They run in the background. **Wait for every one you spawned to report before writing `refine_plan.md` or your Report JSON.** Skip them when a few reads would do.
@@ -6,25 +6,34 @@
6
6
 
7
7
  {{prompt}}
8
8
 
9
+ `prompt` may include a `## Discussion on the spec issue` section — the spec issue's own comment thread, if it has one. On a first pass this is whatever discussion already existed before the spec was labeled ready; on a resumed pass (after you raised questions and a human answered) it's split into "Answers to your open questions (round N)" and "Earlier discussion." Treat the answers section as authoritative — see your system instructions on resuming.
10
+
11
+ `prompt` may also include a `## Priority` section, when the spec issue itself carries a priority label. Treat that as a **ceiling** for every node you produce — see your system instructions on priority. Its absence means no ceiling, not "assume p2."
12
+
9
13
  ### previous_envelope
10
14
 
11
15
  {{previous_envelope}}
12
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.
18
+
13
19
  ### context_handoff_dir
14
20
 
15
21
  {{context_handoff_dir}}
16
22
 
17
23
  ## Task
18
24
 
19
- Decompose the product spec in `prompt` into a feature/story-or-bug tree, following every rule in your system instructions (vertical slices, the `blocked_by` DAG, expand/migrate/contract for wide refactors, no "Blocked by"/"Parent" text of your own).
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).
20
26
 
21
- 1. Explore the codebase only as far as you need to ground the decomposition in what actually exists.
22
- 2. Write your working notes to `<context_handoff_dir>/refine_plan.md`.
23
- 3. Emit your `Report` JSON, declaring that one file in `artifacts`.
27
+ 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`.
24
31
 
25
32
  ## Report
26
33
 
27
- Respond with ONLY valid JSON matching `RefineOutput` — no prose before or after:
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.
35
+
36
+ **Shape 1 — the decomposition is unambiguous: emit `issues`, leave `questions` empty.**
28
37
 
29
38
  ```json
30
39
  {
@@ -39,7 +48,8 @@ Respond with ONLY valid JSON matching `RefineOutput` — no prose before or afte
39
48
  "title": "<feature title>",
40
49
  "body": "## What to build\n\n<end-to-end behavior this feature covers>\n\n## Acceptance criteria\n\n- [ ] <criterion>",
41
50
  "parent": "",
42
- "blocked_by": []
51
+ "blocked_by": [],
52
+ "priority": "p1"
43
53
  },
44
54
  {
45
55
  "key": "S1",
@@ -47,7 +57,8 @@ Respond with ONLY valid JSON matching `RefineOutput` — no prose before or afte
47
57
  "title": "<leaf title>",
48
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>",
49
59
  "parent": "F1",
50
- "blocked_by": []
60
+ "blocked_by": [],
61
+ "priority": "p1"
51
62
  },
52
63
  {
53
64
  "key": "S2",
@@ -55,7 +66,31 @@ Respond with ONLY valid JSON matching `RefineOutput` — no prose before or afte
55
66
  "title": "<leaf title>",
56
67
  "body": "## What to build\n\n<the fix, described end-to-end>\n\n## Acceptance criteria\n\n- [ ] <criterion>",
57
68
  "parent": "F1",
58
- "blocked_by": ["S1"]
69
+ "blocked_by": ["S1"],
70
+ "priority": "p2"
71
+ }
72
+ ],
73
+ "questions": []
74
+ }
75
+ ```
76
+
77
+ **Shape 2 — something material is ambiguous: emit `questions`, leave `issues` empty.**
78
+
79
+ ```json
80
+ {
81
+ "status": "success",
82
+ "summary": "<one sentence: how many questions, and what they're about>",
83
+ "artifacts": ["<context_handoff_dir>/refine_plan.md"],
84
+ "notes_for_next_agent": "<anything a human answering these should know>",
85
+ "issues": [],
86
+ "questions": [
87
+ {
88
+ "id": "Q1",
89
+ "question": "<the question, precise enough to answer in a word or two>",
90
+ "why_it_matters": "<what goes wrong if this is guessed instead of decided>",
91
+ "options": ["<plausible answer>", "<another plausible answer>"],
92
+ "recommendation": "<your own best guess, if you have one>",
93
+ "evidence": ["<file or symbol that framed this question>"]
59
94
  }
60
95
  ]
61
96
  }
@@ -63,8 +98,11 @@ Respond with ONLY valid JSON matching `RefineOutput` — no prose before or afte
63
98
 
64
99
  Rules on the shape (enforced by a gate — a violation sends this back to you as a correction, not a silent acceptance):
65
100
 
101
+ - `issues` and `questions` are mutually exclusive: a non-empty `questions` requires an empty `issues`, and vice versa. Escalating means publishing nothing this round.
66
102
  - `key` is your own local id for this run, unique within `issues` — never a real tracker id.
67
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.
68
104
  - `parent` is another node's `key`, or `""` for a top-level feature/epic.
69
105
  - `blocked_by` is a list of other nodes' `key`s — real dependencies only, and no cycles (through `parent` or `blocked_by`, or both together).
70
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.
107
+ - `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
+ - `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.
@@ -46,6 +46,7 @@ phases`/`spf events` exist so nobody has to guess the schema again).
46
46
  | "add a chain / a phase / an output type / a gate" | `authoring_chains.md` |
47
47
  | "what is SPF" | `spf_overview.md` |
48
48
  | "fold a third-party review tool's findings into the reviewer" | `cookbooks/ocr_reviewer.md` |
49
+ | "try N attempts and pick the best one / best-of-N" | `spf fanout` — see `references/config.md`'s `spf fanout` section |
49
50
  | envelope/gate/session contract | `references/handoff.md` |
50
51
  | trace schema, spend vs. context | `references/observability.md` |
51
52
  | config field reference | `references/config.md` |