@gr8ful/spf 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +321 -0
- package/assets/defaults/spf.config.yaml +141 -0
- package/assets/prompts/builder/system.md +13 -0
- package/assets/prompts/builder/user.md +34 -0
- package/assets/prompts/documenter/system.md +17 -0
- package/assets/prompts/documenter/user.md +48 -0
- package/assets/prompts/planner/system.md +21 -0
- package/assets/prompts/planner/user.md +45 -0
- package/assets/prompts/reviewer/system.md +16 -0
- package/assets/prompts/reviewer/user.md +44 -0
- package/assets/prompts/scout/system.md +20 -0
- package/assets/prompts/scout/user.md +34 -0
- package/assets/skill/SKILL.md +80 -0
- package/assets/skill/cookbooks/authoring_chains.md +193 -0
- package/assets/skill/cookbooks/how_to_prompt_for_the_eng.md +109 -0
- package/assets/skill/cookbooks/roster.md +197 -0
- package/assets/skill/cookbooks/run_adw.md +92 -0
- package/assets/skill/cookbooks/spf_overview.md +111 -0
- package/assets/skill/references/config.md +188 -0
- package/assets/skill/references/handoff.md +162 -0
- package/assets/skill/references/observability.md +184 -0
- package/dist/chains/adw_build.d.ts +12 -0
- package/dist/chains/adw_build.js +27 -0
- package/dist/chains/adw_build_review.d.ts +21 -0
- package/dist/chains/adw_build_review.js +55 -0
- package/dist/chains/adw_build_test.d.ts +21 -0
- package/dist/chains/adw_build_test.js +67 -0
- package/dist/chains/adw_document.d.ts +23 -0
- package/dist/chains/adw_document.js +59 -0
- package/dist/chains/adw_plan.d.ts +12 -0
- package/dist/chains/adw_plan.js +27 -0
- package/dist/chains/adw_plan_build.d.ts +12 -0
- package/dist/chains/adw_plan_build.js +30 -0
- package/dist/chains/adw_plan_build_test.d.ts +16 -0
- package/dist/chains/adw_plan_build_test.js +65 -0
- package/dist/chains/adw_plan_build_test_quality.d.ts +18 -0
- package/dist/chains/adw_plan_build_test_quality.js +66 -0
- package/dist/chains/adw_prompt.d.ts +12 -0
- package/dist/chains/adw_prompt.js +25 -0
- package/dist/chains/adw_quality.d.ts +12 -0
- package/dist/chains/adw_quality.js +32 -0
- package/dist/chains/adw_scout.d.ts +12 -0
- package/dist/chains/adw_scout.js +27 -0
- package/dist/chains/adw_simple_sdlc.d.ts +43 -0
- package/dist/chains/adw_simple_sdlc.js +147 -0
- package/dist/chains/context.d.ts +19 -0
- package/dist/chains/context.js +1 -0
- package/dist/chains/index.d.ts +19 -0
- package/dist/chains/index.js +116 -0
- package/dist/cli/bin.d.ts +15 -0
- package/dist/cli/bin.js +24 -0
- package/dist/cli/commands/abort.d.ts +1 -0
- package/dist/cli/commands/abort.js +42 -0
- package/dist/cli/commands/doctor.d.ts +1 -0
- package/dist/cli/commands/doctor.js +165 -0
- package/dist/cli/commands/eject.d.ts +1 -0
- package/dist/cli/commands/eject.js +55 -0
- package/dist/cli/commands/events.d.ts +1 -0
- package/dist/cli/commands/events.js +49 -0
- package/dist/cli/commands/init.d.ts +1 -0
- package/dist/cli/commands/init.js +62 -0
- package/dist/cli/commands/install-skill.d.ts +1 -0
- package/dist/cli/commands/install-skill.js +122 -0
- package/dist/cli/commands/list.d.ts +1 -0
- package/dist/cli/commands/list.js +13 -0
- package/dist/cli/commands/migrate.d.ts +1 -0
- package/dist/cli/commands/migrate.js +167 -0
- package/dist/cli/commands/phases.d.ts +1 -0
- package/dist/cli/commands/phases.js +25 -0
- package/dist/cli/commands/run.d.ts +3 -0
- package/dist/cli/commands/run.js +27 -0
- package/dist/cli/commands/sessions.d.ts +1 -0
- package/dist/cli/commands/sessions.js +20 -0
- package/dist/cli/commands/trace.d.ts +8 -0
- package/dist/cli/commands/trace.js +10 -0
- package/dist/cli/commands/ui.d.ts +1 -0
- package/dist/cli/commands/ui.js +35 -0
- package/dist/cli/commands/version.d.ts +1 -0
- package/dist/cli/commands/version.js +8 -0
- package/dist/cli/commands/watch.d.ts +8 -0
- package/dist/cli/commands/watch.js +210 -0
- package/dist/cli/gitignore.d.ts +1 -0
- package/dist/cli/gitignore.js +14 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +156 -0
- package/dist/core/agent_cc.d.ts +75 -0
- package/dist/core/agent_cc.js +322 -0
- package/dist/core/agent_flue.d.ts +66 -0
- package/dist/core/agent_flue.js +321 -0
- package/dist/core/agents.d.ts +64 -0
- package/dist/core/agents.js +456 -0
- package/dist/core/changes.d.ts +35 -0
- package/dist/core/changes.js +98 -0
- package/dist/core/console.d.ts +36 -0
- package/dist/core/console.js +156 -0
- package/dist/core/data_types.d.ts +562 -0
- package/dist/core/data_types.js +382 -0
- package/dist/core/gates.d.ts +33 -0
- package/dist/core/gates.js +144 -0
- package/dist/core/git_helper.d.ts +59 -0
- package/dist/core/git_helper.js +115 -0
- package/dist/core/issues/github_provider.d.ts +52 -0
- package/dist/core/issues/github_provider.js +211 -0
- package/dist/core/issues/provider.d.ts +90 -0
- package/dist/core/issues/provider.js +14 -0
- package/dist/core/paths.d.ts +78 -0
- package/dist/core/paths.js +108 -0
- package/dist/core/permissions.d.ts +78 -0
- package/dist/core/permissions.js +187 -0
- package/dist/core/prompts.d.ts +4 -0
- package/dist/core/prompts.js +17 -0
- package/dist/core/quality.d.ts +65 -0
- package/dist/core/quality.js +194 -0
- package/dist/core/runner.d.ts +72 -0
- package/dist/core/runner.js +168 -0
- package/dist/core/session.d.ts +16 -0
- package/dist/core/session.js +60 -0
- package/dist/core/sqlite.d.ts +55 -0
- package/dist/core/sqlite.js +106 -0
- package/dist/core/tracer.d.ts +56 -0
- package/dist/core/tracer.js +246 -0
- package/dist/core/utils.d.ts +40 -0
- package/dist/core/utils.js +108 -0
- package/dist/core/watch.d.ts +44 -0
- package/dist/core/watch.js +194 -0
- package/dist/test/agent_cc.test.d.ts +1 -0
- package/dist/test/agent_cc.test.js +95 -0
- package/dist/test/agent_flue.test.d.ts +1 -0
- package/dist/test/agent_flue.test.js +83 -0
- package/dist/test/data_types.test.d.ts +10 -0
- package/dist/test/data_types.test.js +49 -0
- package/dist/test/ui_server.test.d.ts +1 -0
- package/dist/test/ui_server.test.js +119 -0
- package/dist/test/watch.test.d.ts +1 -0
- package/dist/test/watch.test.js +227 -0
- package/dist/ui/server/app.d.ts +3 -0
- package/dist/ui/server/app.js +98 -0
- package/dist/ui/server/db.d.ts +82 -0
- package/dist/ui/server/db.js +333 -0
- package/dist/ui/server/serve.d.ts +13 -0
- package/dist/ui/server/serve.js +80 -0
- package/dist/ui/server/static.d.ts +5 -0
- package/dist/ui/server/static.js +55 -0
- package/dist/ui/shared/types.d.ts +263 -0
- package/dist/ui/shared/types.js +8 -0
- package/package.json +52 -0
- package/web/assets/index-C7nF068F.css +1 -0
- package/web/assets/index-mzSArcnQ.js +11 -0
- package/web/assets/play-latin-400-normal-GKW-4YV7.woff2 +0 -0
- package/web/assets/play-latin-700-normal-DyPlLDbb.woff2 +0 -0
- package/web/index.html +14 -0
- package/web/logo.svg +6 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# Roster
|
|
2
|
+
|
|
3
|
+
Add or retune agents in `spf.config.yaml`. There's nothing to generate — `spf
|
|
4
|
+
init` seeds a starter `.spf/spf.config.yaml` with a couple of commented
|
|
5
|
+
examples; everything else is inherited from the packaged defaults until you
|
|
6
|
+
override it. `spf doctor` always shows what's actually in effect and where it
|
|
7
|
+
came from.
|
|
8
|
+
|
|
9
|
+
## The rule
|
|
10
|
+
|
|
11
|
+
**One agent, one prompt, one purpose.** A roster entry defines who an agent
|
|
12
|
+
*is*: its model, thinking level, and exactly one system prompt plus one user
|
|
13
|
+
prompt. How it's *used* — the output type, a per-call prompt override —
|
|
14
|
+
lives at the chain call site, never here. See `authoring_chains.md`.
|
|
15
|
+
|
|
16
|
+
## Retune model or thinking
|
|
17
|
+
|
|
18
|
+
```yaml
|
|
19
|
+
agents:
|
|
20
|
+
- name: builder
|
|
21
|
+
model: anthropic/claude-sonnet-4-6 # ALWAYS provider/model-id
|
|
22
|
+
thinking: high # was medium
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Write the model as `provider/model-id`, never a bare id — SPF has no live
|
|
26
|
+
model catalog to check against (that was `pi --list-models`; Flue has no
|
|
27
|
+
public equivalent), so an unqualified or malformed pattern is only caught by
|
|
28
|
+
shape at `agents.validate()` time, and a genuinely wrong id surfaces at the
|
|
29
|
+
first real dispatch. Thinking levels: `off | minimal | low | medium | high |
|
|
30
|
+
xhigh | max` — inert (no error, no effect) on a model that isn't a reasoning
|
|
31
|
+
model.
|
|
32
|
+
|
|
33
|
+
**This is Flue's model vocabulary, not every backend's.** An agent with
|
|
34
|
+
`coding_agent: claude_code` (see below) writes `model:` in Claude Code's own
|
|
35
|
+
vocabulary instead — a bare alias (`sonnet`, `opus`) or a full model name —
|
|
36
|
+
never `provider/model-id`. `agents.validate()` knows which shape check
|
|
37
|
+
applies from `coding_agent`, so mixing the two conventions in the same
|
|
38
|
+
roster is fine as long as each agent's own `model:` matches its own
|
|
39
|
+
backend.
|
|
40
|
+
|
|
41
|
+
**A model change means a fresh session.** `agent_map.json` records the
|
|
42
|
+
model each agent's Flue conversation was created with. When a joined run
|
|
43
|
+
(`--adw-id`) finds the config's model no longer matches, that agent starts a
|
|
44
|
+
**new** conversation rather than resuming — never a bad resume. Thinking
|
|
45
|
+
changes don't invalidate a session; model changes do.
|
|
46
|
+
|
|
47
|
+
## Recolor an agent's lane
|
|
48
|
+
|
|
49
|
+
```yaml
|
|
50
|
+
agents:
|
|
51
|
+
- name: builder
|
|
52
|
+
color: "#22d3ee" # hex; the starter roster ships violet/cyan/amber/red/magenta
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Cosmetic, safe mid-project — rides the `agent_start` event and the
|
|
56
|
+
`agent_sessions` row, so the UI picks it up on the next run without
|
|
57
|
+
touching past sessions. Omit it for the UI's fallback palette.
|
|
58
|
+
|
|
59
|
+
## Coding agent backends
|
|
60
|
+
|
|
61
|
+
`coding_agent: flue` (the default) or `coding_agent: claude_code` — set per
|
|
62
|
+
agent or in `defaults`. Both read the same roster shape (`tools:`,
|
|
63
|
+
`writes:`, `thinking:`), but each backend interprets a couple of fields in
|
|
64
|
+
its own vocabulary:
|
|
65
|
+
|
|
66
|
+
```yaml
|
|
67
|
+
agents:
|
|
68
|
+
- name: builder
|
|
69
|
+
coding_agent: claude_code
|
|
70
|
+
model: sonnet # Claude Code's own alias/name, NOT provider/model-id
|
|
71
|
+
thinking: high
|
|
72
|
+
tools: [read, edit, bash] # same canonical names either way — see "Retune tools" below
|
|
73
|
+
```
|
|
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.
|
|
79
|
+
|
|
80
|
+
**Pointing a `claude_code` agent at Ollama** — local or cloud — needs no
|
|
81
|
+
config at all, just environment variables set before you run `spf` (Claude
|
|
82
|
+
Code's CLI reads them itself):
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# local Ollama
|
|
86
|
+
export ANTHROPIC_BASE_URL=http://localhost:11434
|
|
87
|
+
export ANTHROPIC_AUTH_TOKEN=ollama # any non-empty value; Ollama doesn't check it locally
|
|
88
|
+
|
|
89
|
+
spf build "..." --config .spf/spf.config.yaml # any chain naming a claude_code agent
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
For Ollama's cloud offering, point `ANTHROPIC_BASE_URL` at that instead and
|
|
93
|
+
set `ANTHROPIC_AUTH_TOKEN` to a real Ollama Cloud API key. This is exactly
|
|
94
|
+
the same environment-variable pass-through every agent already gets — no
|
|
95
|
+
SPF-specific plumbing, no `provider:` config section to write.
|
|
96
|
+
|
|
97
|
+
## Retune tools
|
|
98
|
+
|
|
99
|
+
Known tool names: `read`, `bash`, `edit`, `write`, `grep`, `glob` (`find` is
|
|
100
|
+
accepted as an alias for `glob`). These are canonical across BOTH backends —
|
|
101
|
+
Flue maps them to its own lowercase tool functions, Claude Code maps them to
|
|
102
|
+
its own capitalized names (`Read`, `Bash`, ...); a roster entry never has to
|
|
103
|
+
say which. `ls` is a recognized name with **no built-in on either backend**
|
|
104
|
+
— bash/glob cover it, so listing it is harmless but it never mounts as its
|
|
105
|
+
own tool.
|
|
106
|
+
|
|
107
|
+
```yaml
|
|
108
|
+
defaults:
|
|
109
|
+
tools: [read, bash, edit, write, grep, glob]
|
|
110
|
+
|
|
111
|
+
agents:
|
|
112
|
+
- name: reviewer
|
|
113
|
+
tools: [read, grep, glob, bash, write] # explicit list wins over defaults
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Resolution:** the agent's own list wins → else it inherits `defaults.tools`
|
|
117
|
+
→ else unset, meaning every built-in tool. An empty list is not "all tools";
|
|
118
|
+
it's a tool-less agent, and it will stall. `agents.validate()` rejects an
|
|
119
|
+
unknown tool name at startup, before anything spawns.
|
|
120
|
+
|
|
121
|
+
Narrow by role, not by reflex:
|
|
122
|
+
|
|
123
|
+
- Any agent producing a `context_handoff/` artifact needs **`write`** — the
|
|
124
|
+
session runtime under `data_dir` is always writable regardless of
|
|
125
|
+
`writes:` (below), but the agent still needs the `write` tool itself to
|
|
126
|
+
use that grant.
|
|
127
|
+
- Withhold `edit`/`write` only where the restriction *is* the guarantee — a
|
|
128
|
+
reviewer's contract is "change nothing," so dropping `edit` makes that
|
|
129
|
+
structural rather than merely prompted. (`writes: []`, below, is what
|
|
130
|
+
actually enforces it either way.)
|
|
131
|
+
- Recon agents get the full read surface (`read`, `grep`, `glob`) — cheaper
|
|
132
|
+
and more legible in the trace than the equivalent `bash` calls.
|
|
133
|
+
|
|
134
|
+
`harness_engineering` has **no analogue on any current backend** and must
|
|
135
|
+
stay empty — `agents.validate()` fails loudly if any entry is non-empty.
|
|
136
|
+
(Flue's equivalent surface — extra tools, subagents — is added via
|
|
137
|
+
`useTool()`/`useSandbox()` inside `src/core/agent_flue.ts` itself; Claude
|
|
138
|
+
Code's is an MCP server or plugin passed to `agent_cc.ts`. Either way it's
|
|
139
|
+
an engine change, not a config one; see `authoring_chains.md`.)
|
|
140
|
+
|
|
141
|
+
## Add a new agent
|
|
142
|
+
|
|
143
|
+
Three steps, all required — skipping any one fails `agents.validate()` at
|
|
144
|
+
startup, before anything spawns:
|
|
145
|
+
|
|
146
|
+
1. **Prompts.** Create `<name>/system.md` (Purpose + Instructions — static
|
|
147
|
+
identity only) and `<name>/user.md` (one h3 per incoming datum —
|
|
148
|
+
`{{prompt}}`, `{{previous_envelope}}`, `{{context_handoff_dir}}` — then
|
|
149
|
+
the task, then a `## Report` section showing the exact output JSON).
|
|
150
|
+
Copy an existing pair as the shape. If you're overriding in `.spf/`, put
|
|
151
|
+
them under `.spf/prompt_engineering/<name>/`.
|
|
152
|
+
2. **Config entry.** Name, purpose, prompt refs, plus anything that differs
|
|
153
|
+
from `defaults`.
|
|
154
|
+
3. **An output type.** Pick an existing envelope type or add one — see
|
|
155
|
+
`authoring_chains.md`. The user prompt's `## Report` section must show
|
|
156
|
+
exactly that JSON shape.
|
|
157
|
+
|
|
158
|
+
Then name the agent in a chain's `REQUIRED_AGENTS` and call it.
|
|
159
|
+
|
|
160
|
+
## Write permissions — `writes` and `protected_files`
|
|
161
|
+
|
|
162
|
+
`tools` cannot express a safety boundary: `bash` runs anything (including
|
|
163
|
+
`git checkout`, which discards uncommitted work) and `write` reaches any
|
|
164
|
+
path, not only the one report file an agent was granted it for.
|
|
165
|
+
`core/permissions.ts` enforces the real boundary, after the fact, by
|
|
166
|
+
fingerprinting the working tree's change-set before an agent runs and
|
|
167
|
+
comparing it after. A path that was modified beforehand and is clean
|
|
168
|
+
afterward has been **reverted** — a reversion counts as a modification.
|
|
169
|
+
That's what catches the `git checkout` case.
|
|
170
|
+
|
|
171
|
+
```yaml
|
|
172
|
+
defaults:
|
|
173
|
+
protected_files: [.spf/, spf.config.yaml] # the machinery that grades an agent's work
|
|
174
|
+
|
|
175
|
+
agents:
|
|
176
|
+
- name: builder # no `writes` key -> unrestricted, minus protected_files
|
|
177
|
+
- name: scout
|
|
178
|
+
writes: [] # no repo writes; findings still land in context_handoff/
|
|
179
|
+
- name: planner
|
|
180
|
+
writes: [specs/]
|
|
181
|
+
- name: documenter
|
|
182
|
+
writes: [app_docs/, docs/, "**/*.md", "*.md"]
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
A breach is **not** a gate violation — a gate is for work an agent can be
|
|
186
|
+
asked to redo, but a write already happened. Instead: everything the agent
|
|
187
|
+
introduced outside its allowlist is rolled back (tracked files via `git
|
|
188
|
+
checkout --`, untracked files by deletion), anything that was *already*
|
|
189
|
+
dirty before the agent ran is left untouched (the operator's own
|
|
190
|
+
uncommitted work is not this module's to discard), and the phase fails
|
|
191
|
+
naming every path and what happened to it.
|
|
192
|
+
|
|
193
|
+
**The session runtime under `data_dir` is always writable, for every
|
|
194
|
+
agent**, regardless of `writes:`. `writes: []` means read-only with respect
|
|
195
|
+
to the repo, not unable to write its own report.
|
|
196
|
+
|
|
197
|
+
Full field-by-field spec: `references/config.md`.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Run a Chain
|
|
2
|
+
|
|
3
|
+
Run a workflow and report on it. **You run and observe — you never step
|
|
4
|
+
into the process or do the work yourself.**
|
|
5
|
+
|
|
6
|
+
## Step 0 — translate the request
|
|
7
|
+
|
|
8
|
+
**Read [how_to_prompt_for_the_eng.md](how_to_prompt_for_the_eng.md) before
|
|
9
|
+
you launch anything.** The prompt you pass is read by every agent in the
|
|
10
|
+
chain, so it gets written deliberately. This cookbook starts once you have
|
|
11
|
+
that prompt.
|
|
12
|
+
|
|
13
|
+
## Launch
|
|
14
|
+
|
|
15
|
+
`spf list` shows every chain this install knows, its phases, and what it
|
|
16
|
+
requires — the names are shape, not a fixed menu; read the actual output.
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
spf <chain> "<prompt>" # e.g. spf plan-build-test "add a /health endpoint"
|
|
20
|
+
spf <chain> requests/health.md # prompt can be a file path instead of inline text
|
|
21
|
+
spf <chain> "implement the plan" --adw-id a1b2c3d4 # join an existing session
|
|
22
|
+
spf <chain> "..." --config path/to/other.config.yaml # a non-default roster
|
|
23
|
+
spf <chain> "..." --cwd /path/to/other-repo # run against a different repo
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Launch in the background so you can poll while it works. `spf` prints the
|
|
27
|
+
`adw_id` on startup — capture it, everything else keys off it.
|
|
28
|
+
|
|
29
|
+
### Naming a roster
|
|
30
|
+
|
|
31
|
+
The chain says *what runs*; the config says *who runs it*. If the engineer
|
|
32
|
+
references a roster, a config file, or a model tier, pass `--config` — never
|
|
33
|
+
fall through to the default silently. Two things bite:
|
|
34
|
+
|
|
35
|
+
- **Never swap rosters on your own.** A different roster is a different
|
|
36
|
+
cost and a different result. If the default's model looks wrong, say so
|
|
37
|
+
and let the engineer choose.
|
|
38
|
+
- **Switching rosters mid-session breaks resumption.** `agent_map.json`
|
|
39
|
+
records the model each agent's session was created with; a joined run
|
|
40
|
+
whose config now names a different model starts that agent **fresh**
|
|
41
|
+
instead of resuming — deliberate (a bad resume is worse), but it means
|
|
42
|
+
"plan on one roster, build on another" costs the builder its accumulated
|
|
43
|
+
context. Say so when you report it.
|
|
44
|
+
|
|
45
|
+
`--adw-id` is optional on every chain. Given one, the run joins that session
|
|
46
|
+
if it exists, or creates it pinned to exactly that id — same session
|
|
47
|
+
directory, same `context_handoff/`, envelopes appended, each agent resumes
|
|
48
|
+
its own Flue conversation via `agent_map.json`. That's how you chain runs:
|
|
49
|
+
plan under one id, then build under the same id.
|
|
50
|
+
|
|
51
|
+
## Observe
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
spf sessions [--limit N] # recent runs
|
|
55
|
+
spf phases <adw_id> # phase-by-phase status for one run
|
|
56
|
+
spf events <adw_id> [--follow] # the trace, live-tailable
|
|
57
|
+
spf abort <adw_id> # signal a stuck run's process to stop
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`spf events --follow` polls the same rowid cursor the visualizer does — safe
|
|
61
|
+
to leave running. `spf phases` marks each phase ✓/✗/… — remember **every
|
|
62
|
+
phase defaults to fail**, so `✗` may mean it never completed, and `…` means
|
|
63
|
+
still running, not stuck.
|
|
64
|
+
|
|
65
|
+
For a visual view of the same data: `spf ui` — opens a browser, renders
|
|
66
|
+
sessions as cards and runs as swim lanes, phases and tool calls drill in.
|
|
67
|
+
|
|
68
|
+
## When a run is stuck
|
|
69
|
+
|
|
70
|
+
A hung agent produces no events at all, so the trace goes quiet rather than
|
|
71
|
+
red.
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
spf phases <adw_id> # which phase is still "running"
|
|
75
|
+
spf abort <adw_id> # stop it
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`spf abort` sends `SIGTERM` to the OS process running the chain (there's no
|
|
79
|
+
separate per-agent handle to target from a different CLI invocation — Flue
|
|
80
|
+
runs in-process, so stopping the run means stopping that process). A killed
|
|
81
|
+
run finalizes its own trace: it lands on `fail` with its process rows
|
|
82
|
+
closed, never left claiming `running` forever.
|
|
83
|
+
|
|
84
|
+
## Report
|
|
85
|
+
|
|
86
|
+
Tell the engineer, in order: which chain and which roster you launched (name
|
|
87
|
+
the config whenever it wasn't the default), which phase is running now or
|
|
88
|
+
which failed, phase statuses in sequence, and for a failure the gate
|
|
89
|
+
violations or the error verbatim, from `spf phases`/`spf events`. Don't dress
|
|
90
|
+
up a partial run as a success.
|
|
91
|
+
|
|
92
|
+
Full trace schema and what each column means: `references/observability.md`.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# SPF Overview
|
|
2
|
+
|
|
3
|
+
The map, not the procedure — read this once to know how the pieces fit
|
|
4
|
+
before touching any of the other cookbooks.
|
|
5
|
+
|
|
6
|
+
## What SPF is
|
|
7
|
+
|
|
8
|
+
SPF runs **ADWs**: TypeScript chains that own sequencing, retries, and
|
|
9
|
+
acceptance, calling bounded agent phases and deterministic code phases in
|
|
10
|
+
between. "Agent proposes, code disposes" — an agent's response is a claim,
|
|
11
|
+
and code (a gate, a quality check, a permissions check) verifies it before
|
|
12
|
+
the chain moves on.
|
|
13
|
+
|
|
14
|
+
SPF is a global CLI (`npm i -g @gr8ful/spf`), not a template stamped into
|
|
15
|
+
the target repo. It ships:
|
|
16
|
+
|
|
17
|
+
- a packaged **default roster** (`spf.config.yaml`) and **default prompts**,
|
|
18
|
+
usable with zero setup;
|
|
19
|
+
- an optional per-repo **`.spf/` override directory** (`spf init` seeds it) —
|
|
20
|
+
merged on top of the packaged defaults, field by field;
|
|
21
|
+
- an optional per-repo **`.spf/data/`** runtime home for the sqlite trace and
|
|
22
|
+
session files, created on first run, always gitignored.
|
|
23
|
+
|
|
24
|
+
There is no `adws/` tree to stamp, no `bun install`. The default
|
|
25
|
+
coding-agent backend is **Flue** (`@flue/runtime`), an in-process agent
|
|
26
|
+
harness with no subprocess and no separate CLI to have on `PATH`.
|
|
27
|
+
`coding_agent: claude_code` is a second backend — it shells out to your own
|
|
28
|
+
installed `claude` CLI instead, for repos that want to run agents on
|
|
29
|
+
Claude Code (including pointed at a local or cloud Ollama server). See
|
|
30
|
+
`roster.md`.
|
|
31
|
+
|
|
32
|
+
## Layout, once you run something
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
<repo>/
|
|
36
|
+
.spf/ # optional — only if you ran `spf init`
|
|
37
|
+
spf.config.yaml # your overrides, merged over the built-in defaults
|
|
38
|
+
.spf/data/ # runtime — always gitignored
|
|
39
|
+
spf.db # the trace: sessions, phases, events, envelopes, gates
|
|
40
|
+
flue.db # Flue's own conversation store (session resumption)
|
|
41
|
+
sessions/<adw_id>/
|
|
42
|
+
agent_map.json # agent name -> its Flue conversation id + model
|
|
43
|
+
context_handoff/ # the ONE place agents hand files to each other
|
|
44
|
+
<agent_name>/
|
|
45
|
+
prompts/ # exact system.md + user.md sent, saved before the call
|
|
46
|
+
envelope.json # the final parsed, validated response
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`spf doctor` prints exactly what resolved and from where — config paths,
|
|
50
|
+
prompt paths, the trace db path — so "is my override even taking effect" is
|
|
51
|
+
always a one-command answer.
|
|
52
|
+
|
|
53
|
+
## The phase model
|
|
54
|
+
|
|
55
|
+
Three phase kinds, all run through the one primitive, `run.phase(params, fn)`:
|
|
56
|
+
|
|
57
|
+
- **`engineer`** — captures human input; no agent, no gate.
|
|
58
|
+
- **`agent`** — `ph.call({output_type, prompt, gates})`: sends a prompt,
|
|
59
|
+
gets back a typed envelope, verifies it against every declared gate.
|
|
60
|
+
A parse failure or gate violation triggers a bounded same-session
|
|
61
|
+
correction, never a cold restart.
|
|
62
|
+
- **`code`** — deterministic work: running a quality suite, capturing a git
|
|
63
|
+
diff, committing. If you can write down the command, it's `code`, not an
|
|
64
|
+
agent that has to rediscover it every run.
|
|
65
|
+
|
|
66
|
+
Every phase defaults to `fail`; only a clean exit earns `success`. The chain
|
|
67
|
+
calls `run.finish(accepted, reason)` exactly once at the end — separate from
|
|
68
|
+
per-phase success, because a red test suite that *ran correctly* is still a
|
|
69
|
+
successful phase; whether the whole run is *accepted* is a different
|
|
70
|
+
question.
|
|
71
|
+
|
|
72
|
+
## Envelopes
|
|
73
|
+
|
|
74
|
+
Every agent call declares a concrete output type — an envelope schema built
|
|
75
|
+
with Valibot's `envelopeType()`. The agent's final JSON is parsed against
|
|
76
|
+
exactly that type; there are no untyped handoffs. The schema, the JSON shown
|
|
77
|
+
in that agent's `user.md` `## Report` section, and the `output_type:` named
|
|
78
|
+
at the call site are the same fact in three places — the "synced triad."
|
|
79
|
+
Full contract: `references/handoff.md`.
|
|
80
|
+
|
|
81
|
+
## Running a chain
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
spf list # every chain, its phases, what it needs
|
|
85
|
+
spf <chain-name> "<prompt or path/to/prompt.md>" [--config <path>] [--adw-id <id>] [--cwd <dir>]
|
|
86
|
+
spf doctor # confirm the roster/config/quality all resolve first
|
|
87
|
+
spf sessions # recent runs
|
|
88
|
+
spf phases <adw_id> # one run's phase-by-phase status
|
|
89
|
+
spf events <adw_id> --follow # the live trace
|
|
90
|
+
spf ui # the visualizer, browser-based
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`spf <chain-name> ...` and `spf run <chain-name> ...` are identical — the bare
|
|
94
|
+
form exists because naming the chain *is* choosing what runs.
|
|
95
|
+
|
|
96
|
+
## When you've finished reading this
|
|
97
|
+
|
|
98
|
+
Startup discipline still applies: run `spf list`, don't volunteer more state
|
|
99
|
+
than the engineer asked for, and read `how_to_prompt_for_the_eng.md` before
|
|
100
|
+
launching anything.
|
|
101
|
+
|
|
102
|
+
## Where to go next
|
|
103
|
+
|
|
104
|
+
| Need | Go to |
|
|
105
|
+
|---|---|
|
|
106
|
+
| Launch a chain, watch it, report on it | `cookbooks/run_adw.md` (after `how_to_prompt_for_the_eng.md`) |
|
|
107
|
+
| Retune the roster, add an agent | `cookbooks/roster.md` |
|
|
108
|
+
| Add or extend a chain | `cookbooks/authoring_chains.md` |
|
|
109
|
+
| Envelope/gate/session contract | `references/handoff.md` |
|
|
110
|
+
| Trace schema, spend vs. context | `references/observability.md` |
|
|
111
|
+
| Full config field reference | `references/config.md` |
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# Config Reference
|
|
2
|
+
|
|
3
|
+
The full `spf.config.yaml` spec: every field, how defaults merge, and how
|
|
4
|
+
model / thinking / tools map onto each coding-agent backend (Flue by
|
|
5
|
+
default, or Claude Code). The schema is Valibot-enforced —
|
|
6
|
+
`agents.validate()` refuses a bad config before anything spawns — so this is
|
|
7
|
+
semantics and gotchas, not a shape you have to memorize by hand; `spf doctor`
|
|
8
|
+
always shows the resolved, merged result for the repo you're in.
|
|
9
|
+
|
|
10
|
+
## Resolution order
|
|
11
|
+
|
|
12
|
+
1. The packaged built-in default (`assets/defaults/spf.config.yaml` inside
|
|
13
|
+
the installed CLI).
|
|
14
|
+
2. `.spf/spf.config.yaml` in the target repo, if present — merged on top,
|
|
15
|
+
field by field (`defaults`/`observability`/`quality` merge key-by-key;
|
|
16
|
+
`agents` merges by `name`: a matching name patches that entry, a new name
|
|
17
|
+
appends).
|
|
18
|
+
3. An explicit `--config <path>` replaces both — standalone, no built-in
|
|
19
|
+
underneath it.
|
|
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.
|
|
23
|
+
|
|
24
|
+
## Shape
|
|
25
|
+
|
|
26
|
+
```yaml
|
|
27
|
+
defaults:
|
|
28
|
+
coding_agent: flue # the only implemented value
|
|
29
|
+
model: google/gemini-3.6-flash # ALWAYS provider/model-id
|
|
30
|
+
thinking: medium
|
|
31
|
+
tools: [read, bash, edit, write, grep, glob]
|
|
32
|
+
protected_files: [.spf/, spf.config.yaml]
|
|
33
|
+
data_dir: .spf/data
|
|
34
|
+
|
|
35
|
+
observability:
|
|
36
|
+
db: .spf/data/spf.db
|
|
37
|
+
poll_ms: 500
|
|
38
|
+
|
|
39
|
+
quality:
|
|
40
|
+
checks:
|
|
41
|
+
- {name: test, operation: build, argv: ["npm", "test"], timeout_seconds: 600}
|
|
42
|
+
suites:
|
|
43
|
+
test: [test]
|
|
44
|
+
all: [test]
|
|
45
|
+
|
|
46
|
+
agents:
|
|
47
|
+
- name: planner
|
|
48
|
+
model: google/gemini-3.6-flash
|
|
49
|
+
thinking: high
|
|
50
|
+
color: "#a78bfa"
|
|
51
|
+
purpose: Turn a request into a plan the builder can implement without asking questions.
|
|
52
|
+
prompt_engineering:
|
|
53
|
+
system: planner/system.md
|
|
54
|
+
user: planner/user.md
|
|
55
|
+
writes: [specs/]
|
|
56
|
+
tools: [read, grep, glob, bash, write]
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Fields
|
|
60
|
+
|
|
61
|
+
### `defaults`
|
|
62
|
+
|
|
63
|
+
| Field | Type | Meaning |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| `coding_agent` | `"flue"` \| `"claude_code"` | Which backend runs the agent. Default `flue`. `claude_code` shells out to your own installed `claude` CLI — `spf doctor` checks it's on `PATH`. |
|
|
66
|
+
| `model` | string | Vocabulary depends on `coding_agent`: Flue wants `provider/model-id`; Claude Code wants its own bare alias/full name (`sonnet`, `claude-sonnet-5`, ...). Default `google/gemini-3.6-flash`. |
|
|
67
|
+
| `thinking` | enum | `off\|minimal\|low\|medium\|high\|xhigh\|max`. Default `medium`. On a `claude_code` agent this maps to `--effort` (`off`/`minimal` both floor to Claude Code's own minimum — it has no true "disabled" level for a headless run). |
|
|
68
|
+
| `color` | hex string | Lane color fallback for agents that don't set their own. |
|
|
69
|
+
| `harness_engineering` | string[] | **Must stay `[]`** — no analogue on any current backend; a non-empty entry fails validate(). |
|
|
70
|
+
| `tools` | string[] \| null | Roster-wide allowlist. Unset/null = every built-in tool usable. |
|
|
71
|
+
| `protected_files` | string[] | Paths no agent may touch unless named in its own `writes`. Default `[".spf/", "spf.config.yaml"]`. |
|
|
72
|
+
| `data_dir` | path | Runtime home, repo-relative. Default `.spf/data`. |
|
|
73
|
+
|
|
74
|
+
### `observability`
|
|
75
|
+
|
|
76
|
+
| Field | Type | Meaning |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `db` | path | The trace sqlite db. Default `.spf/data/spf.db`. |
|
|
79
|
+
| `poll_ms` | int | UI live-poll cadence. Default `500`. |
|
|
80
|
+
|
|
81
|
+
### `quality`
|
|
82
|
+
|
|
83
|
+
`checks[]`: `{name, area: "frontend"|"backend", operation: "lint"|"typecheck"|"build", argv: string[], timeout_seconds}`.
|
|
84
|
+
`suites`: `{suite_name: [check_name, ...]}`. No packaged default suite exists
|
|
85
|
+
on purpose — a chain that needs one and finds it unconfigured fails loudly
|
|
86
|
+
at `agents.validate()` time, not with a placeholder green. Adding a check
|
|
87
|
+
whose result is really a test run, not a lint/typecheck/build, still needs
|
|
88
|
+
one of those three `operation` values — it only affects how the trace/UI
|
|
89
|
+
labels the check, never what actually runs — `build` is the closest fit.
|
|
90
|
+
|
|
91
|
+
```yaml
|
|
92
|
+
quality:
|
|
93
|
+
checks:
|
|
94
|
+
- { name: typecheck, operation: typecheck, argv: ["npm", "run", "typecheck"], timeout_seconds: 60 }
|
|
95
|
+
- { name: build, operation: build, argv: ["npm", "run", "build"], timeout_seconds: 300 }
|
|
96
|
+
- { name: test, operation: build, argv: ["npm", "test"], timeout_seconds: 300 }
|
|
97
|
+
suites:
|
|
98
|
+
test: [test] # what build-test/plan-build-test's fix loop runs
|
|
99
|
+
all: [typecheck, build, test] # what plan-build-test-quality/quality run
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
A full worked example, including the `defaults.coding_agent`/`watch:`
|
|
103
|
+
sections: `docs/examples/node-typescript.spf.config.yaml` in the spf
|
|
104
|
+
repo (or wherever this skill's own package is installed from).
|
|
105
|
+
|
|
106
|
+
### `agents[]`
|
|
107
|
+
|
|
108
|
+
| Field | Required | Meaning |
|
|
109
|
+
|---|---|---|
|
|
110
|
+
| `name` | yes | The identifier chains use. Chains name agents, never models. |
|
|
111
|
+
| `prompt_engineering.system` / `.user` | yes | Paths to the two prompt files. Resolved against the repo root, then `.spf/`, then `.spf/prompt_engineering/`, then the packaged assets — first hit wins; a total miss throws listing every path tried. |
|
|
112
|
+
| `purpose` | no | One sentence; should match the system prompt's stated purpose. |
|
|
113
|
+
| `coding_agent`, `model`, `thinking`, `color`, `harness_engineering` | no | Override the matching `defaults` key. |
|
|
114
|
+
| `tools` | no | Allowlist. Omitting it means all tools usable. A capability list, not a boundary — see `writes`. |
|
|
115
|
+
| `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). |
|
|
116
|
+
|
|
117
|
+
Output types are deliberately absent from config: an entry defines who an
|
|
118
|
+
agent *is*; the call site defines how it's *used*.
|
|
119
|
+
|
|
120
|
+
## Model resolution
|
|
121
|
+
|
|
122
|
+
**For `coding_agent: flue` (the default):** always write `model` as
|
|
123
|
+
`provider/model-id`. There is no live catalog to validate against (Flue has
|
|
124
|
+
no public model-registry API) — `agents.ts` checks only the static shape at
|
|
125
|
+
`agents.validate()` time; a genuinely wrong provider or id surfaces at the
|
|
126
|
+
first real dispatch instead. Provider credentials come from the
|
|
127
|
+
environment, matching the provider you named (`GEMINI_API_KEY`/
|
|
128
|
+
`GOOGLE_API_KEY` for `google/...`, `ANTHROPIC_API_KEY` for `anthropic/...`,
|
|
129
|
+
etc.) — `spf doctor` checks the common ones are set.
|
|
130
|
+
|
|
131
|
+
**For `coding_agent: claude_code`:** write `model` in Claude Code's own
|
|
132
|
+
vocabulary — a bare alias (`sonnet`, `opus`) or a full model name — never
|
|
133
|
+
`provider/model-id`. `agents.validate()` only checks it's non-empty; a
|
|
134
|
+
genuinely wrong name surfaces at the first real dispatch, same as Flue.
|
|
135
|
+
Credentials come from whatever the `claude` CLI itself is authenticated
|
|
136
|
+
with (`ANTHROPIC_API_KEY`, or its own `claude login` flow) — `spf doctor`
|
|
137
|
+
treats a missing key as informational for this backend, not a failure.
|
|
138
|
+
|
|
139
|
+
The resolved model is recorded per agent in `agent_map.json`, mirrored in
|
|
140
|
+
the `agent_sessions` table. **Changing an agent's model invalidates its
|
|
141
|
+
session** — a joined run starts that agent fresh instead of resuming.
|
|
142
|
+
|
|
143
|
+
## Tools
|
|
144
|
+
|
|
145
|
+
| Tool | Purpose |
|
|
146
|
+
|---|---|
|
|
147
|
+
| `read` | read file contents |
|
|
148
|
+
| `bash` | execute bash commands |
|
|
149
|
+
| `edit` | find/replace edits |
|
|
150
|
+
| `write` | create/overwrite files |
|
|
151
|
+
| `grep` | search file contents |
|
|
152
|
+
| `glob` (alias: `find`) | find files by pattern |
|
|
153
|
+
| `ls` | recognized name, **no built-in on either backend** — harmless to list, never mounts |
|
|
154
|
+
|
|
155
|
+
These names are canonical across backends — a roster entry never says
|
|
156
|
+
which; each backend module (`agent_flue.ts`, `agent_cc.ts`) maps them to
|
|
157
|
+
its own tool vocabulary (Flue's lowercase functions, Claude Code's
|
|
158
|
+
capitalized `Read`/`Bash`/...).
|
|
159
|
+
|
|
160
|
+
**Resolution order:** an agent's own `tools` wins → else `defaults.tools` →
|
|
161
|
+
else unset (all tools usable). An empty list is a tool-less agent, and it
|
|
162
|
+
will stall — not "all tools."
|
|
163
|
+
|
|
164
|
+
## Write permissions — `writes` and `protected_files`
|
|
165
|
+
|
|
166
|
+
Full mechanism and rationale: `roster.md`'s "Write permissions" section.
|
|
167
|
+
Short version: `permissions.ts` fingerprints the working tree's diff before
|
|
168
|
+
and after an agent runs; anything outside its allowlist is rolled back
|
|
169
|
+
(reverting a previously-clean file counts as a change; a file that was
|
|
170
|
+
already dirty is left alone) and the phase fails, naming every path. This is
|
|
171
|
+
not a gate — it's checked in code, not by asking the model to redo work.
|
|
172
|
+
|
|
173
|
+
## Harness engineering — no longer applicable
|
|
174
|
+
|
|
175
|
+
Kept as a field for config-shape stability, but it must be empty. Each
|
|
176
|
+
backend's own equivalent (Flue: extra tools/subagents via `useTool()`/
|
|
177
|
+
`useSandbox()`; Claude Code: an MCP server or plugin) is an engine-level
|
|
178
|
+
change made inside that backend's own module, not config — see
|
|
179
|
+
`authoring_chains.md`.
|
|
180
|
+
|
|
181
|
+
## Pointing `claude_code` at Ollama
|
|
182
|
+
|
|
183
|
+
No config section for this — it's an environment-variable recipe, since
|
|
184
|
+
`agent_cc.ts` passes the operator's environment straight through to the
|
|
185
|
+
`claude` subprocess, exactly like every other env var. Set
|
|
186
|
+
`ANTHROPIC_BASE_URL`/`ANTHROPIC_AUTH_TOKEN` (local or cloud Ollama) before
|
|
187
|
+
running `spf`; see `roster.md`'s "Coding agent backends" section for the
|
|
188
|
+
exact commands.
|