@gr8ful/spf 0.1.5 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -12
- package/assets/skill/references/config.md +3 -2
- package/assets/templates/ts-cc.spf.config.yaml +43 -0
- package/assets/templates/ts-flue-openrouter.spf.config.yaml +38 -0
- package/assets/templates/ts.spf.config.yaml +51 -0
- package/dist/cli/commands/doctor.js +20 -4
- package/dist/cli/commands/init.js +39 -10
- package/dist/cli/commands/watch.js +63 -20
- package/dist/cli/index.js +1 -1
- package/dist/core/agent_cc.d.ts +8 -0
- package/dist/core/agent_cc.js +12 -1
- package/dist/core/console.d.ts +1 -0
- package/dist/core/console.js +1 -1
- package/dist/core/data_types.d.ts +45 -10
- package/dist/core/data_types.js +22 -8
- package/dist/core/issues/bitbucket_provider.d.ts +35 -0
- package/dist/core/issues/bitbucket_provider.js +70 -0
- package/dist/core/issues/github_provider.d.ts +7 -6
- package/dist/core/issues/github_provider.js +15 -20
- package/dist/core/issues/jira_provider.d.ts +63 -0
- package/dist/core/issues/jira_provider.js +145 -0
- package/dist/core/issues/provider.d.ts +37 -14
- package/dist/core/issues/provider.js +12 -4
- package/dist/core/paths.d.ts +2 -0
- package/dist/core/paths.js +2 -0
- package/dist/core/watch.d.ts +3 -2
- package/dist/core/watch.js +32 -24
- package/dist/test/watch.test.js +82 -66
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,11 +28,12 @@ spf scout "describe this repo" # a real, read-only run, no setup required
|
|
|
28
28
|
### Customizing a repo
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
|
-
spf init
|
|
32
|
-
spf
|
|
31
|
+
spf init # seed .spf/spf.config.yaml — override only what you want to change
|
|
32
|
+
spf init --template ts-cc # or start from a packaged, ready-to-run template instead
|
|
33
|
+
spf list # every chain this install knows, its phases, what it needs
|
|
33
34
|
```
|
|
34
35
|
|
|
35
|
-
`spf init` writes a small starter `.spf/spf.config.yaml`, commented, that merges on top of the packaged built-ins field by field — override one model, one prompt, one quality check, and everything else stays inherited. Nothing here needs to exist for `spf` to run; it's how you make one repo's roster diverge from the defaults.
|
|
36
|
+
`spf init` writes a small starter `.spf/spf.config.yaml`, commented, that merges on top of the packaged built-ins field by field — override one model, one prompt, one quality check, and everything else stays inherited. `--template <name>` writes a real, filled-in config instead of the commented-out starter — every packaged template's name prints after `spf init` runs, and the same files live in [`assets/templates/`](assets/templates/) to browse directly. Nothing here needs to exist for `spf` to run; it's how you make one repo's roster diverge from the defaults.
|
|
36
37
|
|
|
37
38
|
### Local development
|
|
38
39
|
|
|
@@ -116,6 +117,24 @@ Config defines who an agent **is**. The chain call site defines how it is **used
|
|
|
116
117
|
|
|
117
118
|
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.
|
|
118
119
|
|
|
120
|
+
#### Proxy or wrapper launchers
|
|
121
|
+
|
|
122
|
+
To route the `claude` command through a wrapper, proxy server, or launcher (e.g., [Ollama](https://ollama.com)), set the `SPF_CLAUDE_CMD` environment variable before running `spf`. Space-separated command chains are supported:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
# Route through Ollama's launcher
|
|
126
|
+
export SPF_CLAUDE_CMD="ollama launch claude"
|
|
127
|
+
spf build "your prompt"
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
# Or use a custom wrapper script
|
|
132
|
+
export SPF_CLAUDE_CMD=/path/to/my-wrapper
|
|
133
|
+
spf build "your prompt"
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
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).
|
|
137
|
+
|
|
119
138
|
---
|
|
120
139
|
|
|
121
140
|
## Phases: three lanes, one primitive
|
|
@@ -219,11 +238,15 @@ spf build-test "implement the plan" --adw-id a1b2c3d4
|
|
|
219
238
|
|
|
220
239
|
## `spf watch`
|
|
221
240
|
|
|
222
|
-
Polls
|
|
241
|
+
Polls an issue tracker for issues labeled `<prefix>:ready`, runs a configured chain against each in its own git worktree, opens a PR against a code host, and tracks it through to merged or blocked — driving the same chains above rather than reimplementing an SDLC. Labels are the whole state machine: `ready → working → review → done`/`blocked`.
|
|
242
|
+
|
|
243
|
+
The tracker (`issue_provider`) and the code host (`code_host`) are independent config choices, not one bundled "provider" — a tracker and a host are independent choices in practice (Jira issues against a Bitbucket repo is a real setup). Supported today: `issue_provider: github | jira`, `code_host: github | bitbucket` — any combination works, including Jira+GitHub or GitHub-issues+Bitbucket.
|
|
223
244
|
|
|
224
245
|
```yaml
|
|
225
|
-
# .spf/spf.config.yaml
|
|
246
|
+
# .spf/spf.config.yaml — GitHub issues + GitHub PRs (the default)
|
|
226
247
|
watch:
|
|
248
|
+
issue_provider: github # default
|
|
249
|
+
code_host: github # default
|
|
227
250
|
repo: owner/name
|
|
228
251
|
label_prefix: spf # polls issues labeled spf:ready
|
|
229
252
|
chain: plan-build-test # any registered chain
|
|
@@ -232,31 +255,68 @@ watch:
|
|
|
232
255
|
concurrency: 2
|
|
233
256
|
```
|
|
234
257
|
|
|
258
|
+
```yaml
|
|
259
|
+
# .spf/spf.config.yaml — Jira issues + Bitbucket PRs
|
|
260
|
+
watch:
|
|
261
|
+
issue_provider: jira
|
|
262
|
+
code_host: bitbucket
|
|
263
|
+
repo: workspace/repo_slug # Bitbucket's own two-part identifier
|
|
264
|
+
label_prefix: spf # polls Jira issues labeled spf:ready
|
|
265
|
+
chain: plan-build-test
|
|
266
|
+
base_branch: main
|
|
267
|
+
jira:
|
|
268
|
+
base_url: https://your-domain.atlassian.net
|
|
269
|
+
project_key: PROJ
|
|
270
|
+
```
|
|
271
|
+
|
|
235
272
|
```bash
|
|
236
|
-
|
|
237
|
-
spf watch init # idempotently create/update the 5 labels below — run this first
|
|
273
|
+
spf watch init # idempotently seed tracker state (no-op for Jira — see below); run this first
|
|
238
274
|
spf watch # foreground daemon; Ctrl-C drains in-flight claims first
|
|
239
275
|
spf watch --once # one poll tick, then exit — good for cron
|
|
240
276
|
spf watch --dry-run # log intended claims/transitions, mutate nothing
|
|
241
277
|
```
|
|
242
278
|
|
|
243
|
-
|
|
279
|
+
No GitHub App, no webhook, no Jira/Bitbucket app install — it's a plain REST poll against whichever combination is configured, same philosophy as the trace db's own polling contract. See [`assets/templates/`](assets/templates/) for full worked configs (also usable directly via `spf init --template <name>`), and `spf install-skill`'s installed skill (`roster.md`, `references/config.md`) for the field-by-field reference.
|
|
244
280
|
|
|
245
|
-
|
|
281
|
+
### GitHub (`issue_provider: github` and/or `code_host: github`)
|
|
246
282
|
|
|
247
|
-
|
|
283
|
+
```bash
|
|
284
|
+
export GITHUB_TOKEN=... # classic PAT; spf doctor checks it's set
|
|
285
|
+
```
|
|
248
286
|
|
|
249
|
-
|
|
287
|
+
`spf watch init` seeds `<prefix>:ready`/`working`/`review`/`done`/`blocked` labels with a color and description each — safe to re-run any time (creates what's missing, corrects any that drifted, leaves the rest alone).
|
|
288
|
+
|
|
289
|
+
A **classic** PAT (fine-grained tokens use different permission names — not covered here), scoped to the minimum that covers every call `spf watch`/`spf watch init` makes on GitHub: creating/editing labels, reading and labeling issues, posting comments, opening PRs, and reading PR/check-run status.
|
|
250
290
|
|
|
251
291
|
| Target repo | Scope | Covers |
|
|
252
292
|
|---|---|---|
|
|
253
293
|
| Private | `repo` | Everything above, full read/write |
|
|
254
294
|
| Public only | `public_repo` | The same, restricted to public repos |
|
|
255
295
|
|
|
256
|
-
**Do not grant the `project` scope.** It's a separate, unrelated permission for GitHub Projects (classic/org/user boards) — `spf watch` doesn't touch Projects at all
|
|
296
|
+
**Do not grant the `project` scope.** It's a separate, unrelated permission for GitHub Projects (classic/org/user boards) — `spf watch` doesn't touch Projects at all, so granting it would just be more access than this tool ever uses.
|
|
257
297
|
|
|
258
298
|
There's no dedicated "issues" or "pull requests" scope on classic PATs — GitHub bundles both into `repo`/`public_repo`, which is why that's the whole table.
|
|
259
299
|
|
|
300
|
+
### Jira (`issue_provider: jira`)
|
|
301
|
+
|
|
302
|
+
```bash
|
|
303
|
+
export JIRA_EMAIL=you@example.com
|
|
304
|
+
export JIRA_API_TOKEN=... # id.atlassian.com -> Security -> API tokens
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
State is modeled as Jira **labels** (`<prefix>:ready`, etc.), mirroring GitHub exactly, rather than native workflow status transitions — the latter would need per-project transition-id mapping, since workflows vary by project/scheme; labels work identically everywhere with zero per-project setup. One caveat: colons are a legal Jira label character and JQL matches on them fine, but they won't show up in Jira's own label autocomplete UI — cosmetic only.
|
|
308
|
+
|
|
309
|
+
`spf watch init` is a no-op here (Jira labels are freeform strings with no color/description registry to seed, unlike GitHub's) — it just reports the labels this run will use.
|
|
310
|
+
|
|
311
|
+
### Bitbucket (`code_host: bitbucket`)
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
export BITBUCKET_EMAIL=you@example.com
|
|
315
|
+
export BITBUCKET_API_TOKEN=... # same Atlassian API token mechanism as Jira above
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
**Bitbucket Cloud app passwords are being fully removed** (brownout window closing July 28, 2026) — this project only supports the replacement, API tokens, which need the account's email alongside the token (username alone no longer works).
|
|
319
|
+
|
|
260
320
|
## What's in this repo
|
|
261
321
|
|
|
262
322
|
```
|
|
@@ -100,8 +100,9 @@ quality:
|
|
|
100
100
|
```
|
|
101
101
|
|
|
102
102
|
A full worked example, including the `defaults.coding_agent`/`watch:`
|
|
103
|
-
sections: `
|
|
104
|
-
|
|
103
|
+
sections: `assets/templates/ts.spf.config.yaml` in the spf package (or
|
|
104
|
+
`spf init --template ts` to write it straight into `.spf/spf.config.yaml`).
|
|
105
|
+
`spf init` with no `--template` prints every packaged template's name.
|
|
105
106
|
|
|
106
107
|
### `agents[]`
|
|
107
108
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# .spf/spf.config.yaml — Claude Code backend, pointed at Ollama (local or
|
|
2
|
+
# cloud) instead of Anthropic's API. `spf init --template ts-cc` writes
|
|
3
|
+
# this file as-is.
|
|
4
|
+
#
|
|
5
|
+
# No spf-side config makes the Ollama redirect happen: `coding_agent:
|
|
6
|
+
# claude_code` shells out to your own installed `claude` CLI, and Claude
|
|
7
|
+
# Code's CLI itself reads ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN to
|
|
8
|
+
# decide where to send requests. agent_cc.ts never special-cases this — it
|
|
9
|
+
# just passes your shell's environment through unmodified to the `claude`
|
|
10
|
+
# subprocess, the same way every other env var reaches an agent.
|
|
11
|
+
#
|
|
12
|
+
# Local Ollama, before running spf:
|
|
13
|
+
# ollama serve # if not already running
|
|
14
|
+
# ollama pull qwen3-coder:30b # or whatever model you want
|
|
15
|
+
# export ANTHROPIC_BASE_URL=http://localhost:11434
|
|
16
|
+
# export ANTHROPIC_AUTH_TOKEN=ollama # any non-empty value — local Ollama doesn't check it
|
|
17
|
+
#
|
|
18
|
+
# Ollama Cloud instead of a local server:
|
|
19
|
+
# export ANTHROPIC_BASE_URL=<your Ollama Cloud endpoint — see docs.ollama.com>
|
|
20
|
+
# export ANTHROPIC_AUTH_TOKEN=<a real Ollama Cloud API key>
|
|
21
|
+
#
|
|
22
|
+
# `spf doctor` checks that `claude` is on PATH and treats a missing
|
|
23
|
+
# ANTHROPIC_API_KEY as informational (claude login is a valid alternative)
|
|
24
|
+
# — it does NOT know about this redirect, so it won't tell you whether
|
|
25
|
+
# ANTHROPIC_BASE_URL/ANTHROPIC_AUTH_TOKEN are actually set. Confirm those
|
|
26
|
+
# yourself before a real run.
|
|
27
|
+
#
|
|
28
|
+
# `model:` becomes whatever model name your Ollama server actually serves
|
|
29
|
+
# once redirected — not one of Claude Code's own aliases (sonnet/opus/...)
|
|
30
|
+
# and not provider/model-id.
|
|
31
|
+
quality:
|
|
32
|
+
checks:
|
|
33
|
+
- { name: typecheck, operation: typecheck, argv: ["npm", "run", "typecheck"], timeout_seconds: 60 }
|
|
34
|
+
- { name: lint, operation: lint, argv: ["npm", "run", "lint"], timeout_seconds: 60 }
|
|
35
|
+
- { name: build, operation: build, argv: ["npm", "run", "build"], timeout_seconds: 300 }
|
|
36
|
+
- { name: test, operation: build, argv: ["npm", "test"], timeout_seconds: 300 }
|
|
37
|
+
suites:
|
|
38
|
+
test: [test]
|
|
39
|
+
all: [typecheck, lint, build, test]
|
|
40
|
+
|
|
41
|
+
defaults:
|
|
42
|
+
coding_agent: claude_code
|
|
43
|
+
model: qwen3-coder:30b # swap for whatever `ollama list` shows on your machine
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# .spf/spf.config.yaml — Flue backend (the default), routed through
|
|
2
|
+
# OpenRouter to a Kimi model. `spf init --template ts-flue-openrouter`
|
|
3
|
+
# writes this file as-is.
|
|
4
|
+
#
|
|
5
|
+
# model: is ALWAYS provider/model-id for Flue — the string's first segment
|
|
6
|
+
# names the provider, and that segment alone decides which env var gets
|
|
7
|
+
# read for credentials. This resolution belongs to Flue's own underlying
|
|
8
|
+
# `pi-ai` library, not to spf: agents.validate() only checks the STRING
|
|
9
|
+
# SHAPE (does it look like provider/id, via agent_flue.ts's resolveModel())
|
|
10
|
+
# — never that the provider is real or that its key is actually set. A
|
|
11
|
+
# wrong provider name or a missing key only surfaces at the first real
|
|
12
|
+
# dispatch, not at validate() time.
|
|
13
|
+
#
|
|
14
|
+
# `spf doctor` has its own small, hand-maintained table of common
|
|
15
|
+
# providers' env-var conventions (src/cli/commands/doctor.ts) for an
|
|
16
|
+
# informational check — pi-ai's real internal provider table isn't
|
|
17
|
+
# exported for spf to read, so this is best-effort, not authoritative.
|
|
18
|
+
# openrouter's own entry in that table: OPENROUTER_API_KEY.
|
|
19
|
+
#
|
|
20
|
+
# Before running spf:
|
|
21
|
+
# export OPENROUTER_API_KEY=sk-or-...
|
|
22
|
+
quality:
|
|
23
|
+
checks:
|
|
24
|
+
- { name: typecheck, operation: typecheck, argv: ["npm", "run", "typecheck"], timeout_seconds: 60 }
|
|
25
|
+
- { name: lint, operation: lint, argv: ["npm", "run", "lint"], timeout_seconds: 60 }
|
|
26
|
+
- { name: build, operation: build, argv: ["npm", "run", "build"], timeout_seconds: 300 }
|
|
27
|
+
- { name: test, operation: build, argv: ["npm", "test"], timeout_seconds: 300 }
|
|
28
|
+
suites:
|
|
29
|
+
test: [test]
|
|
30
|
+
all: [typecheck, lint, build, test]
|
|
31
|
+
|
|
32
|
+
defaults:
|
|
33
|
+
coding_agent: flue
|
|
34
|
+
# Check openrouter.ai/models for the exact current id — "kimi-k2.7" here
|
|
35
|
+
# matches this project's existing fictional-model-version convention
|
|
36
|
+
# (kimi-k3, gemini-3.6, gpt-5.6, claude-sonnet-5, ...), not a live catalog
|
|
37
|
+
# entry verified against a real API.
|
|
38
|
+
model: openrouter/moonshotai/kimi-k2.7
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# .spf/spf.config.yaml for a Node/TypeScript project — `spf init --template
|
|
2
|
+
# ts` writes this file as-is; `spf init` with no --template seeds a starter
|
|
3
|
+
# with the same shape, commented out. This fills it in with real commands
|
|
4
|
+
# so `plan-build-test`/`plan-build-test-quality`/`quality`/`simple-sdlc` —
|
|
5
|
+
# every chain with a REQUIRED_SUITES entry — actually have something to run
|
|
6
|
+
# instead of failing loudly at validate() time.
|
|
7
|
+
#
|
|
8
|
+
# Adjust the argv to your own project: swap `npm` for `pnpm`/`yarn`, point
|
|
9
|
+
# `build`/`typecheck` at whatever scripts you already have in package.json.
|
|
10
|
+
quality:
|
|
11
|
+
checks:
|
|
12
|
+
# `operation` classifies the check for the trace/UI (lint | typecheck |
|
|
13
|
+
# build) — it doesn't have to literally match the argv. Running a test
|
|
14
|
+
# suite doesn't have its own operation value, so `build` is the closest
|
|
15
|
+
# fit; it doesn't change what actually runs.
|
|
16
|
+
- { name: typecheck, operation: typecheck, argv: ["npm", "run", "typecheck"], timeout_seconds: 60 }
|
|
17
|
+
- { name: lint, operation: lint, argv: ["npm", "run", "lint"], timeout_seconds: 60 }
|
|
18
|
+
- { name: build, operation: build, argv: ["npm", "run", "build"], timeout_seconds: 300 }
|
|
19
|
+
- { name: test, operation: build, argv: ["npm", "test"], timeout_seconds: 300 }
|
|
20
|
+
suites:
|
|
21
|
+
# `test:` is what build-test/plan-build-test/simple-sdlc's bounded fix
|
|
22
|
+
# loop runs after every build. Keep it fast — it runs on every attempt.
|
|
23
|
+
test: [test]
|
|
24
|
+
# `all:` is what plan-build-test-quality/quality run — the full bar.
|
|
25
|
+
all: [typecheck, lint, build, test]
|
|
26
|
+
|
|
27
|
+
# Optional: run agents on Claude Code instead of the default Flue backend.
|
|
28
|
+
# Needs the `claude` CLI installed and authenticated (`spf doctor` checks) —
|
|
29
|
+
# no separate provider API key required if you're logged in via `claude
|
|
30
|
+
# login`. Remove this whole block to stay on Flue with a `provider/model-id`
|
|
31
|
+
# roster instead.
|
|
32
|
+
defaults:
|
|
33
|
+
coding_agent: claude_code
|
|
34
|
+
model: sonnet # claude_code's own alias — NOT provider/model-id
|
|
35
|
+
|
|
36
|
+
# Optional: retune one agent without touching the rest of the roster.
|
|
37
|
+
# agents:
|
|
38
|
+
# - name: builder
|
|
39
|
+
# thinking: high
|
|
40
|
+
# writes: [src/, tests/] # narrow what this agent may change in the repo
|
|
41
|
+
|
|
42
|
+
# Optional: poll an issue tracker's `spf:ready`-labeled issues and run a
|
|
43
|
+
# chain against each — see the main README's "spf watch" section for every
|
|
44
|
+
# issue_provider x code_host combination's env vars and required scopes
|
|
45
|
+
# (GitHub, or Jira issues against a Bitbucket repo, or any mix).
|
|
46
|
+
# Needs a GITHUB_TOKEN env var (classic PAT, repo scope) for the defaults below.
|
|
47
|
+
# watch:
|
|
48
|
+
# repo: owner/name
|
|
49
|
+
# label_prefix: spf
|
|
50
|
+
# chain: plan-build-test
|
|
51
|
+
# base_branch: main
|
|
@@ -143,10 +143,26 @@ export function doctorCommand(argv) {
|
|
|
143
143
|
check(report, `${envFile}`, true, existsSync(p) ? `present (${statSync(p).size} bytes)` : "absent — fine if no provider needs a key from it");
|
|
144
144
|
}
|
|
145
145
|
if (cfg.watch.repo.trim()) {
|
|
146
|
-
check(report, "watch.
|
|
147
|
-
check(report, "
|
|
148
|
-
|
|
149
|
-
|
|
146
|
+
check(report, "watch.issue_provider", true, cfg.watch.issue_provider);
|
|
147
|
+
check(report, "watch.code_host", true, cfg.watch.code_host);
|
|
148
|
+
if (cfg.watch.issue_provider === "github" || cfg.watch.code_host === "github") {
|
|
149
|
+
check(report, "GITHUB_TOKEN", Boolean(process.env["GITHUB_TOKEN"]), process.env["GITHUB_TOKEN"]
|
|
150
|
+
? "set"
|
|
151
|
+
: 'not set — spf watch needs a classic PAT with "repo" scope (or "public_repo" for a public-only repo); see README.md\'s "GITHUB_TOKEN scope" section');
|
|
152
|
+
}
|
|
153
|
+
if (cfg.watch.issue_provider === "jira") {
|
|
154
|
+
check(report, "watch.jira", Boolean(cfg.watch.jira.base_url.trim() && cfg.watch.jira.project_key.trim()), cfg.watch.jira.base_url.trim() && cfg.watch.jira.project_key.trim()
|
|
155
|
+
? `${cfg.watch.jira.base_url} (${cfg.watch.jira.project_key})`
|
|
156
|
+
: "watch.jira.base_url and watch.jira.project_key must both be set");
|
|
157
|
+
check(report, "JIRA_EMAIL / JIRA_API_TOKEN", Boolean(process.env["JIRA_EMAIL"] && process.env["JIRA_API_TOKEN"]), process.env["JIRA_EMAIL"] && process.env["JIRA_API_TOKEN"]
|
|
158
|
+
? "set"
|
|
159
|
+
: 'not set — spf watch needs an Atlassian account email plus an API token (id.atlassian.com -> Security -> API tokens); see README.md\'s "spf watch" section');
|
|
160
|
+
}
|
|
161
|
+
if (cfg.watch.code_host === "bitbucket") {
|
|
162
|
+
check(report, "BITBUCKET_EMAIL / BITBUCKET_API_TOKEN", Boolean(process.env["BITBUCKET_EMAIL"] && process.env["BITBUCKET_API_TOKEN"]), process.env["BITBUCKET_EMAIL"] && process.env["BITBUCKET_API_TOKEN"]
|
|
163
|
+
? "set"
|
|
164
|
+
: 'not set — Bitbucket app passwords are being removed; spf watch needs an Atlassian account email plus an API token instead; see README.md\'s "spf watch" section');
|
|
165
|
+
}
|
|
150
166
|
check(report, "watch.chain", Boolean(findChain(cfg.watch.chain)), findChain(cfg.watch.chain) ? cfg.watch.chain : `"${cfg.watch.chain}" is not a registered chain`);
|
|
151
167
|
}
|
|
152
168
|
return finish(report, flags["json"]);
|
|
@@ -1,9 +1,28 @@
|
|
|
1
1
|
/** `spf init` — seed a `.spf/` override directory. Everything else is inherited from the packaged defaults. */
|
|
2
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import * as paths from "../../core/paths.js";
|
|
5
5
|
import { ensureGitignore } from "../gitignore.js";
|
|
6
6
|
import { parseCli } from "../../core/utils.js";
|
|
7
|
+
import { paint } from "../../core/console.js";
|
|
8
|
+
const TEMPLATE_SUFFIX = ".spf.config.yaml";
|
|
9
|
+
/** Every template's short name (e.g. "ts-cc"), derived from disk rather than hand-maintained — never drifts from what's actually packaged. */
|
|
10
|
+
function listTemplates() {
|
|
11
|
+
if (!existsSync(paths.TEMPLATES_DIR))
|
|
12
|
+
return [];
|
|
13
|
+
return readdirSync(paths.TEMPLATES_DIR)
|
|
14
|
+
.filter((f) => f.endsWith(TEMPLATE_SUFFIX))
|
|
15
|
+
.map((f) => f.slice(0, -TEMPLATE_SUFFIX.length))
|
|
16
|
+
.sort();
|
|
17
|
+
}
|
|
18
|
+
function loadTemplate(name) {
|
|
19
|
+
const templatePath = path.join(paths.TEMPLATES_DIR, `${name}${TEMPLATE_SUFFIX}`);
|
|
20
|
+
if (!existsSync(templatePath)) {
|
|
21
|
+
const available = listTemplates();
|
|
22
|
+
throw new Error(`unknown template ${JSON.stringify(name)} — available: ${available.length > 0 ? available.join(", ") : "(none packaged)"}`);
|
|
23
|
+
}
|
|
24
|
+
return readFileSync(templatePath, "utf-8");
|
|
25
|
+
}
|
|
7
26
|
const STARTER_CONFIG = `# .spf/spf.config.yaml — merged ON TOP of spf's packaged built-in defaults.
|
|
8
27
|
# List only what you want to CHANGE; everything else (the roster, prompts,
|
|
9
28
|
# models) is inherited. Run \`spf doctor\` any time to see what's actually in
|
|
@@ -26,14 +45,17 @@ const STARTER_CONFIG = `# .spf/spf.config.yaml — merged ON TOP of spf's packag
|
|
|
26
45
|
# coding_agent: claude_code # run this agent on Claude Code instead of Flue
|
|
27
46
|
# model: sonnet # claude_code's own alias, NOT provider/model-id
|
|
28
47
|
|
|
29
|
-
# Uncomment to enable \`spf watch\` — polls
|
|
48
|
+
# Uncomment to enable \`spf watch\` — polls an issue tracker labeled
|
|
30
49
|
# <label_prefix>:ready and runs \`chain\` against each in its own worktree.
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
50
|
+
# issue_provider and code_host default to "github" and are independent —
|
|
51
|
+
# a Jira tracker against a Bitbucket repo is just issue_provider: jira +
|
|
52
|
+
# code_host: bitbucket + a watch.jira: {base_url, project_key} block. See
|
|
53
|
+
# README.md's "spf watch" section for every combination's env vars and
|
|
54
|
+
# required scopes. \`spf doctor\` checks whatever this resolves to.
|
|
35
55
|
# watch:
|
|
36
|
-
#
|
|
56
|
+
# issue_provider: github # github | jira
|
|
57
|
+
# code_host: github # github | bitbucket
|
|
58
|
+
# repo: owner/name # "owner/name" (github) or "workspace/repo_slug" (bitbucket)
|
|
37
59
|
# label_prefix: spf
|
|
38
60
|
# chain: plan-build-test
|
|
39
61
|
# base_branch: main
|
|
@@ -44,7 +66,7 @@ const STARTER_CONFIG = `# .spf/spf.config.yaml — merged ON TOP of spf's packag
|
|
|
44
66
|
// (engine/, from `spf eject`), and secrets (.env).
|
|
45
67
|
const GITIGNORE_ENTRIES = [".spf/data/", ".spf/engine/", ".env"];
|
|
46
68
|
export function initCommand(argv) {
|
|
47
|
-
const { options, flags } = parseCli(argv, ["cwd"], ["force"]);
|
|
69
|
+
const { options, flags } = parseCli(argv, ["cwd", "template"], ["force"]);
|
|
48
70
|
const anchor = paths.resolveAnchor(options["cwd"]);
|
|
49
71
|
const sfDir = path.join(anchor.repo_root, ".spf");
|
|
50
72
|
mkdirSync(sfDir, { recursive: true });
|
|
@@ -53,10 +75,17 @@ export function initCommand(argv) {
|
|
|
53
75
|
console.log(`${configPath} already exists — leaving it alone (--force to overwrite)`);
|
|
54
76
|
}
|
|
55
77
|
else {
|
|
56
|
-
|
|
57
|
-
|
|
78
|
+
const templateName = options["template"];
|
|
79
|
+
const content = templateName ? loadTemplate(templateName) : STARTER_CONFIG;
|
|
80
|
+
writeFileSync(configPath, content);
|
|
81
|
+
console.log(`wrote ${configPath}${templateName ? ` (from template "${templateName}")` : ""}`);
|
|
58
82
|
}
|
|
59
83
|
ensureGitignore(anchor.repo_root, GITIGNORE_ENTRIES);
|
|
84
|
+
const templates = listTemplates();
|
|
85
|
+
if (templates.length > 0) {
|
|
86
|
+
console.log(`templates available via --template: ${templates.join(", ")}`);
|
|
87
|
+
}
|
|
60
88
|
console.log(`\nnext: spf doctor (confirm everything resolves), then spf scout "describe this repo"`);
|
|
89
|
+
console.log(paint("yellow", `warning: spf ui reads .spf/data/spf.db, which doesn't exist until a run creates it — run spf scout (or any chain) at least once before spf ui.`));
|
|
61
90
|
return 0;
|
|
62
91
|
}
|
|
@@ -12,33 +12,72 @@ import * as agents from "../../core/agents.js";
|
|
|
12
12
|
import * as paths from "../../core/paths.js";
|
|
13
13
|
import { isRepoAt, makeGit } from "../../core/git_helper.js";
|
|
14
14
|
import { GitHubProvider } from "../../core/issues/github_provider.js";
|
|
15
|
+
import { JiraProvider } from "../../core/issues/jira_provider.js";
|
|
16
|
+
import { BitbucketProvider } from "../../core/issues/bitbucket_provider.js";
|
|
15
17
|
import { createWatchState, tick } from "../../core/watch.js";
|
|
16
18
|
import { findChain } from "../../chains/index.js";
|
|
17
19
|
import { SfDb } from "../../ui/server/db.js";
|
|
18
20
|
import { parseCli } from "../../core/utils.js";
|
|
19
21
|
/**
|
|
20
|
-
* Shared by `watch` and `watch init`: resolve config
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* Prints its own error and returns `null` on failure — the caller just
|
|
25
|
-
* needs to `return 1`.
|
|
22
|
+
* Shared by `watch` and `watch init`: resolve config into an `IssueProvider`
|
|
23
|
+
* — checking only what BOTH need. `watch`'s own extra checks (a real git
|
|
24
|
+
* repo, a registered chain) don't apply to seeding labels. Prints its own
|
|
25
|
+
* error and returns `null` on failure — the caller just needs to `return 1`.
|
|
26
26
|
*/
|
|
27
|
-
function
|
|
28
|
-
if (cfg.watch.
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
function resolveIssueProvider(cfg) {
|
|
28
|
+
if (cfg.watch.issue_provider === "github") {
|
|
29
|
+
if (!cfg.watch.repo.trim()) {
|
|
30
|
+
console.error(`watch.repo is not configured — add it to spf.config.yaml's watch: section, e.g. "owner/name"`);
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const token = process.env["GITHUB_TOKEN"];
|
|
34
|
+
if (!token) {
|
|
35
|
+
console.error('GITHUB_TOKEN is not set — spf watch needs a classic PAT with "repo" scope (or "public_repo" for a public-only repo). See README.md\'s "GITHUB_TOKEN scope" section.');
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return new GitHubProvider(cfg.watch.repo, cfg.watch.label_prefix, token);
|
|
39
|
+
}
|
|
40
|
+
if (cfg.watch.issue_provider === "jira") {
|
|
41
|
+
if (!cfg.watch.jira.base_url.trim() || !cfg.watch.jira.project_key.trim()) {
|
|
42
|
+
console.error(`watch.jira.base_url and watch.jira.project_key must both be set when watch.issue_provider is "jira"`);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const email = process.env["JIRA_EMAIL"];
|
|
46
|
+
const token = process.env["JIRA_API_TOKEN"];
|
|
47
|
+
if (!email || !token) {
|
|
48
|
+
console.error('JIRA_EMAIL and JIRA_API_TOKEN must both be set — spf watch needs an Atlassian account email plus an API token (id.atlassian.com -> Security -> API tokens). See README.md\'s "spf watch" section.');
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
return new JiraProvider(cfg.watch.jira.base_url, cfg.watch.jira.project_key, cfg.watch.label_prefix, email, token);
|
|
31
52
|
}
|
|
53
|
+
console.error(`watch.issue_provider ${JSON.stringify(cfg.watch.issue_provider)} is not supported`);
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
/** Same shape as `resolveIssueProvider`, for `watch.code_host`. */
|
|
57
|
+
function resolveCodeHostProvider(cfg) {
|
|
32
58
|
if (!cfg.watch.repo.trim()) {
|
|
33
|
-
console.error(`watch.repo is not configured — add it to spf.config.yaml's watch: section
|
|
59
|
+
console.error(`watch.repo is not configured — add it to spf.config.yaml's watch: section`);
|
|
34
60
|
return null;
|
|
35
61
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
62
|
+
if (cfg.watch.code_host === "github") {
|
|
63
|
+
const token = process.env["GITHUB_TOKEN"];
|
|
64
|
+
if (!token) {
|
|
65
|
+
console.error('GITHUB_TOKEN is not set — spf watch needs a classic PAT with "repo" scope (or "public_repo" for a public-only repo). See README.md\'s "GITHUB_TOKEN scope" section.');
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
return new GitHubProvider(cfg.watch.repo, cfg.watch.label_prefix, token);
|
|
40
69
|
}
|
|
41
|
-
|
|
70
|
+
if (cfg.watch.code_host === "bitbucket") {
|
|
71
|
+
const email = process.env["BITBUCKET_EMAIL"];
|
|
72
|
+
const token = process.env["BITBUCKET_API_TOKEN"];
|
|
73
|
+
if (!email || !token) {
|
|
74
|
+
console.error('BITBUCKET_EMAIL and BITBUCKET_API_TOKEN must both be set — spf watch needs an Atlassian account email plus an API token (Bitbucket app passwords are being removed; API tokens are the replacement). See README.md\'s "spf watch" section.');
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return new BitbucketProvider(cfg.watch.repo, email, token);
|
|
78
|
+
}
|
|
79
|
+
console.error(`watch.code_host ${JSON.stringify(cfg.watch.code_host)} is not supported`);
|
|
80
|
+
return null;
|
|
42
81
|
}
|
|
43
82
|
function isPidAlive(pid) {
|
|
44
83
|
try {
|
|
@@ -78,11 +117,11 @@ export async function watchInitCommand(argv) {
|
|
|
78
117
|
const { options } = parseCli(argv, ["cwd", "config"], []);
|
|
79
118
|
const anchor = paths.resolveAnchor(options["cwd"]);
|
|
80
119
|
const cfg = agents.loadConfig(paths.resolveConfigPaths(anchor, options["config"]).paths);
|
|
81
|
-
const provider =
|
|
120
|
+
const provider = resolveIssueProvider(cfg);
|
|
82
121
|
if (!provider)
|
|
83
122
|
return 1;
|
|
84
123
|
const result = await provider.ensureLabels();
|
|
85
|
-
console.log(`spf watch init: ${cfg.watch.
|
|
124
|
+
console.log(`spf watch init: ${cfg.watch.issue_provider} (label prefix "${cfg.watch.label_prefix}")`);
|
|
86
125
|
for (const name of result.created)
|
|
87
126
|
console.log(` + ${name} (created)`);
|
|
88
127
|
for (const name of result.updated)
|
|
@@ -96,9 +135,12 @@ export async function watchCommand(argv) {
|
|
|
96
135
|
const anchor = paths.resolveAnchor(options["cwd"]);
|
|
97
136
|
const configPaths = paths.resolveConfigPaths(anchor, options["config"]).paths;
|
|
98
137
|
const cfg = agents.loadConfig(configPaths);
|
|
99
|
-
const provider =
|
|
138
|
+
const provider = resolveIssueProvider(cfg);
|
|
100
139
|
if (!provider)
|
|
101
140
|
return 1;
|
|
141
|
+
const codeHost = resolveCodeHostProvider(cfg);
|
|
142
|
+
if (!codeHost)
|
|
143
|
+
return 1;
|
|
102
144
|
if (!isRepoAt(anchor.repo_root)) {
|
|
103
145
|
console.error(`${anchor.repo_root} is not a git repository`);
|
|
104
146
|
return 1;
|
|
@@ -141,6 +183,7 @@ export async function watchCommand(argv) {
|
|
|
141
183
|
};
|
|
142
184
|
const deps = {
|
|
143
185
|
provider,
|
|
186
|
+
codeHost,
|
|
144
187
|
git,
|
|
145
188
|
worktreeGit: makeGit,
|
|
146
189
|
labelPrefix: cfg.watch.label_prefix,
|
|
@@ -184,7 +227,7 @@ export async function watchCommand(argv) {
|
|
|
184
227
|
};
|
|
185
228
|
process.on("SIGINT", stop);
|
|
186
229
|
process.on("SIGTERM", stop);
|
|
187
|
-
console.log(`[spf] watch ${cfg.watch.repo} label "${cfg.watch.label_prefix}:*" chain "${cfg.watch.chain}" concurrency ${cfg.watch.concurrency}${flags["dry-run"] ? " (dry run)" : ""}`);
|
|
230
|
+
console.log(`[spf] watch ${cfg.watch.issue_provider}+${cfg.watch.code_host} ${cfg.watch.repo} label "${cfg.watch.label_prefix}:*" chain "${cfg.watch.chain}" concurrency ${cfg.watch.concurrency}${flags["dry-run"] ? " (dry run)" : ""}`);
|
|
188
231
|
try {
|
|
189
232
|
for (;;) {
|
|
190
233
|
await tick(deps, state);
|
package/dist/cli/index.js
CHANGED
|
@@ -27,7 +27,7 @@ const HELP = `spf — repeatable agents-plus-code workflows (ADWs)
|
|
|
27
27
|
|
|
28
28
|
spf list the chain registry — names, phases, what each needs
|
|
29
29
|
spf <chain> "<prompt>" [options] run a chain (spf run <chain> ... works identically)
|
|
30
|
-
spf init [--force]
|
|
30
|
+
spf init [--force] [--template <name>] seed .spf/spf.config.yaml (from a packaged template, if named)
|
|
31
31
|
spf install-skill [--user] [--force] install the Claude Code skill (repo-local by default)
|
|
32
32
|
spf migrate [--apply] [--force] move an old stamped adws/ tree onto .spf/ (dry run by default)
|
|
33
33
|
spf eject [--target <dir>] [--force] copy the installed engine out for reference/hand-editing
|
package/dist/core/agent_cc.d.ts
CHANGED
|
@@ -7,6 +7,14 @@
|
|
|
7
7
|
* backend needs installed anyway) — shelling out costs SPF zero new
|
|
8
8
|
* dependencies, the same trade the pre-Flue `agent_pi.ts` made for `pi`.
|
|
9
9
|
*
|
|
10
|
+
* The `claude` command is resolved from `PATH` by default. To route it through
|
|
11
|
+
* a wrapper, proxy server, or launcher (e.g., Ollama), set `SPF_CLAUDE_CMD`
|
|
12
|
+
* before running spf. Space-separated command chains are supported:
|
|
13
|
+
* - `SPF_CLAUDE_CMD="claude"` (default)
|
|
14
|
+
* - `SPF_CLAUDE_CMD="ollama launch claude"` (Ollama launcher)
|
|
15
|
+
* The command/launcher must support the full Claude Code CLI interface.
|
|
16
|
+
* When unset, defaults to `claude`.
|
|
17
|
+
*
|
|
10
18
|
* Every flag below was verified against a REAL local run of this exact
|
|
11
19
|
* machine's `claude` CLI (v2.1.237) before being written — not assumed from
|
|
12
20
|
* the SDK's docs, which describe a related but separately-versioned
|
package/dist/core/agent_cc.js
CHANGED
|
@@ -7,6 +7,14 @@
|
|
|
7
7
|
* backend needs installed anyway) — shelling out costs SPF zero new
|
|
8
8
|
* dependencies, the same trade the pre-Flue `agent_pi.ts` made for `pi`.
|
|
9
9
|
*
|
|
10
|
+
* The `claude` command is resolved from `PATH` by default. To route it through
|
|
11
|
+
* a wrapper, proxy server, or launcher (e.g., Ollama), set `SPF_CLAUDE_CMD`
|
|
12
|
+
* before running spf. Space-separated command chains are supported:
|
|
13
|
+
* - `SPF_CLAUDE_CMD="claude"` (default)
|
|
14
|
+
* - `SPF_CLAUDE_CMD="ollama launch claude"` (Ollama launcher)
|
|
15
|
+
* The command/launcher must support the full Claude Code CLI interface.
|
|
16
|
+
* When unset, defaults to `claude`.
|
|
17
|
+
*
|
|
10
18
|
* Every flag below was verified against a REAL local run of this exact
|
|
11
19
|
* machine's `claude` CLI (v2.1.237) before being written — not assumed from
|
|
12
20
|
* the SDK's docs, which describe a related but separately-versioned
|
|
@@ -231,7 +239,10 @@ export async function run(request, onEvent, onSpawn, onExit) {
|
|
|
231
239
|
toolsFlagValue(request.tools),
|
|
232
240
|
"--strict-mcp-config", // see the module doc comment — required, not optional
|
|
233
241
|
];
|
|
234
|
-
const
|
|
242
|
+
const cmdSpec = process.env.SPF_CLAUDE_CMD || "claude";
|
|
243
|
+
const [cmd, ...cmdArgs] = cmdSpec.split(/\s+/);
|
|
244
|
+
const fullArgs = [...cmdArgs, ...args];
|
|
245
|
+
const child = spawn(cmd, fullArgs, { cwd: request.cwd, env: operatorEnv() });
|
|
235
246
|
// The prompt travels as a positional argv element, not stdin — closing it
|
|
236
247
|
// immediately avoids a real, observed ~3s "no stdin data received" stall
|
|
237
248
|
// where `claude` otherwise waits to see whether anything is piped in.
|
package/dist/core/console.d.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* so a CI log reads exactly like a terminal.
|
|
8
8
|
*/
|
|
9
9
|
import type { EnvelopeBase, EventRecord, GateReport, Phase } from "./data_types.ts";
|
|
10
|
+
export declare function paint(style: string, text: string): string;
|
|
10
11
|
interface Tracer {
|
|
11
12
|
event(record: EventRecord): string;
|
|
12
13
|
}
|
package/dist/core/console.js
CHANGED