@gr8ful/spf 0.1.7 → 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.
Files changed (70) hide show
  1. package/README.md +7 -2
  2. package/assets/skill/cookbooks/authoring_chains.md +96 -84
  3. package/assets/skill/cookbooks/roster.md +3 -1
  4. package/assets/skill/references/config.md +5 -2
  5. package/dist/chains/context.d.ts +2 -0
  6. package/dist/chains/index.d.ts +21 -2
  7. package/dist/chains/index.js +73 -104
  8. package/dist/chains/{adw_simple_sdlc.d.ts → simple_sdlc.d.ts} +7 -1
  9. package/dist/chains/{adw_simple_sdlc.js → simple_sdlc.js} +19 -30
  10. package/dist/chains/steps.d.ts +117 -0
  11. package/dist/chains/steps.js +299 -0
  12. package/dist/cli/ask.d.ts +27 -0
  13. package/dist/cli/ask.js +125 -0
  14. package/dist/cli/commands/doctor.js +2 -24
  15. package/dist/cli/commands/init.d.ts +1 -1
  16. package/dist/cli/commands/init.js +82 -9
  17. package/dist/cli/commands/run.d.ts +1 -1
  18. package/dist/cli/commands/run.js +3 -1
  19. package/dist/cli/commands/watch.js +3 -3
  20. package/dist/cli/env_file.d.ts +18 -0
  21. package/dist/cli/env_file.js +99 -0
  22. package/dist/cli/index.js +2 -2
  23. package/dist/cli/interview.d.ts +24 -0
  24. package/dist/cli/interview.js +330 -0
  25. package/dist/core/prompts.d.ts +2 -0
  26. package/dist/core/prompts.js +2 -0
  27. package/dist/core/providers.d.ts +12 -0
  28. package/dist/core/providers.js +24 -0
  29. package/dist/core/quality.d.ts +9 -0
  30. package/dist/core/quality.js +10 -0
  31. package/dist/core/session.d.ts +6 -1
  32. package/dist/core/session.js +7 -3
  33. package/dist/core/tracer.js +1 -1
  34. package/dist/core/utils.d.ts +6 -2
  35. package/dist/core/utils.js +11 -2
  36. package/dist/test/chains.test.d.ts +12 -0
  37. package/dist/test/chains.test.js +86 -0
  38. package/dist/test/env_file.test.d.ts +1 -0
  39. package/dist/test/env_file.test.js +74 -0
  40. package/dist/test/fake_asker.d.ts +23 -0
  41. package/dist/test/fake_asker.js +30 -0
  42. package/dist/test/init_command.test.d.ts +1 -0
  43. package/dist/test/init_command.test.js +66 -0
  44. package/dist/test/interview.test.d.ts +1 -0
  45. package/dist/test/interview.test.js +179 -0
  46. package/dist/test/ui_server.test.js +1 -1
  47. package/dist/ui/shared/types.d.ts +1 -1
  48. package/package.json +5 -2
  49. package/dist/chains/adw_build.d.ts +0 -12
  50. package/dist/chains/adw_build.js +0 -27
  51. package/dist/chains/adw_build_review.d.ts +0 -21
  52. package/dist/chains/adw_build_review.js +0 -55
  53. package/dist/chains/adw_build_test.d.ts +0 -21
  54. package/dist/chains/adw_build_test.js +0 -67
  55. package/dist/chains/adw_document.d.ts +0 -23
  56. package/dist/chains/adw_document.js +0 -59
  57. package/dist/chains/adw_plan.d.ts +0 -12
  58. package/dist/chains/adw_plan.js +0 -27
  59. package/dist/chains/adw_plan_build.d.ts +0 -12
  60. package/dist/chains/adw_plan_build.js +0 -30
  61. package/dist/chains/adw_plan_build_test.d.ts +0 -16
  62. package/dist/chains/adw_plan_build_test.js +0 -65
  63. package/dist/chains/adw_plan_build_test_quality.d.ts +0 -18
  64. package/dist/chains/adw_plan_build_test_quality.js +0 -66
  65. package/dist/chains/adw_prompt.d.ts +0 -12
  66. package/dist/chains/adw_prompt.js +0 -25
  67. package/dist/chains/adw_quality.d.ts +0 -12
  68. package/dist/chains/adw_quality.js +0 -32
  69. package/dist/chains/adw_scout.d.ts +0 -12
  70. 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 # seed .spf/spf.config.yamloverride only what you want to change
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` writes a small starter `.spf/spf.config.yaml`, commented, that merges on top of the packaged built-ins field by field override one model, one prompt, one quality check, and everything else stays inherited. `--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
+ 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
- Creating a new chain, extending an existing one, and adding the engine
4
- primitives a chain needs (an output type, a gate) are one skill with three
5
- doors. All three live in `src/` inside the SPF package itself — there is no
6
- per-repo copy to edit. If you need to change engine behavior for a specific
7
- target repo without forking the package, that's `spf eject` (prints the path
8
- to the installed package's `src/` for you to copy and load from your own
9
- `.spf/` — engine-level changes are the one thing `.spf/` config can't express).
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 — write the chain module
39
+ ## Step 2 — compose the chain from steps
38
40
 
