@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.
- package/README.md +168 -29
- package/assets/defaults/spf.config.yaml +68 -0
- package/assets/prompts/refiner/system.md +42 -6
- package/assets/prompts/refiner/user.md +46 -8
- package/assets/skill/SKILL.md +1 -0
- package/assets/skill/references/config.md +192 -5
- package/assets/templates/ts-flue-ollama.spf.config.yaml +25 -0
- package/assets/templates/ts.spf.config.yaml +12 -2
- package/dist/chains/index.d.ts +11 -0
- package/dist/chains/index.js +38 -3
- package/dist/chains/repo_chains.js +1 -0
- package/dist/chains/simple_sdlc.js +1 -1
- package/dist/chains/steps.d.ts +8 -17
- package/dist/chains/steps.js +102 -14
- package/dist/cli/commands/doctor.js +112 -3
- package/dist/cli/commands/estimate.d.ts +82 -0
- package/dist/cli/commands/estimate.js +317 -0
- package/dist/cli/commands/fanout.d.ts +40 -0
- package/dist/cli/commands/fanout.js +401 -0
- package/dist/cli/commands/init.js +23 -1
- package/dist/cli/commands/run.js +9 -2
- package/dist/cli/commands/trace.d.ts +18 -0
- package/dist/cli/commands/trace.js +22 -3
- package/dist/cli/commands/watch.d.ts +8 -0
- package/dist/cli/commands/watch.js +95 -33
- package/dist/cli/index.js +13 -1
- package/dist/cli/interview.js +9 -5
- package/dist/core/agents.d.ts +56 -0
- package/dist/core/agents.js +152 -1
- package/dist/core/data_types.d.ts +340 -5
- package/dist/core/data_types.js +185 -5
- package/dist/core/fanout.d.ts +229 -0
- package/dist/core/fanout.js +313 -0
- package/dist/core/gates.d.ts +8 -0
- package/dist/core/gates.js +48 -2
- package/dist/core/git_helper.d.ts +98 -0
- package/dist/core/git_helper.js +127 -0
- package/dist/core/issues/github_provider.d.ts +46 -6
- package/dist/core/issues/github_provider.js +120 -5
- package/dist/core/issues/jira_provider.d.ts +83 -12
- package/dist/core/issues/jira_provider.js +110 -4
- package/dist/core/issues/provider.d.ts +124 -19
- package/dist/core/issues/provider.js +24 -7
- package/dist/core/notify/channel.d.ts +1 -1
- package/dist/core/notify/notifier.d.ts +16 -0
- package/dist/core/notify/notifier.js +36 -0
- package/dist/core/otel.d.ts +64 -9
- package/dist/core/otel.js +78 -14
- package/dist/core/refine.d.ts +45 -8
- package/dist/core/refine.js +98 -24
- package/dist/core/runner.d.ts +8 -0
- package/dist/core/runner.js +7 -0
- package/dist/core/session.d.ts +25 -0
- package/dist/core/session.js +97 -28
- package/dist/core/tiering.d.ts +145 -0
- package/dist/core/tiering.js +235 -0
- package/dist/core/tracer.d.ts +10 -0
- package/dist/core/tracer.js +12 -0
- package/dist/core/watch.d.ts +174 -11
- package/dist/core/watch.js +531 -33
- package/dist/ui/server/db.d.ts +39 -0
- package/dist/ui/server/db.js +61 -0
- package/package.json +2 -1
- package/dist/test/agent_cc.test.d.ts +0 -1
- package/dist/test/agent_cc.test.js +0 -95
- package/dist/test/agent_flue.test.d.ts +0 -1
- package/dist/test/agent_flue.test.js +0 -83
- package/dist/test/chains.test.d.ts +0 -12
- package/dist/test/chains.test.js +0 -92
- package/dist/test/data_types.test.d.ts +0 -10
- package/dist/test/data_types.test.js +0 -220
- package/dist/test/env_file.test.d.ts +0 -1
- package/dist/test/env_file.test.js +0 -74
- package/dist/test/fake_asker.d.ts +0 -23
- package/dist/test/fake_asker.js +0 -30
- package/dist/test/git_helper.test.d.ts +0 -1
- package/dist/test/git_helper.test.js +0 -59
- package/dist/test/hermetic_git.d.ts +0 -1
- package/dist/test/hermetic_git.js +0 -22
- package/dist/test/init_command.test.d.ts +0 -14
- package/dist/test/init_command.test.js +0 -136
- package/dist/test/interview.test.d.ts +0 -15
- package/dist/test/interview.test.js +0 -425
- package/dist/test/notify.test.d.ts +0 -1
- package/dist/test/notify.test.js +0 -174
- package/dist/test/ollama_provider.test.d.ts +0 -1
- package/dist/test/ollama_provider.test.js +0 -103
- package/dist/test/otel.test.d.ts +0 -26
- package/dist/test/otel.test.js +0 -512
- package/dist/test/paths.test.d.ts +0 -1
- package/dist/test/paths.test.js +0 -68
- package/dist/test/refine.test.d.ts +0 -1
- package/dist/test/refine.test.js +0 -189
- package/dist/test/repo_chains.test.d.ts +0 -21
- package/dist/test/repo_chains.test.js +0 -416
- package/dist/test/signoff.test.d.ts +0 -1
- package/dist/test/signoff.test.js +0 -329
- package/dist/test/ui_server.test.d.ts +0 -7
- package/dist/test/ui_server.test.js +0 -120
- package/dist/test/watch.test.d.ts +0 -1
- 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
|
|
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
|
|
387
|
-
publishes what it produces as real tracker
|
|
388
|
-
plus story/bug/task leaves, linked
|
|
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 #
|
|
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
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
|
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
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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`
|
|
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,
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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.
|
|
22
|
-
2.
|
|
23
|
-
3.
|
|
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.
|
package/assets/skill/SKILL.md
CHANGED
|
@@ -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` |
|