@gr8ful/spf 0.4.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.
- package/README.md +122 -4
- package/assets/defaults/spf.config.yaml +6 -0
- package/assets/prompts/reviewer/system.md +1 -1
- package/assets/skill/SKILL.md +1 -0
- package/assets/skill/cookbooks/authoring_chains.md +90 -7
- package/assets/skill/cookbooks/ocr_reviewer.md +196 -0
- package/assets/skill/cookbooks/roster.md +15 -4
- package/assets/skill/cookbooks/spf_overview.md +1 -0
- package/assets/skill/references/config.md +69 -4
- package/assets/skill/references/observability.md +11 -2
- package/assets/templates/ts-flue-ollama.spf.config.yaml +67 -0
- package/assets/templates/ts.spf.config.yaml +5 -0
- package/dist/chains/context.d.ts +30 -0
- package/dist/chains/index.d.ts +94 -10
- package/dist/chains/index.js +70 -5
- package/dist/chains/repo_chains.d.ts +139 -0
- package/dist/chains/repo_chains.js +428 -0
- package/dist/chains/simple_sdlc.d.ts +74 -1
- package/dist/chains/simple_sdlc.js +134 -4
- package/dist/chains/steps.d.ts +215 -20
- package/dist/chains/steps.js +429 -61
- package/dist/cli/ask.d.ts +14 -1
- package/dist/cli/ask.js +32 -2
- package/dist/cli/commands/doctor.d.ts +1 -1
- package/dist/cli/commands/doctor.js +319 -11
- package/dist/cli/commands/init.d.ts +12 -0
- package/dist/cli/commands/init.js +78 -1
- package/dist/cli/commands/list.js +42 -5
- package/dist/cli/commands/run.js +25 -2
- package/dist/cli/commands/watch.d.ts +18 -0
- package/dist/cli/commands/watch.js +147 -10
- package/dist/cli/index.js +60 -3
- package/dist/cli/interview.js +65 -10
- package/dist/core/agent_cc.d.ts +40 -1
- package/dist/core/agent_cc.js +51 -4
- package/dist/core/agent_flue.js +28 -4
- package/dist/core/agents.d.ts +8 -0
- package/dist/core/agents.js +43 -3
- package/dist/core/data_types.d.ts +104 -4
- package/dist/core/data_types.js +99 -2
- package/dist/core/git_helper.d.ts +29 -0
- package/dist/core/git_helper.js +41 -1
- package/dist/core/ollama_provider.d.ts +70 -0
- package/dist/core/ollama_provider.js +208 -0
- package/dist/core/otel.d.ts +352 -0
- package/dist/core/otel.js +793 -0
- package/dist/core/providers.js +4 -0
- package/dist/core/refine.js +11 -3
- package/dist/core/session.js +39 -2
- package/dist/core/tracer.d.ts +31 -2
- package/dist/core/tracer.js +69 -11
- package/dist/core/watch.d.ts +11 -0
- package/dist/core/watch.js +17 -2
- package/dist/test/chains.test.js +8 -3
- package/dist/test/data_types.test.js +140 -2
- package/dist/test/git_helper.test.d.ts +1 -0
- package/dist/test/git_helper.test.js +59 -0
- package/dist/test/hermetic_git.d.ts +1 -0
- package/dist/test/hermetic_git.js +22 -0
- package/dist/test/init_command.test.d.ts +14 -1
- package/dist/test/init_command.test.js +54 -1
- package/dist/test/interview.test.d.ts +15 -1
- package/dist/test/interview.test.js +127 -0
- package/dist/test/ollama_provider.test.d.ts +1 -0
- package/dist/test/ollama_provider.test.js +103 -0
- package/dist/test/otel.test.d.ts +26 -0
- package/dist/test/otel.test.js +512 -0
- package/dist/test/refine.test.js +64 -1
- package/dist/test/repo_chains.test.d.ts +21 -0
- package/dist/test/repo_chains.test.js +416 -0
- package/dist/test/signoff.test.d.ts +1 -0
- package/dist/test/signoff.test.js +329 -0
- package/dist/test/ui_server.test.d.ts +7 -1
- package/dist/test/ui_server.test.js +1 -0
- package/dist/test/watch.test.js +124 -1
- package/package.json +5 -5
|
@@ -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
|
|
|
@@ -116,7 +123,8 @@ Full mechanism: the main README's "`spf watch`" section. Field reference:
|
|
|
116
123
|
|---|---|---|
|
|
117
124
|
| `issue_provider` | `"github"` \| `"jira"` | The tracker `spf watch` polls. Default `github`. |
|
|
118
125
|
| `code_host` | `"github"` \| `"bitbucket"` | Where PRs open — independent of `issue_provider` (Jira issues against a Bitbucket repo is a real setup). Default `github`. |
|
|
119
|
-
| `repo` | string |
|
|
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). |
|
|
120
128
|
| `label_prefix` | string | State-machine label prefix — polls/writes `<prefix>:ready`, `<prefix>:working`, etc. Default `spf`. |
|
|
121
129
|
| `chain` | string | Which registered chain runs per claimed `<prefix>:ready` issue. Default `plan-build-test`. |
|
|
122
130
|
| `base_branch` | string | Branch worktrees fork from and PRs target. Default `main`. |
|
|
@@ -146,7 +154,9 @@ additionally gets `<prefix>:refined`, so a human can review and promote it to
|
|
|
146
154
|
`<prefix>:ready` when it's worth building — the refine lane never
|
|
147
155
|
auto-promotes anything.
|
|
148
156
|
|
|
157
|
+
### `notifications`
|
|
149
158
|
|
|
159
|
+
Optional outbound push for unattended work — `spf watch`'s daemon lifecycle,
|
|
150
160
|
and every chain run (`spf <chain>` / `spf run`, including watch's own
|
|
151
161
|
per-issue runs). Interactive commands (`doctor`, `list`, `sessions`,
|
|
152
162
|
`phases`, `events`, `init`, `ui`, `migrate`, `eject`, `abort`, `version`)
|
|
@@ -187,6 +197,33 @@ run's exit code. See the main README's "Notifications" section for how to
|
|
|
187
197
|
get each channel's webhook URL, and `spf init`'s interview, which asks for
|
|
188
198
|
this section and collects the URL straight into `.env`.
|
|
189
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
|
+
|
|
190
227
|
### `agents[]`
|
|
191
228
|
|
|
192
229
|
| Field | Required | Meaning |
|
|
@@ -197,6 +234,7 @@ this section and collects the URL straight into `.env`.
|
|
|
197
234
|
| `coding_agent`, `model`, `thinking`, `color`, `harness_engineering` | no | Override the matching `defaults` key. |
|
|
198
235
|
| `tools` | no | Allowlist. Omitting it means all tools usable. A capability list, not a boundary — see `writes`. |
|
|
199
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. |
|
|
200
238
|
|
|
201
239
|
Output types are deliberately absent from config: an entry defines who an
|
|
202
240
|
agent *is*; the call site defines how it's *used*.
|
|
@@ -210,7 +248,15 @@ no public model-registry API) — `agents.ts` checks only the static shape at
|
|
|
210
248
|
first real dispatch instead. Provider credentials come from the
|
|
211
249
|
environment, matching the provider you named (`GEMINI_API_KEY`/
|
|
212
250
|
`GOOGLE_API_KEY` for `google/...`, `ANTHROPIC_API_KEY` for `anthropic/...`,
|
|
213
|
-
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.
|
|
214
260
|
|
|
215
261
|
**For `coding_agent: claude_code`:** write `model` in Claude Code's own
|
|
216
262
|
vocabulary — a bare alias (`sonnet`, `opus`) or a full model name — never
|
|
@@ -269,4 +315,23 @@ No config section for this — it's an environment-variable recipe, since
|
|
|
269
315
|
`claude` subprocess, exactly like every other env var. Set
|
|
270
316
|
`ANTHROPIC_BASE_URL`/`ANTHROPIC_AUTH_TOKEN` (local or cloud Ollama) before
|
|
271
317
|
running `spf`; see `roster.md`'s "Coding agent backends" section for the
|
|
272
|
-
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`
|
|
35
|
-
|
|
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
|
|
@@ -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
|
|
@@ -61,6 +61,11 @@ 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
|
package/dist/chains/context.d.ts
CHANGED
|
@@ -27,4 +27,34 @@ export interface ChainContext {
|
|
|
27
27
|
* ignores this field.
|
|
28
28
|
*/
|
|
29
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;
|
|
30
60
|
}
|
package/dist/chains/index.d.ts
CHANGED
|
@@ -1,14 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The chain registry — the one place a CLI-facing short name
|
|
3
|
-
* chain
|
|
4
|
-
*
|
|
2
|
+
* The chain registry — the one place a CLI-facing short name resolves to a
|
|
3
|
+
* chain. `spf list` reads it; `spf <name>`/`spf run <name>` dispatch through
|
|
4
|
+
* it. Replaces guessing a module filename from the CLI argument.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* `
|
|
9
|
-
*
|
|
10
|
-
* `
|
|
11
|
-
*
|
|
6
|
+
* TWO TIERS, and the distinction is load-bearing:
|
|
7
|
+
*
|
|
8
|
+
* 1. BUILT-INS — `CHAINS`, below. Compiled in, always present, the same in
|
|
9
|
+
* every repo spf is ever run against. Every one but `simple-sdlc` is a
|
|
10
|
+
* flat `steps` list composed from `./steps.ts`'s primitives — no module
|
|
11
|
+
* of its own, and no hand-written `phases`/`requiredAgents`/
|
|
12
|
+
* `requiredSuites` to keep in sync with what actually runs; those are
|
|
13
|
+
* derived from the step list itself. `simple-sdlc`'s three-commit,
|
|
14
|
+
* pinned-baseline shape is still imperative — see `./simple_sdlc.ts` —
|
|
15
|
+
* via the `run` escape hatch below.
|
|
16
|
+
* 2. REPO-LOCAL CHAINS — registered at CLI startup from
|
|
17
|
+
* `<repo>/.spf/chains/*.yaml` by `registerRepoChains()`, having been
|
|
18
|
+
* read by `./repo_chains.ts`. These are DATA: a yaml file names built-in
|
|
19
|
+
* step factories and passes them params. It cannot introduce code, and
|
|
20
|
+
* it runs on the same `steps.runSteps()` driver as a built-in, because
|
|
21
|
+
* "agent proposes, code disposes" only means anything if the code that
|
|
22
|
+
* disposes is SPF's code — not the target repo's.
|
|
23
|
+
*
|
|
24
|
+
* `CHAINS` stays a built-in-only const: nothing ever pushes into it, and
|
|
25
|
+
* `src/test/chains.test.ts` pins its exact name set. Repo chains live in a
|
|
26
|
+
* separate module-level list that `registerRepoChains()` REPLACES (never
|
|
27
|
+
* appends to), so a second registration — a long-lived process, `spf watch`
|
|
28
|
+
* re-reading after an edit, a test — cannot double-register a name. Read
|
|
29
|
+
* both tiers through `allChains()`; built-ins always come first, and
|
|
30
|
+
* `findChain()` resolves them first, so a built-in name can never be
|
|
31
|
+
* shadowed by a file on disk.
|
|
12
32
|
*/
|
|
13
33
|
import type { ChainContext } from "./context.ts";
|
|
14
34
|
import * as steps from "./steps.ts";
|
|
@@ -19,15 +39,79 @@ export interface ChainDefinition {
|
|
|
19
39
|
phases: string;
|
|
20
40
|
/** Static for most chains; prompt's depends on --agent, so it's a function there. */
|
|
21
41
|
requiredAgents: string[] | ((options: Record<string, string>) => string[]);
|
|
22
|
-
|
|
42
|
+
/** Static unless a step's suite can be overridden by --suite (qualityCheck/fixLoop), in which case it's a function — see steps.deriveRequiredSuites. */
|
|
43
|
+
requiredSuites: string[] | ((options: Record<string, string>) => string[]);
|
|
23
44
|
/** The declarative path — a flat step list, run by runChain() via steps.runSteps(). */
|
|
24
45
|
steps?: steps.Step[];
|
|
25
46
|
/** The imperative escape hatch for a chain too shaped by its own logic to be a flat list (simple-sdlc). */
|
|
26
47
|
run?: (ctx: ChainContext, options?: Record<string, string>) => Promise<number>;
|
|
48
|
+
/**
|
|
49
|
+
* Absolute path of the `.spf/chains/*.yaml` this chain was loaded from —
|
|
50
|
+
* set only for a repo-local chain, `undefined` for a built-in (a built-in
|
|
51
|
+
* has no source file: it IS spf). The CLI copies it into
|
|
52
|
+
* `ChainContext.chain_source` so the run's trace records whose definition
|
|
53
|
+
* ran, and `spf list`/`spf doctor` use its presence to mark which chains
|
|
54
|
+
* came from the repo.
|
|
55
|
+
*/
|
|
56
|
+
source?: string;
|
|
27
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Build a step-based ChainDefinition, deriving phases/requiredAgents/
|
|
60
|
+
* requiredSuites from its steps.
|
|
61
|
+
*
|
|
62
|
+
* Exported so `./repo_chains.ts` builds a yaml-defined chain through the
|
|
63
|
+
* EXACT same function as a built-in. That is deliberate and should not be
|
|
64
|
+
* "simplified" into a second, parallel constructor: a repo chain must be
|
|
65
|
+
* indistinguishable downstream — same derivation of phases/agents/suites,
|
|
66
|
+
* same `steps` array, same `runSteps` driver, one run path — or the
|
|
67
|
+
* guarantees the built-ins are tested for stop applying to it.
|
|
68
|
+
*/
|
|
69
|
+
export declare function stepChain(name: string, describe: string, list: steps.Step[]): ChainDefinition;
|
|
28
70
|
export declare const CHAINS: ChainDefinition[];
|
|
71
|
+
/**
|
|
72
|
+
* Every built-in name, reserved. `repo_chains.ts` checks against this to
|
|
73
|
+
* refuse a repo chain that reuses one — a collision is reported as a
|
|
74
|
+
* problem, never resolved by shadowing either way round.
|
|
75
|
+
*/
|
|
76
|
+
export declare const BUILTIN_CHAIN_NAMES: ReadonlySet<string>;
|
|
77
|
+
/**
|
|
78
|
+
* Install the repo-local chains (and the problems found reading them) for
|
|
79
|
+
* this process. Call once, early, from the CLI entry point.
|
|
80
|
+
*
|
|
81
|
+
* REPLACES rather than appends, so it is idempotent: calling it twice with
|
|
82
|
+
* the same input leaves exactly one copy of each chain. That matters because
|
|
83
|
+
* a long-running `spf watch` may re-read `.spf/chains/` after an edit, and
|
|
84
|
+
* because a partial second registration (say, one file fixed) must not leave
|
|
85
|
+
* the previous file's stale definition behind.
|
|
86
|
+
*
|
|
87
|
+
* The `problems` list is stored, not thrown and not printed: loading is
|
|
88
|
+
* never allowed to fail a command that has nothing to do with the broken
|
|
89
|
+
* file (`spf trace`, `spf sessions`, `spf --version` must all still work in
|
|
90
|
+
* a repo with one malformed yaml). `spf list` and `spf doctor` surface them;
|
|
91
|
+
* dispatch surfaces them when the name someone typed is missing.
|
|
92
|
+
*/
|
|
93
|
+
export declare function registerRepoChains(chains: ChainDefinition[], problems: {
|
|
94
|
+
file: string;
|
|
95
|
+
message: string;
|
|
96
|
+
}[]): void;
|
|
97
|
+
/** Built-ins first, then repo-local chains — the order `spf list` prints and `findChain` resolves in. */
|
|
98
|
+
export declare function allChains(): ChainDefinition[];
|
|
99
|
+
/** What went wrong reading `.spf/chains/*.yaml`, for `spf list`/`spf doctor` to report. */
|
|
100
|
+
export declare function repoChainProblems(): {
|
|
101
|
+
file: string;
|
|
102
|
+
message: string;
|
|
103
|
+
}[];
|
|
104
|
+
/**
|
|
105
|
+
* Resolve a CLI-facing name. Built-ins are consulted FIRST and win
|
|
106
|
+
* unconditionally — a repo chain that reuses a built-in name is rejected at
|
|
107
|
+
* load time (see `repo_chains.ts`), so this ordering is a second line of
|
|
108
|
+
* defence rather than the policy itself, and it is what makes `spf build` in
|
|
109
|
+
* any repo mean what the docs say it means.
|
|
110
|
+
*/
|
|
29
111
|
export declare function findChain(name: string): ChainDefinition | undefined;
|
|
30
112
|
export declare function resolveRequiredAgents(chain: ChainDefinition, options: Record<string, string>): string[];
|
|
113
|
+
/** Same idea as resolveRequiredAgents(), for the suite(s) --suite can override before validate() ever runs. */
|
|
114
|
+
export declare function resolveRequiredSuites(chain: ChainDefinition, options: Record<string, string>): string[];
|
|
31
115
|
/**
|
|
32
116
|
* Run a chain, whichever path it defines: `run` (the imperative escape
|
|
33
117
|
* hatch) if it has one, otherwise `steps` through the shared driver. Both
|
package/dist/chains/index.js
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import * as steps from "./steps.js";
|
|
2
2
|
import * as simpleSdlc from "./simple_sdlc.js";
|
|
3
|
-
/**
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Build a step-based ChainDefinition, deriving phases/requiredAgents/
|
|
5
|
+
* requiredSuites from its steps.
|
|
6
|
+
*
|
|
7
|
+
* Exported so `./repo_chains.ts` builds a yaml-defined chain through the
|
|
8
|
+
* EXACT same function as a built-in. That is deliberate and should not be
|
|
9
|
+
* "simplified" into a second, parallel constructor: a repo chain must be
|
|
10
|
+
* indistinguishable downstream — same derivation of phases/agents/suites,
|
|
11
|
+
* same `steps` array, same `runSteps` driver, one run path — or the
|
|
12
|
+
* guarantees the built-ins are tested for stop applying to it.
|
|
13
|
+
*/
|
|
14
|
+
export function stepChain(name, describe, list) {
|
|
5
15
|
return {
|
|
6
16
|
name,
|
|
7
17
|
describe,
|
|
@@ -63,19 +73,74 @@ export const CHAINS = [
|
|
|
63
73
|
name: "simple-sdlc",
|
|
64
74
|
describe: "the work is real and its shape is not obvious — plan, build, test, review, document; 3 commits",
|
|
65
75
|
phases: "engineer(request) -> planner -> git(commit_plan) -> builder -> code(test) [-> builder(fix) -> code(test) ...] " +
|
|
66
|
-
"-> reviewer [-> builder(revise) -> reviewer ...] -> code(retest, if revised) -> git(commit_build) " +
|
|
76
|
+
"-> reviewer [-> builder(revise) -> reviewer ...] -> code(retest, if revised) -> engineer(signoff) -> git(commit_build) " +
|
|
67
77
|
"-> code(changes) -> documenter -> git(commit_docs)",
|
|
68
78
|
requiredAgents: simpleSdlc.REQUIRED_AGENTS,
|
|
69
79
|
requiredSuites: simpleSdlc.REQUIRED_SUITES,
|
|
70
80
|
run: simpleSdlc.main,
|
|
71
81
|
},
|
|
72
82
|
];
|
|
83
|
+
/**
|
|
84
|
+
* Every built-in name, reserved. `repo_chains.ts` checks against this to
|
|
85
|
+
* refuse a repo chain that reuses one — a collision is reported as a
|
|
86
|
+
* problem, never resolved by shadowing either way round.
|
|
87
|
+
*/
|
|
88
|
+
export const BUILTIN_CHAIN_NAMES = new Set(CHAINS.map((c) => c.name));
|
|
89
|
+
// ── tier 2: repo-local chains, registered at startup ────────────────────
|
|
90
|
+
/**
|
|
91
|
+
* Module-level, not a parameter threaded through every call site: `spf list`,
|
|
92
|
+
* `spf <chain>`, `spf watch`, `spf doctor` and the interview all resolve
|
|
93
|
+
* chains by name from unrelated places, and passing a registry object to
|
|
94
|
+
* each would mean four more chances for one of them to be looking at the
|
|
95
|
+
* built-ins only. One process, one repo, one registration.
|
|
96
|
+
*/
|
|
97
|
+
let REPO_CHAINS = [];
|
|
98
|
+
let REPO_CHAIN_PROBLEMS = [];
|
|
99
|
+
/**
|
|
100
|
+
* Install the repo-local chains (and the problems found reading them) for
|
|
101
|
+
* this process. Call once, early, from the CLI entry point.
|
|
102
|
+
*
|
|
103
|
+
* REPLACES rather than appends, so it is idempotent: calling it twice with
|
|
104
|
+
* the same input leaves exactly one copy of each chain. That matters because
|
|
105
|
+
* a long-running `spf watch` may re-read `.spf/chains/` after an edit, and
|
|
106
|
+
* because a partial second registration (say, one file fixed) must not leave
|
|
107
|
+
* the previous file's stale definition behind.
|
|
108
|
+
*
|
|
109
|
+
* The `problems` list is stored, not thrown and not printed: loading is
|
|
110
|
+
* never allowed to fail a command that has nothing to do with the broken
|
|
111
|
+
* file (`spf trace`, `spf sessions`, `spf --version` must all still work in
|
|
112
|
+
* a repo with one malformed yaml). `spf list` and `spf doctor` surface them;
|
|
113
|
+
* dispatch surfaces them when the name someone typed is missing.
|
|
114
|
+
*/
|
|
115
|
+
export function registerRepoChains(chains, problems) {
|
|
116
|
+
REPO_CHAINS = [...chains];
|
|
117
|
+
REPO_CHAIN_PROBLEMS = [...problems];
|
|
118
|
+
}
|
|
119
|
+
/** Built-ins first, then repo-local chains — the order `spf list` prints and `findChain` resolves in. */
|
|
120
|
+
export function allChains() {
|
|
121
|
+
return [...CHAINS, ...REPO_CHAINS];
|
|
122
|
+
}
|
|
123
|
+
/** What went wrong reading `.spf/chains/*.yaml`, for `spf list`/`spf doctor` to report. */
|
|
124
|
+
export function repoChainProblems() {
|
|
125
|
+
return [...REPO_CHAIN_PROBLEMS];
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Resolve a CLI-facing name. Built-ins are consulted FIRST and win
|
|
129
|
+
* unconditionally — a repo chain that reuses a built-in name is rejected at
|
|
130
|
+
* load time (see `repo_chains.ts`), so this ordering is a second line of
|
|
131
|
+
* defence rather than the policy itself, and it is what makes `spf build` in
|
|
132
|
+
* any repo mean what the docs say it means.
|
|
133
|
+
*/
|
|
73
134
|
export function findChain(name) {
|
|
74
|
-
return CHAINS.find((c) => c.name === name);
|
|
135
|
+
return CHAINS.find((c) => c.name === name) ?? REPO_CHAINS.find((c) => c.name === name);
|
|
75
136
|
}
|
|
76
137
|
export function resolveRequiredAgents(chain, options) {
|
|
77
138
|
return typeof chain.requiredAgents === "function" ? chain.requiredAgents(options) : chain.requiredAgents;
|
|
78
139
|
}
|
|
140
|
+
/** Same idea as resolveRequiredAgents(), for the suite(s) --suite can override before validate() ever runs. */
|
|
141
|
+
export function resolveRequiredSuites(chain, options) {
|
|
142
|
+
return typeof chain.requiredSuites === "function" ? chain.requiredSuites(options) : chain.requiredSuites;
|
|
143
|
+
}
|
|
79
144
|
/**
|
|
80
145
|
* Run a chain, whichever path it defines: `run` (the imperative escape
|
|
81
146
|
* hatch) if it has one, otherwise `steps` through the shared driver. Both
|
|
@@ -86,5 +151,5 @@ export function resolveRequiredAgents(chain, options) {
|
|
|
86
151
|
export async function runChain(chain, ctx, options = {}) {
|
|
87
152
|
if (chain.run)
|
|
88
153
|
return chain.run(ctx, options);
|
|
89
|
-
return steps.runSteps(ctx, resolveRequiredAgents(chain, options), chain
|
|
154
|
+
return steps.runSteps(ctx, resolveRequiredAgents(chain, options), resolveRequiredSuites(chain, options), chain.steps, options);
|
|
90
155
|
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo-local chains: `<repo>/.spf/chains/*.yaml` read as DATA.
|
|
3
|
+
*
|
|
4
|
+
* WHAT THIS IS. A target repo can compose its own chain out of the step
|
|
5
|
+
* factories `./steps.ts` already exports, by naming them in a yaml file and
|
|
6
|
+
* passing them params. One file, one chain:
|
|
7
|
+
*
|
|
8
|
+
* # .spf/chains/ship-it.yaml
|
|
9
|
+
* name: ship-it
|
|
10
|
+
* describe: plan, build, test, land — with our own reviewer in the loop
|
|
11
|
+
* steps:
|
|
12
|
+
* - step: request
|
|
13
|
+
* - step: plan
|
|
14
|
+
* owner: architect
|
|
15
|
+
* - step: build
|
|
16
|
+
* retries: 2
|
|
17
|
+
* extraGates: [jsonParses]
|
|
18
|
+
* - step: fixLoop
|
|
19
|
+
* suite: test
|
|
20
|
+
* - step: commit
|
|
21
|
+
* onlyIfAccepted: true
|
|
22
|
+
*
|
|
23
|
+
* WHY DATA, AND NOT CODE. SPF's contract is "agent proposes, code disposes",
|
|
24
|
+
* and the code that disposes is SPF'S code. A chain that could `import`
|
|
25
|
+
* something out of the target repo would move the disposer into the repo
|
|
26
|
+
* being worked on — the agent's own blast radius — and there would be
|
|
27
|
+
* nothing left holding the line. So a repo chain names existing factories
|
|
28
|
+
* and nothing else: no expressions, no shell, no module paths, no way to
|
|
29
|
+
* introduce behavior that isn't already compiled into spf and covered by
|
|
30
|
+
* spf's tests. It is also what keeps the promise that a target repo is
|
|
31
|
+
* zero-setup and language-agnostic: the only thing it ever gains is `.spf/`,
|
|
32
|
+
* with no build step, no dependency on this package, and nothing to compile.
|
|
33
|
+
*
|
|
34
|
+
* The counterpart rule lives in `steps.ts` (see GATE_ALLOWLIST): params may
|
|
35
|
+
* only ADD. A repo chain can demand more checking than a built-in does; it
|
|
36
|
+
* can never demand less.
|
|
37
|
+
*
|
|
38
|
+
* ONE RUN PATH. Every chain here is built with `index.ts`'s `stepChain()` —
|
|
39
|
+
* the very same function the built-ins use — so it is a plain
|
|
40
|
+
* `ChainDefinition` with a `steps` array, its `phases`/`requiredAgents`/
|
|
41
|
+
* `requiredSuites` derived by the same code, executed by the same
|
|
42
|
+
* `steps.runSteps()` driver. There is deliberately no second interpreter,
|
|
43
|
+
* no "yaml runtime", and no branch anywhere downstream on "is this a repo
|
|
44
|
+
* chain": the only difference a repo chain carries is `source`, the file it
|
|
45
|
+
* came from, which exists so a run can be traced back to a definition that
|
|
46
|
+
* may since have been edited.
|
|
47
|
+
*
|
|
48
|
+
* NEVER THROWS. `loadRepoChains()` returns `{ chains, problems }`. That is
|
|
49
|
+
* not politeness: this function runs at CLI startup for EVERY command (see
|
|
50
|
+
* `cli/index.ts`), so a single malformed yaml must not be able to break
|
|
51
|
+
* `spf sessions`, `spf trace`, `spf doctor` or `spf --version` in a repo
|
|
52
|
+
* that has one. Anything that goes wrong — unreadable directory, invalid
|
|
53
|
+
* yaml, unknown step, wrong param type, bad description, unknown gate,
|
|
54
|
+
* colliding name — becomes a `{ file, message }` problem carrying enough
|
|
55
|
+
* detail to fix the file, and the other files still load.
|
|
56
|
+
*
|
|
57
|
+
* THE VOCABULARY IS THE FACTORY SIGNATURE. A step's yaml name is its
|
|
58
|
+
* exported function name (`fixLoop`, `promptOnly`, `publishIssues`) and its
|
|
59
|
+
* params are that function's `opts` keys, camelCase and all (`extraGates`,
|
|
60
|
+
* `onlyIfAccepted`, `fromPlan`). No snake_case aliasing, no renaming layer:
|
|
61
|
+
* a second spelling of the same thing is a second thing to keep in sync, and
|
|
62
|
+
* a chain author reading `steps.ts` (or `spf list`) would be reading a
|
|
63
|
+
* vocabulary that isn't the one they type. The schemas below are the ONE
|
|
64
|
+
* place that mapping is written down; each mirrors exactly one factory's
|
|
65
|
+
* `opts`, and adding a param to a factory without adding it here simply
|
|
66
|
+
* means yaml cannot reach it yet (a safe, loud default: unknown params are
|
|
67
|
+
* rejected, never ignored).
|
|
68
|
+
*/
|
|
69
|
+
import * as v from "valibot";
|
|
70
|
+
import type * as paths from "../core/paths.ts";
|
|
71
|
+
import * as steps from "./steps.ts";
|
|
72
|
+
import { type ChainDefinition } from "./index.ts";
|
|
73
|
+
/** What went wrong in one file, and which file. */
|
|
74
|
+
export interface RepoChainProblem {
|
|
75
|
+
/** Absolute path — the thing the operator has to open and edit. */
|
|
76
|
+
file: string;
|
|
77
|
+
message: string;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* A step's yaml surface: the schema its params must satisfy, and how to
|
|
81
|
+
* build the actual `Step` from them.
|
|
82
|
+
*
|
|
83
|
+
* `params` is redundant with the schema's own entries, and kept anyway: it
|
|
84
|
+
* lets an unknown param be reported as `unknown param "ownr" — allowed:
|
|
85
|
+
* owner, description, ...`, which is what an author needs, instead of
|
|
86
|
+
* valibot's structural complaint about an unexpected key.
|
|
87
|
+
*/
|
|
88
|
+
export interface StepSpec {
|
|
89
|
+
schema: v.GenericSchema;
|
|
90
|
+
build: (params: any) => steps.Step;
|
|
91
|
+
params: readonly string[];
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* One entry per exported flat-step factory in `steps.ts`. The key is what a
|
|
95
|
+
* yaml `step:` names.
|
|
96
|
+
*
|
|
97
|
+
* `qualityCheck`/`fixLoop` default their `suite` HERE as well as in the
|
|
98
|
+
* factory signature: the factory's default lives on the whole `opts` object
|
|
99
|
+
* (`opts = { suite: "test" }`), which a yaml-supplied `{}` would satisfy
|
|
100
|
+
* without ever supplying `suite`. Defaulting in the schema is what makes
|
|
101
|
+
* `- step: fixLoop` with no params mean the same thing as `fixLoop()`.
|
|
102
|
+
*/
|
|
103
|
+
export declare const STEP_SPECS: Record<string, StepSpec>;
|
|
104
|
+
/** Every step name a yaml file may use — for schema validation and for error messages. */
|
|
105
|
+
export declare const STEP_NAMES: readonly string[];
|
|
106
|
+
/**
|
|
107
|
+
* The whole file. One document, one chain — deliberately not a list: the
|
|
108
|
+
* filename then documents which chain lives where, and a problem can point
|
|
109
|
+
* at a file the operator can open, which is the only handle they have.
|
|
110
|
+
*
|
|
111
|
+
* `steps` is validated loosely here (each entry only has to be an object
|
|
112
|
+
* naming a `step`); the per-step params are validated against that step's
|
|
113
|
+
* own schema afterwards, because which schema applies depends on the value
|
|
114
|
+
* of `step`.
|
|
115
|
+
*/
|
|
116
|
+
export declare const RepoChainFileSchema: v.StrictObjectSchema<{
|
|
117
|
+
readonly name: v.SchemaWithPipe<readonly [v.StringSchema<"name is required — it is what `spf <name>` types">, v.RegexAction<string, "name must be lowercase letters/digits/._- and start with a letter or digit (it is typed on the command line)">]>;
|
|
118
|
+
readonly describe: v.SchemaWithPipe<readonly [v.StringSchema<"describe is required — it is the line `spf list` prints">, v.MinLengthAction<string, 1, "describe must not be empty">]>;
|
|
119
|
+
readonly steps: v.SchemaWithPipe<readonly [v.ArraySchema<v.LooseObjectSchema<{
|
|
120
|
+
readonly step: v.StringSchema<"each step entry needs a `step:` naming a step factory">;
|
|
121
|
+
}, undefined>, "steps must be a list">, v.MinLengthAction<({
|
|
122
|
+
step: string;
|
|
123
|
+
} & {
|
|
124
|
+
[key: string]: unknown;
|
|
125
|
+
})[], 1, "steps must name at least one step — a chain with no steps would run nothing and report success">]>;
|
|
126
|
+
}, undefined>;
|
|
127
|
+
export type RepoChainFile = v.InferOutput<typeof RepoChainFileSchema>;
|
|
128
|
+
/**
|
|
129
|
+
* Read every chain in `<anchor.spf_dir>/chains/`.
|
|
130
|
+
*
|
|
131
|
+
* Never throws (see the module header). Returns the chains that loaded and a
|
|
132
|
+
* problem per file that didn't; a file contributes at most one chain and, on
|
|
133
|
+
* failure, exactly one problem — the first thing wrong with it, because the
|
|
134
|
+
* second is usually a consequence of the first.
|
|
135
|
+
*/
|
|
136
|
+
export declare function loadRepoChains(anchor: paths.RepoAnchor): {
|
|
137
|
+
chains: ChainDefinition[];
|
|
138
|
+
problems: RepoChainProblem[];
|
|
139
|
+
};
|