@gr8ful/spf 0.4.0 → 0.5.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 (80) hide show
  1. package/README.md +122 -4
  2. package/assets/defaults/spf.config.yaml +6 -0
  3. package/assets/prompts/reviewer/system.md +1 -1
  4. package/assets/skill/SKILL.md +1 -0
  5. package/assets/skill/cookbooks/authoring_chains.md +90 -7
  6. package/assets/skill/cookbooks/ocr_reviewer.md +196 -0
  7. package/assets/skill/cookbooks/roster.md +15 -4
  8. package/assets/skill/cookbooks/spf_overview.md +1 -0
  9. package/assets/skill/references/config.md +69 -4
  10. package/assets/skill/references/observability.md +11 -2
  11. package/assets/templates/ts-flue-ollama.spf.config.yaml +67 -0
  12. package/assets/templates/ts.spf.config.yaml +5 -0
  13. package/dist/chains/context.d.ts +30 -0
  14. package/dist/chains/index.d.ts +94 -10
  15. package/dist/chains/index.js +70 -5
  16. package/dist/chains/repo_chains.d.ts +139 -0
  17. package/dist/chains/repo_chains.js +428 -0
  18. package/dist/chains/simple_sdlc.d.ts +74 -1
  19. package/dist/chains/simple_sdlc.js +134 -4
  20. package/dist/chains/steps.d.ts +215 -20
  21. package/dist/chains/steps.js +429 -61
  22. package/dist/cli/ask.d.ts +14 -1
  23. package/dist/cli/ask.js +32 -2
  24. package/dist/cli/commands/doctor.d.ts +1 -1
  25. package/dist/cli/commands/doctor.js +319 -11
  26. package/dist/cli/commands/init.d.ts +12 -0
  27. package/dist/cli/commands/init.js +78 -1
  28. package/dist/cli/commands/list.js +42 -5
  29. package/dist/cli/commands/run.js +25 -2
  30. package/dist/cli/commands/watch.d.ts +18 -0
  31. package/dist/cli/commands/watch.js +158 -10
  32. package/dist/cli/index.js +60 -3
  33. package/dist/cli/interview.js +65 -10
  34. package/dist/core/agent_cc.d.ts +40 -1
  35. package/dist/core/agent_cc.js +51 -4
  36. package/dist/core/agent_flue.js +28 -4
  37. package/dist/core/agents.d.ts +8 -0
  38. package/dist/core/agents.js +43 -3
  39. package/dist/core/data_types.d.ts +104 -4
  40. package/dist/core/data_types.js +99 -2
  41. package/dist/core/git_helper.d.ts +29 -0
  42. package/dist/core/git_helper.js +41 -1
  43. package/dist/core/ollama_provider.d.ts +70 -0
  44. package/dist/core/ollama_provider.js +208 -0
  45. package/dist/core/otel.d.ts +352 -0
  46. package/dist/core/otel.js +793 -0
  47. package/dist/core/paths.d.ts +3 -0
  48. package/dist/core/paths.js +48 -1
  49. package/dist/core/providers.js +4 -0
  50. package/dist/core/refine.js +11 -3
  51. package/dist/core/session.js +39 -2
  52. package/dist/core/tracer.d.ts +31 -2
  53. package/dist/core/tracer.js +69 -11
  54. package/dist/core/watch.d.ts +11 -0
  55. package/dist/core/watch.js +17 -2
  56. package/dist/test/chains.test.js +8 -3
  57. package/dist/test/data_types.test.js +140 -2
  58. package/dist/test/git_helper.test.d.ts +1 -0
  59. package/dist/test/git_helper.test.js +59 -0
  60. package/dist/test/hermetic_git.d.ts +1 -0
  61. package/dist/test/hermetic_git.js +22 -0
  62. package/dist/test/init_command.test.d.ts +14 -1
  63. package/dist/test/init_command.test.js +54 -1
  64. package/dist/test/interview.test.d.ts +15 -1
  65. package/dist/test/interview.test.js +127 -0
  66. package/dist/test/ollama_provider.test.d.ts +1 -0
  67. package/dist/test/ollama_provider.test.js +103 -0
  68. package/dist/test/otel.test.d.ts +26 -0
  69. package/dist/test/otel.test.js +512 -0
  70. package/dist/test/paths.test.d.ts +1 -0
  71. package/dist/test/paths.test.js +68 -0
  72. package/dist/test/refine.test.js +64 -1
  73. package/dist/test/repo_chains.test.d.ts +21 -0
  74. package/dist/test/repo_chains.test.js +416 -0
  75. package/dist/test/signoff.test.d.ts +1 -0
  76. package/dist/test/signoff.test.js +329 -0
  77. package/dist/test/ui_server.test.d.ts +7 -1
  78. package/dist/test/ui_server.test.js +1 -0
  79. package/dist/test/watch.test.js +124 -1
  80. package/package.json +5 -5
package/README.md CHANGED
@@ -120,7 +120,7 @@ Config defines who an agent **is**. The chain call site defines how it is **used
120
120
 
121
121
  ### A second backend: Claude Code
122
122
 
