@gr8ful/spf 0.3.0 → 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 +55 -5
- 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 +38 -2
- package/assets/templates/ts-cc.spf.config.yaml +3 -3
- package/assets/templates/ts.spf.config.yaml +10 -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 +6 -0
- package/dist/cli/commands/init.js +30 -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 +69 -8
- package/dist/cli/index.js +3 -3
- package/dist/cli/interview.js +17 -0
- package/dist/core/data_types.d.ts +78 -0
- package/dist/core/data_types.js +42 -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 +1 -1
- package/dist/core/refine.d.ts +39 -0
- package/dist/core/refine.js +144 -0
- package/dist/core/watch.d.ts +56 -1
- package/dist/core/watch.js +200 -11
- package/dist/test/chains.test.js +1 -0
- package/dist/test/init_command.test.js +17 -0
- package/dist/test/interview.test.js +4 -3
- package/dist/test/refine.test.d.ts +1 -0
- package/dist/test/refine.test.js +126 -0
- package/dist/test/watch.test.js +173 -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
|
|
@@ -38,6 +38,8 @@ On a real terminal, `spf init` asks a short interview — which coding agent (`c
|
|
|
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
|
|---|---|---|
|
|
@@ -385,7 +435,7 @@ super-portable-software-factory/
|
|
|
385
435
|
└── assets/
|
|
386
436
|
├── defaults/ # the packaged default roster
|
|
387
437
|
├── prompts/ # default system.md + user.md per starter agent
|
|
388
|
-
└── skill/ #
|
|
438
|
+
└── skill/ # a Claude Code skill — `spf init` installs it by default (`--no-skills` to opt out), or `spf install-skill` by hand
|
|
389
439
|
```
|
|
390
440
|
|
|
391
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.
|
|
@@ -108,9 +108,45 @@ sections: `assets/templates/ts.spf.config.yaml` in the spf package (or
|
|
|
108
108
|
`spf init --template ts` to write it straight into `.spf/spf.config.yaml`).
|
|
109
109
|
`spf init` with no `--template` prints every packaged template's name.
|
|
110
110
|
|
|
111
|
-
### `
|
|
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
|
+
|
|
112
149
|
|
|
113
|
-
Optional outbound push for unattended work — `spf watch`'s daemon lifecycle,
|
|
114
150
|
and every chain run (`spf <chain>` / `spf run`, including watch's own
|
|
115
151
|
per-issue runs). Interactive commands (`doctor`, `list`, `sessions`,
|
|
116
152
|
`phases`, `events`, `init`, `ui`, `migrate`, `eject`, `abort`, `version`)
|
|
@@ -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,6 +64,14 @@ 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
|
|
67
75
|
|
|
68
76
|
# Optional: push notifications for unattended work — spf watch's daemon
|
|
69
77
|
# lifecycle, and every chain run (including watch's own per-issue runs).
|
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
|
}
|
|
@@ -143,6 +143,12 @@ export function doctorCommand(argv) {
|
|
|
143
143
|
: 'not set — Bitbucket app passwords are being removed; spf watch needs an Atlassian account email plus an API token instead; see README.md\'s "spf watch" section');
|
|
144
144
|
}
|
|
145
145
|
check(report, "watch.chain", Boolean(findChain(cfg.watch.chain)), findChain(cfg.watch.chain) ? cfg.watch.chain : `"${cfg.watch.chain}" is not a registered chain`);
|
|
146
|
+
if (cfg.watch.refine.enabled) {
|
|
147
|
+
check(report, "watch.refine.chain", Boolean(findChain(cfg.watch.refine.chain)), findChain(cfg.watch.refine.chain) ? cfg.watch.refine.chain : `"${cfg.watch.refine.chain}" is not a registered chain`);
|
|
148
|
+
check(report, "watch.refine issue authoring", cfg.watch.issue_provider === "github", cfg.watch.issue_provider === "github"
|
|
149
|
+
? "github supports issue authoring (createIssue/sub-issues)"
|
|
150
|
+
: `watch.issue_provider is ${JSON.stringify(cfg.watch.issue_provider)} — the refine lane needs "github" (Jira issue authoring isn't implemented yet)`);
|
|
151
|
+
}
|
|
146
152
|
}
|
|
147
153
|
if (cfg.notifications.events !== "off") {
|
|
148
154
|
check(report, "notifications.events", true, cfg.notifications.events);
|
|
@@ -10,6 +10,14 @@
|
|
|
10
10
|
* `src/cli/index.ts`). Piped input, `--yes`, or `--template <name>` all
|
|
11
11
|
* fall through to the original non-interactive behavior unchanged — a
|
|
12
12
|
* scripted `spf init` must never hang waiting on stdin.
|
|
13
|
+
*
|
|
14
|
+
* Every path here also installs the repo-local Claude Code skill (the same
|
|
15
|
+
* work `spf install-skill` does by hand) unless `--no-skills` is passed —
|
|
16
|
+
* on every run, not just the first: `install-skill` is idempotent (a no-op
|
|
17
|
+
* once the skill is already up to date), so this never re-does work or
|
|
18
|
+
* clobbers a locally-edited skill file. `--user` (installing to
|
|
19
|
+
* `~/.claude/skills/spf` instead) stays a `spf install-skill` invocation of
|
|
20
|
+
* its own; `init` only ever writes the repo-local default.
|
|
13
21
|
*/
|
|
14
22
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
15
23
|
import path from "node:path";
|
|
@@ -22,6 +30,7 @@ import { paint } from "../../core/console.js";
|
|
|
22
30
|
import { createAsker, isInteractive, InterviewAborted } from "../ask.js";
|
|
23
31
|
import { gatherContext, runInterview } from "../interview.js";
|
|
24
32
|
import { readEnvFile, upsertEnvFile, writeEnvExample } from "../env_file.js";
|
|
33
|
+
import { installSkillCommand } from "./install-skill.js";
|
|
25
34
|
const TEMPLATE_SUFFIX = ".spf.config.yaml";
|
|
26
35
|
/** Every template's short name (e.g. "ts-cc"), derived from disk rather than hand-maintained — never drifts from what's actually packaged. */
|
|
27
36
|
function listTemplates() {
|
|
@@ -67,8 +76,8 @@ const STARTER_CONFIG = `# .spf/spf.config.yaml — merged ON TOP of spf's packag
|
|
|
67
76
|
# explicit Flue-style provider/model-id strings, and an agent's own model
|
|
68
77
|
# always wins over defaults.model — so those three keep running on whatever
|
|
69
78
|
# backend you just switched to, with a model id it can't resolve, unless
|
|
70
|
-
# you override their model here too (builder/scout have no model of
|
|
71
|
-
# own in the packaged roster, so they need no override). See
|
|
79
|
+
# you override their model here too (builder/scout/refiner have no model of
|
|
80
|
+
# their own in the packaged roster, so they need no override). See
|
|
72
81
|
# assets/templates/ts.spf.config.yaml (or --template ts) for the full
|
|
73
82
|
# working pattern.
|
|
74
83
|
|
|
@@ -86,6 +95,13 @@ const STARTER_CONFIG = `# .spf/spf.config.yaml — merged ON TOP of spf's packag
|
|
|
86
95
|
# label_prefix: spf
|
|
87
96
|
# chain: plan-build-test
|
|
88
97
|
# base_branch: main
|
|
98
|
+
# # Optional second lane: decompose a spf:spec-ready product spec into a
|
|
99
|
+
# # feature/story-or-bug tree of real issues. Off by default; needs
|
|
100
|
+
# # issue_provider: github — issue authoring isn't implemented for Jira yet.
|
|
101
|
+
# refine:
|
|
102
|
+
# enabled: true
|
|
103
|
+
# chain: refine
|
|
104
|
+
# concurrency: 1
|
|
89
105
|
|
|
90
106
|
# Uncomment to push notifications for unattended work — spf watch's daemon
|
|
91
107
|
# lifecycle, and every chain run (spf <chain> / spf run, including watch's
|
|
@@ -113,10 +129,18 @@ const GENERATED_HEADER = `# .spf/spf.config.yaml — written by \`spf init\`'s i
|
|
|
113
129
|
# (gitignored) — .env.example lists the key names only.
|
|
114
130
|
`;
|
|
115
131
|
export async function initCommand(argv) {
|
|
116
|
-
const { options, flags } = parseCli(argv, ["cwd", "template"], ["force", "yes"]);
|
|
132
|
+
const { options, flags } = parseCli(argv, ["cwd", "template"], ["force", "yes", "no-skills"]);
|
|
117
133
|
const anchor = paths.resolveAnchor(options["cwd"]);
|
|
118
134
|
const sfDir = path.join(anchor.repo_root, ".spf");
|
|
119
135
|
mkdirSync(sfDir, { recursive: true });
|
|
136
|
+
// Idempotent (a no-op once the skill is already up to date, a `.new`
|
|
137
|
+
// sibling rather than an overwrite for a locally-edited file) — safe to
|
|
138
|
+
// call on every `spf init`, not just the first.
|
|
139
|
+
const installSkill = () => {
|
|
140
|
+
if (flags["no-skills"])
|
|
141
|
+
return;
|
|
142
|
+
installSkillCommand(options["cwd"] ? ["--cwd", options["cwd"]] : []);
|
|
143
|
+
};
|
|
120
144
|
const configPath = path.join(sfDir, "spf.config.yaml");
|
|
121
145
|
const templateName = options["template"];
|
|
122
146
|
const interactive = !templateName && !flags["yes"] && isInteractive();
|
|
@@ -129,6 +153,7 @@ export async function initCommand(argv) {
|
|
|
129
153
|
writeFileSync(configPath, content);
|
|
130
154
|
console.log(`wrote ${configPath}${templateName ? ` (from template "${templateName}")` : ""}`);
|
|
131
155
|
}
|
|
156
|
+
installSkill();
|
|
132
157
|
}
|
|
133
158
|
else {
|
|
134
159
|
const asker = createAsker();
|
|
@@ -138,6 +163,7 @@ export async function initCommand(argv) {
|
|
|
138
163
|
if (!overwrite) {
|
|
139
164
|
console.log("leaving the existing config alone (--force to skip this prompt)");
|
|
140
165
|
asker.close();
|
|
166
|
+
installSkill();
|
|
141
167
|
ensureGitignore(anchor.repo_root, GITIGNORE_ENTRIES);
|
|
142
168
|
return 0;
|
|
143
169
|
}
|
|
@@ -155,6 +181,7 @@ export async function initCommand(argv) {
|
|
|
155
181
|
if (Object.keys(result.env).length > 0)
|
|
156
182
|
upsertEnvFile(anchor.repo_root, result.env);
|
|
157
183
|
writeEnvExample(anchor.repo_root, result.envExampleKeys);
|
|
184
|
+
installSkill();
|
|
158
185
|
// The same merge-then-validate pipeline `spf doctor` runs — catches a
|
|
159
186
|
// bad answer (e.g. a suite naming an unconfigured check) right after
|
|
160
187
|
// writing, not at the user's first real chain run. Non-fatal: the
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `spf install-skill` — copy the packaged Claude Code skill (`assets/skill/`)
|
|
3
|
-
* into a target repo (or `~/.claude/skills/spf` with `--user`)
|
|
4
|
-
*
|
|
3
|
+
* into a target repo (or `~/.claude/skills/spf` with `--user`). `spf init`
|
|
4
|
+
* calls this itself, every run, unless `--no-skills` is passed — it's the
|
|
5
|
+
* command name a user (or a script) still reaches for by hand: to reinstall
|
|
6
|
+
* after an edit, to target `--user` instead of repo-local, or on a repo
|
|
7
|
+
* that never ran `spf init` at all (a pure-built-ins setup).
|
|
5
8
|
*
|
|
6
9
|
* Idempotent via a manifest (`.spf-skill-version`: {package, version,
|
|
7
10
|
* files: {relpath: sha256}}) written at the skill root:
|
|
@@ -9,5 +9,6 @@ export function listCommand() {
|
|
|
9
9
|
console.log();
|
|
10
10
|
}
|
|
11
11
|
console.log(`spf <name> "<prompt>" [--config <path>] [--adw-id <id>] [--cwd <dir>] (spf run <name> ... works identically)`);
|
|
12
|
+
console.log(`spf watch polls a tracker and runs one of these chains per issue — spf doctor shows the current config.`);
|
|
12
13
|
return 0;
|
|
13
14
|
}
|