@gr8ful/spf 0.5.0 → 0.6.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 +61 -17
- package/assets/defaults/spf.config.yaml +68 -0
- package/assets/prompts/refiner/system.md +32 -6
- package/assets/prompts/refiner/user.md +37 -5
- package/assets/skill/SKILL.md +1 -0
- package/assets/skill/references/config.md +153 -4
- package/assets/templates/ts-flue-ollama.spf.config.yaml +25 -0
- package/assets/templates/ts.spf.config.yaml +6 -0
- 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 +27 -9
- package/dist/chains/steps.js +82 -13
- package/dist/cli/commands/doctor.js +108 -1
- 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 +19 -0
- package/dist/cli/commands/trace.d.ts +18 -0
- package/dist/cli/commands/trace.js +22 -3
- package/dist/cli/commands/watch.js +51 -26
- package/dist/cli/index.js +12 -0
- package/dist/core/agents.d.ts +56 -0
- package/dist/core/agents.js +152 -1
- package/dist/core/data_types.d.ts +232 -0
- package/dist/core/data_types.js +135 -0
- 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 +24 -1
- 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 +7 -1
- package/dist/core/issues/github_provider.js +33 -2
- package/dist/core/issues/jira_provider.d.ts +5 -1
- package/dist/core/issues/jira_provider.js +26 -3
- package/dist/core/issues/provider.d.ts +53 -2
- 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/paths.d.ts +3 -0
- package/dist/core/paths.js +48 -1
- 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 +89 -9
- package/dist/core/watch.js +193 -19
- 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/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
|
@@ -383,9 +383,10 @@ A product spec isn't individually workable — it needs to become a feature,
|
|
|
383
383
|
broken down into user stories and bugs, before the build lane above has
|
|
384
384
|
anything to claim. `watch.refine` is a second lane over the same poll loop
|
|
385
385
|
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 via
|
|
386
|
+
chain (`refine` by default — `request → scout → refiner → publish`) against
|
|
387
|
+
the spec in its own worktree, and publishes what it produces as real tracker
|
|
388
|
+
issues — a feature/epic container plus story/bug/task leaves, linked via
|
|
389
|
+
GitHub's native sub-issue hierarchy.
|
|
389
390
|
|
|
390
391
|
```yaml
|
|
391
392
|
watch:
|
|
@@ -406,22 +407,62 @@ into twenty stories doesn't turn into twenty unattended chain runs and twenty
|
|
|
406
407
|
PRs with nobody having looked at the breakdown first. Once you do promote a
|
|
407
408
|
leaf, the existing build lane picks it up completely unchanged.
|
|
408
409
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
410
|
+
Grounding the decomposition in real code is structural, not just prose: a
|
|
411
|
+
`scout` phase maps the subsystems the spec touches before the refiner ever
|
|
412
|
+
runs, and its findings flow straight into the refiner as context. This makes
|
|
413
|
+
`scout` a required agent for the `refine` chain — a roster that pruned it
|
|
414
|
+
fails `spf watch` startup by name, the same as any other missing required
|
|
415
|
+
agent.
|
|
416
|
+
|
|
417
|
+
#### Human-in-the-loop escalation
|
|
418
|
+
|
|
419
|
+
The refiner is instructed not to guess on anything material (scope, data
|
|
420
|
+
model, an external dependency choice, a UX contract, a breaking change, or
|
|
421
|
+
anything that would contradict an existing ADR — see
|
|
422
|
+
`assets/prompts/refiner/system.md`'s "Ask, don't decide"). When it hits real
|
|
423
|
+
ambiguity it raises questions instead of publishing a partial tree, and the
|
|
424
|
+
spec moves through an extra loop before it reaches `done`:
|
|
425
|
+
|
|
426
|
+
```text
|
|
427
|
+
spec-ready → refining ──┬─→ done / blocked (published a tree)
|
|
428
|
+
└─→ needs-feedback (raised questions)
|
|
429
|
+
│ a human answers in the issue's comments,
|
|
430
|
+
│ then adds continue-refinement
|
|
431
|
+
▼
|
|
432
|
+
refining (again, same adw_id) → ...
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
Answer the refiner's questions as comments on the spec issue, then add the
|
|
436
|
+
`<prefix>:continue-refinement` label. `spf watch` resumes the **same**
|
|
437
|
+
`adw_id` — the refiner's own coding-agent session continues, with the
|
|
438
|
+
comment thread folded into its prompt — so it never re-derives context it
|
|
439
|
+
already had, and it can loop through as many rounds as it takes; there's no
|
|
440
|
+
cap. If channels are configured (see "Notifications" below), a
|
|
441
|
+
`spec_needs_feedback` event fires every round.
|
|
442
|
+
|
|
443
|
+
The spec issue itself moves through a `spec-ready → refining →
|
|
444
|
+
needs-feedback ⇄ refining → done`/`blocked` lifecycle (the `needs-feedback ⇄
|
|
445
|
+
refining` loop only when the refiner actually escalates), and a summary
|
|
446
|
+
comment lists every issue it created — noting how many rounds of feedback it
|
|
447
|
+
took, if any. Once a spec reaches `done`, `spf watch` also closes the issue
|
|
448
|
+
on the tracker (best-effort: a tracker or a failed close never turns a
|
|
449
|
+
successfully refined spec back to `blocked`). Try it by hand first, against
|
|
450
|
+
a real spec, before turning on the daemon:
|
|
413
451
|
|
|
414
452
|
```bash
|
|
415
453
|
spf refine "<spec text or path/to/spec.md>" --issue 42 # --issue renders a "## Parent: #42" back-reference
|
|
416
454
|
```
|
|
417
455
|
|
|
418
|
-
`spf watch init` seeds the type labels alongside the state ones
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
456
|
+
`spf watch init` seeds the type labels alongside the state ones — **re-run
|
|
457
|
+
it** after upgrading to this version, so it can create the new
|
|
458
|
+
`<prefix>:needs-feedback` and `<prefix>:continue-refinement` labels. This
|
|
459
|
+
lane's prompt (`assets/prompts/refiner/`) is adapted from a "tracer-bullet
|
|
460
|
+
ticket" decomposition skill — vertical slices, a `blocked_by` dependency
|
|
461
|
+
graph, and an expand/migrate/contract sequence for wide mechanical refactors
|
|
462
|
+
— with a gate (`gates.refinementWellFormed`) added on top to enforce the
|
|
423
463
|
container/leaf shape that skill left as prose convention rather than a
|
|
424
|
-
checked rule
|
|
464
|
+
checked rule, now also enforcing that a refinement never publishes issues and
|
|
465
|
+
raises questions in the same round.
|
|
425
466
|
|
|
426
467
|
### GitHub (`issue_provider: github` and/or `code_host: github`)
|
|
427
468
|
|
|
@@ -482,10 +523,13 @@ notifications:
|
|
|
482
523
|
```
|
|
483
524
|
|
|
484
525
|
`events` is the whole filter: `errors` sends only failed runs/phases, blocked
|
|
485
|
-
issues,
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
526
|
+
issues, watch errors, and a spec needing feedback (`spec_needs_feedback` —
|
|
527
|
+
see "Human-in-the-loop escalation" above; it's `error`-level on purpose, the
|
|
528
|
+
same class of event as a blocked issue, so an `errors`-scope channel sees it
|
|
529
|
+
too); `all` adds every milestone — run started/finished, issue claimed, PR
|
|
530
|
+
opened, issue done. A channel's own `events` overrides the top-level scope
|
|
531
|
+
for just that channel. `spf doctor` reports whether each configured
|
|
532
|
+
channel's env var is set.
|
|
489
533
|
|
|
490
534
|
The webhook URL is a secret and lives only in `.env` — `webhook_url_env`
|
|
491
535
|
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.
|
|
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.
|
|
@@ -45,9 +77,3 @@ If even a batch can't stay green alone, keep the same three-stage sequence but l
|
|
|
45
77
|
- 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
78
|
- 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
79
|
- `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,32 @@
|
|
|
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
|
+
|
|
9
11
|
### previous_envelope
|
|
10
12
|
|
|
11
13
|
{{previous_envelope}}
|
|
12
14
|
|
|
15
|
+
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.
|
|
16
|
+
|
|
13
17
|
### context_handoff_dir
|
|
14
18
|
|
|
15
19
|
{{context_handoff_dir}}
|
|
16
20
|
|
|
17
21
|
## Task
|
|
18
22
|
|
|
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).
|
|
23
|
+
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
24
|
|
|
21
|
-
1.
|
|
22
|
-
2.
|
|
23
|
-
3.
|
|
25
|
+
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.
|
|
26
|
+
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.
|
|
27
|
+
3. Write your working notes to `<context_handoff_dir>/refine_plan.md`.
|
|
28
|
+
4. Emit your `Report` JSON, declaring that one file in `artifacts`.
|
|
24
29
|
|
|
25
30
|
## Report
|
|
26
31
|
|
|
27
|
-
Respond with ONLY valid JSON matching `RefineOutput` — no prose before or after
|
|
32
|
+
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.
|
|
33
|
+
|
|
34
|
+
**Shape 1 — the decomposition is unambiguous: emit `issues`, leave `questions` empty.**
|
|
28
35
|
|
|
29
36
|
```json
|
|
30
37
|
{
|
|
@@ -57,14 +64,39 @@ Respond with ONLY valid JSON matching `RefineOutput` — no prose before or afte
|
|
|
57
64
|
"parent": "F1",
|
|
58
65
|
"blocked_by": ["S1"]
|
|
59
66
|
}
|
|
67
|
+
],
|
|
68
|
+
"questions": []
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**Shape 2 — something material is ambiguous: emit `questions`, leave `issues` empty.**
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"status": "success",
|
|
77
|
+
"summary": "<one sentence: how many questions, and what they're about>",
|
|
78
|
+
"artifacts": ["<context_handoff_dir>/refine_plan.md"],
|
|
79
|
+
"notes_for_next_agent": "<anything a human answering these should know>",
|
|
80
|
+
"issues": [],
|
|
81
|
+
"questions": [
|
|
82
|
+
{
|
|
83
|
+
"id": "Q1",
|
|
84
|
+
"question": "<the question, precise enough to answer in a word or two>",
|
|
85
|
+
"why_it_matters": "<what goes wrong if this is guessed instead of decided>",
|
|
86
|
+
"options": ["<plausible answer>", "<another plausible answer>"],
|
|
87
|
+
"recommendation": "<your own best guess, if you have one>",
|
|
88
|
+
"evidence": ["<file or symbol that framed this question>"]
|
|
89
|
+
}
|
|
60
90
|
]
|
|
61
91
|
}
|
|
62
92
|
```
|
|
63
93
|
|
|
64
94
|
Rules on the shape (enforced by a gate — a violation sends this back to you as a correction, not a silent acceptance):
|
|
65
95
|
|
|
96
|
+
- `issues` and `questions` are mutually exclusive: a non-empty `questions` requires an empty `issues`, and vice versa. Escalating means publishing nothing this round.
|
|
66
97
|
- `key` is your own local id for this run, unique within `issues` — never a real tracker id.
|
|
67
98
|
- 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
99
|
- `parent` is another node's `key`, or `""` for a top-level feature/epic.
|
|
69
100
|
- `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
101
|
- `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.
|
|
102
|
+
- `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` |
|
|
@@ -12,10 +12,10 @@ always shows the resolved, merged result for the repo you're in.
|
|
|
12
12
|
1. The packaged built-in default (`assets/defaults/spf.config.yaml` inside
|
|
13
13
|
the installed CLI).
|
|
14
14
|
2. `.spf/spf.config.yaml` in the target repo, if present — merged on top,
|
|
15
|
-
field by field (`defaults`/`observability`/`quality`/`watch`/`notifications`/`review`
|
|
15
|
+
field by field (`defaults`/`observability`/`quality`/`watch`/`notifications`/`review`/`tiering`
|
|
16
16
|
merge key-by-key — `notifications.channels` replaces wholesale, same as
|
|
17
|
-
`quality.checks`; `agents` merges by
|
|
18
|
-
entry, a new name appends).
|
|
17
|
+
`quality.checks` and `tiering.tiers`/`tiering.roles`; `agents` merges by
|
|
18
|
+
`name`: a matching name patches that entry, a new name appends).
|
|
19
19
|
3. An explicit `--config <path>` replaces both — standalone, no built-in
|
|
20
20
|
underneath it.
|
|
21
21
|
|
|
@@ -76,6 +76,29 @@ agents:
|
|
|
76
76
|
| `env_allowlist` | string[] \| null | Roster-wide env allowlist, back-filled the same way as `writes`. Unset/null = every agent gets the full operator environment (see `env_allowlist` under `agents[]`). |
|
|
77
77
|
| `protected_files` | string[] | Paths no agent may touch unless named in its own `writes`. Default `[".spf/", "spf.config.yaml"]`. |
|
|
78
78
|
| `data_dir` | path | Runtime home, repo-relative. Default `.spf/data`. |
|
|
79
|
+
| `max_run_cost` | number > 0 (USD) | Stops the NEXT agent call once a run has already spent this much — checked before each call, never after. A single call is never capped (a run can overshoot by one whole call), and a chain with only one agent dispatch (`scout`, `prompt`, `build`) can never trip it at all. Absent (default) = unbounded. Throws `BudgetExceeded`, which fails the phase closed. |
|
|
80
|
+
| `max_run_tokens` | integer > 0 | Same semantics as `max_run_cost`, on `sessions.total_tokens` instead of cost. Absent (default) = unbounded. |
|
|
81
|
+
|
|
82
|
+
### `spf fanout`
|
|
83
|
+
|
|
84
|
+
`spf fanout <chain> "<prompt>" [--n 3] [--concurrency N] [--base <branch>]
|
|
85
|
+
[--adw-id <id>] [--first-success] [--config <path>] [--cwd <dir>]` — best-of-N:
|
|
86
|
+
runs `<chain>` N times in parallel, each in its own git worktree and branch
|
|
87
|
+
(`spf/fanout/<base-adw-id>-<i>`), then picks ONE winner by code (succeeded >
|
|
88
|
+
fewest gate failures > most gate passes > lowest cost > fewest tokens >
|
|
89
|
+
lowest wall time > adw_id) and deletes the rest. SPF never merges the
|
|
90
|
+
winner — it prints the branch and the basis; a human runs `git merge`. The
|
|
91
|
+
chain named MUST have a commit step (`plan-build`, `plan-build-test`,
|
|
92
|
+
`plan-build-test-quality`, `simple-sdlc`, or a repo-local chain with one) —
|
|
93
|
+
`spf fanout` refuses any chain without one, since a winner with no commit
|
|
94
|
+
leaves its only copy of the work as uncommitted edits in a worktree.
|
|
95
|
+
`--first-success` opts into stopping early once one attempt succeeds
|
|
96
|
+
(first-past-the-post) instead of the default, which runs every attempt to
|
|
97
|
+
completion for a true N-way comparison. `spf fanout --clean <base-adw-id>`
|
|
98
|
+
removes any worktrees/branches a killed or discarded run left behind under
|
|
99
|
+
that id — a `--adw-id` is never safe to reuse against session rows still in
|
|
100
|
+
the shared db, so a rerun with the same id fails loudly instead of mixing
|
|
101
|
+
the two runs' cost/tokens/gates together.
|
|
79
102
|
|
|
80
103
|
### `observability`
|
|
81
104
|
|
|
@@ -130,6 +153,7 @@ Full mechanism: the main README's "`spf watch`" section. Field reference:
|
|
|
130
153
|
| `base_branch` | string | Branch worktrees fork from and PRs target. Default `main`. |
|
|
131
154
|
| `poll_ms` | int | Tick interval. Default `60000`. |
|
|
132
155
|
| `concurrency` | int ≥1 | Max issues claimed and run at once, the build lane's own budget (independent of `refine.concurrency`). Default `2`. |
|
|
156
|
+
| `chain_options` | map of string -> string | Options passed straight through to `chain` (and `refine.chain`) for every unattended dispatch — the same shape an interactive `spf <chain> --suite <name>` builds, e.g. `{suite: strict}` or `{agent: some-agent}`. Default `{}`. Only useful for a chain whose behavior actually reads the option (a step-derived chain's `--suite`; an imperative chain ignores an option it doesn't know about). |
|
|
133
157
|
| `jira.base_url` / `jira.project_key` | string | Only consulted when `issue_provider: jira`. |
|
|
134
158
|
| `refine.enabled` | bool | Turns on the second lane: decompose a `<prefix>:spec-ready` product spec into a feature/story-or-bug tree of real issues, instead of running `chain` against it directly (a spec isn't individually workable). Default `false` — off by default, so an existing `watch:` config is unaffected by upgrading. Needs `issue_provider: github` — `spf watch` fails loudly at startup otherwise, since issue authoring (create + link a hierarchy) isn't implemented for Jira yet. |
|
|
135
159
|
| `refine.chain` | string | Which registered chain runs per claimed spec. Default `refine`. |
|
|
@@ -140,6 +164,8 @@ watch:
|
|
|
140
164
|
repo: owner/name
|
|
141
165
|
label_prefix: spf
|
|
142
166
|
chain: plan-build-test
|
|
167
|
+
chain_options:
|
|
168
|
+
suite: strict # only if plan-build-test's chain declares a step-derived requiredSuites
|
|
143
169
|
refine:
|
|
144
170
|
enabled: true # decompose spf:spec-ready specs into a feature/story tree
|
|
145
171
|
chain: refine
|
|
@@ -154,6 +180,24 @@ additionally gets `<prefix>:refined`, so a human can review and promote it to
|
|
|
154
180
|
`<prefix>:ready` when it's worth building — the refine lane never
|
|
155
181
|
auto-promotes anything.
|
|
156
182
|
|
|
183
|
+
The `refine` chain grounds its decomposition with a `scout` phase before the
|
|
184
|
+
refiner runs, so `scout` is a required agent for it — a roster that pruned
|
|
185
|
+
it fails `spf watch` startup by name.
|
|
186
|
+
|
|
187
|
+
The refine lane's own state machine has an extra loop beyond
|
|
188
|
+
`spec-ready → refining → done`/`blocked`: when the refiner raises material
|
|
189
|
+
ambiguity instead of a tree (see `assets/prompts/refiner/system.md`'s "Ask,
|
|
190
|
+
don't decide"), the spec moves to `<prefix>:needs-feedback` with a comment
|
|
191
|
+
naming its questions, instead of publishing anything. A human answers in the
|
|
192
|
+
issue's comments and adds `<prefix>:continue-refinement`; `spf watch` claims
|
|
193
|
+
that label back into `refining` and resumes the **same** `adw_id` — the
|
|
194
|
+
comment thread (split into "answers to your open questions" and "earlier
|
|
195
|
+
discussion") is folded into the resumed prompt, and the refiner's own
|
|
196
|
+
coding-agent session continues rather than starting cold. This can loop any
|
|
197
|
+
number of rounds. `spf watch init` must be re-run after upgrading to this
|
|
198
|
+
version to seed the two new labels. Once a spec is fully published it's also
|
|
199
|
+
closed on the tracker (GitHub only, best-effort), not just labeled `done`.
|
|
200
|
+
|
|
157
201
|
### `notifications`
|
|
158
202
|
|
|
159
203
|
Optional outbound push for unattended work — `spf watch`'s daemon lifecycle,
|
|
@@ -165,7 +209,7 @@ default; adding it is entirely additive.
|
|
|
165
209
|
|
|
166
210
|
| Field | Type | Meaning |
|
|
167
211
|
|---|---|---|
|
|
168
|
-
| `events` | `"off"` \| `"errors"` \| `"all"` | The whole filter. `off` (default): nothing. `errors`: only failed runs/phases, blocked issues, watch errors. `all`: every curated milestone (run started, issue claimed, PR opened, ...) plus errors. |
|
|
212
|
+
| `events` | `"off"` \| `"errors"` \| `"all"` | The whole filter. `off` (default): nothing. `errors`: only failed runs/phases, blocked issues, watch errors, and a spec needing feedback (`spec_needs_feedback` — `error`-level on purpose, same class as a blocked issue). `all`: every curated milestone (run started, issue claimed, PR opened, ...) plus errors. |
|
|
169
213
|
| `timeout_ms` | int | Per-request timeout for a channel's HTTP POST. Default `5000`. |
|
|
170
214
|
| `channels[]` | array | See below. |
|
|
171
215
|
|
|
@@ -224,6 +268,111 @@ the repo (never `ENGINEER_NAME`/`$USER`, which are spoofable and fall back to
|
|
|
224
268
|
the literal string `"engineer"`) — an unattended run's AI-only commit never
|
|
225
269
|
carries one, because nobody said yes to attest to.
|
|
226
270
|
|
|
271
|
+
### `tiering`
|
|
272
|
+
|
|
273
|
+
Risk-tiered per-role model routing (SPF #14) — a run-global cost/strength
|
|
274
|
+
shift on top of the static roster, decided ONCE at run start from signals
|
|
275
|
+
code can read without asking an agent anything (the chain's name and the
|
|
276
|
+
prompt's word count), never re-evaluated mid-run. **OFF by default, and
|
|
277
|
+
`enabled: false`/absent is a TOTAL no-op**: every agent dispatches at
|
|
278
|
+
exactly the model its roster entry names, byte-identical to before this key
|
|
279
|
+
existed — the same discipline `defaults.max_run_cost`/`max_run_tokens` hold
|
|
280
|
+
themselves to.
|
|
281
|
+
|
|
282
|
+
**Top-level, not nested under `defaults:`.** `loadConfig`'s back-fill loop
|
|
283
|
+
copies a fixed list of `defaults` keys down onto every agent that hasn't set
|
|
284
|
+
them, and a stray copy would be silently *stripped* at parse rather than
|
|
285
|
+
rejected — a top-level key sits outside that loop entirely, so the trap
|
|
286
|
+
cannot apply here. Merges **key-by-key** with a base config, same as
|
|
287
|
+
`review`/`notifications`: a repo that only flips `enabled: true` keeps the
|
|
288
|
+
base's `tiers`/`roles`; `tiers` and `roles` are each a **whole replace** on
|
|
289
|
+
override (a repo's own ladder replaces the packaged one wholesale, never an
|
|
290
|
+
unordered splice of both).
|
|
291
|
+
|
|
292
|
+
| Field | Type | Meaning |
|
|
293
|
+
|---|---|---|
|
|
294
|
+
| `enabled` | bool | Default `false`. `true` turns on the ladder walk described below; `false` (or the key absent) is a total no-op, checked nowhere and dispatched nowhere. |
|
|
295
|
+
| `tiers` | array, WEAKEST FIRST | The ladder. A risk level shifts every routed role UP or DOWN this list by the same step — order is the whole semantics, which is why this is a sequence and not a mapping. Default `[]`. |
|
|
296
|
+
| `tiers[].name` | string | What `roles` values point at. |
|
|
297
|
+
| `tiers[].coding_agent` | `"flue"` \| `"claude_code"` | Which backend's vocabulary this rung's `model` speaks. Default `flue`, same default an agent's own `coding_agent` uses. A tier changes an agent's `model` and **nothing else** — `coding_agent` always stays the agent's own — so a rung can only route roles whose backend matches (**rule T**, below). |
|
|
298
|
+
| `tiers[].model` | string | Same vocabulary as an agent's own `model:` for that backend: `provider/model-id` for `flue`, Claude Code's bare alias/full name for `claude_code`. No per-provider table — for `flue` the provider is already the string's first segment. |
|
|
299
|
+
| `roles` | map of agent name -> tier name | The baseline tier per **role**. Naming an agent here is the operator's statement "route this one by tier" — the resolved tier then wins over that agent's own `model:`. An agent **not** named here is never retiered; its `model:` stands, untouched. That is the whole precedence rule. Default `{}`. |
|
|
300
|
+
|
|
301
|
+
```yaml
|
|
302
|
+
tiering:
|
|
303
|
+
enabled: true
|
|
304
|
+
tiers:
|
|
305
|
+
- { name: cheap, coding_agent: flue, model: ollama/granite4.1:8b }
|
|
306
|
+
- { name: strong, coding_agent: flue, model: ollama/qwen3.8:27b-mlx }
|
|
307
|
+
roles:
|
|
308
|
+
scout: cheap
|
|
309
|
+
builder: strong
|
|
310
|
+
reviewer: strong
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
**The risk signal.** Two integer-weighted signals, summed: the chain's own
|
|
314
|
+
kind (`scout`/`prompt`/`document`/`quality` weigh `-1`; `simple-sdlc`/
|
|
315
|
+
`plan-build-test-quality`/`refine` weigh `+1`; everything else, including
|
|
316
|
+
every repo-local `.spf/chains/*.yaml` chain, weighs `0`) and the prompt's
|
|
317
|
+
word count (`-1` at ≤60 words, `+1` at ≥400, `0` between). The sum
|
|
318
|
+
classifies to `low`/`standard`/`high`: reaching `high` needs only one
|
|
319
|
+
signal to agree (a long prompt forces `high` outright), reaching `low`
|
|
320
|
+
needs both to agree — demoting a role a rung is the expensive mistake (a
|
|
321
|
+
wrong answer, a whole re-run); promoting one is the cheap mistake (some
|
|
322
|
+
extra tokens), so the classifier is deliberately asymmetric toward the
|
|
323
|
+
cheap failure. `standard` (step `0`) leaves every role on its configured
|
|
324
|
+
baseline rung.
|
|
325
|
+
|
|
326
|
+
**Rule T — the backend-compatibility rule.** `coding_agent` is a per-agent
|
|
327
|
+
field and model vocabulary is backend-dependent, so a tier is only usable
|
|
328
|
+
for a role whose `coding_agent` matches the tier's own declared
|
|
329
|
+
`coding_agent`. `enabled: true` on a repo that inherited the packaged
|
|
330
|
+
flue-shaped ladder (every repo does, via key-by-key merge, until it
|
|
331
|
+
declares its own) while running a `claude_code` roster fails **loudly, by
|
|
332
|
+
name** at `agents.validate()` time, before anything spawns — never a silent
|
|
333
|
+
skip, and never a `fireworks/...` id handed to Claude Code.
|
|
334
|
+
|
|
335
|
+
**Degradation.** A rung that is unusable — a model tag `probeServedOllamaTags`
|
|
336
|
+
found absent from a local Ollama server's `/models` list, or a rule-T
|
|
337
|
+
backend mismatch — is walked **down**, never up: a degradation must never
|
|
338
|
+
silently escalate spend. If every rung at or below the target is unusable,
|
|
339
|
+
the role dispatches on its own configured `model:` unchanged, plus one noted
|
|
340
|
+
line. The availability probe **fails open**: an unreachable server drops
|
|
341
|
+
nothing rather than silently downgrading every agent to the bottom rung.
|
|
342
|
+
|
|
343
|
+
**Visibility.** Because a retiered agent's *configured* `model:` (what
|
|
344
|
+
`spf.config.yaml` says) and its *effective* model (what actually dispatched)
|
|
345
|
+
can now disagree, every run traces one `tiering` log event carrying the full
|
|
346
|
+
resolved routing, and prints one console line per retiered agent
|
|
347
|
+
(`[spf] tiering <agent> <tier> (<configured> -> <effective>) risk=<risk>`).
|
|
348
|
+
`spf doctor` reports the same resolved ladder and every routed role's
|
|
349
|
+
effective model when `tiering.enabled` — see below.
|
|
350
|
+
|
|
351
|
+
`spf init`'s interview does not write this block: it collects one model for
|
|
352
|
+
the whole roster, and a `tiering` block over a single-model roster is a
|
|
353
|
+
no-op by construction. `spf init --template ts-flue-ollama` is the one
|
|
354
|
+
packaged template that ships it **on**, with two tags measured live against
|
|
355
|
+
a real local Ollama server; the non-interactive starter (`--yes` / piped
|
|
356
|
+
stdin) shows the shape commented out. Every other packaged template
|
|
357
|
+
inherits the built-in default's populated-but-`enabled: false` ladder
|
|
358
|
+
through key-by-key merge — harmless until a repo opts in, and invisible to
|
|
359
|
+
`spf doctor` until it does (see below).
|
|
360
|
+
|
|
361
|
+
**`spf doctor`**, only when `cfg.tiering.enabled` — a disabled ladder is
|
|
362
|
+
invisible to doctor, on purpose, so a repo that never opted in (which is
|
|
363
|
+
every packaged template except `ts-flue-ollama`, plus every `spf init`
|
|
364
|
+
starter) can't fail on an unset provider key for a rung that will never
|
|
365
|
+
dispatch:
|
|
366
|
+
|
|
367
|
+
- the same provider-key check the roster gets, run over `tiers[].model`
|
|
368
|
+
instead, branching on **the tier's own** `coding_agent`;
|
|
369
|
+
- the `OLLAMA_BASE_URL` reachability probe now also fires for an `ollama/*`
|
|
370
|
+
rung even when no `cfg.agents[]` entry itself names one;
|
|
371
|
+
- a new report section: the resolved ladder, every routed role's effective
|
|
372
|
+
model (including roles whose effective model equals their configured
|
|
373
|
+
one), any degradation note, and — probed the same way `probeServedOllamaTags`
|
|
374
|
+
does, fail-open — which `ollama/*` rungs are and aren't in `ollama list`.
|
|
375
|
+
|
|
227
376
|
### `agents[]`
|
|
228
377
|
|
|
229
378
|
| Field | Required | Meaning |
|
|
@@ -65,3 +65,28 @@ agents:
|
|
|
65
65
|
# a 3B model — a lighter tag here is a real, not merely theoretical,
|
|
66
66
|
# option.
|
|
67
67
|
model: ollama/granite4.1:8b
|
|
68
|
+
|
|
69
|
+
# Risk-tiered per-role model routing (SPF #14) — ON, local only. The two
|
|
70
|
+
# tags below are the MEASURED ones (last session, live on localhost:11434)
|
|
71
|
+
# and they ALREADY appear in this template today — ollama/qwen3.8:27b-mlx at
|
|
72
|
+
# :48 (defaults.model), :58 (planner), :60 (reviewer) and
|
|
73
|
+
# ollama/granite4.1:8b at :67 (documenter). This block introduces no new
|
|
74
|
+
# model string.
|
|
75
|
+
tiering:
|
|
76
|
+
enabled: true
|
|
77
|
+
# coding_agent: flue matches this template's `defaults.coding_agent: flue`
|
|
78
|
+
# (:46) — every roster agent here is flue, so every role is routable by
|
|
79
|
+
# these rungs. Stated explicitly rather than left to the default, because
|
|
80
|
+
# it is the field that keeps a rung from being handed to a backend that
|
|
81
|
+
# cannot parse it (rule T — see assets/skill/references/config.md's
|
|
82
|
+
# "tiering" section).
|
|
83
|
+
tiers:
|
|
84
|
+
- { name: scout, coding_agent: flue, model: ollama/granite4.1:8b } # cheap read/summarize/triage
|
|
85
|
+
- { name: builder, coding_agent: flue, model: ollama/qwen3.8:27b-mlx } # first-attempt implementation
|
|
86
|
+
roles:
|
|
87
|
+
scout: scout
|
|
88
|
+
documenter: scout
|
|
89
|
+
planner: builder
|
|
90
|
+
builder: builder
|
|
91
|
+
refiner: builder
|
|
92
|
+
reviewer: builder
|
|
@@ -69,6 +69,12 @@ agents:
|
|
|
69
69
|
# label_prefix: spf
|
|
70
70
|
# chain: plan-build-test
|
|
71
71
|
# base_branch: main
|
|
72
|
+
# # Options threaded straight through to `chain` (and refine.chain below)
|
|
73
|
+
# # for every unattended dispatch — the same shape an interactive
|
|
74
|
+
# # `spf <chain> --suite <name>` builds. Only useful for a chain whose
|
|
75
|
+
# # behavior actually reads the option; empty/omitted by default.
|
|
76
|
+
# # chain_options:
|
|
77
|
+
# # suite: strict
|
|
72
78
|
# # Optional second lane: decompose a spf:spec-ready product spec into a
|
|
73
79
|
# # feature/story-or-bug tree of real issues instead of building it
|
|
74
80
|
# # directly. Off by default; needs issue_provider: github (the default
|
package/dist/chains/index.d.ts
CHANGED
|
@@ -118,5 +118,16 @@ export declare function resolveRequiredSuites(chain: ChainDefinition, options: R
|
|
|
118
118
|
* CLI dispatch sites (`spf <chain>` and `spf watch`) go through this, never
|
|
119
119
|
* `chain.run(...)` directly — the whole reason to route through here is that
|
|
120
120
|
* a `steps`-only chain has no `run` to call.
|
|
121
|
+
*
|
|
122
|
+
* Also the one call site both dispatch paths share on the way OUT — success
|
|
123
|
+
* or thrown error alike, `finally` runs either way — which is what makes it
|
|
124
|
+
* the right seam for `otel.releaseOtelExporter()` (see otel.ts's RUN-SCOPED
|
|
125
|
+
* CLEANUP note / #26): a `spf watch` daemon calls this once per issue, in
|
|
126
|
+
* the same process, for as long as it runs, so a run's exporter has to be
|
|
127
|
+
* dropped here rather than living until the whole daemon exits. `session.
|
|
128
|
+
* finalize()` is the same idea for the run itself — it stops the process-
|
|
129
|
+
* wide signal handler from reaching this (now finished) run, and closes its
|
|
130
|
+
* Tracer's sqlite handle, so a long `spf watch` daemon does not hold every
|
|
131
|
+
* issue it has ever processed reachable for the rest of its life.
|
|
121
132
|
*/
|
|
122
133
|
export declare function runChain(chain: ChainDefinition, ctx: ChainContext, options?: Record<string, string>): Promise<number>;
|