@gr8ful/spf 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +177 -9
  2. package/assets/defaults/spf.config.yaml +22 -0
  3. package/assets/prompts/refiner/system.md +53 -0
  4. package/assets/prompts/refiner/user.md +70 -0
  5. package/assets/prompts/reviewer/system.md +1 -1
  6. package/assets/skill/SKILL.md +1 -0
  7. package/assets/skill/cookbooks/authoring_chains.md +90 -7
  8. package/assets/skill/cookbooks/ocr_reviewer.md +196 -0
  9. package/assets/skill/cookbooks/roster.md +15 -4
  10. package/assets/skill/cookbooks/spf_overview.md +1 -0
  11. package/assets/skill/references/config.md +104 -3
  12. package/assets/skill/references/observability.md +11 -2
  13. package/assets/templates/ts-cc.spf.config.yaml +3 -3
  14. package/assets/templates/ts-flue-ollama.spf.config.yaml +67 -0
  15. package/assets/templates/ts.spf.config.yaml +15 -2
  16. package/dist/chains/context.d.ts +39 -0
  17. package/dist/chains/index.d.ts +94 -10
  18. package/dist/chains/index.js +75 -5
  19. package/dist/chains/repo_chains.d.ts +139 -0
  20. package/dist/chains/repo_chains.js +428 -0
  21. package/dist/chains/simple_sdlc.d.ts +74 -1
  22. package/dist/chains/simple_sdlc.js +134 -4
  23. package/dist/chains/steps.d.ts +237 -18
  24. package/dist/chains/steps.js +477 -58
  25. package/dist/cli/ask.d.ts +14 -1
  26. package/dist/cli/ask.js +32 -2
  27. package/dist/cli/commands/doctor.d.ts +1 -1
  28. package/dist/cli/commands/doctor.js +324 -10
  29. package/dist/cli/commands/init.d.ts +12 -0
  30. package/dist/cli/commands/init.js +108 -4
  31. package/dist/cli/commands/install-skill.js +5 -2
  32. package/dist/cli/commands/list.js +43 -5
  33. package/dist/cli/commands/run.js +29 -2
  34. package/dist/cli/commands/watch.d.ts +18 -0
  35. package/dist/cli/commands/watch.js +214 -16
  36. package/dist/cli/index.js +63 -6
  37. package/dist/cli/interview.js +81 -9
  38. package/dist/core/agent_cc.d.ts +40 -1
  39. package/dist/core/agent_cc.js +51 -4
  40. package/dist/core/agent_flue.js +28 -4
  41. package/dist/core/agents.d.ts +8 -0
  42. package/dist/core/agents.js +43 -3
  43. package/dist/core/data_types.d.ts +182 -4
  44. package/dist/core/data_types.js +141 -2
  45. package/dist/core/gates.d.ts +13 -0
  46. package/dist/core/gates.js +103 -0
  47. package/dist/core/git_helper.d.ts +29 -0
  48. package/dist/core/git_helper.js +41 -1
  49. package/dist/core/issues/github_provider.d.ts +35 -9
  50. package/dist/core/issues/github_provider.js +76 -28
  51. package/dist/core/issues/jira_provider.d.ts +14 -1
  52. package/dist/core/issues/jira_provider.js +9 -7
  53. package/dist/core/issues/provider.d.ts +77 -15
  54. package/dist/core/issues/provider.js +7 -4
  55. package/dist/core/notify/channel.d.ts +1 -1
  56. package/dist/core/ollama_provider.d.ts +70 -0
  57. package/dist/core/ollama_provider.js +208 -0
  58. package/dist/core/otel.d.ts +352 -0
  59. package/dist/core/otel.js +793 -0
  60. package/dist/core/providers.js +4 -0
  61. package/dist/core/refine.d.ts +39 -0
  62. package/dist/core/refine.js +152 -0
  63. package/dist/core/session.js +39 -2
  64. package/dist/core/tracer.d.ts +31 -2
  65. package/dist/core/tracer.js +69 -11
  66. package/dist/core/watch.d.ts +67 -1
  67. package/dist/core/watch.js +217 -13
  68. package/dist/test/chains.test.js +9 -3
  69. package/dist/test/data_types.test.js +140 -2
  70. package/dist/test/git_helper.test.d.ts +1 -0
  71. package/dist/test/git_helper.test.js +59 -0
  72. package/dist/test/hermetic_git.d.ts +1 -0
  73. package/dist/test/hermetic_git.js +22 -0
  74. package/dist/test/init_command.test.d.ts +14 -1
  75. package/dist/test/init_command.test.js +71 -1
  76. package/dist/test/interview.test.d.ts +15 -1
  77. package/dist/test/interview.test.js +131 -3
  78. package/dist/test/ollama_provider.test.d.ts +1 -0
  79. package/dist/test/ollama_provider.test.js +103 -0
  80. package/dist/test/otel.test.d.ts +26 -0
  81. package/dist/test/otel.test.js +512 -0
  82. package/dist/test/refine.test.d.ts +1 -0
  83. package/dist/test/refine.test.js +189 -0
  84. package/dist/test/repo_chains.test.d.ts +21 -0
  85. package/dist/test/repo_chains.test.js +416 -0
  86. package/dist/test/signoff.test.d.ts +1 -0
  87. package/dist/test/signoff.test.js +329 -0
  88. package/dist/test/ui_server.test.d.ts +7 -1
  89. package/dist/test/ui_server.test.js +1 -0
  90. package/dist/test/watch.test.js +297 -6
  91. package/package.json +5 -5
