@gr8ful/spf 0.1.6 → 0.2.1
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 +7 -2
- package/assets/skill/cookbooks/authoring_chains.md +96 -84
- package/assets/skill/cookbooks/roster.md +3 -1
- package/assets/skill/references/config.md +5 -2
- package/assets/templates/ts-cc.spf.config.yaml +16 -0
- package/assets/templates/ts-flue-openrouter.spf.config.yaml +15 -0
- package/assets/templates/ts.spf.config.yaml +19 -4
- package/dist/chains/context.d.ts +2 -0
- package/dist/chains/index.d.ts +21 -2
- package/dist/chains/index.js +73 -104
- package/dist/chains/{adw_simple_sdlc.d.ts → simple_sdlc.d.ts} +7 -1
- package/dist/chains/{adw_simple_sdlc.js → simple_sdlc.js} +19 -30
- package/dist/chains/steps.d.ts +117 -0
- package/dist/chains/steps.js +299 -0
- package/dist/cli/ask.d.ts +27 -0
- package/dist/cli/ask.js +125 -0
- package/dist/cli/commands/doctor.js +2 -24
- package/dist/cli/commands/init.d.ts +1 -1
- package/dist/cli/commands/init.js +92 -9
- package/dist/cli/commands/run.d.ts +1 -1
- package/dist/cli/commands/run.js +3 -1
- package/dist/cli/commands/watch.js +26 -4
- package/dist/cli/env_file.d.ts +18 -0
- package/dist/cli/env_file.js +99 -0
- package/dist/cli/index.js +2 -2
- package/dist/cli/interview.d.ts +24 -0
- package/dist/cli/interview.js +330 -0
- package/dist/core/issues/jira_provider.d.ts +9 -0
- package/dist/core/issues/jira_provider.js +20 -2
- package/dist/core/prompts.d.ts +2 -0
- package/dist/core/prompts.js +2 -0
- package/dist/core/providers.d.ts +12 -0
- package/dist/core/providers.js +24 -0
- package/dist/core/quality.d.ts +9 -0
- package/dist/core/quality.js +10 -0
- package/dist/core/session.d.ts +6 -1
- package/dist/core/session.js +7 -3
- package/dist/core/tracer.js +1 -1
- package/dist/core/utils.d.ts +6 -2
- package/dist/core/utils.js +11 -2
- package/dist/core/watch.d.ts +12 -0
- package/dist/core/watch.js +11 -0
- package/dist/test/chains.test.d.ts +12 -0
- package/dist/test/chains.test.js +86 -0
- package/dist/test/env_file.test.d.ts +1 -0
- package/dist/test/env_file.test.js +74 -0
- package/dist/test/fake_asker.d.ts +23 -0
- package/dist/test/fake_asker.js +30 -0
- package/dist/test/init_command.test.d.ts +1 -0
- package/dist/test/init_command.test.js +66 -0
- package/dist/test/interview.test.d.ts +1 -0
- package/dist/test/interview.test.js +179 -0
- package/dist/test/ui_server.test.js +1 -1
- package/dist/test/watch.test.js +41 -1
- package/dist/ui/shared/types.d.ts +1 -1
- package/package.json +5 -2
- package/dist/chains/adw_build.d.ts +0 -12
- package/dist/chains/adw_build.js +0 -27
- package/dist/chains/adw_build_review.d.ts +0 -21
- package/dist/chains/adw_build_review.js +0 -55
- package/dist/chains/adw_build_test.d.ts +0 -21
- package/dist/chains/adw_build_test.js +0 -67
- package/dist/chains/adw_document.d.ts +0 -23
- package/dist/chains/adw_document.js +0 -59
- package/dist/chains/adw_plan.d.ts +0 -12
- package/dist/chains/adw_plan.js +0 -27
- package/dist/chains/adw_plan_build.d.ts +0 -12
- package/dist/chains/adw_plan_build.js +0 -30
- package/dist/chains/adw_plan_build_test.d.ts +0 -16
- package/dist/chains/adw_plan_build_test.js +0 -65
- package/dist/chains/adw_plan_build_test_quality.d.ts +0 -18
- package/dist/chains/adw_plan_build_test_quality.js +0 -66
- package/dist/chains/adw_prompt.d.ts +0 -12
- package/dist/chains/adw_prompt.js +0 -25
- package/dist/chains/adw_quality.d.ts +0 -12
- package/dist/chains/adw_quality.js +0 -32
- package/dist/chains/adw_scout.d.ts +0 -12
- package/dist/chains/adw_scout.js +0 -27
package/README.md
CHANGED
|
@@ -28,12 +28,15 @@ spf scout "describe this repo" # a real, read-only run, no setup required
|
|
|
28
28
|
### Customizing a repo
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
|
-
spf init #
|
|
31
|
+
spf init # on a TTY: an interview — agent, model, quality checks, spf watch, secrets
|
|
32
|
+
spf init --yes # skip the interview — seed the commented, all-defaults starter config instead
|
|
32
33
|
spf init --template ts-cc # or start from a packaged, ready-to-run template instead
|
|
33
34
|
spf list # every chain this install knows, its phases, what it needs
|
|
34
35
|
```
|
|
35
36
|
|
|
36
|
-
`spf init`
|
|
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.
|
|
38
|
+
|
|
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.
|
|
37
40
|
|
|
38
41
|
### Local development
|
|
39
42
|
|
|
@@ -242,6 +245,8 @@ Polls an issue tracker for issues labeled `<prefix>:ready`, runs a configured ch
|
|
|
242
245
|
|
|
243
246
|
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.
|
|
244
247
|
|
|
248
|
+
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.
|
|
249
|
+
|
|
245
250
|
```yaml
|
|
246
251
|
# .spf/spf.config.yaml — GitHub issues + GitHub PRs (the default)
|
|
247
252
|
watch:
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
# Authoring Chains
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
primitives a chain needs (an output type, a gate) are one skill with
|
|
5
|
-
doors. All three live in `src/` inside the SPF package itself — there
|
|
6
|
-
per-repo copy to edit. If you need to change engine behavior for a
|
|
7
|
-
target repo without forking the package, that's `spf eject` (prints
|
|
8
|
-
to the installed package's `src/` for you to copy and load from your
|
|
9
|
-
`.spf/` — engine-level changes are the one thing `.spf/` config can't
|
|
3
|
+
Composing a new chain, extending an existing one, and adding the engine
|
|
4
|
+
primitives a chain needs (an output type, a gate, a step) are one skill with
|
|
5
|
+
three doors. All three live in `src/` inside the SPF package itself — there
|
|
6
|
+
is no per-repo copy to edit. If you need to change engine behavior for a
|
|
7
|
+
specific target repo without forking the package, that's `spf eject` (prints
|
|
8
|
+
the path to the installed package's `src/` for you to copy and load from your
|
|
9
|
+
own `.spf/` — engine-level changes are the one thing `.spf/` config can't
|
|
10
|
+
express).
|
|
10
11
|
|
|
11
12
|
## Step 1 — design the chain before writing code
|
|
12
13
|
|
|
13
14
|
Lay out the phases as a table: name, kind, owner, output type (if `agent`),
|
|
14
|
-
gates. This is the same table `spf list` will end up describing
|
|
15
|
+
gates. This is the same table `spf list` will end up describing (derived from
|
|
16
|
+
the step list itself — see Step 2 — so it can't drift from what actually runs).
|
|
15
17
|
|
|
16
18
|
| Phase | Kind | Owner | Output type | Gates |
|
|
17
19
|
|---|---|---|---|---|
|
|
@@ -34,62 +36,52 @@ gates. This is the same table `spf list` will end up describing.
|
|
|
34
36
|
(`quality.runSuite`, `changes.capture`, `run.git.commitAll`). If a human
|
|
35
37
|
could write the exact steps down without judgment, it's `code`.
|
|
36
38
|
|
|
37
|
-
## Step 2 —
|
|
39
|
+
## Step 2 — compose the chain from steps
|
|
38
40
|
|
|
39
|
-
|
|
40
|
-
`
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
Almost every chain is a flat array of **steps** — named primitives from
|
|
42
|
+
`src/chains/steps.ts`, each one a `run.phase(...)` call (or a small bounded
|
|
43
|
+
group of them, for the two loops). There is no module to write and nothing to
|
|
44
|
+
register anywhere else: a `CHAINS` entry built with `stepChain()` *is* the
|
|
45
|
+
registration, in `src/chains/index.ts`.
|
|
43
46
|
|
|
44
47
|
```ts
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
import * as quality from "../core/quality.ts";
|
|
53
|
-
import * as session from "../core/session.ts";
|
|
54
|
-
import { BuildOutput, PlanOutput, makeAgentCall, makePhaseParams } from "../core/data_types.ts";
|
|
55
|
-
import type { ChainContext } from "./context.ts";
|
|
56
|
-
|
|
57
|
-
export const REQUIRED_AGENTS = ["planner", "builder"];
|
|
58
|
-
export const REQUIRED_SUITES: string[] = ["test"]; // [] if this chain runs no quality suite
|
|
59
|
-
|
|
60
|
-
export async function main(ctx: ChainContext): Promise<number> {
|
|
61
|
-
const { prompt, config_paths, adw_id, cwd } = ctx;
|
|
62
|
-
const cfg = agents.loadConfig(config_paths);
|
|
63
|
-
agents.validate(cfg, REQUIRED_AGENTS, REQUIRED_SUITES, cwd);
|
|
64
|
-
const run = session.ensure(cfg, adw_id, cwd);
|
|
65
|
-
|
|
66
|
-
await run.phase(
|
|
67
|
-
makePhaseParams({ name: "request", kind: "engineer", owner: run.engineer, description: "Capture the incoming ask" }),
|
|
68
|
-
async (ph) => { ph.log({ input: prompt }); },
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
const plan = await run.phase(
|
|
72
|
-
makePhaseParams({ name: "plan", kind: "agent", owner: "planner", description: "Turn the request into an implementable plan" }),
|
|
73
|
-
(ph) => ph.call(makeAgentCall({ output_type: PlanOutput, prompt, gates: [gates.artifactsExist] })),
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
const build = await run.phase(
|
|
77
|
-
makePhaseParams({ name: "build", kind: "agent", owner: "builder", description: "Implement the plan" }),
|
|
78
|
-
(ph) => ph.call(makeAgentCall({ output_type: BuildOutput, prompt, previous: plan, gates: [gates.diffMatchesClaims] })),
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
await run.phase(
|
|
82
|
-
makePhaseParams({ name: "commit", kind: "code", owner: "git", description: "Commit the implementation" }),
|
|
83
|
-
async () => run.git.commitAll(build.commit_message || `spf: ${prompt.slice(0, 72)}`),
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
return run.finish();
|
|
87
|
-
}
|
|
48
|
+
stepChain("plan-build-test", "the standard chain — plan, build, test, commit", [
|
|
49
|
+
steps.request(),
|
|
50
|
+
steps.plan(),
|
|
51
|
+
steps.build(),
|
|
52
|
+
steps.fixLoop({ suite: "test" }),
|
|
53
|
+
steps.commit({ onlyIfAccepted: true }),
|
|
54
|
+
]),
|
|
88
55
|
```
|
|
89
56
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
57
|
+
`stepChain()` derives `phases` (the `spf list` display string),
|
|
58
|
+
`requiredAgents`, and `requiredSuites` from the step list itself — nothing to
|
|
59
|
+
keep in sync by hand. The available steps:
|
|
60
|
+
|
|
61
|
+
| Step | What it replaces | Notes |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| `request({description?, logBaseline?})` | the opening `engineer(request)` phase | every chain starts with this |
|
|
64
|
+
| `plan()` | a `planner` phase producing `PlanOutput` | — |
|
|
65
|
+
| `build({fromPlan?, retries?})` | a `builder` phase producing `BuildOutput` | `fromPlan` only changes the phase's description |
|
|
66
|
+
| `scout()` | a `scout` phase producing `ScoutOutput` | read-only |
|
|
67
|
+
| `promptOnly()` | the `prompt` chain's one step | owner comes from `--agent`, default `builder` |
|
|
68
|
+
| `qualityCheck({suite})` | one deterministic quality/test phase | never fails its own phase — see below |
|
|
69
|
+
| `fixLoop({suite, max?, owner?})` | a bounded check→fix loop | owns its own iteration and phase naming (`test_1`, `fix_1`, ...) |
|
|
70
|
+
| `reviseLoop({max?})` | a bounded review→revise loop | same idea, for `reviewer`/`builder` |
|
|
71
|
+
| `changes({base?})` | a `git diff`-against-a-base code phase | feeds `document()` |
|
|
72
|
+
| `document()` | a `documenter` phase | requires a preceding `changes()` |
|
|
73
|
+
| `commit({onlyIfAccepted?})` | a `git commit` code phase | commits whatever the last agent step produced |
|
|
74
|
+
|
|
75
|
+
**Why the loops are steps, not a `for` you write in the chain.** `fixLoop`
|
|
76
|
+
and `reviseLoop` own their bounded iteration internally, so a chain's step
|
|
77
|
+
list is always flat — no loop or conditional syntax at the composition
|
|
78
|
+
layer. This is also what keeps a future declarative (YAML) chain tractable:
|
|
79
|
+
it only ever needs to name steps and pass them tuning params.
|
|
80
|
+
|
|
81
|
+
Steps read and write a shared `ChainState` (`prompt`, `options`, `previous` —
|
|
82
|
+
the last agent envelope, `accepted`/`reason` for `run.finish()`, etc.) so a
|
|
83
|
+
step never has to be told what the step before it produced; it just reads
|
|
84
|
+
`state.previous`.
|
|
93
85
|
|
|
94
86
|
**Before you ship it:** run it against a scratch repo (`spf <name> "..." --cwd
|
|
95
87
|
/tmp/scratch-repo`), then `spf phases <adw_id>` and `spf events <adw_id>` to
|
|
@@ -98,47 +90,67 @@ expect, and open one `envelope.json` under
|
|
|
98
90
|
`.spf/data/sessions/<adw_id>/<agent>/` to confirm it matches the type you
|
|
99
91
|
declared.
|
|
100
92
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
Insert a `run.phase(...)` block in sequence; nothing else in the chain needs
|
|
104
|
-
to change unless the new phase's output feeds a later one (thread it through
|
|
105
|
-
as a local variable, the way `plan`/`build` are threaded above).
|
|
93
|
+
### When a chain doesn't fit the step vocabulary
|
|
106
94
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
95
|
+
`simple-sdlc` (`src/chains/simple_sdlc.ts`) is the one exception: three
|
|
96
|
+
commits, a pinned baseline, and a conditional retest don't collapse into a
|
|
97
|
+
flat list. A chain like that is still a module exporting `REQUIRED_AGENTS`,
|
|
98
|
+
`REQUIRED_SUITES`, and `main(ctx: ChainContext): Promise<number>`, registered
|
|
99
|
+
in `CHAINS` with a `run:` field instead of `steps:`. Reuse
|
|
100
|
+
`steps.startRun`/`commitEnvelope`/`logChangeset` and `quality.record` rather
|
|
101
|
+
than re-copying them — read `simple_sdlc.ts` before reaching for this escape
|
|
102
|
+
hatch; it is meant for the rare chain whose control flow genuinely doesn't
|
|
103
|
+
reduce to a list, not a shortcut around learning the step vocabulary.
|
|
110
104
|
|
|
111
|
-
## Step
|
|
105
|
+
## Step 3 — add a phase to an existing chain
|
|
112
106
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
107
|
+
Most of the time this means adding, removing, or reordering an entry in a
|
|
108
|
+
`stepChain(...)` array in `src/chains/index.ts` — nothing else needs to
|
|
109
|
+
change unless the new step's output feeds a later one (it will, automatically,
|
|
110
|
+
if it writes to `state.previous`/`state.changeset`/etc., since every step
|
|
111
|
+
reads from the same shared state).
|
|
112
|
+
|
|
113
|
+
If no existing step does what you need, write one in `src/chains/steps.ts`:
|
|
114
|
+
a function returning a `Step` (a `(run, state) => Promise<void>`), doing one
|
|
115
|
+
`run.phase(...)` call (or a small bounded group, if it's loop-shaped like
|
|
116
|
+
`fixLoop`/`reviseLoop`). Declare what it needs via the optional
|
|
117
|
+
`requiredAgents`/`requiredSuites`/`label` properties so
|
|
118
|
+
`deriveRequiredAgents`/`deriveRequiredSuites`/`derivePhases` pick it up
|
|
119
|
+
automatically — `makeStep(fn, meta)` attaches these for you.
|
|
120
|
+
|
|
121
|
+
## Step 4 — the bounded fix loop, if you need a different shape than `fixLoop()` provides
|
|
122
|
+
|
|
123
|
+
`steps.fixLoop({suite, max, owner})` already covers the common case — a
|
|
124
|
+
known check, and if it fails, the builder repairs it, bounded so a chain
|
|
125
|
+
can't spin forever, never leaving an unverified fix on the last iteration.
|
|
126
|
+
Reach for it first. If you need a genuinely different shape (a different
|
|
127
|
+
envelope type feeding the fix, say), its body in `src/chains/steps.ts` is the
|
|
128
|
+
reference implementation to start from:
|
|
116
129
|
|
|
117
130
|
```ts
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
for (let i = 1; i <= MAX_FIX_LOOPS; i++) {
|
|
121
|
-
test = await run.phase(
|
|
131
|
+
for (let i = 1; i <= max; i++) {
|
|
132
|
+
const result = await run.phase(
|
|
122
133
|
makePhaseParams({ name: `test_${i}`, kind: "code", owner: "quality", description: "Run the suite" }),
|
|
123
|
-
async (ph) => { const
|
|
134
|
+
async (ph) => { const r = quality.runTests(run); quality.record(ph, r); return r; },
|
|
124
135
|
);
|
|
125
|
-
if (
|
|
126
|
-
|
|
136
|
+
if (result.passed) break;
|
|
137
|
+
if (i === max) break; // never leave an unverified fix on the table
|
|
138
|
+
state.previous = await run.phase(
|
|
127
139
|
makePhaseParams({ name: `fix_${i}`, kind: "agent", owner: "builder", retries: 1, description: "Repair what the suite reported" }),
|
|
128
|
-
(ph) => ph.call(makeAgentCall({ output_type: BuildOutput, prompt, previous: quality.asEnvelope(
|
|
140
|
+
(ph) => ph.call(makeAgentCall({ output_type: BuildOutput, prompt: state.prompt, previous: quality.asEnvelope(result, "tests"), gates: [gates.diffMatchesClaims] })),
|
|
129
141
|
);
|
|
130
142
|
}
|
|
131
|
-
return run.finish(test !== null && test.passed, `the suite still failed after ${MAX_FIX_LOOPS} fix attempt(s)`);
|
|
132
143
|
```
|
|
133
144
|
|
|
134
145
|
A failing suite does **not** fail its own phase — the runner did its job,
|
|
135
146
|
the *code under test* is what failed. It's `run.finish()`'s job, at the end,
|
|
136
|
-
to decide whether the whole run is accepted
|
|
147
|
+
to decide whether the whole run is accepted — which is why a step sets
|
|
148
|
+
`state.accepted`/`state.reason` rather than throwing.
|
|
137
149
|
|
|
138
150
|
## Adding an engine primitive
|
|
139
151
|
|
|
140
|
-
These live in `src/core/`, not in a chain. Chains
|
|
141
|
-
only, no business logic.
|
|
152
|
+
These live in `src/core/`, not in a chain or a step. Chains and steps stay
|
|
153
|
+
thin: sequencing only, no business logic.
|
|
142
154
|
|
|
143
155
|
### Adding an output type
|
|
144
156
|
|
|
@@ -153,7 +165,7 @@ export const MyOutput = envelopeType("MyOutput", {
|
|
|
153
165
|
export type MyOutputT = v.InferOutput<typeof MyOutput.schema>;
|
|
154
166
|
```
|
|
155
167
|
|
|
156
|
-
Then the two other legs of the synced triad: the calling
|
|
168
|
+
Then the two other legs of the synced triad: the calling step's
|
|
157
169
|
`output_type: MyOutput`, and the agent's `user.md` `## Report` section
|
|
158
170
|
showing the exact JSON shape. All three must move together — see
|
|
159
171
|
`references/handoff.md`.
|
|
@@ -155,7 +155,9 @@ startup, before anything spawns:
|
|
|
155
155
|
`authoring_chains.md`. The user prompt's `## Report` section must show
|
|
156
156
|
exactly that JSON shape.
|
|
157
157
|
|
|
158
|
-
Then
|
|
158
|
+
Then use it as a step's `owner` (`steps.build()`, `steps.plan()`, ...) or write
|
|
159
|
+
a new step naming it — see `authoring_chains.md`. A chain's `requiredAgents` is
|
|
160
|
+
derived from its step list, not hand-maintained.
|
|
159
161
|
|
|
160
162
|
## Write permissions — `writes` and `protected_files`
|
|
161
163
|
|
|
@@ -18,8 +18,11 @@ always shows the resolved, merged result for the repo you're in.
|
|
|
18
18
|
3. An explicit `--config <path>` replaces both — standalone, no built-in
|
|
19
19
|
underneath it.
|
|
20
20
|
|
|
21
|
-
`spf init` seeds step 2
|
|
22
|
-
|
|
21
|
+
`spf init` seeds step 2 — on a TTY, via an interview that asks the fields
|
|
22
|
+
below and appends whatever secrets they imply to `.env`; non-interactively
|
|
23
|
+
(`--yes`, `--template <name>`, or no TTY) it writes a commented starter
|
|
24
|
+
instead. Omitting `.spf/spf.config.yaml` entirely means running off pure
|
|
25
|
+
built-ins, which is a fully supported, valid state.
|
|
23
26
|
|
|
24
27
|
## Shape
|
|
25
28
|
|
|
@@ -41,3 +41,19 @@ quality:
|
|
|
41
41
|
defaults:
|
|
42
42
|
coding_agent: claude_code
|
|
43
43
|
model: qwen3-coder:30b # swap for whatever `ollama list` shows on your machine
|
|
44
|
+
|
|
45
|
+
# REQUIRED, not just an example: the packaged default roster pins
|
|
46
|
+
# planner/reviewer/documenter to their own explicit Flue-style
|
|
47
|
+
# provider/model-id strings, which an agent's own model always wins over
|
|
48
|
+
# defaults.model above — switching coding_agent globally does NOT reset
|
|
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 correctly
|
|
52
|
+
# inherit defaults.model above and need no override here.
|
|
53
|
+
agents:
|
|
54
|
+
- name: planner
|
|
55
|
+
model: qwen3-coder:30b
|
|
56
|
+
- name: reviewer
|
|
57
|
+
model: qwen3-coder:30b
|
|
58
|
+
- name: documenter
|
|
59
|
+
model: qwen3-coder:30b
|
|
@@ -36,3 +36,18 @@ defaults:
|
|
|
36
36
|
# (kimi-k3, gemini-3.6, gpt-5.6, claude-sonnet-5, ...), not a live catalog
|
|
37
37
|
# entry verified against a real API.
|
|
38
38
|
model: openrouter/moonshotai/kimi-k2.7
|
|
39
|
+
|
|
40
|
+
# Not required the way the claude_code templates' agents: override is (an
|
|
41
|
+
# agent's own model always wins over defaults.model, but Flue accepts any
|
|
42
|
+
# provider/model-id, so the packaged roster's planner/reviewer/documenter
|
|
43
|
+
# — fireworks/openai models, unrelated to OpenRouter — would still resolve
|
|
44
|
+
# and run fine, just outside OPENROUTER_API_KEY's reach). Overridden here
|
|
45
|
+
# anyway so every agent actually demonstrates the OpenRouter routing this
|
|
46
|
+
# template is about, not just the three without their own packaged model.
|
|
47
|
+
agents:
|
|
48
|
+
- name: planner
|
|
49
|
+
model: openrouter/moonshotai/kimi-k2.7
|
|
50
|
+
- name: reviewer
|
|
51
|
+
model: openrouter/moonshotai/kimi-k2.7
|
|
52
|
+
- name: documenter
|
|
53
|
+
model: openrouter/moonshotai/kimi-k2.7
|
|
@@ -27,14 +27,29 @@ quality:
|
|
|
27
27
|
# Optional: run agents on Claude Code instead of the default Flue backend.
|
|
28
28
|
# Needs the `claude` CLI installed and authenticated (`spf doctor` checks) —
|
|
29
29
|
# no separate provider API key required if you're logged in via `claude
|
|
30
|
-
# login`. Remove this whole block
|
|
31
|
-
# roster instead.
|
|
30
|
+
# login`. Remove this whole block (and the agents: override below) to stay
|
|
31
|
+
# on Flue with a `provider/model-id` roster instead.
|
|
32
32
|
defaults:
|
|
33
33
|
coding_agent: claude_code
|
|
34
34
|
model: sonnet # claude_code's own alias — NOT provider/model-id
|
|
35
35
|
|
|
36
|
-
#
|
|
37
|
-
#
|
|
36
|
+
# REQUIRED alongside coding_agent: claude_code above, not just an example:
|
|
37
|
+
# the packaged default roster pins planner/reviewer/documenter to their own
|
|
38
|
+
# explicit Flue-style provider/model-id strings, which an agent's own model
|
|
39
|
+
# always wins over defaults.model — switching coding_agent globally does
|
|
40
|
+
# NOT reset those three, so they'd run on Claude Code with a model id it
|
|
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, so they
|
|
43
|
+
# correctly inherit defaults.model above and need no override here.
|
|
44
|
+
agents:
|
|
45
|
+
- name: planner
|
|
46
|
+
model: sonnet
|
|
47
|
+
- name: reviewer
|
|
48
|
+
model: sonnet
|
|
49
|
+
- name: documenter
|
|
50
|
+
model: sonnet
|
|
51
|
+
|
|
52
|
+
# Optional: retune one agent further without touching the rest of the roster.
|
|
38
53
|
# - name: builder
|
|
39
54
|
# thinking: high
|
|
40
55
|
# writes: [src/, tests/] # narrow what this agent may change in the repo
|
package/dist/chains/context.d.ts
CHANGED
|
@@ -16,4 +16,6 @@ export interface ChainContext {
|
|
|
16
16
|
adw_id: string | null;
|
|
17
17
|
/** Absolute. The anchor session.ensure() resolves repo_root and data_dir from. */
|
|
18
18
|
cwd: string;
|
|
19
|
+
/** The CLI name (`"plan-build-test"`), for session.ensure()'s trace record — see core/session.ts. */
|
|
20
|
+
chain_name: string;
|
|
19
21
|
}
|
package/dist/chains/index.d.ts
CHANGED
|
@@ -2,18 +2,37 @@
|
|
|
2
2
|
* The chain registry — the one place a CLI-facing short name maps to a
|
|
3
3
|
* chain module. `spf list` reads this; `spf <name>`/`spf run <name>` dispatch
|
|
4
4
|
* through it. Replaces guessing a module filename from the CLI argument.
|
|
5
|
+
*
|
|
6
|
+
* Every chain but `simple-sdlc` is a flat `steps` list composed from
|
|
7
|
+
* `./steps.ts`'s primitives — no module of its own, and no hand-written
|
|
8
|
+
* `phases`/`requiredAgents`/`requiredSuites` to keep in sync with what
|
|
9
|
+
* actually runs; those are derived from the step list itself.
|
|
10
|
+
* `simple-sdlc`'s three-commit, pinned-baseline shape is still imperative —
|
|
11
|
+
* see `./simple_sdlc.ts` — via the `run` escape hatch below.
|
|
5
12
|
*/
|
|
6
13
|
import type { ChainContext } from "./context.ts";
|
|
14
|
+
import * as steps from "./steps.ts";
|
|
7
15
|
export interface ChainDefinition {
|
|
8
16
|
/** The name typed on the CLI: `spf <name> "..."`. */
|
|
9
17
|
name: string;
|
|
10
18
|
describe: string;
|
|
11
19
|
phases: string;
|
|
12
|
-
/** Static for most chains;
|
|
20
|
+
/** Static for most chains; prompt's depends on --agent, so it's a function there. */
|
|
13
21
|
requiredAgents: string[] | ((options: Record<string, string>) => string[]);
|
|
14
22
|
requiredSuites: string[];
|
|
15
|
-
|
|
23
|
+
/** The declarative path — a flat step list, run by runChain() via steps.runSteps(). */
|
|
24
|
+
steps?: steps.Step[];
|
|
25
|
+
/** The imperative escape hatch for a chain too shaped by its own logic to be a flat list (simple-sdlc). */
|
|
26
|
+
run?: (ctx: ChainContext, options?: Record<string, string>) => Promise<number>;
|
|
16
27
|
}
|
|
17
28
|
export declare const CHAINS: ChainDefinition[];
|
|
18
29
|
export declare function findChain(name: string): ChainDefinition | undefined;
|
|
19
30
|
export declare function resolveRequiredAgents(chain: ChainDefinition, options: Record<string, string>): string[];
|
|
31
|
+
/**
|
|
32
|
+
* Run a chain, whichever path it defines: `run` (the imperative escape
|
|
33
|
+
* hatch) if it has one, otherwise `steps` through the shared driver. Both
|
|
34
|
+
* CLI dispatch sites (`spf <chain>` and `spf watch`) go through this, never
|
|
35
|
+
* `chain.run(...)` directly — the whole reason to route through here is that
|
|
36
|
+
* a `steps`-only chain has no `run` to call.
|
|
37
|
+
*/
|
|
38
|
+
export declare function runChain(chain: ChainDefinition, ctx: ChainContext, options?: Record<string, string>): Promise<number>;
|
package/dist/chains/index.js
CHANGED
|
@@ -1,111 +1,68 @@
|
|
|
1
|
-
import * as
|
|
2
|
-
import * as
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
import * as steps from "./steps.js";
|
|
2
|
+
import * as simpleSdlc from "./simple_sdlc.js";
|
|
3
|
+
/** Build a step-based ChainDefinition, deriving phases/requiredAgents/requiredSuites from its steps. */
|
|
4
|
+
function stepChain(name, describe, list) {
|
|
5
|
+
return {
|
|
6
|
+
name,
|
|
7
|
+
describe,
|
|
8
|
+
phases: steps.derivePhases(list),
|
|
9
|
+
requiredAgents: steps.deriveRequiredAgents(list),
|
|
10
|
+
requiredSuites: steps.deriveRequiredSuites(list),
|
|
11
|
+
steps: list,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
13
14
|
export const CHAINS = [
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
describe: "there is a suite to satisfy — build, test, bounded fix loop",
|
|
57
|
-
phases: "engineer(request) -> builder -> code(test) [-> builder(fix) -> code(test) ...]",
|
|
58
|
-
requiredAgents: adwBuildTest.REQUIRED_AGENTS,
|
|
59
|
-
requiredSuites: adwBuildTest.REQUIRED_SUITES,
|
|
60
|
-
run: adwBuildTest.main,
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
name: "plan-build-test",
|
|
64
|
-
describe: "the standard chain — plan, build, test, commit",
|
|
65
|
-
phases: "engineer(request) -> planner -> builder -> code(test) [-> fix loop] -> git(commit)",
|
|
66
|
-
requiredAgents: adwPlanBuildTest.REQUIRED_AGENTS,
|
|
67
|
-
requiredSuites: adwPlanBuildTest.REQUIRED_SUITES,
|
|
68
|
-
run: adwPlanBuildTest.main,
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
name: "plan-build-test-quality",
|
|
72
|
-
describe: "the repo has quality commands worth enforcing beyond tests — same, plus lint/typecheck/build gates",
|
|
73
|
-
phases: "engineer(request) -> planner -> builder -> code(quality:all) [-> fix loop] -> git(commit)",
|
|
74
|
-
requiredAgents: adwPlanBuildTestQuality.REQUIRED_AGENTS,
|
|
75
|
-
requiredSuites: adwPlanBuildTestQuality.REQUIRED_SUITES,
|
|
76
|
-
run: adwPlanBuildTestQuality.main,
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
name: "build-review",
|
|
80
|
-
describe: "\"is this what was asked for\" matters more than \"does it run\"",
|
|
81
|
-
phases: "engineer(request) -> builder -> reviewer [-> revise loop]",
|
|
82
|
-
requiredAgents: adwBuildReview.REQUIRED_AGENTS,
|
|
83
|
-
requiredSuites: adwBuildReview.REQUIRED_SUITES,
|
|
84
|
-
run: adwBuildReview.main,
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
name: "quality",
|
|
88
|
-
describe: "lint, typecheck, build — no agents at all",
|
|
89
|
-
phases: "engineer(request) -> code(quality:all)",
|
|
90
|
-
requiredAgents: adwQuality.REQUIRED_AGENTS,
|
|
91
|
-
requiredSuites: adwQuality.REQUIRED_SUITES,
|
|
92
|
-
run: adwQuality.main,
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
name: "document",
|
|
96
|
-
describe: "write up the work that was just done, from the diff",
|
|
97
|
-
phases: "engineer(request) -> code(changes) -> documenter",
|
|
98
|
-
requiredAgents: adwDocument.REQUIRED_AGENTS,
|
|
99
|
-
requiredSuites: adwDocument.REQUIRED_SUITES,
|
|
100
|
-
run: adwDocument.main,
|
|
101
|
-
},
|
|
15
|
+
stepChain("prompt", "one agent, one prompt, traced end to end — --agent <name> picks who (default: builder)", [
|
|
16
|
+
steps.request(),
|
|
17
|
+
steps.promptOnly(),
|
|
18
|
+
]),
|
|
19
|
+
stepChain("scout", "read-only recon; nothing changes", [steps.request(), steps.scout()]),
|
|
20
|
+
stepChain("plan", "turn a request into an implementable plan", [steps.request(), steps.plan()]),
|
|
21
|
+
stepChain("build", "implement an existing plan", [steps.request(), steps.build({ fromPlan: false, retries: 1 })]),
|
|
22
|
+
stepChain("plan-build", "small, well-understood work — plan, build, commit", [
|
|
23
|
+
steps.request(),
|
|
24
|
+
steps.plan(),
|
|
25
|
+
steps.build(),
|
|
26
|
+
steps.commit(),
|
|
27
|
+
]),
|
|
28
|
+
stepChain("build-test", "there is a suite to satisfy — build, test, bounded fix loop", [
|
|
29
|
+
steps.request(),
|
|
30
|
+
steps.build({ fromPlan: false }),
|
|
31
|
+
steps.fixLoop({ suite: "test" }),
|
|
32
|
+
]),
|
|
33
|
+
stepChain("plan-build-test", "the standard chain — plan, build, test, commit", [
|
|
34
|
+
steps.request(),
|
|
35
|
+
steps.plan(),
|
|
36
|
+
steps.build(),
|
|
37
|
+
steps.fixLoop({ suite: "test" }),
|
|
38
|
+
steps.commit({ onlyIfAccepted: true }),
|
|
39
|
+
]),
|
|
40
|
+
stepChain("plan-build-test-quality", "the repo has quality commands worth enforcing beyond tests — same, plus lint/typecheck/build gates", [
|
|
41
|
+
steps.request(),
|
|
42
|
+
steps.plan(),
|
|
43
|
+
steps.build(),
|
|
44
|
+
steps.fixLoop({ suite: "all" }),
|
|
45
|
+
steps.commit({ onlyIfAccepted: true }),
|
|
46
|
+
]),
|
|
47
|
+
stepChain("build-review", '"is this what was asked for" matters more than "does it run"', [
|
|
48
|
+
steps.request(),
|
|
49
|
+
steps.build({ fromPlan: false }),
|
|
50
|
+
steps.reviseLoop(),
|
|
51
|
+
]),
|
|
52
|
+
stepChain("quality", "lint, typecheck, build — no agents at all", [
|
|
53
|
+
steps.request({ description: "Capture why quality verification was requested" }),
|
|
54
|
+
steps.qualityCheck({ suite: "all" }),
|
|
55
|
+
]),
|
|
56
|
+
stepChain("document", "write up the work that was just done, from the diff", [steps.request(), steps.changes(), steps.document()]),
|
|
102
57
|
{
|
|
103
58
|
name: "simple-sdlc",
|
|
104
59
|
describe: "the work is real and its shape is not obvious — plan, build, test, review, document; 3 commits",
|
|
105
|
-
phases: "engineer(request) -> planner -> builder -> code(test) ->
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
60
|
+
phases: "engineer(request) -> planner -> git(commit_plan) -> builder -> code(test) [-> builder(fix) -> code(test) ...] " +
|
|
61
|
+
"-> reviewer [-> builder(revise) -> reviewer ...] -> code(retest, if revised) -> git(commit_build) " +
|
|
62
|
+
"-> code(changes) -> documenter -> git(commit_docs)",
|
|
63
|
+
requiredAgents: simpleSdlc.REQUIRED_AGENTS,
|
|
64
|
+
requiredSuites: simpleSdlc.REQUIRED_SUITES,
|
|
65
|
+
run: simpleSdlc.main,
|
|
109
66
|
},
|
|
110
67
|
];
|
|
111
68
|
export function findChain(name) {
|
|
@@ -114,3 +71,15 @@ export function findChain(name) {
|
|
|
114
71
|
export function resolveRequiredAgents(chain, options) {
|
|
115
72
|
return typeof chain.requiredAgents === "function" ? chain.requiredAgents(options) : chain.requiredAgents;
|
|
116
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Run a chain, whichever path it defines: `run` (the imperative escape
|
|
76
|
+
* hatch) if it has one, otherwise `steps` through the shared driver. Both
|
|
77
|
+
* CLI dispatch sites (`spf <chain>` and `spf watch`) go through this, never
|
|
78
|
+
* `chain.run(...)` directly — the whole reason to route through here is that
|
|
79
|
+
* a `steps`-only chain has no `run` to call.
|
|
80
|
+
*/
|
|
81
|
+
export async function runChain(chain, ctx, options = {}) {
|
|
82
|
+
if (chain.run)
|
|
83
|
+
return chain.run(ctx, options);
|
|
84
|
+
return steps.runSteps(ctx, resolveRequiredAgents(chain, options), chain.requiredSuites, chain.steps, options);
|
|
85
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Simple SDLC — plan, build, test, review, document, committing as it goes.
|
|
3
3
|
*
|
|
4
4
|
* Usage:
|
|
5
5
|
* spf simple-sdlc "<prompt or path/to/prompt.md>" [--config <path>] [--adw-id a1b2c3d4]
|
|
@@ -36,6 +36,12 @@
|
|
|
36
36
|
* The documenter measures against the commit this run STARTED from, not against
|
|
37
37
|
* `main`, because by then the run has moved `main` itself. That baseline is
|
|
38
38
|
* pinned before the first commit phase and printed in the request phase.
|
|
39
|
+
*
|
|
40
|
+
* This is the one chain still shaped by hand rather than by a `steps` list —
|
|
41
|
+
* three commits, a pinned baseline, and a conditional retest don't collapse
|
|
42
|
+
* cleanly into the flat, loop-free vocabulary `./steps.ts` provides for every
|
|
43
|
+
* other chain. It still reuses that module's shared helpers (`startRun`,
|
|
44
|
+
* `commitEnvelope`, `logChangeset`) rather than keeping its own copies.
|
|
39
45
|
*/
|
|
40
46
|
import type { ChainContext } from "./context.ts";
|
|
41
47
|
export declare const REQUIRED_AGENTS: string[];
|