39
- A chain is a module exporting `REQUIRED_AGENTS`, `REQUIRED_SUITES`, and
40
- `main(ctx: ChainContext): Promise<number>`. No shebang, no `import.meta.main`
41
- block the CLI's chain registry (`src/chains/index.ts`) calls `main()`
42
- directly, and `dist/cli/bin.js` is the only entry point that ever runs.
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
- * ADW <Name> — <one sentence: what this chain is for and when to pick it>.
47
- *
48
- * Phases: engineer(request) -> scout -> planner -> builder -> code(test) -> git(commit)
49
- */
50
- import * as agents from "../core/agents.ts";
51
- import * as gates from "../core/gates.ts";
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
- Then register it in `src/chains/index.ts`'s `CHAINS` array (name, describe,
91
- phases string, `requiredAgents`/`requiredSuites`, `run`)that's what makes
92
- it show up in `spf list` and dispatchable as `spf <name>`.
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
- ## Step 3 add a phase to an existing chain
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
- Removing a phase: delete the block; check nothing downstream reads its
108
- return value, and check `REQUIRED_AGENTS`/`REQUIRED_SUITES` no longer needs
109
- whatever only that phase used.
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 4 — add a bounded fix loop
105
+ ## Step 3 — add a phase to an existing chain
112
106
 
113
- The pattern every quality-gated chain uses run a check, and if it fails,
114
- loop the builder back in with the failure as `previous`, bounded so a chain
115
- can't spin forever:
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
- const MAX_FIX_LOOPS = 3;
119
- let test: QualityResult | null = null;
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 result = quality.runTests(run); ph.log({ passed: result.passed }); return result; },
134
+ async (ph) => { const r = quality.runTests(run); quality.record(ph, r); return r; },
124
135
  );
125
- if (test.passed) break;
126
- await run.phase(
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(test!, "tests"), gates: [gates.diffMatchesClaims] })),
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 stay thin: sequencing
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 chain's
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 name the agent in a chain's `REQUIRED_AGENTS` and call it.
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 with a commented starter; omitting it entirely means
22
- running off pure built-ins, which is a fully supported, valid state.
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
 
@@ -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
  }
@@ -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; adw_prompt's depends on --agent, so it's a function there. */
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
- run: (ctx: ChainContext, options?: Record<string, string>) => Promise<number>;
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>;
@@ -1,111 +1,68 @@
1
- import * as adwPrompt from "./adw_prompt.js";
2
- import * as adwScout from "./adw_scout.js";
3
- import * as adwPlan from "./adw_plan.js";
4
- import * as adwBuild from "./adw_build.js";
5
- import * as adwPlanBuild from "./adw_plan_build.js";
6
- import * as adwBuildTest from "./adw_build_test.js";
7
- import * as adwPlanBuildTest from "./adw_plan_build_test.js";
8
- import * as adwPlanBuildTestQuality from "./adw_plan_build_test_quality.js";
9
- import * as adwBuildReview from "./adw_build_review.js";
10
- import * as adwQuality from "./adw_quality.js";
11
- import * as adwDocument from "./adw_document.js";
12
- import * as adwSimpleSdlc from "./adw_simple_sdlc.js";
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
- name: "prompt",
16
- describe: "one agent, one prompt, traced end to end — --agent <name> picks who (default: builder)",
17
- phases: "engineer(request) -> <agent>",
18
- requiredAgents: (options) => [options["agent"] ?? "builder"],
19
- requiredSuites: [],
20
- run: adwPrompt.main,
21
- },
22
- {
23
- name: "scout",
24
- describe: "read-only recon; nothing changes",
25
- phases: "engineer(request) -> scout",
26
- requiredAgents: adwScout.REQUIRED_AGENTS,
27
- requiredSuites: adwScout.REQUIRED_SUITES,
28
- run: adwScout.main,
29
- },
30
- {
31
- name: "plan",
32
- describe: "turn a request into an implementable plan",
33
- phases: "engineer(request) -> planner",
34
- requiredAgents: adwPlan.REQUIRED_AGENTS,
35
- requiredSuites: adwPlan.REQUIRED_SUITES,
36
- run: adwPlan.main,
37
- },
38
- {
39
- name: "build",
40
- describe: "implement an existing plan",
41
- phases: "engineer(request) -> builder",
42
- requiredAgents: adwBuild.REQUIRED_AGENTS,
43
- requiredSuites: adwBuild.REQUIRED_SUITES,
44
- run: adwBuild.main,
45
- },
46
- {
47
- name: "plan-build",
48
- describe: "small, well-understood work — plan, build, commit",
49
- phases: "engineer(request) -> planner -> builder -> git(commit)",
50
- requiredAgents: adwPlanBuild.REQUIRED_AGENTS,
51
- requiredSuites: adwPlanBuild.REQUIRED_SUITES,
52
- run: adwPlanBuild.main,
53
- },
54
- {
55
- name: "build-test",
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) -> reviewer -> code(changes) -> documenter",
106
- requiredAgents: adwSimpleSdlc.REQUIRED_AGENTS,
107
- requiredSuites: adwSimpleSdlc.REQUIRED_SUITES,
108
- run: adwSimpleSdlc.main,
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
- * ADW Simple SDLC — plan, build, test, review, document, committing as it goes.
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[];