@@ -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` |
@@ -12,7 +12,7 @@ always shows the resolved, merged result for the repo you're in.
12
12
  1. The packaged built-in default (`assets/defaults/spf.config.yaml` inside
13
13
  the installed CLI).
14
14
  2. `.spf/spf.config.yaml` in the target repo, if present — merged on top,
15
- field by field (`defaults`/`observability`/`quality`/`watch`/`notifications`
15
+ field by field (`defaults`/`observability`/`quality`/`watch`/`notifications`/`review`
16
16
  merge key-by-key — `notifications.channels` replaces wholesale, same as
17
17
  `quality.checks`; `agents` merges by `name`: a matching name patches that
18
18
  entry, a new name appends).
@@ -72,6 +72,8 @@ agents:
72
72
  | `color` | hex string | Lane color fallback for agents that don't set their own. |
73
73
  | `harness_engineering` | string[] | **Must stay `[]`** — no analogue on any current backend; a non-empty entry fails validate(). |
74
74
  | `tools` | string[] \| null | Roster-wide allowlist. Unset/null = every built-in tool usable. |
75
+ | `writes` | string[] \| null | Roster-wide write allowlist, back-filled onto any agent that doesn't set its own — see `writes` under `agents[]` below for the three-state semantics. |
76
+ | `env_allowlist` | string[] \| null | Roster-wide env allowlist, back-filled the same way as `writes`. Unset/null = every agent gets the full operator environment (see `env_allowlist` under `agents[]`). |
75
77
  | `protected_files` | string[] | Paths no agent may touch unless named in its own `writes`. Default `[".spf/", "spf.config.yaml"]`. |
76
78
  | `data_dir` | path | Runtime home, repo-relative. Default `.spf/data`. |
77
79
 
@@ -81,6 +83,11 @@ agents:
81
83
  |---|---|---|
82
84
  | `db` | path | The trace sqlite db. Default `.spf/data/spf.db`. |
83
85
  | `poll_ms` | int | UI live-poll cadence. Default `500`. |
86
+ | `otel.endpoint` | string | OTLP/HTTP collector endpoint (e.g., `https://your-host/v1/traces`). Omit to disable OTel export. |
87
+ | `otel.headers` | object | Optional HTTP headers (e.g., auth tokens). Each value is a string. |
88
+ | `otel.service_name` | string | Optional service name in exported spans. Default `spf`. |
89
+
90
+ **No ambient env activation**: OTEL export requires explicit `observability.otel` config — the `OTEL_EXPORTER_OTLP_ENDPOINT` shell variable is never consulted. An unrelated shell env variable must not become a data-egress switch.
84
91
 
85
92
  ### `quality`
86
93
 
@@ -108,6 +115,45 @@ sections: `assets/templates/ts.spf.config.yaml` in the spf package (or
108
115
  `spf init --template ts` to write it straight into `.spf/spf.config.yaml`).