123
- Set `coding_agent: claude_code` on any agent (or in `defaults`) to run it on your own installed [Claude Code](https://claude.com/product/claude-code) CLI instead of Flue — `spf doctor` checks it's on `PATH`. Model names follow Claude Code's own vocabulary (a bare alias like `sonnet`, not `provider/model-id`); everything else — `tools`, `writes`, `thinking` — stays the same shape. Pointing a `claude_code` agent at a local or cloud [Ollama](https://ollama.com) server needs no config at all — just `ANTHROPIC_BASE_URL`/`ANTHROPIC_AUTH_TOKEN` set before you run `spf`, since Claude Code's CLI reads those itself.
123
+ Set `coding_agent: claude_code` on any agent (or in `defaults`) to run it on your own installed [Claude Code](https://claude.com/product/claude-code) CLI instead of Flue — `spf doctor` checks whatever `SPF_CLAUDE_CMD`'s first token resolves to on `PATH` (`claude` itself, by default). Model names follow Claude Code's own vocabulary (a bare alias like `sonnet`, not `provider/model-id`); everything else — `tools`, `writes`, `thinking` — stays the same shape. Pointing a `claude_code` agent at a local or cloud [Ollama](https://ollama.com) server needs no config at all — just `ANTHROPIC_BASE_URL`/`ANTHROPIC_AUTH_TOKEN` set before you run `spf`, since Claude Code's CLI reads those itself.
124
124
 
125
125
  #### Proxy or wrapper launchers
126
126
 
@@ -128,17 +128,52 @@ To route the `claude` command through a wrapper, proxy server, or launcher (e.g.
128
128
 
129
129
  ```bash
130
130
  # Route through Ollama's launcher
131
- export SPF_CLAUDE_CMD="ollama launch claude"
131
+ export SPF_CLAUDE_CMD="ollama launch claude --model granite4.1:8b"
132
132
  spf build "your prompt"
133
133
  ```
134
134
 
135
+ `ollama launch <cmd>` uses cobra flag parsing, which treats anything typed after it as its own flags unless a literal `--` says otherwise — without one, `claude`'s own flags (`-p`, `--json-schema`, ...) fail with `unknown shorthand flag: 'p' in -p` before `claude` is ever reached. `agent_cc.ts` detects exactly this `ollama launch ...` shape and inserts that `--` automatically, so you never add the separator by hand for this specific launcher.
136
+
137
+ That `--` alone is not enough to reach `claude`, though: `ollama launch` also needs its OWN `--model <tag>` flag (a tag from `ollama list`), typed BEFORE the auto-inserted `--`, whenever it runs headless — which it always does under SPF, since SPF spawns with piped stdio. Without it, `ollama launch` falls back to an interactive model picker that can never run, and fails one step later than the `--` problem, with `model selection requires an interactive terminal; use --model to run in headless mode`. A `--model` typed after the `--` doesn't help — at that point it belongs to `claude`, not to `ollama launch`. So `SPF_CLAUDE_CMD` must include `ollama launch`'s `--model` yourself, exactly as written above; `spf doctor` hard-fails if it's missing.
138
+
135
139
  ```bash
136
140
  # Or use a custom wrapper script
137
141
  export SPF_CLAUDE_CMD=/path/to/my-wrapper
138
142
  spf build "your prompt"
139
143
  ```
140
144
 
141
- The command/launcher must support the full Claude Code CLI interface: `-p` for prompt, `--json-schema`, `--model`, `--session-id`/`--resume`, `--output-format stream-json`, and all other flags `agent_cc` uses. When unset, `SPF_CLAUDE_CMD` defaults to `claude` (resolved from `PATH` normally).
145
+ The command/launcher must support the full Claude Code CLI interface: `-p` for prompt, `--json-schema`, `--model`, `--session-id`/`--resume`, `--output-format stream-json`, and all other flags `agent_cc` uses. When unset, `SPF_CLAUDE_CMD` defaults to `claude` (resolved from `PATH` normally). A cmdSpec that already contains its own literal `--` is left completely alone — `agent_cc.ts` never inserts a second one.
146
+
147
+ ### flue + local Ollama
148
+
149
+ Point the default `flue` backend at a local Ollama server the same way you'd pick any other Flue provider — the model string's own prefix, `ollama/<tag>` (whatever `ollama list` shows on your machine) instead of `openai/...`/`anthropic/...`:
150
+
151
+ ```yaml
152
+ defaults:
153
+ coding_agent: flue
154
+ model: ollama/qwen3.8:27b-mlx
155
+ ```
156
+
157
+ ```bash
158
+ export OLLAMA_BASE_URL=http://localhost:11434/v1 # default if unset
159
+ spf build "your prompt"
160
+ ```
161
+
162
+ That's the whole config change. `ollama` is a keyless provider — `spf doctor` and the `spf init` interview both know this: no API key is ever asked for or checked, and interview.ts asks for `OLLAMA_BASE_URL` instead when you pick `ollama` as your provider. A ready-to-run starting point ships at [`assets/templates/ts-flue-ollama.spf.config.yaml`](assets/templates/ts-flue-ollama.spf.config.yaml) (`spf init --template ts-flue-ollama`).
163
+
164
+ Two things worth knowing before pointing a full roster at local models: tool-calling — the injected `sf_report` contract every agent's structured output rides on — worked reliably in testing down to a 3B-parameter model, which isn't a "only frontier models get tools" situation. And context-window occupancy reporting is disabled for `ollama/*` models specifically, which turns off threshold-based compaction rather than reporting a number Ollama's OpenAI-compatible API doesn't actually provide per model.
165
+
166
+ ---
167
+
168
+ ## Isolation: post-hoc, not a sandbox
169
+
170
+ Agent isolation in SPF is enforced **after the fact**, not upfront. This matters because the `tools:` capability list is not a sandbox — a `bash` tool can run `git checkout` to discard changes, and a `write` tool can reach any path, regardless of the allowlist you write. So permission is verified the way every other claim in this system is: the working tree is fingerprinted before an agent runs, then compared after. If the agent touched anything outside its `writes:` allowlist (or a `protected_files:` path it wasn't given access to), the phase fails and anything the agent *introduced* outside its allowlist is rolled back. What it cannot undo, it names: a file that was already dirty before the agent ran is left alone rather than discarded, and uncommitted work the agent reverted cannot be restored. And because the check is post-hoc and tree-scoped, nothing outside the working tree is in scope at all — a push, a network call, a write above `repo_root`. When running an agent on the claude_code backend, SPF spawns it with `--permission-mode bypassPermissions --dangerously-skip-permissions` because the backend has no need to enforce — SPF will enforce in code instead, within those bounds.
171
+
172
+ For unattended work, `spf watch`'s PR-merge is the only human accountability checkpoint: the daemon opens a PR for each automated run, and a human approves the merge. For any other workflow, this enforcement is the backbone of letting a bounded agent proposal survive code's inspection without a human having to read it in between — but it is bounded, not a sandbox, and the scope above is what a reviewer should actually rely on. (Attended `simple-sdlc` runs get a second checkpoint of their own — see below.)
173
+
174
+ See `core/permissions.ts` for the implementation — the snapshot/compare logic, not the config terms.
175
+
176
+ `simple-sdlc`'s `commit_build` phase is the one place in this codebase where an AI reviewer's `approved` flag alone would otherwise gate a commit — everywhere else the reviewer either doesn't run (`plan-build-test`, ...) or nothing commits on its verdict (`build-review` has no commit step). Run attended, that phase asks before committing — default **no**, never `review.approved` itself, bounded by `review.signoff_timeout_seconds` (default 300s; expiry answers no) — and only an explicit, recorded "yes" ever earns a `Signed-off-by:` trailer, built from `git config user.name`/`user.email` at the repo, never an env var. Run unattended (`spf watch`, CI), there is nobody to ask: `spf watch`'s own human gate is the PR merge above, informed by the reviewer's digest in the PR body, and the phase either proceeds on the AI verdict alone with a loud warning (`review.require_human_signoff: false`, this release's default — fail-open until `spf watch` itself becomes signoff-aware) or fails closed and asks you to rerun attended (`require_human_signoff: true`). See `assets/skill/references/config.md`'s `review` section for the two knobs, and [`assets/skill/cookbooks/ocr_reviewer.md`](assets/skill/cookbooks/ocr_reviewer.md) if you want the reviewer to fold a third-party review tool's findings into its own verdict as one more piece of evidence.
142
177
 
143
178
  ---
144
179
 
@@ -214,7 +249,7 @@ Files stay the raw record (`envelope.json`, `agent_map.json`, Flue's own convers
214
249
 
215
250
  ```bash
216
251
  spf list # every chain, its phases, what it needs
217
- spf <chain> "<prompt or path/to/prompt.md>" [--config <path>] [--adw-id a1b2c3d4] [--cwd <dir>]
252
+ spf <chain> "<prompt or path/to/prompt.md>" [--config <path>] [--adw-id a1b2c3d4] [--cwd <dir>] [--suite <name>]
218
253
  ```
219
254
 
220
255
  | Chain | Phases | Reach for it when |
@@ -232,6 +267,8 @@ spf <chain> "<prompt or path/to/prompt.md>" [--config <path>] [--adw-id a1b2c3d4
232
267
  | `document` | code(git diff), documenter | write up what just shipped |
233
268
  | `simple-sdlc` | plan, build, test, review, document | the work is real and its shape is not obvious |
234
269
 
270
+ A chain with a compiled-in quality suite (`build-test`, `plan-build-test`, `plan-build-test-quality`, `quality`) accepts `--suite <name>` to override it for one run — `spf list` marks which chains this applies to with `(--suite overrides)`. `simple-sdlc`'s suite is not overridable this way; passing `--suite` to it is rejected rather than silently ignored.
271
+
235
272
  `--adw-id` is optional everywhere. Omit it and a fresh id is minted and printed. Supply it and the run joins that session — same session directory, each agent **resumes its existing context window** instead of starting cold. That's how you chain runs:
236
273
 
237
274
  ```bash
@@ -239,6 +276,36 @@ spf plan "add a /health endpoint" # prints adw_id a1b2c3d
239
276
  spf build-test "implement the plan" --adw-id a1b2c3d4
240
277
  ```
241
278
 
279
+ ### Repo-local chains
280
+
281
+ Every chain above is built in. A target repo can also compose its own out of
282
+ the same step factories, with zero forking and zero build step, by dropping a
283
+ file in `.spf/chains/*.yaml` — `spf init` scaffolds `.spf/chains/example.yaml`
284
+ (fully commented out) showing the shape:
285
+
286
+ ```yaml
287
+ # .spf/chains/ship-it.yaml — spf ship-it "<prompt>" / spf run ship-it "<prompt>"
288
+ name: ship-it
289
+ describe: plan, build, test, land — with our own reviewer in the loop
290
+ steps:
291
+ - step: request
292
+ - step: plan
293
+ owner: architect
294
+ - step: build
295
+ extraGates: [jsonParses] # additive only — a step's built-in gates can never be removed
296
+ - step: fixLoop
297
+ suite: test
298
+ - step: commit
299
+ onlyIfAccepted: true
300
+ ```
301
+
302
+ It shows up in `spf list`/`spf doctor` exactly like a built-in, and runs on
303
+ the identical `stepChain()` driver — there is no second interpreter. The full
304
+ step vocabulary, the gate rules, and the `spf watch` main-anchor divergence
305
+ (a chain edited on an issue branch is *not* what an in-flight `spf watch` run
306
+ uses) are documented in the installed skill's "Repo-local chains" section
307
+ (`.claude/skills/spf/cookbooks/authoring_chains.md`, after `spf init`).
308
+
242
309
  ---
243
310
 
244
311
  ## `spf watch`
@@ -278,6 +345,29 @@ watch:
278
345
  project_key: PROJ
279
346
  ```
280
347
 
348
+ ```yaml
349
+ # .spf/spf.config.yaml — GitHub issues + Bitbucket PRs, in TWO DIFFERENT repos
350
+ watch:
351
+ issue_provider: github
352
+ code_host: bitbucket
353
+ repo: workspace/repo_slug # Bitbucket's repo — the CODE HOST always reads plain `repo`
354
+ issue_repo: owner/name # GitHub's repo — only needed here, where issue tracker != code host
355
+ label_prefix: spf # polls GitHub issues labeled spf:ready
356
+ chain: plan-build-test
357
+ base_branch: main
358
+ ```
359
+
360
+ `repo` always names the code host's own repo; `issue_repo` overrides it for
361
+ the issue-tracker side. This only matters for `issue_provider: github` +
362
+ `code_host: bitbucket` — the one combination where they're genuinely
363
+ different repos in different systems, not the same repo worn two ways
364
+ (`github`+`github` is one repo by construction; `issue_provider: jira` never
365
+ reads `repo` at all, so it's never ambiguous). Leave `issue_repo` unset
366
+ everywhere else. `spf doctor` flags this specific combination when
367
+ `issue_repo` is missing, since the silent failure mode — polling GitHub with
368
+ the Bitbucket identifier — finds no matching issues and looks exactly like
369
+ nothing being configured at all.
370
+
281
371
  ```bash
282
372
  spf watch init # idempotently seed tracker state (no-op for Jira — see below); run this first
283
373
  spf watch # foreground daemon; Ctrl-C drains in-flight claims first
@@ -422,6 +512,34 @@ genuinely informative, just worth expecting.
422
512
  Full field reference: `spf install-skill`'s installed skill
423
513
  (`references/config.md`).
424
514
 
515
+ ## Observability
516
+
517
+ Every run produces a complete trace: all events, phases, agent calls, and tool invocations stream into SQLite as they happen. The local trace stays the source of truth — prompts, envelopes, tool arguments, and your source code never leave the machine. Token counts and costs ride alongside.
518
+
519
+ ```bash
520
+ spf ui # browser-based visualizer over the trace
521
+ spf events <adw_id> --follow # live event stream, tailable
522
+ ```
523
+
524
+ The default export is SQLite only (`.spf/data/spf.db`). Optionally, you can export **spans only** (phase/agent/tool timing and allowlisted metadata) to an OpenTelemetry collector for integration with a trace UI or observability platform:
525
+
526
+ ```yaml
527
+ observability:
528
+ db: .spf/data/spf.db
529
+ poll_ms: 500
530
+ otel:
531
+ endpoint: https://your-otel-collector/v1/traces
532
+ service_name: spf # optional; default "spf"
533
+ headers: # optional; e.g. auth headers
534
+ Authorization: Bearer ...
535
+ ```
536
+
537
+ OTEL export is **explicit config only** — an unrelated shell variable cannot become a data-egress switch. OTEL is strictly a spans-only export: each phase is a span with child spans for agent calls and tool calls, annotated with phase status, agent model, token/cost counts, and gate results. This export never blocks a run: if the collector is slow or unreachable, SPF continues normally and logs a single line per run when export fails (not one per batch), and reports the number of dropped spans on the final flush as `spf.otel.dropped_spans` for the backend to surface. The complete trace stays in SQLite regardless.
538
+
539
+ **Attribute allowlist**: only phase name/kind/owner/status, chain name, adw_id, agent name/model/coding_agent, gate name + passed + violation count, token counts and cost, and durations. Prompts, envelopes, tool arguments, and your source code never leave — that guarantee is enforced in code, not just in documentation.
540
+
541
+ See `assets/skill/references/config.md`'s `observability` section for the full field reference.
542
+
425
543
  ## What's in this repo
426
544
 
427
545
  ```
@@ -34,6 +34,12 @@ defaults:
34
34
  observability:
35
35
  db: .spf/data/spf.db # tracer writes here directly; the UI polls it
36
36
  poll_ms: 500 # visualizer live-poll cadence
37
+ # Optional OpenTelemetry spans export (explicit config only; no ambient env activation)
38
+ # otel:
39
+ # endpoint: https://your-otel-collector/v1/traces
40
+ # service_name: spf # optional; default "spf"
41
+ # headers: # optional; e.g. auth headers
42
+ # Authorization: Bearer ...
37
43
 
38
44
  # No default quality checks or suites are shipped — an unconfigured chain
39
45
  # that needs one fails loudly at agents.validate() time (before anything
@@ -13,4 +13,4 @@ Confirm that what was built is what was asked for. This is not testing.
13
13
  - Change nothing. Findings go back to the builder — that is the only repair path.
14
14
  - `approved` is true ONLY when every requirement is met and `blocking` is empty. Every blocking item names the specific gap, so the builder can fix it without guessing.
15
15
  - You inherit the operator's shell environment — their PATH, toolchains and credentials are already live. Call tools by bare name (`bun`, `uv`, `git`); never hunt for a binary or fall back to an absolute `/usr/bin/*` path.
16
- - 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.
16
+ - Judge any command you run by its exit status, never by scanning its output for words — except tools whose output is the product, such as `git diff`, or a review tool you were asked to consult, which you read as evidence, never as a verdict. `error` or `not found` inside passing output is text, not a failure.
@@ -45,6 +45,7 @@ phases`/`spf events` exist so nobody has to guess the schema again).
45
45
  | "add/retune an agent or model" | `roster.md` |
46
46
  | "add a chain / a phase / an output type / a gate" | `authoring_chains.md` |
47
47
  | "what is SPF" | `spf_overview.md` |
48
+ | "fold a third-party review tool's findings into the reviewer" | `cookbooks/ocr_reviewer.md` |
48
49
  | envelope/gate/session contract | `references/handoff.md` |
49
50
  | trace schema, spend vs. context | `references/observability.md` |
50
51
  | config field reference | `references/config.md` |
@@ -1,13 +1,86 @@
1
1
  # Authoring Chains
2
2
 
3
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).
4
+ primitives a chain needs (an output type, a gate, a step) is one skill with
5
+ four doors. Engine primitives (a new gate, a new envelope type, a modified
6
+ phase primitive) live in `src/` inside the SPF package itself those do
7
+ require forking the package, because they change what every repo's chains
8
+ can mean. But COMPOSING a chain out of the existing step factories does not:
9
+ that's what a repo-local chain (below) is for. Read that section first if
10
+ you just want your own chain in your own repo — reach for the rest of this
11
+ document only once you've decided you need a new primitive.
12
+
13
+ ## Repo-local chains (`.spf/chains/*.yaml`)
14
+
15
+ The zero-fork door: a `.spf/chains/*.yaml` file in a target repo names
16
+ existing step factories from `spf`'s own `src/chains/steps.ts` and passes
17
+ them params — never imports or runs code from the target repo. `spf init`
18
+ scaffolds `.spf/chains/example.yaml`, fully commented out, showing the shape.
19
+
20
+ ```yaml
21
+ # .spf/chains/ship-it.yaml — spf ship-it "<prompt>" / spf run ship-it "<prompt>"
22
+ name: ship-it
23
+ describe: plan, build, test, land — with our own reviewer in the loop
24
+ steps:
25
+ - step: request # every chain opens with this
26
+ - step: plan
27
+ owner: architect # any agent named in spf.config.yaml
28
+ - step: build
29
+ retries: 2
30
+ extraGates: [jsonParses] # additive — see below
31
+ - step: fixLoop
32
+ suite: test
33
+ - step: commit
34
+ onlyIfAccepted: true
35
+ ```
36
+
37
+ **Shape.** One file, one chain — the filename is a handle a problem can point
38
+ at. `steps` is a flat list; each entry names a `step:` and its params sit as
39
+ FLAT SIBLINGS of `step:` — never nested under a `params:` key. Param names are
40
+ exactly the step factory's `opts` keys, camelCase (`extraGates`, `fixExtraGates`,
41
+ `onlyIfAccepted`, ...). An unknown param, an unknown step, or a param of the
42
+ wrong type is a load-time problem naming the step index and what's allowed —
43
+ never a silently-ignored typo and never a runtime surprise.
44
+
45
+ **Step vocabulary** (`repo_chains.STEP_NAMES`) — the same factories the table
46
+ in Step 2 below documents: `request`, `plan`, `build`, `scout`, `promptOnly`,
47
+ `qualityCheck`, `fixLoop`, `reviseLoop`, `commit`, `changes`, `document`,
48
+ `refine`, `publishIssues`.
49
+
50
+ **Gates are additive only, and per-step.** A step's built-in gates
51
+ (`diffMatchesClaims` on `build`, `verdictConsistent` on `reviseLoop`'s review
52
+ phase, ...) are non-removable — there is no `gates:` param that replaces
53
+ them, and there must never be one (see `GATE_ALLOWLIST`'s comment in
54
+ `steps.ts` for why: the first thing anyone deletes under deadline pressure is
55
+ the gate that keeps failing). `extraGates` can only ADD from an explicit
56
+ allowlist SCOPED to what that param's envelope actually supports:
57
+ `artifactsExist`/`filesNonEmpty`/`jsonParses` anywhere (they only read
58
+ `envelope.artifacts`, present on every envelope); `diffMatchesClaims` only on
59
+ a param whose phase produces a `BuildOutput` (`build.extraGates`,
60
+ `fixLoop.fixExtraGates`, `reviseLoop.reviseExtraGates`); `verdictConsistent`
61
+ only on `reviseLoop.extraGates` (the review phase, a `ReviewOutput`). Naming
62
+ a gate outside its param's list is a load problem, not a chain that loads
63
+ clean and then fails (or vacuously passes) its gate on every run.
64
+
65
+ **Names are guarded.** A chain name must be lowercase letters/digits/`._-`
66
+ (it's typed on the command line). It cannot collide with an `spf` subcommand
67
+ (`run`, `list`, `init`, `watch`, ...) — `spf watch` would always run the
68
+ daemon, never a same-named chain. Two repo-chain files can't claim the same
69
+ `name` either: the trace's `chain_name` column has to keep meaning one thing.
70
+
71
+ **The `spf watch` divergence.** `spf watch` registers `.spf/chains/` from the
72
+ MAIN repo anchor ONCE, at daemon start — not per-issue, not per-worktree. A
73
+ chain file edited on an issue branch (inside the worktree `spf watch` checks
74
+ that branch out into) is NOT what runs for that issue; the daemon keeps using
75
+ whatever `.spf/chains/` looked like when it started. This is deliberate: the
76
+ disposer stays the OPERATOR's, never the branch's, which is exactly what
77
+ keeps an agent from rewriting its own quality gate mid-run by editing a chain
78
+ file as part of the change it's making.
79
+
80
+ Everything below this section is about the OTHER three doors: designing a
81
+ brand-new built-in chain, adding a step to the vocabulary above, or adding an
82
+ engine primitive (gate/envelope type/phase primitive) — none of which a
83
+ target repo needs, or can do, on its own.
11
84
 
12
85
  ## Step 1 — design the chain before writing code
13
86
 
@@ -24,6 +97,16 @@ the step list itself — see Step 2 — so it can't drift from what actually run
24
97
  | test | code | quality | — | (suite pass/fail is the phase's own result) |
25
98
  | commit | code | git | — | — |
26
99
 
100
+ If you need to change engine behavior for a specific target repo without forking
101
+ the package, that's `spf eject`. It copies the installed engine's compiled
102
+ artifacts (`dist/core/` and `dist/chains/`) out to `.spf/engine/` for reference
103
+ or reading — but understand that these files are **not wired into any spf
104
+ command**: editing them changes nothing about how spf itself runs. Engine-level
105
+ changes (a new gate, a new envelope type, a modified phase primitive) have no
106
+ config surface by design, because the codebase is a single shared engine all
107
+ repos trust to sequence their work the same way. If you are genuinely changing
108
+ the engine semantics, you are forking the package itself, not ejecting a copy.
109
+
27
110
  **Ownership rules**, non-negotiable:
28
111
 
29
112
  - **`engineer`** phases capture input only — no agent, no code decision.
@@ -0,0 +1,196 @@
1
+ # Using `alibaba/open-code-review` as reviewer evidence
2
+
3
+ Opt-in, repo-level, docs-only: nothing in the packaged reviewer prompt
4
+ mentions `open-code-review` ("ocr"), and nothing ships that runs it. This
5
+ cookbook is for a repo that wants its *own* reviewer to shell out to ocr and
6
+ fold its findings in — a second opinion the reviewer reads as evidence, never
7
+ a second decision-maker.
8
+
9
+ **The rule this whole cookbook exists to protect: the reviewer agent stays
10
+ the sole producer of `ReviewOutput`.** `ocr review`/`ocr scan` is a bash
11
+ command whose stdout the reviewer reads, the same as `git diff` — nothing
12
+ new gates a commit, because the reviewer's own `approved`/`findings`/
13
+ `blocking` is still the only envelope anything downstream ever consumes. If
14
+ you want that packaged-prompt wording, see the one line
15
+ `assets/prompts/reviewer/system.md` already carries: "except tools whose
16
+ output is the product ... which you read as evidence, never as a verdict."
17
+
18
+ You wire this in with a repo-local prompt override, not a fork of `spf`
19
+ itself — `.spf/prompt_engineering/<agent>/` resolves before the packaged
20
+ default (see `core/paths.ts`'s `resolvePromptRef`: repo root, then
21
+ `.spf/<ref>`, then `.spf/prompt_engineering/<ref>`, then the packaged asset —
22
+ first hit wins). Copy `reviewer/system.md` into
23
+ `.spf/prompt_engineering/reviewer/system.md`, add the ocr instructions, and
24
+ every chain that names `reviewer` picks it up with no code change.
25
+
26
+ ## (a) Credentials — set `env_allowlist` before you add ocr
27
+
28
+ The reviewer's `bash` tool does not run in a sandbox; it inherits whatever
29
+ environment this agent's request carries. Today, unless you configure
30
+ otherwise, that is the FULL operator environment — every key in the shell
31
+ `spf` was launched from, plus anything loaded from the repo's `.env`
32
+ (`AgentConfigSchema.env_allowlist`, `core/agents.ts`'s `agentEnv()`: unset
33
+ means "don't filter at all"). That is fine for a reviewer that only ever
34
+ runs `git diff`/`grep`/`bun test`. It stops being fine the moment `bash` can
35
+ also run `ocr`, because ocr is a THIRD-PARTY BINARY THAT INVOKES ITS OWN LLM
36
+ — it can send anything reachable in that environment (your `ANTHROPIC_API_KEY`,
37
+ `GITHUB_TOKEN`, cloud credentials, database URLs in `.env`) to wherever its
38
+ own model call goes, and nothing in `spf` observes that call to say otherwise.
39
+
40
+ Before pointing the reviewer's prompt at ocr, give that agent an explicit
41
+ allowlist so its `bash` tool — and therefore ocr — only ever sees what ocr
42
+ itself actually needs, plus the baseline `PATH`/`HOME`/`USER`/`LANG`/`TERM`/
43
+ `TMPDIR` every backend keeps regardless.
44
+
45
+ **What `env_allowlist` actually scopes depends on the reviewer's backend.**
46
+ On `coding_agent: claude_code`, it is the environment of the WHOLE `claude`
47
+ subprocess, not just its `bash` tool (`core/agent_cc.ts`'s `spawn(cmd,
48
+ fullArgs, { cwd, env: request.env ?? operatorEnv() })`) — so an allowlist
49
+ scoped only to ocr's own keys strips `ANTHROPIC_API_KEY` from the `claude`
50
+ CLI itself, and the reviewer stops authenticating (unless it relies on
51
+ `claude login`, since `HOME` is in the baseline and that flow's credentials
52
+ live under it). A `claude_code` reviewer must therefore also keep its own
53
+ backend credential in the list. On the default `flue` backend it is safe as
54
+ written below, because `request.env` there only reaches the tool sandbox
55
+ (`local({cwd, env})` in `core/agent_flue.ts`) — Flue's own provider
56
+ credentials are read from the parent's `process.env` regardless of this
57
+ list.
58
+
59
+ ```yaml
60
+ # .spf/spf.config.yaml — patches the packaged "reviewer" entry by name
61
+ agents:
62
+ - name: reviewer
63
+ env_allowlist:
64
+ - OCR_API_KEY # whatever credential ocr's own model call needs
65
+ - OCR_BASE_URL # if ocr talks to a self-hosted or proxied endpoint
66
+ ```
67
+
68
+ Agent config entries merge by name (`core/agents.ts`'s `mergeAgentLists`) and
69
+ patch by field, so this adds `env_allowlist` to the reviewer without having
70
+ to repeat its `model`/`prompt_engineering`/`writes`/`tools`. Do this even if
71
+ you think the reviewer "doesn't have secrets to leak" — the allowlist is
72
+ what makes that true, not an assumption about what happens to be in the
73
+ shell today.
74
+
75
+ ## (b) Writes — ocr must not put a file in the repo
76
+
77
+ The packaged reviewer runs `writes: []` — read-only with respect to the
78
+ repo, enforced in code after the call (`core/permissions.ts`), not by asking
79
+ the model nicely. That enforcement has no retry path: it is not a gate a
80
+ violation can re-prompt its way past. A path ocr drops anywhere in the
81
+ working tree becomes a `PermissionBreach`, which `agents.execute()` throws
82
+ straight out of the phase — the run aborts, the same session does not get a
83
+ chance to fix it, and whatever ocr wrote gets rolled back if it can be
84
+ (deleted, if untracked; left alone and reported if it collided with a file
85
+ that was already dirty).
86
+
87
+ Two things to do before wiring ocr in for real, not after the first failed
88
+ run teaches you the hard way:
89
+
90
+ - **Verify ocr's cache/output behavior first**, outside of `spf` — run it
91
+ by hand in a scratch clone and check `git status` afterward. Some
92
+ review/scan tools default to writing a cache directory or a report file
93
+ next to the code they scanned.
94
+ - **Point any output ocr can be configured to write at a path outside the
95
+ repo, or under the run's own data directory** — `context_handoff_dir` is
96
+ always writable regardless of `writes: []` (`core/permissions.ts`'s
97
+ `alwaysWritable()`: the session runtime under `defaults.data_dir` is
98
+ granted to every agent, read-only ones included, because it is where an
99
+ agent hands its own report to the next phase, not repo content). The
100
+ reviewer's own prompt template already receives it as `{{context_handoff_dir}}`
101
+ — reuse that instead of a path ocr would put inside the tracked tree.
102
+
103
+ If ocr has no such flag, prefer piping its output to stdout and letting the
104
+ reviewer's bash call capture it that way — no file, nothing to breach.
105
+
106
+ ## (c) Accounting honesty — ocr's spend is invisible to spf
107
+
108
+ `spf` counts tokens and dollars in exactly one place:
109
+ `core/agents.ts`'s `execute()`, around the ONE backend call it makes per
110
+ agent turn (Flue or Claude Code), via `run.addUsage()`. That is the
111
+ reviewer's own LLM call. `ocr review`/`ocr scan` invoked from inside that
112
+ call's `bash` tool is a subprocess the tracer sees as a `tool_call` event —
113
+ arguments, an ok/error flag, and captured output (both clipped at 20,000
114
+ characters) — not an exit status; a non-zero `ocr` exit usually still comes
115
+ back as ordinary successful tool output with the code inside the text, the
116
+ same as any other shell command — never as a second `agent_start`/
117
+ `agent_end` pair, because it isn't one from `spf`'s point of view: it is a
118
+ program the reviewer ran, the same shape as `bun test`. Whatever ocr's own
119
+ model call cost, on whatever credential you scoped in (a), is spent and
120
+ billed entirely outside `spf`'s usage tracking (`spf sessions`, the `cost`
121
+ column, the UI's usage panel — none of them will show it).
122
+
123
+ Be honest with yourself and whoever reads the trace about what this means:
124
+ the session total you see for a `reviewer` phase is a **floor**, not the
125
+ full cost of that review, whenever ocr ran. And whether ocr ran at all on a
126
+ given run is at the MODEL's discretion — it's an instruction in a prompt,
127
+ not a step in a chain, so a reviewer that judges the diff sufficient on its
128
+ own is free to never invoke it. If you need to know it ran, ask the prompt
129
+ to say so in its own `notes_for_next_agent`, and read the phase's
130
+ `tool_call` events for the `ocr` invocation itself — that is the only
131
+ record of it that exists.
132
+
133
+ ## (d) `spf doctor` does not check for ocr
134
+
135
+ Deliberately. `spf doctor` checks binaries it has a specific, packaged
136
+ reason to expect: `quality.checks[].argv[0]` (your configured lint/test/build
137
+ commands), the `claude` CLI when `coding_agent: claude_code`, provider env
138
+ keys for whatever `model:` a roster entry names. None of those surfaces ever
139
+ mention ocr, because nothing packaged expects it to exist — it is named only
140
+ inside a prompt file YOU wrote, in a place `doctor` has no reason to read. A
141
+ missing or broken `ocr` binary therefore fails silently from `doctor`'s point
142
+ of view; the reviewer will simply get a failed-command result back from its
143
+ own `bash` tool call, the same as any other missing binary, and has to
144
+ handle that in its judgment like any other tool failure — never a `spf
145
+ doctor` line telling you in advance.
146
+
147
+ ## (e) A worked prompt-override snippet
148
+
149
+ `.spf/prompt_engineering/reviewer/system.md` — the packaged file, with one
150
+ instruction added (keep everything else; this only appends):
151
+
152
+ ```markdown
153
+ # Reviewer Agent
154
+
155
+ ## Purpose
156
+
157
+ Confirm that what was built is what was asked for. This is not testing.
158
+
159
+ ## Instructions
160
+
161
+ - Your spec is `<context_handoff_dir>/plan.md` when that file exists — the plan is the refined ask. Otherwise the spec is `prompt`, verbatim.
162
+ - Judge the code on disk, never the builder's summary of it. Start from `previous_envelope.changed_files`, read them, and use `git diff` for anything the envelope did not mention.
163
+ - Break the spec into concrete requirements and rule on each one: met, or not met with the evidence — a `file:line`, or exactly what is missing.
164
+ - Not your job: running tests, style opinions, refactors, or anything the request did not ask for. Work the request never asked for is not blocking on its own; work the request DID ask for and is missing always is.
165
+ - Change nothing. Findings go back to the builder — that is the only repair path.
166
+ - `approved` is true ONLY when every requirement is met and `blocking` is empty. Every blocking item names the specific gap, so the builder can fix it without guessing.
167
+ - You inherit the operator's shell environment, filtered to this agent's `env_allowlist` — call tools by bare name (`bun`, `uv`, `git`, `ocr`); never hunt for a binary or fall back to an absolute `/usr/bin/*` path.
168
+ - Judge any command you run by its exit status, never by scanning its output for words — except tools whose output is the product, such as `git diff`, or a review tool you were asked to consult, which you read as evidence, never as a verdict.
169
+
170
+ ## One more evidence source: `ocr`
171
+
172
+ Before writing your findings, run `ocr review --format json` (falls back to
173
+ `ocr scan` if `review` is unavailable in this ocr version) against the
174
+ files in `previous_envelope.changed_files`, piping its output rather than
175
+ writing it to any path in the repo. Treat whatever it reports exactly like a
176
+ `git diff` hunk: evidence to weigh, in your own words, against the spec —
177
+ never a verdict you pass through. Fold anything it found that maps to a real
178
+ requirement into your own `findings`/`blocking`; ignore style opinions it
179
+ raises that the spec never asked about. You alone decide `approved` — ocr
180
+ has no vote. If `ocr` is not on `PATH` or the command errors, note that in
181
+ `notes_for_next_agent` and review from the diff alone; a missing second
182
+ opinion is not a blocking finding about the CODE.
183
+ ```
184
+
185
+ And the config half, from (a):
186
+
187
+ ```yaml
188
+ # .spf/spf.config.yaml
189
+ agents:
190
+ - name: reviewer
191
+ env_allowlist:
192
+ - OCR_API_KEY
193
+ - OCR_BASE_URL
194
+ ```
195
+
196
+ That's the whole integration: one prompt file, one config patch, zero code.
@@ -72,10 +72,13 @@ agents:
72
72
  tools: [read, edit, bash] # same canonical names either way — see "Retune tools" below
73
73
  ```
74
74
 
75
- `claude_code` shells out to your own installed `claude` CLI (`spf doctor`
76
- checks it's on `PATH`) — it needs no separate npm install, since SPF never
77
- depends on it directly. A missing `ANTHROPIC_API_KEY` is informational, not
78
- a hard failure: Claude Code also supports its own `claude login` flow.
75
+ `claude_code` shells out to your own installed `claude` CLI it needs no
76
+ separate npm install, since SPF never depends on it directly. `spf doctor`
77
+ checks whatever `SPF_CLAUDE_CMD`'s first token resolves to on `PATH` (see
78
+ below for routing through a wrapper or launcher), falling back to the
79
+ literal `claude` when `SPF_CLAUDE_CMD` is unset. A missing `ANTHROPIC_API_KEY`
80
+ is informational, not a hard failure: Claude Code also supports its own
81
+ `claude login` flow.
79
82
 
80
83
  **Pointing a `claude_code` agent at Ollama** — local or cloud — needs no
81
84
  config at all, just environment variables set before you run `spf` (Claude
@@ -94,6 +97,14 @@ set `ANTHROPIC_AUTH_TOKEN` to a real Ollama Cloud API key. This is exactly
94
97
  the same environment-variable pass-through every agent already gets — no
95
98
  SPF-specific plumbing, no `provider:` config section to write.
96
99
 
100
+ There's a second, different way to reach Ollama through `claude_code`: route
101
+ the `claude` command itself through `ollama launch claude` via
102
+ `SPF_CLAUDE_CMD`, instead of pointing `ANTHROPIC_BASE_URL` at Ollama's
103
+ OpenAI-compatible surface. That launcher form needs its own `--model <tag>`
104
+ flag (from `ollama list`) in the command string, since SPF always spawns
105
+ headless — see README.md's "Proxy or wrapper launchers" section for the
106
+ full command and why `--model` is mandatory there, not optional.
107
+
97
108
  ## Retune tools
98
109
 
99
110
  Known tool names: `read`, `bash`, `edit`, `write`, `grep`, `glob` (`find` is
@@ -106,6 +106,7 @@ launching anything.
106
106
  | Launch a chain, watch it, report on it | `cookbooks/run_adw.md` (after `how_to_prompt_for_the_eng.md`) |
107
107
  | Retune the roster, add an agent | `cookbooks/roster.md` |
108
108
  | Add or extend a chain | `cookbooks/authoring_chains.md` |
109
+ | Fold a third-party review tool's findings into the reviewer's verdict | `cookbooks/ocr_reviewer.md` |
109
110
  | Envelope/gate/session contract | `references/handoff.md` |
110
111
  | Trace schema, spend vs. context | `references/observability.md` |
111
112
  | Full config field reference | `references/config.md` |