@gr8ful/spf 0.2.1 → 0.4.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 +106 -6
- package/assets/defaults/spf.config.yaml +16 -0
- package/assets/prompts/refiner/system.md +53 -0
- package/assets/prompts/refiner/user.md +70 -0
- package/assets/skill/references/config.md +83 -3
- package/assets/templates/ts-cc.spf.config.yaml +3 -3
- package/assets/templates/ts.spf.config.yaml +22 -2
- package/dist/chains/context.d.ts +9 -0
- package/dist/chains/index.js +5 -0
- package/dist/chains/steps.d.ts +24 -0
- package/dist/chains/steps.js +55 -4
- package/dist/cli/commands/doctor.js +18 -0
- package/dist/cli/commands/init.js +44 -3
- package/dist/cli/commands/install-skill.js +5 -2
- package/dist/cli/commands/list.js +1 -0
- package/dist/cli/commands/run.js +5 -1
- package/dist/cli/commands/watch.js +86 -8
- package/dist/cli/index.js +7 -3
- package/dist/cli/interview.d.ts +2 -0
- package/dist/cli/interview.js +107 -3
- package/dist/core/agents.js +4 -1
- package/dist/core/console.d.ts +13 -1
- package/dist/core/console.js +51 -1
- package/dist/core/data_types.d.ts +133 -0
- package/dist/core/data_types.js +72 -0
- package/dist/core/gates.d.ts +13 -0
- package/dist/core/gates.js +103 -0
- package/dist/core/issues/github_provider.d.ts +35 -9
- package/dist/core/issues/github_provider.js +76 -28
- package/dist/core/issues/jira_provider.d.ts +14 -1
- package/dist/core/issues/jira_provider.js +9 -7
- package/dist/core/issues/provider.d.ts +77 -15
- package/dist/core/issues/provider.js +7 -4
- package/dist/core/notify/channel.d.ts +32 -0
- package/dist/core/notify/channel.js +14 -0
- package/dist/core/notify/notifier.d.ts +42 -0
- package/dist/core/notify/notifier.js +100 -0
- package/dist/core/notify/slack_channel.d.ts +13 -0
- package/dist/core/notify/slack_channel.js +30 -0
- package/dist/core/notify/teams_channel.d.ts +17 -0
- package/dist/core/notify/teams_channel.js +38 -0
- package/dist/core/notify/webhook_channel.d.ts +13 -0
- package/dist/core/notify/webhook_channel.js +19 -0
- package/dist/core/refine.d.ts +39 -0
- package/dist/core/refine.js +144 -0
- package/dist/core/runner.d.ts +7 -0
- package/dist/core/runner.js +4 -1
- package/dist/core/session.js +3 -0
- package/dist/core/watch.d.ts +66 -1
- package/dist/core/watch.js +267 -15
- package/dist/test/chains.test.js +1 -0
- package/dist/test/data_types.test.js +34 -1
- package/dist/test/init_command.test.js +17 -0
- package/dist/test/interview.test.js +119 -0
- package/dist/test/notify.test.d.ts +1 -0
- package/dist/test/notify.test.js +174 -0
- package/dist/test/refine.test.d.ts +1 -0
- package/dist/test/refine.test.js +126 -0
- package/dist/test/watch.test.js +286 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ Everyone can get an agent to write code once. Almost nobody gets the same result
|
|
|
15
15
|
npm i -g @gr8ful/spf
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
No Bun, no separate coding-agent binary to install first, no template stamped into your repo. `spf` ships with a packaged default agent roster and default prompts — it runs against any repo with zero setup, and only writes files into that repo if you explicitly ask it to (`spf init`, `spf install-skill`).
|
|
18
|
+
No Bun, no separate coding-agent binary to install first, no template stamped into your repo. `spf` ships with a packaged default agent roster and default prompts — it runs against any repo with zero setup, and only writes files into that repo if you explicitly ask it to (`spf init`, which also installs the Claude Code skill unless you pass `--no-skills`; `spf install-skill` does just the skill by hand).
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
21
|
cd your-repo
|
|
@@ -34,10 +34,12 @@ spf init --template ts-cc # or start from a packaged, ready-to-run template
|
|
|
34
34
|
spf list # every chain this install knows, its phases, what it needs
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
On a real terminal, `spf init` asks a short interview — which coding agent (`claude_code` or `flue`) and model, which quality checks to gate on, whether to turn on `spf watch` and against which tracker/code host — and writes `.spf/spf.config.yaml` with only what you answered differently from the packaged defaults, plus whatever secrets those answers imply appended to `.env` (already gitignored, and already auto-loaded by every command) and their key names mirrored into a committable `.env.example`. Re-running it later shows any existing `.env` value masked and keeps it on an empty answer, so rotating one secret doesn't mean re-answering everything. Piped input, `--yes`, or `--template <name>` all skip the interview and fall back to the original non-interactive behavior — a scripted `spf init` never blocks on stdin.
|
|
37
|
+
On a real terminal, `spf init` asks a short interview — which coding agent (`claude_code` or `flue`) and model (optionally customized per agent instead of one model for the whole roster), which quality checks to gate on, whether to turn on `spf watch` and against which tracker/code host, and whether to push notifications to Slack/Teams/a webhook — and writes `.spf/spf.config.yaml` with only what you answered differently from the packaged defaults, plus whatever secrets those answers imply appended to `.env` (already gitignored, and already auto-loaded by every command) and their key names mirrored into a committable `.env.example`. Re-running it later shows any existing `.env` value masked and keeps it on an empty answer, so rotating one secret doesn't mean re-answering everything. Piped input, `--yes`, or `--template <name>` all skip the interview and fall back to the original non-interactive behavior — a scripted `spf init` never blocks on stdin.
|
|
38
38
|
|
|
39
39
|
Without an interview, `spf init` writes the same small starter `.spf/spf.config.yaml`, commented, that merges on top of the packaged built-ins field by field. `--template <name>` writes a real, filled-in config instead of the commented-out starter — every packaged template's name prints after `spf init` runs, and the same files live in [`assets/templates/`](assets/templates/) to browse directly. Nothing here needs to exist for `spf` to run; it's how you make one repo's roster diverge from the defaults.
|
|
40
40
|
|
|
41
|
+
Every `spf init` run — interview or not — also installs the repo-local Claude Code skill (`.claude/skills/spf`), the same work `spf install-skill` does by hand: pass `--no-skills` to skip it. It's idempotent (a no-op once the skill is already current, and writes a `.new` sibling instead of overwriting a file you've locally edited), so re-running `spf init` never clobbers anything there. `spf install-skill --user` (installing to `~/.claude/skills/spf` instead) is still its own separate invocation.
|
|
42
|
+
|
|
41
43
|
### Local development
|
|
42
44
|
|
|
43
45
|
Working from a clone instead of the published package:
|
|
@@ -108,7 +110,7 @@ agents:
|
|
|
108
110
|
writes: [specs/] # the plan is all it may leave in the repo
|
|
109
111
|
```
|
|
110
112
|
|
|
111
|
-
|
|
113
|
+
Six starter agents ship in the box: `planner`, `builder`, `scout` (read-only recon), `refiner` (decomposes a product spec for `spf watch`'s refine lane — see "`spf watch`" below), `reviewer`, and `documenter`. There is no tester, because running a suite is a known command and therefore code, not an agent's job.
|
|
112
114
|
|
|
113
115
|
Every agent gets its own model, thinking level, prompts, and tools. Give the planner a frontier model and the builder a cheap fast one. Give the reviewer no ability to write code at all.
|
|
114
116
|
|
|
@@ -243,6 +245,8 @@ spf build-test "implement the plan" --adw-id a1b2c3d4
|
|
|
243
245
|
|
|
244
246
|
Polls an issue tracker for issues labeled `<prefix>:ready`, runs a configured chain against each in its own git worktree, opens a PR against a code host, and tracks it through to merged or blocked — driving the same chains above rather than reimplementing an SDLC. Labels are the whole state machine: `ready → working → review → done`/`blocked`.
|
|
245
247
|
|
|
248
|
+
A second, optional lane (`watch.refine`, off by default) decomposes a `<prefix>:spec-ready` product spec into a feature/story-or-bug tree of real issues instead — see "Refining specs" below.
|
|
249
|
+
|
|
246
250
|
The tracker (`issue_provider`) and the code host (`code_host`) are independent config choices, not one bundled "provider" — a tracker and a host are independent choices in practice (Jira issues against a Bitbucket repo is a real setup). Supported today: `issue_provider: github | jira`, `code_host: github | bitbucket` — any combination works, including Jira+GitHub or GitHub-issues+Bitbucket.
|
|
247
251
|
|
|
248
252
|
The easiest way into any of this is `spf init`'s interview: it asks whether to enable `spf watch`, which tracker and code host, and collects exactly the env vars that combination needs (below) straight into `.env` — no hand-editing YAML or hunting down which credential pair a given combination wants.
|
|
@@ -283,15 +287,61 @@ spf watch --dry-run # log intended claims/transitions, mutate nothing
|
|
|
283
287
|
|
|
284
288
|
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.
|
|
285
289
|
|
|
290
|
+
### Refining specs (`watch.refine`)
|
|
291
|
+
|
|
292
|
+
A product spec isn't individually workable — it needs to become a feature,
|
|
293
|
+
broken down into user stories and bugs, before the build lane above has
|
|
294
|
+
anything to claim. `watch.refine` is a second lane over the same poll loop
|
|
295
|
+
that does exactly that: it polls `<prefix>:spec-ready`, runs a decomposition
|
|
296
|
+
chain (`refine` by default) against the spec in its own worktree, and
|
|
297
|
+
publishes what it produces as real tracker issues — a feature/epic container
|
|
298
|
+
plus story/bug/task leaves, linked via GitHub's native sub-issue hierarchy.
|
|
299
|
+
|
|
300
|
+
```yaml
|
|
301
|
+
watch:
|
|
302
|
+
issue_provider: github # required — issue authoring isn't implemented for Jira yet
|
|
303
|
+
repo: owner/name
|
|
304
|
+
refine:
|
|
305
|
+
enabled: true
|
|
306
|
+
chain: refine # any chain that ends in steps.publishIssues()
|
|
307
|
+
concurrency: 1 # this lane's own budget, independent of watch.concurrency
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Every generated issue carries a `<prefix>:type:epic|feature|story|bug|task`
|
|
311
|
+
label. A container (a feature/epic — something else names it as `parent`)
|
|
312
|
+
gets only that label; a leaf additionally gets `<prefix>:refined` — **never**
|
|
313
|
+
`<prefix>:ready`. Promoting a leaf to `<prefix>:ready` is a deliberate human
|
|
314
|
+
decision: the refine lane never auto-promotes anything, so a spec fanning out
|
|
315
|
+
into twenty stories doesn't turn into twenty unattended chain runs and twenty
|
|
316
|
+
PRs with nobody having looked at the breakdown first. Once you do promote a
|
|
317
|
+
leaf, the existing build lane picks it up completely unchanged.
|
|
318
|
+
|
|
319
|
+
The spec issue itself gets a `spec-ready → refining → done`/`blocked`
|
|
320
|
+
lifecycle, same shape as the build lane, and a summary comment listing every
|
|
321
|
+
issue it created. Try it by hand first, against a real spec, before turning
|
|
322
|
+
on the daemon:
|
|
323
|
+
|
|
324
|
+
```bash
|
|
325
|
+
spf refine "<spec text or path/to/spec.md>" --issue 42 # --issue renders a "## Parent: #42" back-reference
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
`spf watch init` seeds the type labels alongside the state ones. This lane's
|
|
329
|
+
prompt (`assets/prompts/refiner/`) is adapted from a "tracer-bullet ticket"
|
|
330
|
+
decomposition skill — vertical slices, a `blocked_by` dependency graph, and
|
|
331
|
+
an expand/migrate/contract sequence for wide mechanical refactors — with a
|
|
332
|
+
gate (`gates.refinementWellFormed`) added on top to enforce the
|
|
333
|
+
container/leaf shape that skill left as prose convention rather than a
|
|
334
|
+
checked rule.
|
|
335
|
+
|
|
286
336
|
### GitHub (`issue_provider: github` and/or `code_host: github`)
|
|
287
337
|
|
|
288
338
|
```bash
|
|
289
339
|
export GITHUB_TOKEN=... # classic PAT; spf doctor checks it's set
|
|
290
340
|
```
|
|
291
341
|
|
|
292
|
-
`spf watch init` seeds `<prefix>:ready`/`working`/`review`/`done`/`blocked` labels with a color and description
|
|
342
|
+
`spf watch init` seeds `<prefix>:ready`/`working`/`review`/`done`/`blocked`/`spec-ready`/`refining`/`refined` labels, plus `<prefix>:type:epic`/`feature`/`story`/`bug`/`task` (used by the refine lane whether or not it's enabled), each with a color and description — safe to re-run any time (creates what's missing, corrects any that drifted, leaves the rest alone).
|
|
293
343
|
|
|
294
|
-
A **classic** PAT (fine-grained tokens use different permission names — not covered here), scoped to the minimum that covers every call `spf watch`/`spf watch init` makes on GitHub: creating/editing labels, reading and labeling issues, posting comments, opening PRs,
|
|
344
|
+
A **classic** PAT (fine-grained tokens use different permission names — not covered here), scoped to the minimum that covers every call `spf watch`/`spf watch init` makes on GitHub: creating/editing labels, reading and labeling issues, posting comments, opening PRs, reading PR/check-run status, and — with `watch.refine.enabled` — creating issues and linking them via the sub-issues API. All of it is already covered by `repo`/`public_repo`; refine needs no additional scope.
|
|
295
345
|
|
|
296
346
|
| Target repo | Scope | Covers |
|
|
297
347
|
|---|---|---|
|
|
@@ -322,6 +372,56 @@ export BITBUCKET_API_TOKEN=... # same Atlassian API token mechanism as Jira ab
|
|
|
322
372
|
|
|
323
373
|
**Bitbucket Cloud app passwords are being fully removed** (brownout window closing July 28, 2026) — this project only supports the replacement, API tokens, which need the account's email alongside the token (username alone no longer works).
|
|
324
374
|
|
|
375
|
+
## Notifications
|
|
376
|
+
|
|
377
|
+
Optional, off by default: push a curated set of milestones to Slack,
|
|
378
|
+
Microsoft Teams, or a generic webhook. It's scoped to **unattended work**:
|
|
379
|
+
`spf watch`'s daemon lifecycle, and every chain run (`spf <chain>` / `spf
|
|
380
|
+
run`, including watch's own per-issue runs). Interactive commands — `doctor`,
|
|
381
|
+
`list`, `sessions`, `phases`, `events`, `init`, `ui`, `migrate`, `eject`,
|
|
382
|
+
`abort`, `version` — never notify; you're already looking at the terminal
|
|
383
|
+
for those.
|
|
384
|
+
|
|
385
|
+
```yaml
|
|
386
|
+
# .spf/spf.config.yaml
|
|
387
|
+
notifications:
|
|
388
|
+
events: errors # off (default) | errors | all
|
|
389
|
+
channels:
|
|
390
|
+
- kind: slack # slack | teams | webhook
|
|
391
|
+
webhook_url_env: SLACK_WEBHOOK_URL # optional; this is the default for slack
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
`events` is the whole filter: `errors` sends only failed runs/phases, blocked
|
|
395
|
+
issues, and watch errors; `all` adds every milestone — run started/finished,
|
|
396
|
+
issue claimed, PR opened, issue done. A channel's own `events` overrides the
|
|
397
|
+
top-level scope for just that channel. `spf doctor` reports whether each
|
|
398
|
+
configured channel's env var is set.
|
|
399
|
+
|
|
400
|
+
The webhook URL is a secret and lives only in `.env` — `webhook_url_env`
|
|
401
|
+
names the key, never the URL itself, matching `GITHUB_TOKEN`/
|
|
402
|
+
`JIRA_API_TOKEN`. Defaults per kind: `SLACK_WEBHOOK_URL`, `TEAMS_WEBHOOK_URL`,
|
|
403
|
+
`SPF_WEBHOOK_URL`. `spf init`'s interview asks for this section and collects
|
|
404
|
+
the URL straight into `.env`, same as every other credential.
|
|
405
|
+
|
|
406
|
+
Getting each channel's URL:
|
|
407
|
+
|
|
408
|
+
- **Slack** — [Sending messages using Incoming Webhooks](https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks): create a Slack app, enable Incoming Webhooks, "Add New Webhook to Workspace".
|
|
409
|
+
- **Microsoft Teams** — [Send messages in Teams using incoming webhooks](https://support.microsoft.com/en-us/office/post-a-workflow-when-a-webhook-request-is-received-in-microsoft-teams-8ae491c7-0394-4861-ba59-055e33f75498): in the target channel, add a Workflows webhook template (search for one along the lines of "Post to a channel when a webhook request is received" / "Send webhook alerts to a channel" — Microsoft's own naming here has shifted between revisions) and copy the generated URL. This is the *only* supported path now — the old Office 365 connector webhook has been retired by Microsoft.
|
|
410
|
+
- **webhook** — any endpoint that accepts a JSON POST of the event: Discord,
|
|
411
|
+
n8n, Zapier, a homegrown receiver.
|
|
412
|
+
|
|
413
|
+
Delivery never blocks or fails a run: an unconfigured/misconfigured channel
|
|
414
|
+
is skipped with one warning, and a failed POST logs one line and is
|
|
415
|
+
swallowed — never changes a run's exit code. One thing worth knowing under
|
|
416
|
+
`events: all`: `spf watch` runs its per-issue chains in-process, so a failed
|
|
417
|
+
issue produces **two** notifications for the same failure — `run_failed`
|
|
418
|
+
(keyed to the chain's own `adw_id`, e.g. `issue-142`) from the chain-run
|
|
419
|
+
side, and `issue_blocked` (keyed to the issue) from watch itself. Both are
|
|
420
|
+
genuinely informative, just worth expecting.
|
|
421
|
+
|
|
422
|
+
Full field reference: `spf install-skill`'s installed skill
|
|
423
|
+
(`references/config.md`).
|
|
424
|
+
|
|
325
425
|
## What's in this repo
|
|
326
426
|
|
|
327
427
|
```
|
|
@@ -335,7 +435,7 @@ super-portable-software-factory/
|
|
|
335
435
|
└── assets/
|
|
336
436
|
├── defaults/ # the packaged default roster
|
|
337
437
|
├── prompts/ # default system.md + user.md per starter agent
|
|
338
|
-
└── skill/ #
|
|
438
|
+
└── skill/ # a Claude Code skill — `spf init` installs it by default (`--no-skills` to opt out), or `spf install-skill` by hand
|
|
339
439
|
```
|
|
340
440
|
|
|
341
441
|
There's no template stamped into your repo. Everything above ships inside the installed npm package; a repo you run `spf` against only ever gains a `.spf/` directory, and only if you ask for one.
|
|
@@ -101,6 +101,22 @@ agents:
|
|
|
101
101
|
# No tester agent: running the suite is a known command, so it is a kind="code"
|
|
102
102
|
# phase over core/quality.ts. See SKILL.md hard rule 8.
|
|
103
103
|
|
|
104
|
+
- name: refiner
|
|
105
|
+
thinking: high
|
|
106
|
+
color: "#34d399"
|
|
107
|
+
purpose: Decompose a product spec into a feature/story-or-bug tree of vertical slices; change nothing.
|
|
108
|
+
prompt_engineering:
|
|
109
|
+
system: refiner/system.md
|
|
110
|
+
user: refiner/user.md
|
|
111
|
+
writes: [] # read-only w.r.t. the repo; refine_plan.md lands in context_handoff_dir, not here
|
|
112
|
+
tools: # full recon; write only for refine_plan.md, no edit
|
|
113
|
+
- read
|
|
114
|
+
- grep
|
|
115
|
+
- find
|
|
116
|
+
- ls
|
|
117
|
+
- bash
|
|
118
|
+
- write
|
|
119
|
+
|
|
104
120
|
- name: reviewer
|
|
105
121
|
model: openai/gpt-5.6-terra
|
|
106
122
|
thinking: high
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Refiner Agent
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Decompose a product spec into a feature/story-or-bug tree of vertical slices the factory can build one at a time. Change nothing.
|
|
6
|
+
|
|
7
|
+
## Instructions
|
|
8
|
+
|
|
9
|
+
- Read-only: explore the codebase to ground the decomposition, never write to it.
|
|
10
|
+
- Use the project's domain glossary vocabulary in every title and description, if the repo has one. Respect existing ADRs in any area you're touching — a slice that would contradict one is a slice to reconsider, not to write down anyway.
|
|
11
|
+
- You inherit the operator's shell environment — their PATH, toolchains and credentials are already live. Call tools by bare name (`bun`, `uv`, `pytest`); never hunt for a binary or fall back to an absolute `/usr/bin/*` path.
|
|
12
|
+
- Judge any command you run by its exit status, never by scanning its output for words. `error` or `not found` inside passing output is text, not a failure.
|
|
13
|
+
- Write your working notes to `<context_handoff_dir>/refine_plan.md` before emitting your Report JSON.
|
|
14
|
+
|
|
15
|
+
## The tree
|
|
16
|
+
|
|
17
|
+
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.
|
|
18
|
+
|
|
19
|
+
Emit a **flat list**, not nested JSON: each node names its `parent` by another node's `key`, or leaves `parent` empty for a top-level feature/epic. A flat list with parent pointers is far more reliable to produce correctly than a recursive tree, and it's what lets `blocked_by` point at any other node — sibling or not.
|
|
20
|
+
|
|
21
|
+
## Vertical-slice rules (for every leaf)
|
|
22
|
+
|
|
23
|
+
- Each slice cuts a narrow but COMPLETE path through every layer it touches (schema, API, UI, tests) — vertical, never a horizontal slice of one layer.
|
|
24
|
+
- A completed slice is demoable or verifiable on its own.
|
|
25
|
+
- Size each slice to fit in a single fresh context window for the builder that will implement it — when in doubt, split.
|
|
26
|
+
- Any prefactoring the spec implies should be its own slice, sequenced first via `blocked_by`, not folded into the first "real" slice.
|
|
27
|
+
|
|
28
|
+
## Dependencies: a DAG, not a tree
|
|
29
|
+
|
|
30
|
+
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
|
+
|
|
32
|
+
## Wide refactors — the one exception to vertical slicing
|
|
33
|
+
|
|
34
|
+
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):
|
|
35
|
+
|
|
36
|
+
- **expand**: add the new form beside the old so nothing breaks yet.
|
|
37
|
+
- **migrate**: batch the call sites by blast radius (per package, per directory) — each batch its own leaf, `blocked_by` the expand leaf. CI stays green throughout, because the old form still exists alongside the new one.
|
|
38
|
+
- **contract**: delete the old form — `blocked_by` every migrate batch.
|
|
39
|
+
|
|
40
|
+
If even a batch can't stay green alone, keep the same three-stage sequence but let the migrate batches share an integration branch that all block a final integrate-and-verify leaf — green is promised only there, not at every batch.
|
|
41
|
+
|
|
42
|
+
## Writing titles and bodies
|
|
43
|
+
|
|
44
|
+
- Title and body describe end-to-end behavior from the user's (or the next engineer's) perspective — not a layer-by-layer implementation list.
|
|
45
|
+
- 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
|
+
- 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
|
+
- `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.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Refine Task
|
|
2
|
+
|
|
3
|
+
## Variables
|
|
4
|
+
|
|
5
|
+
### prompt
|
|
6
|
+
|
|
7
|
+
{{prompt}}
|
|
8
|
+
|
|
9
|
+
### previous_envelope
|
|
10
|
+
|
|
11
|
+
{{previous_envelope}}
|
|
12
|
+
|
|
13
|
+
### context_handoff_dir
|
|
14
|
+
|
|
15
|
+
{{context_handoff_dir}}
|
|
16
|
+
|
|
17
|
+
## Task
|
|
18
|
+
|
|
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).
|
|
20
|
+
|
|
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`.
|
|
24
|
+
|
|
25
|
+
## Report
|
|
26
|
+
|
|
27
|
+
Respond with ONLY valid JSON matching `RefineOutput` — no prose before or after:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"status": "success",
|
|
32
|
+
"summary": "<one sentence: how many features and how many leaves, and the shape of the dependency chain>",
|
|
33
|
+
"artifacts": ["<context_handoff_dir>/refine_plan.md"],
|
|
34
|
+
"notes_for_next_agent": "<anything the publish step or a human reviewer should know>",
|
|
35
|
+
"issues": [
|
|
36
|
+
{
|
|
37
|
+
"key": "F1",
|
|
38
|
+
"kind": "feature",
|
|
39
|
+
"title": "<feature title>",
|
|
40
|
+
"body": "## What to build\n\n<end-to-end behavior this feature covers>\n\n## Acceptance criteria\n\n- [ ] <criterion>",
|
|
41
|
+
"parent": "",
|
|
42
|
+
"blocked_by": []
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"key": "S1",
|
|
46
|
+
"kind": "story",
|
|
47
|
+
"title": "<leaf title>",
|
|
48
|
+
"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
|
+
"parent": "F1",
|
|
50
|
+
"blocked_by": []
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"key": "S2",
|
|
54
|
+
"kind": "bug",
|
|
55
|
+
"title": "<leaf title>",
|
|
56
|
+
"body": "## What to build\n\n<the fix, described end-to-end>\n\n## Acceptance criteria\n\n- [ ] <criterion>",
|
|
57
|
+
"parent": "F1",
|
|
58
|
+
"blocked_by": ["S1"]
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Rules on the shape (enforced by a gate — a violation sends this back to you as a correction, not a silent acceptance):
|
|
65
|
+
|
|
66
|
+
- `key` is your own local id for this run, unique within `issues` — never a real tracker id.
|
|
67
|
+
- 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
|
+
- `parent` is another node's `key`, or `""` for a top-level feature/epic.
|
|
69
|
+
- `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
|
+
- `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.
|
|
@@ -12,9 +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`
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
field by field (`defaults`/`observability`/`quality`/`watch`/`notifications`
|
|
16
|
+
merge key-by-key — `notifications.channels` replaces wholesale, same as
|
|
17
|
+
`quality.checks`; `agents` merges by `name`: a matching name patches that
|
|
18
|
+
entry, a new name appends).
|
|
18
19
|
3. An explicit `--config <path>` replaces both — standalone, no built-in
|
|
19
20
|
underneath it.
|
|
20
21
|
|
|
@@ -107,6 +108,85 @@ sections: `assets/templates/ts.spf.config.yaml` in the spf package (or
|
|
|
107
108
|
`spf init --template ts` to write it straight into `.spf/spf.config.yaml`).
|
|
108
109
|
`spf init` with no `--template` prints every packaged template's name.
|
|
109
110
|
|
|
111
|
+
### `watch`
|
|
112
|
+
|
|
113
|
+
Full mechanism: the main README's "`spf watch`" section. Field reference:
|
|
114
|
+
|
|
115
|
+
| Field | Type | Meaning |
|
|
116
|
+
|---|---|---|
|
|
117
|
+
| `issue_provider` | `"github"` \| `"jira"` | The tracker `spf watch` polls. Default `github`. |
|
|
118
|
+
| `code_host` | `"github"` \| `"bitbucket"` | Where PRs open — independent of `issue_provider` (Jira issues against a Bitbucket repo is a real setup). Default `github`. |
|
|
119
|
+
| `repo` | string | Required once watch is actually run (not schema-validated — fails loudly at `spf watch` startup instead). `"owner/name"` for `code_host: github`, `"workspace/repo_slug"` for `code_host: bitbucket`. |
|
|
120
|
+
| `label_prefix` | string | State-machine label prefix — polls/writes `<prefix>:ready`, `<prefix>:working`, etc. Default `spf`. |
|
|
121
|
+
| `chain` | string | Which registered chain runs per claimed `<prefix>:ready` issue. Default `plan-build-test`. |
|
|
122
|
+
| `base_branch` | string | Branch worktrees fork from and PRs target. Default `main`. |
|
|
123
|
+
| `poll_ms` | int | Tick interval. Default `60000`. |
|
|
124
|
+
| `concurrency` | int ≥1 | Max issues claimed and run at once, the build lane's own budget (independent of `refine.concurrency`). Default `2`. |
|
|
125
|
+
| `jira.base_url` / `jira.project_key` | string | Only consulted when `issue_provider: jira`. |
|
|
126
|
+
| `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. |
|
|
127
|
+
| `refine.chain` | string | Which registered chain runs per claimed spec. Default `refine`. |
|
|
128
|
+
| `refine.concurrency` | int ≥1 | The refine lane's own budget, separate from `concurrency`. Default `1`. |
|
|
129
|
+
|
|
130
|
+
```yaml
|
|
131
|
+
watch:
|
|
132
|
+
repo: owner/name
|
|
133
|
+
label_prefix: spf
|
|
134
|
+
chain: plan-build-test
|
|
135
|
+
refine:
|
|
136
|
+
enabled: true # decompose spf:spec-ready specs into a feature/story tree
|
|
137
|
+
chain: refine
|
|
138
|
+
concurrency: 1
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Generated issues carry a second, independent label vocabulary —
|
|
142
|
+
`<prefix>:type:epic|feature|story|bug|task` — seeded by `spf watch init`
|
|
143
|
+
alongside the state labels. A feature/epic (a container: some other node
|
|
144
|
+
names it as `parent`) gets only its type label; a leaf (story/bug/task)
|
|
145
|
+
additionally gets `<prefix>:refined`, so a human can review and promote it to
|
|
146
|
+
`<prefix>:ready` when it's worth building — the refine lane never
|
|
147
|
+
auto-promotes anything.
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
and every chain run (`spf <chain>` / `spf run`, including watch's own
|
|
151
|
+
per-issue runs). Interactive commands (`doctor`, `list`, `sessions`,
|
|
152
|
+
`phases`, `events`, `init`, `ui`, `migrate`, `eject`, `abort`, `version`)
|
|
153
|
+
never notify — you're already looking at the terminal for those. Off by
|
|
154
|
+
default; adding it is entirely additive.
|
|
155
|
+
|
|
156
|
+
| Field | Type | Meaning |
|
|
157
|
+
|---|---|---|
|
|
158
|
+
| `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. |
|
|
159
|
+
| `timeout_ms` | int | Per-request timeout for a channel's HTTP POST. Default `5000`. |
|
|
160
|
+
| `channels[]` | array | See below. |
|
|
161
|
+
|
|
162
|
+
`channels[].kind`: `"slack"` \| `"teams"` \| `"webhook"`. `channels[].events`
|
|
163
|
+
overrides `events` for just that channel (unset = inherit). `webhook_url_env`
|
|
164
|
+
names the `.env` key holding the secret URL — never the URL itself, matching
|
|
165
|
+
`GITHUB_TOKEN`/`JIRA_API_TOKEN`. Empty/omitted uses the kind's own default:
|
|
166
|
+
`SLACK_WEBHOOK_URL`, `TEAMS_WEBHOOK_URL`, `SPF_WEBHOOK_URL`. `name` is a
|
|
167
|
+
cosmetic label for warning lines when you have two channels of the same
|
|
168
|
+
kind.
|
|
169
|
+
|
|
170
|
+
```yaml
|
|
171
|
+
notifications:
|
|
172
|
+
events: errors
|
|
173
|
+
channels:
|
|
174
|
+
- kind: slack
|
|
175
|
+
webhook_url_env: SLACK_WEBHOOK_URL # optional; this is the default for slack
|
|
176
|
+
- kind: teams
|
|
177
|
+
events: all # per-channel override
|
|
178
|
+
- kind: webhook
|
|
179
|
+
webhook_url_env: OPS_WEBHOOK_URL
|
|
180
|
+
name: ops-bus
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Delivery never blocks or fails a run: a channel with an unset env var is
|
|
184
|
+
skipped with one warning at startup (`spf doctor` reports the same thing as
|
|
185
|
+
a check); a failed POST logs one line and is swallowed, never changing the
|
|
186
|
+
run's exit code. See the main README's "Notifications" section for how to
|
|
187
|
+
get each channel's webhook URL, and `spf init`'s interview, which asks for
|
|
188
|
+
this section and collects the URL straight into `.env`.
|
|
189
|
+
|
|
110
190
|
### `agents[]`
|
|
111
191
|
|
|
112
192
|
| Field | Required | Meaning |
|
|
@@ -47,9 +47,9 @@ defaults:
|
|
|
47
47
|
# provider/model-id strings, which an agent's own model always wins over
|
|
48
48
|
# defaults.model above — switching coding_agent globally does NOT reset
|
|
49
49
|
# those three, so they'd run on Claude Code (redirected at Ollama) with a
|
|
50
|
-
# model id Ollama has never heard of, and fail outright. builder/scout
|
|
51
|
-
# have no model of their own in the packaged roster, so they
|
|
52
|
-
# inherit defaults.model above and need no override here.
|
|
50
|
+
# model id Ollama has never heard of, and fail outright. builder/scout/
|
|
51
|
+
# refiner have no model of their own in the packaged roster, so they
|
|
52
|
+
# correctly inherit defaults.model above and need no override here.
|
|
53
53
|
agents:
|
|
54
54
|
- name: planner
|
|
55
55
|
model: qwen3-coder:30b
|
|
@@ -39,8 +39,8 @@ defaults:
|
|
|
39
39
|
# always wins over defaults.model — switching coding_agent globally does
|
|
40
40
|
# NOT reset those three, so they'd run on Claude Code with a model id it
|
|
41
41
|
# can't resolve at all ("There's an issue with the selected model...").
|
|
42
|
-
# builder/scout have no model of their own in the packaged roster,
|
|
43
|
-
# correctly inherit defaults.model above and need no override here.
|
|
42
|
+
# builder/scout/refiner have no model of their own in the packaged roster,
|
|
43
|
+
# so they correctly inherit defaults.model above and need no override here.
|
|
44
44
|
agents:
|
|
45
45
|
- name: planner
|
|
46
46
|
model: sonnet
|
|
@@ -64,3 +64,23 @@ agents:
|
|
|
64
64
|
# label_prefix: spf
|
|
65
65
|
# chain: plan-build-test
|
|
66
66
|
# base_branch: main
|
|
67
|
+
# # Optional second lane: decompose a spf:spec-ready product spec into a
|
|
68
|
+
# # feature/story-or-bug tree of real issues instead of building it
|
|
69
|
+
# # directly. Off by default; needs issue_provider: github (the default
|
|
70
|
+
# # above) — issue authoring isn't implemented for Jira yet.
|
|
71
|
+
# refine:
|
|
72
|
+
# enabled: true
|
|
73
|
+
# chain: refine
|
|
74
|
+
# concurrency: 1
|
|
75
|
+
|
|
76
|
+
# Optional: push notifications for unattended work — spf watch's daemon
|
|
77
|
+
# lifecycle, and every chain run (including watch's own per-issue runs).
|
|
78
|
+
# Interactive commands (doctor, list, sessions, ...) never notify. events:
|
|
79
|
+
# "errors" sends only failures/blocked issues; "all" adds every milestone.
|
|
80
|
+
# The webhook URL is a secret — put it in .env under the key named below,
|
|
81
|
+
# never here. See the main README's "Notifications" section.
|
|
82
|
+
# notifications:
|
|
83
|
+
# events: errors # off (default) | errors | all
|
|
84
|
+
# channels:
|
|
85
|
+
# - kind: slack # slack | teams | webhook
|
|
86
|
+
# webhook_url_env: SLACK_WEBHOOK_URL
|
package/dist/chains/context.d.ts
CHANGED
|
@@ -18,4 +18,13 @@ export interface ChainContext {
|
|
|
18
18
|
cwd: string;
|
|
19
19
|
/** The CLI name (`"plan-build-test"`), for session.ensure()'s trace record — see core/session.ts. */
|
|
20
20
|
chain_name: string;
|
|
21
|
+
/**
|
|
22
|
+
* The originating tracker issue's id, only meaningful to the `refine`
|
|
23
|
+
* chain — `steps.publishIssues()` renders it as a `## Parent: #<id>`
|
|
24
|
+
* back-reference on every issue it creates (`core/refine.ts`'s
|
|
25
|
+
* `renderBody`). `null`/omitted for a manual run with no source issue
|
|
26
|
+
* (a bare `spf refine "<spec text>"`, no `--issue`). Every other chain
|
|
27
|
+
* ignores this field.
|
|
28
|
+
*/
|
|
29
|
+
issue_id?: string | null;
|
|
21
30
|
}
|
package/dist/chains/index.js
CHANGED
|
@@ -54,6 +54,11 @@ export const CHAINS = [
|
|
|
54
54
|
steps.qualityCheck({ suite: "all" }),
|
|
55
55
|
]),
|
|
56
56
|
stepChain("document", "write up the work that was just done, from the diff", [steps.request(), steps.changes(), steps.document()]),
|
|
57
|
+
stepChain("refine", "decompose a product spec into a feature/story-or-bug tree of tracker issues — spf watch's spec-ready lane", [
|
|
58
|
+
steps.request(),
|
|
59
|
+
steps.refine(),
|
|
60
|
+
steps.publishIssues(),
|
|
61
|
+
]),
|
|
57
62
|
{
|
|
58
63
|
name: "simple-sdlc",
|
|
59
64
|
describe: "the work is real and its shape is not obvious — plan, build, test, review, document; 3 commits",
|
package/dist/chains/steps.d.ts
CHANGED
|
@@ -35,6 +35,12 @@ export interface ChainState {
|
|
|
35
35
|
changeset: ChangeSet | null;
|
|
36
36
|
/** HEAD, pinned by request({logBaseline: true}) before the run commits anything. */
|
|
37
37
|
baseline: string;
|
|
38
|
+
/**
|
|
39
|
+
* The originating tracker issue, only meaningful to `publishIssues()` —
|
|
40
|
+
* lifted from `ChainContext.issue_id` (see its doc comment) because a
|
|
41
|
+
* `Step` only ever sees `(run, state)`, never the `ChainContext` itself.
|
|
42
|
+
*/
|
|
43
|
+
issue_id: string | null;
|
|
38
44
|
/** The run's own acceptance criterion — distinct from "every phase succeeded"; see Run.finish(). */
|
|
39
45
|
accepted: boolean;
|
|
40
46
|
/** Why not, when accepted is false — passed straight to run.finish(). */
|
|
@@ -109,6 +115,24 @@ export declare function changes(opts?: {
|
|
|
109
115
|
}): Step;
|
|
110
116
|
/** Write up the captured change. Requires a preceding changes() step. */
|
|
111
117
|
export declare function document(): Step;
|
|
118
|
+
/** Decompose the spec in `prompt` into a feature/story tree — see `RefinedIssueSchema`'s doc comment. Gated so a malformed tree (wrong container/leaf kinds, an unresolved reference, a dependency cycle) re-prompts the same session before publishIssues() ever runs. */
|
|
119
|
+
export declare function refine(): Step;
|
|
120
|
+
/**
|
|
121
|
+
* Create the tree `refine()` produced on the tracker, in dependency order,
|
|
122
|
+
* and link each node to its parent. A `code` phase, not an agent one — the
|
|
123
|
+
* decision-making (topological order, label assignment, `## Blocked by`
|
|
124
|
+
* rendering) is `core/refine.ts`'s job; this step is sequencing only, per
|
|
125
|
+
* SKILL.md's "chains stay thin" rule. Requires a preceding refine() step.
|
|
126
|
+
*
|
|
127
|
+
* Writes what it created to `<context_handoff_dir>/refine_publish.json` —
|
|
128
|
+
* the side channel `cli/commands/watch.ts`'s `runRefine` reads after the
|
|
129
|
+
* chain returns, since a chain's own return value is just an exit code.
|
|
130
|
+
* `spf watch`'s own marker/comment/transition bookkeeping for the spec
|
|
131
|
+
* issue lives entirely in `core/watch.ts`'s `runSpec`, never here — a bare
|
|
132
|
+
* `spf refine` run (no daemon, no spec issue in play) still needs this step
|
|
133
|
+
* to work standalone.
|
|
134
|
+
*/
|
|
135
|
+
export declare function publishIssues(): Step;
|
|
112
136
|
export declare function deriveRequiredAgents(steps: Step[]): string[] | ((options: Record<string, string>) => string[]);
|
|
113
137
|
export declare function deriveRequiredSuites(steps: Step[]): string[];
|
|
114
138
|
/** A display string for `spf list` — derived so it can no longer drift from what actually runs. */
|
package/dist/chains/steps.js
CHANGED
|
@@ -20,16 +20,19 @@
|
|
|
20
20
|
* chain tractable: it would only ever need to name steps and pass them
|
|
21
21
|
* tuning params, never express control flow.
|
|
22
22
|
*/
|
|
23
|
+
import { writeFileSync } from "node:fs";
|
|
24
|
+
import path from "node:path";
|
|
23
25
|
import * as changesLib from "../core/changes.js";
|
|
24
26
|
import * as gates from "../core/gates.js";
|
|
25
27
|
import * as quality from "../core/quality.js";
|
|
26
28
|
import * as agentsCfg from "../core/agents.js";
|
|
27
29
|
import * as session from "../core/session.js";
|
|
30
|
+
import * as refineLib from "../core/refine.js";
|
|
28
31
|
import { DOCUMENT_NOTES } from "../core/prompts.js";
|
|
29
|
-
import { BuildOutput, DocumentOutput, GenericOutput, PlanOutput, ReviewOutput, ScoutOutput, makeAgentCall, makeChangeCapture, makePhaseParams, } from "../core/data_types.js";
|
|
32
|
+
import { BuildOutput, DocumentOutput, GenericOutput, PlanOutput, RefineOutput, ReviewOutput, ScoutOutput, makeAgentCall, makeChangeCapture, makePhaseParams, } from "../core/data_types.js";
|
|
30
33
|
import { Run } from "../core/runner.js";
|
|
31
|
-
function makeState(prompt, options) {
|
|
32
|
-
return { prompt, options, previous: null, quality: null, review: null, changeset: null, baseline: "", accepted: true, reason: "" };
|
|
34
|
+
function makeState(prompt, options, issueId) {
|
|
35
|
+
return { prompt, options, previous: null, quality: null, review: null, changeset: null, baseline: "", issue_id: issueId, accepted: true, reason: "" };
|
|
33
36
|
}
|
|
34
37
|
function makeStep(fn, meta = {}) {
|
|
35
38
|
const step = fn;
|
|
@@ -258,6 +261,54 @@ export function document() {
|
|
|
258
261
|
};
|
|
259
262
|
return makeStep(fn, { requiredAgents: ["documenter"], label: "documenter" });
|
|
260
263
|
}
|
|
264
|
+
/** Decompose the spec in `prompt` into a feature/story tree — see `RefinedIssueSchema`'s doc comment. Gated so a malformed tree (wrong container/leaf kinds, an unresolved reference, a dependency cycle) re-prompts the same session before publishIssues() ever runs. */
|
|
265
|
+
export function refine() {
|
|
266
|
+
return agentStep({
|
|
267
|
+
name: "refine",
|
|
268
|
+
owner: "refiner",
|
|
269
|
+
output_type: RefineOutput,
|
|
270
|
+
description: "Decompose the spec into a feature/story tree of vertical slices",
|
|
271
|
+
gates: [gates.refinementWellFormed],
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Create the tree `refine()` produced on the tracker, in dependency order,
|
|
276
|
+
* and link each node to its parent. A `code` phase, not an agent one — the
|
|
277
|
+
* decision-making (topological order, label assignment, `## Blocked by`
|
|
278
|
+
* rendering) is `core/refine.ts`'s job; this step is sequencing only, per
|
|
279
|
+
* SKILL.md's "chains stay thin" rule. Requires a preceding refine() step.
|
|
280
|
+
*
|
|
281
|
+
* Writes what it created to `<context_handoff_dir>/refine_publish.json` —
|
|
282
|
+
* the side channel `cli/commands/watch.ts`'s `runRefine` reads after the
|
|
283
|
+
* chain returns, since a chain's own return value is just an exit code.
|
|
284
|
+
* `spf watch`'s own marker/comment/transition bookkeeping for the spec
|
|
285
|
+
* issue lives entirely in `core/watch.ts`'s `runSpec`, never here — a bare
|
|
286
|
+
* `spf refine` run (no daemon, no spec issue in play) still needs this step
|
|
287
|
+
* to work standalone.
|
|
288
|
+
*/
|
|
289
|
+
export function publishIssues() {
|
|
290
|
+
const fn = async (run, state) => {
|
|
291
|
+
const envelope = state.previous;
|
|
292
|
+
if (!envelope || !Array.isArray(envelope.issues)) {
|
|
293
|
+
throw new Error("publishIssues() requires a preceding refine() step in the chain's step list");
|
|
294
|
+
}
|
|
295
|
+
await run.phase(makePhaseParams({
|
|
296
|
+
name: "publish",
|
|
297
|
+
kind: "code",
|
|
298
|
+
owner: "tracker",
|
|
299
|
+
description: "Create the feature/story tree on the tracker, in dependency order, and link each to its parent",
|
|
300
|
+
}), async (ph) => {
|
|
301
|
+
const tracker = refineLib.resolveAuthoringProvider(run.cfg);
|
|
302
|
+
const created = await refineLib.publish(tracker, envelope.issues, {
|
|
303
|
+
labelPrefix: run.cfg.watch.label_prefix,
|
|
304
|
+
specIssueId: state.issue_id,
|
|
305
|
+
});
|
|
306
|
+
writeFileSync(path.join(run.context_handoff_dir, "refine_publish.json"), JSON.stringify(created.map((c) => ({ id: c.issue.id, title: c.issue.title, kind: c.kind, isLeaf: c.isLeaf })), null, 2));
|
|
307
|
+
ph.log({ created: created.length, leaves: created.filter((c) => c.isLeaf).length });
|
|
308
|
+
});
|
|
309
|
+
};
|
|
310
|
+
return makeStep(fn, { label: "code(publish)" });
|
|
311
|
+
}
|
|
261
312
|
// ── layer 3: derive ChainDefinition fields from a step list ─────────────
|
|
262
313
|
export function deriveRequiredAgents(steps) {
|
|
263
314
|
const dynamicSteps = steps.filter((s) => typeof s.requiredAgents === "function");
|
|
@@ -291,7 +342,7 @@ export function derivePhases(steps) {
|
|
|
291
342
|
/** Run a chain's step list start to finish: prologue, every step in order, then run.finish(). */
|
|
292
343
|
export async function runSteps(ctx, requiredAgents, requiredSuites, steps, options = {}) {
|
|
293
344
|
const run = startRun(ctx, requiredAgents, requiredSuites);
|
|
294
|
-
const state = makeState(ctx.prompt, options);
|
|
345
|
+
const state = makeState(ctx.prompt, options, ctx.issue_id ?? null);
|
|
295
346
|
for (const step of steps) {
|
|
296
347
|
await step(run, state);
|
|
297
348
|
}
|