109
116
  `spf init` with no `--template` prints every packaged template's name.
110
117
 
118
+ ### `watch`
119
+
120
+ Full mechanism: the main README's "`spf watch`" section. Field reference:
121
+
122
+ | Field | Type | Meaning |
123
+ |---|---|---|
124
+ | `issue_provider` | `"github"` \| `"jira"` | The tracker `spf watch` polls. Default `github`. |
125
+ | `code_host` | `"github"` \| `"bitbucket"` | Where PRs open — independent of `issue_provider` (Jira issues against a Bitbucket repo is a real setup). Default `github`. |
126
+ | `repo` | string | The **code host's** repo — required once watch is actually run (not schema-validated — fails loudly at `spf watch` startup instead). `"owner/name"` for `code_host: github`, `"workspace/repo_slug"` for `code_host: bitbucket`. |
127
+ | `issue_repo` | string | Overrides `repo` for the **issue tracker** side, only meaningful (and only needed) for `issue_provider: github` + `code_host: bitbucket` — the one combination where the tracker and the code host are genuinely different repos in different systems. Unset (the default) falls back to `repo`, which is exactly right for `github`+`github` (one repo) and for `issue_provider: jira` (which never reads `repo` at all). |
128
+ | `label_prefix` | string | State-machine label prefix — polls/writes `<prefix>:ready`, `<prefix>:working`, etc. Default `spf`. |
129
+ | `chain` | string | Which registered chain runs per claimed `<prefix>:ready` issue. Default `plan-build-test`. |
130
+ | `base_branch` | string | Branch worktrees fork from and PRs target. Default `main`. |
131
+ | `poll_ms` | int | Tick interval. Default `60000`. |
132
+ | `concurrency` | int ≥1 | Max issues claimed and run at once, the build lane's own budget (independent of `refine.concurrency`). Default `2`. |
133
+ | `jira.base_url` / `jira.project_key` | string | Only consulted when `issue_provider: jira`. |
134
+ | `refine.enabled` | bool | Turns on the second lane: decompose a `<prefix>:spec-ready` product spec into a feature/story-or-bug tree of real issues, instead of running `chain` against it directly (a spec isn't individually workable). Default `false` — off by default, so an existing `watch:` config is unaffected by upgrading. Needs `issue_provider: github` — `spf watch` fails loudly at startup otherwise, since issue authoring (create + link a hierarchy) isn't implemented for Jira yet. |
135
+ | `refine.chain` | string | Which registered chain runs per claimed spec. Default `refine`. |
136
+ | `refine.concurrency` | int ≥1 | The refine lane's own budget, separate from `concurrency`. Default `1`. |
137
+
138
+ ```yaml
139
+ watch:
140
+ repo: owner/name
141
+ label_prefix: spf
142
+ chain: plan-build-test
143
+ refine:
144
+ enabled: true # decompose spf:spec-ready specs into a feature/story tree
145
+ chain: refine
146
+ concurrency: 1
147
+ ```
148
+
149
+ Generated issues carry a second, independent label vocabulary —
150
+ `<prefix>:type:epic|feature|story|bug|task` — seeded by `spf watch init`
151
+ alongside the state labels. A feature/epic (a container: some other node
152
+ names it as `parent`) gets only its type label; a leaf (story/bug/task)
153
+ additionally gets `<prefix>:refined`, so a human can review and promote it to
154
+ `<prefix>:ready` when it's worth building — the refine lane never
155
+ auto-promotes anything.
156
+
111
157
  ### `notifications`
112
158
 
113
159
  Optional outbound push for unattended work — `spf watch`'s daemon lifecycle,
@@ -151,6 +197,33 @@ run's exit code. See the main README's "Notifications" section for how to
151
197
  get each channel's webhook URL, and `spf init`'s interview, which asks for
152
198
  this section and collects the URL straight into `.env`.
153
199
 
200
+ ### `review`
201
+
202
+ The human sign-off gate in front of `simple-sdlc`'s `commit_build` phase —
203
+ the one place in this codebase an AI reviewer's `approved` flag alone would
204
+ otherwise gate a commit (`build-review` has no commit step, and no other
205
+ chain runs a reviewer at all). See the main README's "Isolation" section for
206
+ the full behavior; this is just the two knobs.
207
+
208
+ | Field | Type | Meaning |
209
+ |---|---|---|
210
+ | `require_human_signoff` | bool | `false` (default, this release): an unattended run (`spf watch`, CI, no TTY) proceeds on the reviewer's verdict alone, with a loud warning printed and traced every time. `true`: an unattended run instead fails the phase closed — rerun attended, or route the work through `spf watch`, whose own human gate is the PR merge. Never affects an attended run's prompt, which is always shown either way. |
211
+ | `signoff_timeout_seconds` | number ≥1 | Bounds the attended confirm prompt. Default `300`. Not required to be an integer — `1.5` parses and becomes a 1500ms timer. Expiry resolves to the prompt's own default — **not accepted**, same as if the answer had been "no" — never an unbounded `stdin` read inside `run.phase()`. |
212
+
213
+ ```yaml
214
+ review:
215
+ require_human_signoff: false # this release's default — see above
216
+ signoff_timeout_seconds: 300
217
+ ```
218
+
219
+ The confirm prompt's own default is always `false` — never `review.approved`,
220
+ so an AI's own verdict can never auto-approve itself by way of an unanswered
221
+ default. A `Signed-off-by:` trailer is appended to the commit only on a
222
+ recorded explicit "yes," built from `git config user.name`/`user.email` at
223
+ the repo (never `ENGINEER_NAME`/`$USER`, which are spoofable and fall back to
224
+ the literal string `"engineer"`) — an unattended run's AI-only commit never
225
+ carries one, because nobody said yes to attest to.
226
+
154
227
  ### `agents[]`
155
228
 
156
229
  | Field | Required | Meaning |
@@ -161,6 +234,7 @@ this section and collects the URL straight into `.env`.
161
234
  | `coding_agent`, `model`, `thinking`, `color`, `harness_engineering` | no | Override the matching `defaults` key. |
162
235
  | `tools` | no | Allowlist. Omitting it means all tools usable. A capability list, not a boundary — see `writes`. |
163
236
  | `writes` | no | What this agent may modify **in the repo**, enforced after every call. `undefined`/`null` = unrestricted (still barred from `protected_files`); `[]` = no repo writes; a list = only those paths (trailing `/` = directory prefix, `*` = one path segment, `**` = crosses segments, anything else = exact path). |
237
+ | `env_allowlist` | no | Opt-in filter on the environment handed to this agent's subprocess/sandbox. `undefined` (default) = the full operator environment, unchanged. A list = only those keys, plus the baseline (`PATH`, `HOME`, `USER`, `LANG`, `TERM`, `TMPDIR`) either backend keeps regardless. |
164
238
 
165
239
  Output types are deliberately absent from config: an entry defines who an
166
240
  agent *is*; the call site defines how it's *used*.
@@ -174,7 +248,15 @@ no public model-registry API) — `agents.ts` checks only the static shape at
174
248
  first real dispatch instead. Provider credentials come from the
175
249
  environment, matching the provider you named (`GEMINI_API_KEY`/
176
250
  `GOOGLE_API_KEY` for `google/...`, `ANTHROPIC_API_KEY` for `anthropic/...`,
177
- etc.) — `spf doctor` checks the common ones are set.
251
+ etc.) — `spf doctor` checks the common ones are set. `ollama/...` is the one
252
+ keyless provider (`PROVIDER_ENV_KEYS.ollama` is `[]`) — instead of a key, set
253
+ `OLLAMA_BASE_URL` (default `http://localhost:11434/v1` if unset) to point at
254
+ your server; the `spf init` interview asks for this instead of a secret when
255
+ you pick `ollama`, and `spf doctor` probes it (informational — a down server
256
+ is reported, never a hard failure). See README.md's "flue + local Ollama"
257
+ section for the full walkthrough and
258
+ [`assets/templates/ts-flue-ollama.spf.config.yaml`](../../templates/ts-flue-ollama.spf.config.yaml)
259
+ for a ready-to-run starting config.
178
260
 
179
261
  **For `coding_agent: claude_code`:** write `model` in Claude Code's own
180
262
  vocabulary — a bare alias (`sonnet`, `opus`) or a full model name — never
@@ -233,4 +315,23 @@ No config section for this — it's an environment-variable recipe, since
233
315
  `claude` subprocess, exactly like every other env var. Set
234
316
  `ANTHROPIC_BASE_URL`/`ANTHROPIC_AUTH_TOKEN` (local or cloud Ollama) before
235
317
  running `spf`; see `roster.md`'s "Coding agent backends" section for the
236
- exact commands.
318
+ exact commands. `spf doctor` probes `ANTHROPIC_BASE_URL` (informational — a
319
+ down endpoint or wrong path is reported, never a hard failure) and flags a
320
+ base URL that already ends in `/v1` as a likely double-path mistake, since
321
+ the `claude` CLI appends `/v1/messages` itself.
322
+
323
+ Routing through `SPF_CLAUDE_CMD="ollama launch claude --model granite4.1:8b"`
324
+ instead needs an explicit `--` before `claude`'s own flags (cobra flag
325
+ parsing otherwise consumes them as `ollama launch`'s own) — `agent_cc.ts`
326
+ detects this exact `ollama launch ...` token shape and inserts that
327
+ separator automatically, so you never add it by hand. The `--model` before
328
+ that separator is NOT optional, though: it's `ollama launch`'s own flag, and
329
+ it's mandatory in headless mode (SPF always pipes stdio, so the interactive
330
+ model picker `ollama launch` falls back to without it can never run) — a
331
+ `--model` typed after the `--` belongs to `claude`, not to `ollama launch`,
332
+ and doesn't help. `spf doctor` hard-fails a `SPF_CLAUDE_CMD` missing it. See
333
+ README.md's "Proxy or wrapper launchers" section for the full explanation.
334
+
335
+ For `flue` (the default backend) pointed at Ollama instead of `claude_code`
336
+ — i.e. `model: ollama/<tag>` — see "Model resolution" above and README.md's
337
+ "flue + local Ollama" section.
@@ -31,8 +31,17 @@ Location comes from `observability.db` in `spf.config.yaml`, default
31
31
  | `phase_end` | the block exits; carries the resolved status |
32
32
  | `error` | a throw inside a phase block |
33
33
 
34
- `parent_id` nests spans, so an agent phase expands into its tool-call spans
35
- in the UI.
34
+ `parent_id` is reserved and structurally empty today SPF's phases are flat
35
+ siblings, and nothing writes nesting into it. The UI reconstructs phase/
36
+ tool-call nesting from `phase_id` plus agent-call bracketing instead of
37
+ reading `parent_id`.
38
+
39
+ **Optional OTel export.** When `observability.otel.endpoint` is set in
40
+ `spf.config.yaml`, a lossy, allowlisted projection of phase/agent/tool spans
41
+ (status, model, token/cost counts, gate results — never prompts, envelopes,
42
+ tool arguments, or source code) is also pushed to an OTLP/HTTP collector,
43
+ fire-and-forget. SQLite remains the source of truth regardless; see
44
+ `config.md`'s `observability.otel.*` rows for the field reference.
36
45
 
37
46
  **Spend is itemized per phase.** `agent_end.usage` carries tokens *and*
38
47
  dollars for each component Flue reports (matching pi-ai's field names
@@ -47,9 +47,9 @@ defaults:
47
47
  # provider/model-id strings, which an agent's own model always wins over
48
48
  # defaults.model above — switching coding_agent globally does NOT reset
49
49
  # those three, so they'd run on Claude Code (redirected at Ollama) with a
50
- # model id Ollama has never heard of, and fail outright. builder/scout
51
- # have no model of their own in the packaged roster, so they correctly
52
- # inherit defaults.model above and need no override here.
50
+ # model id Ollama has never heard of, and fail outright. builder/scout/
51
+ # refiner have no model of their own in the packaged roster, so they
52
+ # correctly inherit defaults.model above and need no override here.
53
53
  agents:
54
54
  - name: planner
55
55
  model: qwen3-coder:30b
@@ -0,0 +1,67 @@
1
+ # .spf/spf.config.yaml — Flue backend (the default), routed at a local
2
+ # Ollama server instead of a hosted provider. `spf init --template
3
+ # ts-flue-ollama` writes this file as-is.
4
+ #
5
+ # model: is ALWAYS provider/model-id for Flue — "ollama/<tag>", where <tag>
6
+ # is whatever `ollama list` shows on the machine actually running the model
7
+ # (agent_flue.ts registers each distinct "ollama/<tag>" it sees with Flue's
8
+ # own provider registry the first time it's dispatched — no separate model
9
+ # catalog to keep in sync here). agents.validate() only checks the STRING
10
+ # SHAPE (provider/id, via agent_flue.ts's resolveModel()) — never that the
11
+ # tag actually exists on the server. A wrong or not-yet-pulled tag only
12
+ # surfaces at the first real dispatch, as an "Unknown model" error from
13
+ # Ollama itself, not at validate() time.
14
+ #
15
+ # The two model ids below (qwen3.8:27b-mlx, granite4.1:8b) are EXAMPLES from
16
+ # one spike machine's own `ollama list` output, not a stable public catalog —
17
+ # replace both with whatever `ollama list` shows on YOUR machine before
18
+ # running spf. Nothing here pins a specific model family; the split is
19
+ # purely "heavier reasoning agent" vs. "lighter/faster agent", same as any
20
+ # other template's planner-vs-builder split.
21
+ #
22
+ # ollama is a KEYLESS provider (providers.ts's PROVIDER_ENV_KEYS.ollama is
23
+ # `[]`) — no API key to export. What IS required:
24
+ # export OLLAMA_BASE_URL=http://localhost:11434/v1 # default if unset
25
+ # `spf doctor` probes OLLAMA_BASE_URL/models (informational, never a hard
26
+ # failure) so a server that isn't running shows up before your first real
27
+ # run does, not during it.
28
+ #
29
+ # Context-window occupancy reporting is disabled for every ollama/* model:
30
+ # agent_flue.ts registers these models with contextWindow: 0 (spike-verified
31
+ # safe — the same convention agent_flue.ts already uses for
32
+ # context_window: 0 elsewhere), which turns off threshold-based compaction
33
+ # rather than reporting a fabricated number Ollama's OpenAI-compatible API
34
+ # doesn't actually provide per-model.
35
+ quality:
36
+ checks:
37
+ - { name: typecheck, operation: typecheck, argv: ["npm", "run", "typecheck"], timeout_seconds: 60 }
38
+ - { name: lint, operation: lint, argv: ["npm", "run", "lint"], timeout_seconds: 60 }
39
+ - { name: build, operation: build, argv: ["npm", "run", "build"], timeout_seconds: 300 }
40
+ - { name: test, operation: build, argv: ["npm", "test"], timeout_seconds: 300 }
41
+ suites:
42
+ test: [test]
43
+ all: [typecheck, lint, build, test]
44
+
45
+ defaults:
46
+ coding_agent: flue
47
+ # EXAMPLE tag — replace with a heavier model from your own `ollama list`.
48
+ model: ollama/qwen3.8:27b-mlx
49
+
50
+ # Overridden here (unlike the OpenRouter template's rationale, which applies
51
+ # equally): the packaged roster's planner/reviewer/documenter pin their own
52
+ # Flue-style provider/model-id strings (fireworks/openai models), unrelated
53
+ # to a local Ollama server — left alone, those three would still resolve and
54
+ # run fine, just against a hosted provider's key this template never asks
55
+ # for. Overridden so every agent actually demonstrates local-only routing.
56
+ agents:
57
+ - name: planner
58
+ model: ollama/qwen3.8:27b-mlx
59
+ - name: reviewer
60
+ model: ollama/qwen3.8:27b-mlx
61
+ - name: documenter
62
+ # EXAMPLE tag — replace with a lighter/faster model from your own
63
+ # `ollama list`. Tool-calling (the injected sf_report contract every
64
+ # agent's structured output rides on) was spike-verified to work down to
65
+ # a 3B model — a lighter tag here is a real, not merely theoretical,
66
+ # option.
67
+ model: ollama/granite4.1:8b
@@ -39,8 +39,8 @@ defaults:
39
39
  # always wins over defaults.model — switching coding_agent globally does
40
40
  # NOT reset those three, so they'd run on Claude Code with a model id it
41
41
  # can't resolve at all ("There's an issue with the selected model...").
42
- # builder/scout have no model of their own in the packaged roster, so they
43
- # correctly inherit defaults.model above and need no override here.
42
+ # builder/scout/refiner have no model of their own in the packaged roster,
43
+ # so they correctly inherit defaults.model above and need no override here.
44
44
  agents:
45
45
  - name: planner
46
46
  model: sonnet
@@ -61,9 +61,22 @@ agents:
61
61
  # Needs a GITHUB_TOKEN env var (classic PAT, repo scope) for the defaults below.
62
62
  # watch:
63
63
  # repo: owner/name
64
+ # # issue_repo: owner/name # ONLY for issue_provider: github + code_host: bitbucket,
65
+ # # # where the issue tracker and code host are genuinely
66
+ # # # different repos — repo above is then the BITBUCKET
67
+ # # # repo, and issue_repo is the GitHub one. Unused/unset
68
+ # # # for every other combination.
64
69
  # label_prefix: spf
65
70
  # chain: plan-build-test
66
71
  # base_branch: main
72
+ # # Optional second lane: decompose a spf:spec-ready product spec into a
73
+ # # feature/story-or-bug tree of real issues instead of building it
74
+ # # directly. Off by default; needs issue_provider: github (the default
75
+ # # above) — issue authoring isn't implemented for Jira yet.
76
+ # refine:
77
+ # enabled: true
78
+ # chain: refine
79
+ # concurrency: 1
67
80
 
68
81
  # Optional: push notifications for unattended work — spf watch's daemon
69
82
  # lifecycle, and every chain run (including watch's own per-issue runs).
@@ -18,4 +18,43 @@ export interface ChainContext {
18
18
  cwd: string;
19
19
  /** The CLI name (`"plan-build-test"`), for session.ensure()'s trace record — see core/session.ts. */
20
20
  chain_name: string;
21
+ /**
22
+ * The originating tracker issue's id, only meaningful to the `refine`
23
+ * chain — `steps.publishIssues()` renders it as a `## Parent: #<id>`
24
+ * back-reference on every issue it creates (`core/refine.ts`'s
25
+ * `renderBody`). `null`/omitted for a manual run with no source issue
26
+ * (a bare `spf refine "<spec text>"`, no `--issue`). Every other chain
27
+ * ignores this field.
28
+ */
29
+ issue_id?: string | null;
30
+ /**
31
+ * True when no human is at the keyboard for this run — `spf watch`'s
32
+ * daemon lane, a cron/CI invocation — false for an interactive
33
+ * `spf <chain> "..."`.
34
+ *
35
+ * REQUIRED, deliberately: it is not a flag a call site may forget. Every
36
+ * ChainContext construction site has to state which lane it is, because
37
+ * "unattended" is what makes several safety rules non-negotiable rather
38
+ * than merely advisable — most concretely, a repo-local chain
39
+ * (`.spf/chains/*.yaml`) may only ADD gates, never drop a built-in one
40
+ * (see `steps.ts`'s GATE_ALLOWLIST comment): there is nobody watching the
41
+ * console to notice that `diffMatchesClaims` was quietly turned off. A
42
+ * boolean that defaults to `false` would make the dangerous lane the one
43
+ * you get by accident.
44
+ */
45
+ unattended: boolean;
46
+ /**
47
+ * Absolute path of the YAML file this chain was loaded from, when it is a
48
+ * repo-local chain (`.spf/chains/<name>.yaml` — see
49
+ * `chains/repo_chains.ts`). `undefined` for a built-in chain, which has no
50
+ * file: it IS spf's own code.
51
+ *
52
+ * `steps.startRun()` writes this into the trace as a single
53
+ * `chain_source` log event, so a session in the UI or in `spf trace` can
54
+ * always answer "whose chain definition ran this?" — the answer stops
55
+ * being obvious the moment a target repo can ship its own chains, and the
56
+ * trace is the only durable record (the YAML file itself may have been
57
+ * edited by the time anyone reads the run back).
58
+ */
59
+ chain_source?: string;
21
60
  }