@gr8ful/spf 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +158 -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/paths.d.ts +3 -0
- package/dist/core/paths.js +48 -1
- 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/paths.test.d.ts +1 -0
- package/dist/test/paths.test.js +68 -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
package/dist/core/agent_cc.d.ts
CHANGED
|
@@ -11,10 +11,49 @@
|
|
|
11
11
|
* a wrapper, proxy server, or launcher (e.g., Ollama), set `SPF_CLAUDE_CMD`
|
|
12
12
|
* before running spf. Space-separated command chains are supported:
|
|
13
13
|
* - `SPF_CLAUDE_CMD="claude"` (default)
|
|
14
|
-
* - `SPF_CLAUDE_CMD="ollama launch claude"` (Ollama launcher
|
|
14
|
+
* - `SPF_CLAUDE_CMD="ollama launch claude --model <tag>"` (Ollama launcher —
|
|
15
|
+
* the `--model` is `ollama launch`'s OWN flag, and is mandatory in
|
|
16
|
+
* headless mode; see below)
|
|
15
17
|
* The command/launcher must support the full Claude Code CLI interface.
|
|
16
18
|
* When unset, defaults to `claude`.
|
|
17
19
|
*
|
|
20
|
+
* Wrapper contract for the flags this module appends (`-p`, `--json-schema`,
|
|
21
|
+
* `--model`, ...): a wrapper token chain is spawned as `[...cmdTokens,
|
|
22
|
+
* ...args]`, so a plain passthrough shim needs nothing special, and a
|
|
23
|
+
* cmdSpec that already contains its own literal `--` is left completely
|
|
24
|
+
* alone — `args` lands after it exactly as written. `ollama launch <cmd>
|
|
25
|
+
* [flags...]` is the one documented shape that does NOT self-supply that
|
|
26
|
+
* separator: it uses cobra flag parsing, which treats anything typed after
|
|
27
|
+
* `launch <cmd>` as ITS OWN flags unless a literal `--` says otherwise —
|
|
28
|
+
* spike-verified live: `SPF_CLAUDE_CMD="ollama launch claude"` alone dies
|
|
29
|
+
* with `unknown shorthand flag: 'p' in -p` before `claude` ever starts
|
|
30
|
+
* (scratchpad/ollama-spike/probe2-claudecode-ollama/run3_ollama_launch_claude.log).
|
|
31
|
+
*
|
|
32
|
+
* The `--` insertion below fixes THAT failure, but is NOT by itself
|
|
33
|
+
* sufficient to reach `claude` — it only trades one error for the next one.
|
|
34
|
+
* `ollama launch` also requires its OWN `--model <tag>` flag, typed BEFORE
|
|
35
|
+
* the `--` separator, whenever it's run headless: with no model flag it
|
|
36
|
+
* falls back to an interactive model picker, and SPF always spawns with
|
|
37
|
+
* piped (non-interactive) stdio, so that picker can never run. Spike-verified
|
|
38
|
+
* live: `--` alone (no `--model` anywhere) dies one step later with `Error:
|
|
39
|
+
* model selection requires an interactive terminal; use --model to run in
|
|
40
|
+
* headless mode` (.../run4_ollama_launch_claude_dashdash.log) — that log IS
|
|
41
|
+
* the "just add `--`" experiment, and it fails. Putting `--model <tag>`
|
|
42
|
+
* AFTER the separator doesn't help either: at that point it's parsed as
|
|
43
|
+
* `claude`'s own `--model`, not `ollama launch`'s, so `ollama launch` still
|
|
44
|
+
* sees no model and still fails the same way. Only supplying `ollama
|
|
45
|
+
* launch`'s `--model` BEFORE the separator succeeds end-to-end
|
|
46
|
+
* (.../run5_ollama_launch_claude_full.log). So the operator's `SPF_CLAUDE_CMD`
|
|
47
|
+
* itself must read `ollama launch claude --model <tag>` (tag from `ollama
|
|
48
|
+
* list`) — this module can insert the `--`, but cannot supply the model tag
|
|
49
|
+
* on the operator's behalf; `doctor.ts` hard-fails a `SPF_CLAUDE_CMD` that
|
|
50
|
+
* omits it, since this is a static, deterministic misconfiguration.
|
|
51
|
+
*
|
|
52
|
+
* This module special-cases exactly the `ollama launch ...` token shape
|
|
53
|
+
* (with no `--` already present) and inserts the separator automatically at
|
|
54
|
+
* that position — never for any other wrapper, and never a second `--` if
|
|
55
|
+
* the operator already wrote one themselves.
|
|
56
|
+
*
|
|
18
57
|
* Every flag below was verified against a REAL local run of this exact
|
|
19
58
|
* machine's `claude` CLI (v2.1.237) before being written — not assumed from
|
|
20
59
|
* the SDK's docs, which describe a related but separately-versioned
|
package/dist/core/agent_cc.js
CHANGED
|
@@ -11,10 +11,49 @@
|
|
|
11
11
|
* a wrapper, proxy server, or launcher (e.g., Ollama), set `SPF_CLAUDE_CMD`
|
|
12
12
|
* before running spf. Space-separated command chains are supported:
|
|
13
13
|
* - `SPF_CLAUDE_CMD="claude"` (default)
|
|
14
|
-
* - `SPF_CLAUDE_CMD="ollama launch claude"` (Ollama launcher
|
|
14
|
+
* - `SPF_CLAUDE_CMD="ollama launch claude --model <tag>"` (Ollama launcher —
|
|
15
|
+
* the `--model` is `ollama launch`'s OWN flag, and is mandatory in
|
|
16
|
+
* headless mode; see below)
|
|
15
17
|
* The command/launcher must support the full Claude Code CLI interface.
|
|
16
18
|
* When unset, defaults to `claude`.
|
|
17
19
|
*
|
|
20
|
+
* Wrapper contract for the flags this module appends (`-p`, `--json-schema`,
|
|
21
|
+
* `--model`, ...): a wrapper token chain is spawned as `[...cmdTokens,
|
|
22
|
+
* ...args]`, so a plain passthrough shim needs nothing special, and a
|
|
23
|
+
* cmdSpec that already contains its own literal `--` is left completely
|
|
24
|
+
* alone — `args` lands after it exactly as written. `ollama launch <cmd>
|
|
25
|
+
* [flags...]` is the one documented shape that does NOT self-supply that
|
|
26
|
+
* separator: it uses cobra flag parsing, which treats anything typed after
|
|
27
|
+
* `launch <cmd>` as ITS OWN flags unless a literal `--` says otherwise —
|
|
28
|
+
* spike-verified live: `SPF_CLAUDE_CMD="ollama launch claude"` alone dies
|
|
29
|
+
* with `unknown shorthand flag: 'p' in -p` before `claude` ever starts
|
|
30
|
+
* (scratchpad/ollama-spike/probe2-claudecode-ollama/run3_ollama_launch_claude.log).
|
|
31
|
+
*
|
|
32
|
+
* The `--` insertion below fixes THAT failure, but is NOT by itself
|
|
33
|
+
* sufficient to reach `claude` — it only trades one error for the next one.
|
|
34
|
+
* `ollama launch` also requires its OWN `--model <tag>` flag, typed BEFORE
|
|
35
|
+
* the `--` separator, whenever it's run headless: with no model flag it
|
|
36
|
+
* falls back to an interactive model picker, and SPF always spawns with
|
|
37
|
+
* piped (non-interactive) stdio, so that picker can never run. Spike-verified
|
|
38
|
+
* live: `--` alone (no `--model` anywhere) dies one step later with `Error:
|
|
39
|
+
* model selection requires an interactive terminal; use --model to run in
|
|
40
|
+
* headless mode` (.../run4_ollama_launch_claude_dashdash.log) — that log IS
|
|
41
|
+
* the "just add `--`" experiment, and it fails. Putting `--model <tag>`
|
|
42
|
+
* AFTER the separator doesn't help either: at that point it's parsed as
|
|
43
|
+
* `claude`'s own `--model`, not `ollama launch`'s, so `ollama launch` still
|
|
44
|
+
* sees no model and still fails the same way. Only supplying `ollama
|
|
45
|
+
* launch`'s `--model` BEFORE the separator succeeds end-to-end
|
|
46
|
+
* (.../run5_ollama_launch_claude_full.log). So the operator's `SPF_CLAUDE_CMD`
|
|
47
|
+
* itself must read `ollama launch claude --model <tag>` (tag from `ollama
|
|
48
|
+
* list`) — this module can insert the `--`, but cannot supply the model tag
|
|
49
|
+
* on the operator's behalf; `doctor.ts` hard-fails a `SPF_CLAUDE_CMD` that
|
|
50
|
+
* omits it, since this is a static, deterministic misconfiguration.
|
|
51
|
+
*
|
|
52
|
+
* This module special-cases exactly the `ollama launch ...` token shape
|
|
53
|
+
* (with no `--` already present) and inserts the separator automatically at
|
|
54
|
+
* that position — never for any other wrapper, and never a second `--` if
|
|
55
|
+
* the operator already wrote one themselves.
|
|
56
|
+
*
|
|
18
57
|
* Every flag below was verified against a REAL local run of this exact
|
|
19
58
|
* machine's `claude` CLI (v2.1.237) before being written — not assumed from
|
|
20
59
|
* the SDK's docs, which describe a related but separately-versioned
|
|
@@ -240,9 +279,17 @@ export async function run(request, onEvent, onSpawn, onExit) {
|
|
|
240
279
|
"--strict-mcp-config", // see the module doc comment — required, not optional
|
|
241
280
|
];
|
|
242
281
|
const cmdSpec = process.env.SPF_CLAUDE_CMD || "claude";
|
|
243
|
-
const
|
|
244
|
-
const
|
|
245
|
-
|
|
282
|
+
const cmdTokens = cmdSpec.split(/\s+/).filter(Boolean);
|
|
283
|
+
const [cmd, ...cmdArgs] = cmdTokens;
|
|
284
|
+
// See the module doc comment for why `ollama launch ...` (and ONLY that
|
|
285
|
+
// shape) gets an auto-inserted `--`: cobra flag parsing otherwise consumes
|
|
286
|
+
// `args`' own flags (e.g. `-p`) as `ollama launch`'s, before `claude` is
|
|
287
|
+
// ever reached. Any cmdSpec that already contains a literal `--` token is
|
|
288
|
+
// left completely alone — `args` is appended after it exactly as written,
|
|
289
|
+
// never a second separator.
|
|
290
|
+
const needsOllamaLaunchSeparator = cmdTokens[0] === "ollama" && cmdTokens[1] === "launch" && !cmdArgs.includes("--");
|
|
291
|
+
const fullArgs = needsOllamaLaunchSeparator ? [...cmdArgs, "--", ...args] : [...cmdArgs, ...args];
|
|
292
|
+
const child = spawn(cmd, fullArgs, { cwd: request.cwd, env: request.env ?? operatorEnv() });
|
|
246
293
|
// The prompt travels as a positional argv element, not stdin — closing it
|
|
247
294
|
// immediately avoids a real, observed ~3s "no stdin data received" stall
|
|
248
295
|
// where `claude` otherwise waits to see whether anything is piped in.
|
package/dist/core/agent_flue.js
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
import { AgentRunError, createBashTool, createEditTool, createGlobTool, createGrepTool, createReadTool, createWriteTool, init, observe, useDataWriter, useModel, useSandbox, useTool, } from "@flue/runtime";
|
|
26
26
|
import { local, sqlite, start } from "@flue/runtime/node";
|
|
27
27
|
import { UsageBreakdown, makeAgentResult } from "./data_types.js";
|
|
28
|
+
import { registerOllamaModel } from "./ollama_provider.js";
|
|
28
29
|
import { nowIso, operatorEnv } from "./utils.js";
|
|
29
30
|
const RESULT_SNIPPET_CHARS = 20_000; // tool output rides along whole; clip only guards pathological cases
|
|
30
31
|
const ARG_VALUE_CHARS = 20_000; // args too — the UI scrolls, it must not be handed cut-off data
|
|
@@ -156,10 +157,12 @@ function resolveBuiltinTools(names) {
|
|
|
156
157
|
* `undefined`/`null` toolNames = every builtin (Flue's own default when no
|
|
157
158
|
* `tools` override is given — matches SPF's "unset = all tools usable").
|
|
158
159
|
* `local()`'s env does NOT inherit process.env by default (only PATH/HOME/
|
|
159
|
-
* USER/LANG/TERM/TMPDIR) — operatorEnv()
|
|
160
|
+
* USER/LANG/TERM/TMPDIR) — `env` is `request.env ?? operatorEnv()` from the
|
|
161
|
+
* caller, restoring today's actual behavior unless the agent's own
|
|
162
|
+
* `env_allowlist` narrowed it (see agents.ts).
|
|
160
163
|
*/
|
|
161
|
-
function sandboxFor(toolNames, cwd) {
|
|
162
|
-
const base = local({ cwd, env
|
|
164
|
+
function sandboxFor(toolNames, cwd, env) {
|
|
165
|
+
const base = local({ cwd, env });
|
|
163
166
|
if (!toolNames)
|
|
164
167
|
return base;
|
|
165
168
|
const factories = resolveBuiltinTools(toolNames);
|
|
@@ -188,7 +191,7 @@ function sfAgentRender({ id }) {
|
|
|
188
191
|
if (!spec)
|
|
189
192
|
throw new Error(`agent_flue: no render spec registered for conversation ${id} — run() must set it before dispatching`);
|
|
190
193
|
useModel(spec.model, { thinkingLevel: spec.thinking });
|
|
191
|
-
useSandbox(sandboxFor(spec.toolNames, spec.cwd));
|
|
194
|
+
useSandbox(sandboxFor(spec.toolNames, spec.cwd, spec.env));
|
|
192
195
|
const writeReport = useDataWriter("sf_report");
|
|
193
196
|
useTool({
|
|
194
197
|
name: "sf_report",
|
|
@@ -265,6 +268,26 @@ function contextTokensOf(usage) {
|
|
|
265
268
|
* not attempted here.
|
|
266
269
|
*/
|
|
267
270
|
export async function run(request, onEvent, onSpawn, onExit) {
|
|
271
|
+
// Ollama has no pi-ai/Flue built-in provider — it must be registered as a
|
|
272
|
+
// custom `createProvider(...)` before anything dispatches against it (see
|
|
273
|
+
// ollama_provider.ts's header comment for why: dummy api key, union
|
|
274
|
+
// re-registration, exact version pin). Doing this BEFORE `ensureRuntime()`
|
|
275
|
+
// means it's in place before that call's `start()` ever gets a chance to
|
|
276
|
+
// run its own default-provider registration — irrelevant today (pi-ai ships
|
|
277
|
+
// no "ollama" built-in to collide with) but keeps the ordering the one a
|
|
278
|
+
// future built-in couldn't quietly undermine. `resolveModel` is a cheap
|
|
279
|
+
// string split regardless of provider, so the check above costs nothing
|
|
280
|
+
// extra for a non-ollama agent — but `registerOllamaModel`'s own dynamic
|
|
281
|
+
// imports are NOT a load-time saving for the ollama case either (this
|
|
282
|
+
// module's own unconditional `@flue/runtime/node` import already pulls in
|
|
283
|
+
// pi-ai's full runtime for every run; see ollama_provider.ts's header
|
|
284
|
+
// comment for the measured numbers). What the lazy boundary actually buys
|
|
285
|
+
// is keeping ollama-only symbols off the module graph of anything that
|
|
286
|
+
// imports this module for `resolveModel()` alone (doctor.ts, interview.ts)
|
|
287
|
+
// without ever dispatching an ollama call.
|
|
288
|
+
const [provider, modelId] = resolveModel(request.model);
|
|
289
|
+
if (provider === "ollama")
|
|
290
|
+
await registerOllamaModel(modelId);
|
|
268
291
|
await ensureRuntime(request.flue_db_path);
|
|
269
292
|
REGISTRY.set(request.session_id, {
|
|
270
293
|
model: request.model,
|
|
@@ -274,6 +297,7 @@ export async function run(request, onEvent, onSpawn, onExit) {
|
|
|
274
297
|
systemText: request.system_prompt,
|
|
275
298
|
outputSchema: request.output_schema,
|
|
276
299
|
outputTypeName: request.output_type_name,
|
|
300
|
+
env: request.env ?? operatorEnv(),
|
|
277
301
|
});
|
|
278
302
|
const pid = process.pid ?? -1;
|
|
279
303
|
onSpawn?.(pid);
|
package/dist/core/agents.d.ts
CHANGED
|
@@ -8,6 +8,14 @@
|
|
|
8
8
|
* disposes.
|
|
9
9
|
*/
|
|
10
10
|
import { GateReport, makeEventRecord, type AgentCall, type AgentConfig, type EnvelopeBase, type Phase, type SFConfig } from "./data_types.ts";
|
|
11
|
+
/**
|
|
12
|
+
* `undefined` (no `env_allowlist` configured — the default) means "don't
|
|
13
|
+
* filter at all"; both backends treat that as `request.env ?? operatorEnv()`,
|
|
14
|
+
* i.e. today's unfiltered behavior, byte-identical. Configured, this filters
|
|
15
|
+
* the operator's own environment down to the named keys plus the baseline
|
|
16
|
+
* `local()` would keep anyway.
|
|
17
|
+
*/
|
|
18
|
+
export declare function agentEnv(agent: AgentConfig): Record<string, string> | undefined;
|
|
11
19
|
export declare class GateFailure extends Error {
|
|
12
20
|
}
|
|
13
21
|
/**
|
package/dist/core/agents.js
CHANGED
|
@@ -17,8 +17,32 @@ import * as paths from "./paths.js";
|
|
|
17
17
|
import * as permissions from "./permissions.js";
|
|
18
18
|
import * as prompts from "./prompts.js";
|
|
19
19
|
import { GateReport, UsageBreakdown, makeEventRecord, SFConfigSchema, } from "./data_types.js";
|
|
20
|
-
import { newId } from "./utils.js";
|
|
20
|
+
import { newId, operatorEnv } from "./utils.js";
|
|
21
21
|
const JSON_FIX_ATTEMPTS = 2; // continue-with-correction attempts for malformed JSON
|
|
22
|
+
// Kept alongside agent_flue.ts's own comment on this same list — `local()`'s
|
|
23
|
+
// sandbox keeps these regardless of any `env` override, so an allowlist that
|
|
24
|
+
// omitted them would silently lose them there but not on agent_cc.ts's plain
|
|
25
|
+
// `spawn()`. Folding them in here keeps both backends' filtered env identical.
|
|
26
|
+
const ENV_BASELINE_KEYS = ["PATH", "HOME", "USER", "LANG", "TERM", "TMPDIR"];
|
|
27
|
+
/**
|
|
28
|
+
* `undefined` (no `env_allowlist` configured — the default) means "don't
|
|
29
|
+
* filter at all"; both backends treat that as `request.env ?? operatorEnv()`,
|
|
30
|
+
* i.e. today's unfiltered behavior, byte-identical. Configured, this filters
|
|
31
|
+
* the operator's own environment down to the named keys plus the baseline
|
|
32
|
+
* `local()` would keep anyway.
|
|
33
|
+
*/
|
|
34
|
+
export function agentEnv(agent) {
|
|
35
|
+
if (!agent.env_allowlist)
|
|
36
|
+
return undefined;
|
|
37
|
+
const operator = operatorEnv();
|
|
38
|
+
const keep = new Set([...ENV_BASELINE_KEYS, ...agent.env_allowlist]);
|
|
39
|
+
const env = {};
|
|
40
|
+
for (const key of keep) {
|
|
41
|
+
if (operator[key] !== undefined)
|
|
42
|
+
env[key] = operator[key];
|
|
43
|
+
}
|
|
44
|
+
return env;
|
|
45
|
+
}
|
|
22
46
|
export class GateFailure extends Error {
|
|
23
47
|
}
|
|
24
48
|
/**
|
|
@@ -57,7 +81,19 @@ function mergeAgentLists(base, override) {
|
|
|
57
81
|
}
|
|
58
82
|
return merged;
|
|
59
83
|
}
|
|
60
|
-
/**
|
|
84
|
+
/**
|
|
85
|
+
* `defaults`/`observability`/`quality`/`watch`/`notifications`/`review` merge
|
|
86
|
+
* key-by-key; `agents` merges by name.
|
|
87
|
+
*
|
|
88
|
+
* THE SILENT-DROP TRAP (adversarial history, not a hypothetical): this
|
|
89
|
+
* function returns a FIXED-SHAPE object literal — every top-level `SFConfig`
|
|
90
|
+
* key has to be named on BOTH sides of every merged field here, by hand, or
|
|
91
|
+
* a `.spf/spf.config.yaml` value for it is silently discarded before
|
|
92
|
+
* `v.parse(SFConfigSchema, raw)` ever sees it. Adding a key to
|
|
93
|
+
* `SFConfigSchema` without adding it here is exactly that bug — see
|
|
94
|
+
* `data_types.test.ts`'s merge-survival test for `review`, which is the
|
|
95
|
+
* regression guard this comment is here to justify.
|
|
96
|
+
*/
|
|
61
97
|
function mergeRawConfig(base, override) {
|
|
62
98
|
return {
|
|
63
99
|
defaults: { ...(base.defaults || {}), ...(override.defaults || {}) },
|
|
@@ -67,6 +103,9 @@ function mergeRawConfig(base, override) {
|
|
|
67
103
|
// channels is a whole-array replace on override, same as quality.checks —
|
|
68
104
|
// you don't want an override's channels appended to the built-in's.
|
|
69
105
|
notifications: { ...(base.notifications || {}), ...(override.notifications || {}) },
|
|
106
|
+
// review.require_human_signoff / review.signoff_timeout_seconds — see
|
|
107
|
+
// data_types.ts's ReviewConfigSchema doc comment for why this key exists.
|
|
108
|
+
review: { ...(base.review || {}), ...(override.review || {}) },
|
|
70
109
|
agents: mergeAgentLists(base.agents || [], override.agents || []),
|
|
71
110
|
};
|
|
72
111
|
}
|
|
@@ -90,7 +129,7 @@ export function loadConfig(configPaths) {
|
|
|
90
129
|
}
|
|
91
130
|
const defaults = raw.defaults || {};
|
|
92
131
|
for (const agent of raw.agents || []) {
|
|
93
|
-
for (const key of ["coding_agent", "model", "thinking", "color", "tools", "writes"]) {
|
|
132
|
+
for (const key of ["coding_agent", "model", "thinking", "color", "tools", "writes", "env_allowlist"]) {
|
|
94
133
|
if (key in defaults && !(key in agent))
|
|
95
134
|
agent[key] = defaults[key];
|
|
96
135
|
}
|
|
@@ -231,6 +270,7 @@ export async function execute(run, phase, call) {
|
|
|
231
270
|
output_type_name: call.output_type.name,
|
|
232
271
|
cwd: run.repo_root,
|
|
233
272
|
flue_db_path: path.join(run.data_dir, "flue.db"),
|
|
273
|
+
env: agentEnv(agent),
|
|
234
274
|
};
|
|
235
275
|
const forward = eventForwarder(run, phase, agent.name, agent.coding_agent);
|
|
236
276
|
const onSpawn = (pid) => run.tracer.processStart(run.adw_id, "agent", agent.name, pid, `${agent.coding_agent} ${agent.name} ${agent.model}`);
|
|
@@ -376,6 +376,7 @@ export declare const AgentConfigSchema: v.ObjectSchema<{
|
|
|
376
376
|
readonly harness_engineering: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, () => never[]>;
|
|
377
377
|
readonly tools: v.OptionalSchema<v.NullableSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>, undefined>;
|
|
378
378
|
readonly writes: v.OptionalSchema<v.NullableSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>, undefined>;
|
|
379
|
+
readonly env_allowlist: v.OptionalSchema<v.NullableSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>, undefined>;
|
|
379
380
|
}, undefined>;
|
|
380
381
|
export type AgentConfig = v.InferOutput<typeof AgentConfigSchema>;
|
|
381
382
|
export declare const ConfigDefaultsSchema: v.ObjectSchema<{
|
|
@@ -389,9 +390,35 @@ export declare const ConfigDefaultsSchema: v.ObjectSchema<{
|
|
|
389
390
|
readonly data_dir: v.OptionalSchema<v.StringSchema<undefined>, ".spf/data">;
|
|
390
391
|
}, undefined>;
|
|
391
392
|
export type ConfigDefaults = v.InferOutput<typeof ConfigDefaultsSchema>;
|
|
393
|
+
/**
|
|
394
|
+
* OpenTelemetry span export — OFF unless this block exists, and `endpoint` is
|
|
395
|
+
* required BY THIS SCHEMA rather than defaulted, because presence of the block
|
|
396
|
+
* IS the activation switch. No ambient environment variable can turn export on
|
|
397
|
+
* (notably NOT `OTEL_EXPORTER_OTLP_ENDPOINT`): an unrelated shell variable must
|
|
398
|
+
* never become a data-egress switch. See `core/otel.ts`'s header for the full
|
|
399
|
+
* set of constraints, including the attribute allowlist that keeps repo source
|
|
400
|
+
* code (tool args, prompts, envelopes, the request text) off the wire.
|
|
401
|
+
*
|
|
402
|
+
* `endpoint` is URL-validated so a typo fails at config load rather than as a
|
|
403
|
+
* silent per-run export failure. Either the full OTLP traces path
|
|
404
|
+
* (`https://collector:4318/v1/traces`) or a bare origin (`/v1/traces` is
|
|
405
|
+
* appended — see `resolveTracesUrl`). `headers` is where a collector's auth
|
|
406
|
+
* token goes; its VALUES are treated as secrets and never logged.
|
|
407
|
+
*/
|
|
408
|
+
export declare const OTelConfigSchema: v.ObjectSchema<{
|
|
409
|
+
readonly endpoint: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.UrlAction<string, undefined>]>;
|
|
410
|
+
readonly headers: v.OptionalSchema<v.RecordSchema<v.StringSchema<undefined>, v.StringSchema<undefined>, undefined>, undefined>;
|
|
411
|
+
readonly service_name: v.OptionalSchema<v.StringSchema<undefined>, "spf">;
|
|
412
|
+
}, undefined>;
|
|
413
|
+
export type OTelConfig = v.InferOutput<typeof OTelConfigSchema>;
|
|
392
414
|
export declare const ObservabilityConfigSchema: v.ObjectSchema<{
|
|
393
415
|
readonly db: v.OptionalSchema<v.StringSchema<undefined>, ".spf/data/spf.db">;
|
|
394
416
|
readonly poll_ms: v.OptionalSchema<v.NumberSchema<undefined>, 500>;
|
|
417
|
+
readonly otel: v.OptionalSchema<v.ObjectSchema<{
|
|
418
|
+
readonly endpoint: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.UrlAction<string, undefined>]>;
|
|
419
|
+
readonly headers: v.OptionalSchema<v.RecordSchema<v.StringSchema<undefined>, v.StringSchema<undefined>, undefined>, undefined>;
|
|
420
|
+
readonly service_name: v.OptionalSchema<v.StringSchema<undefined>, "spf">;
|
|
421
|
+
}, undefined>, undefined>;
|
|
395
422
|
}, undefined>;
|
|
396
423
|
export type ObservabilityConfig = v.InferOutput<typeof ObservabilityConfigSchema>;
|
|
397
424
|
/**
|
|
@@ -409,8 +436,25 @@ export type ObservabilityConfig = v.InferOutput<typeof ObservabilityConfigSchema
|
|
|
409
436
|
* watch` startup, not here — an empty string parses fine (this schema has
|
|
410
437
|
* no opinion on whether watch is even configured), matching the same
|
|
411
438
|
* "fails loudly before anything spawns, not eagerly at parse time" pattern
|
|
412
|
-
* `quality:` already uses.
|
|
413
|
-
*
|
|
439
|
+
* `quality:` already uses. `repo` always names `code_host`'s own repo —
|
|
440
|
+
* its shape depends on `code_host`: "owner/name" for github,
|
|
441
|
+
* "workspace/repo_slug" for bitbucket. `resolveCodeHostProvider`
|
|
442
|
+
* (`cli/commands/watch.ts`) always reads it.
|
|
443
|
+
*
|
|
444
|
+
* `issue_repo` exists for exactly one combination where that single field
|
|
445
|
+
* stops being enough: `issue_provider: github` with `code_host: bitbucket`
|
|
446
|
+
* — GitHub-issues-against-a-Bitbucket-repo, a real setup this project
|
|
447
|
+
* explicitly supports, where the issue tracker and the code host are
|
|
448
|
+
* genuinely different repos in different systems, not the same repo worn
|
|
449
|
+
* two ways. Every other combination stays a single field: `issue_provider:
|
|
450
|
+
* github` + `code_host: github` is one repo by construction; `issue_provider:
|
|
451
|
+
* jira` never reads `repo` at all (it uses `jira.base_url`/`project_key`),
|
|
452
|
+
* so `repo` unambiguously belongs to whichever code host is configured.
|
|
453
|
+
* `resolveIssueProvider`'s github branch and `core/refine.ts`'s
|
|
454
|
+
* `resolveAuthoringProvider` (issue authoring always targets the issue
|
|
455
|
+
* tracker, never the code host) both read `issue_repo || repo` — leaving
|
|
456
|
+
* `issue_repo` unset is a complete no-op, so no existing config needs to
|
|
457
|
+
* change.
|
|
414
458
|
*/
|
|
415
459
|
export declare const WatchIssueProviderSchema: v.PicklistSchema<["github", "jira"], undefined>;
|
|
416
460
|
export type WatchIssueProviderKind = v.InferOutput<typeof WatchIssueProviderSchema>;
|
|
@@ -442,6 +486,7 @@ export declare const WatchConfigSchema: v.ObjectSchema<{
|
|
|
442
486
|
readonly issue_provider: v.OptionalSchema<v.PicklistSchema<["github", "jira"], undefined>, "github">;
|
|
443
487
|
readonly code_host: v.OptionalSchema<v.PicklistSchema<["github", "bitbucket"], undefined>, "github">;
|
|
444
488
|
readonly repo: v.OptionalSchema<v.StringSchema<undefined>, "">;
|
|
489
|
+
readonly issue_repo: v.OptionalSchema<v.StringSchema<undefined>, "">;
|
|
445
490
|
readonly label_prefix: v.OptionalSchema<v.StringSchema<undefined>, "spf">;
|
|
446
491
|
readonly chain: v.OptionalSchema<v.StringSchema<undefined>, "plan-build-test">;
|
|
447
492
|
readonly base_branch: v.OptionalSchema<v.StringSchema<undefined>, "main">;
|
|
@@ -501,6 +546,34 @@ export declare const NotificationsConfigSchema: v.ObjectSchema<{
|
|
|
501
546
|
}, undefined>, undefined>, () => never[]>;
|
|
502
547
|
}, undefined>;
|
|
503
548
|
export type NotificationsConfig = v.InferOutput<typeof NotificationsConfigSchema>;
|
|
549
|
+
/**
|
|
550
|
+
* `simple_sdlc`'s human-signoff gate — see the ACCEPTED ADVERSARIAL
|
|
551
|
+
* AMENDMENTS on the review-accountability thread. `simple_sdlc.ts`'s
|
|
552
|
+
* `commit_build` predicate is the ONLY place in this codebase where an AI
|
|
553
|
+
* reviewer's `approved` flag gates a commit; `decideSignoff` (same file)
|
|
554
|
+
* turns that PROPOSAL into a human's DISPOSAL wherever a human is at the
|
|
555
|
+
* keyboard, and reads these two knobs when there isn't one.
|
|
556
|
+
*
|
|
557
|
+
* `require_human_signoff` defaults to FALSE for this release, deliberately:
|
|
558
|
+
* flipping it to fail-closed-by-default would break every unattended
|
|
559
|
+
* `simple-sdlc` run (`spf watch`, CI) the day this shipped, before `spf
|
|
560
|
+
* watch` itself is signoff-aware (its own human gate today is the PR merge,
|
|
561
|
+
* now informed by the reviewer digest — see `cli/commands/watch.ts`). An
|
|
562
|
+
* unattended run instead proceeds on the AI verdict alone with a LOUD
|
|
563
|
+
* one-time warning (`decideSignoff`'s `AI_ONLY_SIGNOFF_WARNING`) until that
|
|
564
|
+
* changes. Set `true` and an unattended run fails the phase CLOSED instead —
|
|
565
|
+
* see `decideSignoff`'s fail-closed branch — rather than silently
|
|
566
|
+
* auto-approving because nobody typed at a prompt that was never shown.
|
|
567
|
+
*
|
|
568
|
+
* `signoff_timeout_seconds` bounds the interactive prompt itself: expiry
|
|
569
|
+
* means NOT accepted (the confirm's own default), never an unbounded stdin
|
|
570
|
+
* read inside `run.phase()` — see `cli/ask.ts`'s `confirm(..., {timeoutMs})`.
|
|
571
|
+
*/
|
|
572
|
+
export declare const ReviewConfigSchema: v.ObjectSchema<{
|
|
573
|
+
readonly require_human_signoff: v.OptionalSchema<v.BooleanSchema<undefined>, false>;
|
|
574
|
+
readonly signoff_timeout_seconds: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.MinValueAction<number, 1, undefined>]>, 300>;
|
|
575
|
+
}, undefined>;
|
|
576
|
+
export type ReviewConfig = v.InferOutput<typeof ReviewConfigSchema>;
|
|
504
577
|
export declare const SFConfigSchema: v.ObjectSchema<{
|
|
505
578
|
readonly defaults: v.OptionalSchema<v.ObjectSchema<{
|
|
506
579
|
readonly coding_agent: v.OptionalSchema<v.PicklistSchema<["flue", "claude_code"], undefined>, "flue">;
|
|
@@ -524,9 +597,21 @@ export declare const SFConfigSchema: v.ObjectSchema<{
|
|
|
524
597
|
readonly observability: v.OptionalSchema<v.ObjectSchema<{
|
|
525
598
|
readonly db: v.OptionalSchema<v.StringSchema<undefined>, ".spf/data/spf.db">;
|
|
526
599
|
readonly poll_ms: v.OptionalSchema<v.NumberSchema<undefined>, 500>;
|
|
600
|
+
readonly otel: v.OptionalSchema<v.ObjectSchema<{
|
|
601
|
+
readonly endpoint: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.UrlAction<string, undefined>]>;
|
|
602
|
+
readonly headers: v.OptionalSchema<v.RecordSchema<v.StringSchema<undefined>, v.StringSchema<undefined>, undefined>, undefined>;
|
|
603
|
+
readonly service_name: v.OptionalSchema<v.StringSchema<undefined>, "spf">;
|
|
604
|
+
}, undefined>, undefined>;
|
|
527
605
|
}, undefined>, () => {
|
|
528
606
|
db: string;
|
|
529
607
|
poll_ms: number;
|
|
608
|
+
otel?: {
|
|
609
|
+
endpoint: string;
|
|
610
|
+
headers?: {
|
|
611
|
+
[x: string]: string;
|
|
612
|
+
} | undefined;
|
|
613
|
+
service_name: string;
|
|
614
|
+
} | undefined;
|
|
530
615
|
}>;
|
|
531
616
|
readonly agents: v.OptionalSchema<v.ArraySchema<v.ObjectSchema<{
|
|
532
617
|
readonly name: v.StringSchema<undefined>;
|
|
@@ -542,6 +627,7 @@ export declare const SFConfigSchema: v.ObjectSchema<{
|
|
|
542
627
|
readonly harness_engineering: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, () => never[]>;
|
|
543
628
|
readonly tools: v.OptionalSchema<v.NullableSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>, undefined>;
|
|
544
629
|
readonly writes: v.OptionalSchema<v.NullableSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>, undefined>;
|
|
630
|
+
readonly env_allowlist: v.OptionalSchema<v.NullableSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>, undefined>;
|
|
545
631
|
}, undefined>, undefined>, () => never[]>;
|
|
546
632
|
readonly quality: v.OptionalSchema<v.ObjectSchema<{
|
|
547
633
|
readonly checks: v.OptionalSchema<v.ArraySchema<v.ObjectSchema<{
|
|
@@ -568,6 +654,7 @@ export declare const SFConfigSchema: v.ObjectSchema<{
|
|
|
568
654
|
readonly issue_provider: v.OptionalSchema<v.PicklistSchema<["github", "jira"], undefined>, "github">;
|
|
569
655
|
readonly code_host: v.OptionalSchema<v.PicklistSchema<["github", "bitbucket"], undefined>, "github">;
|
|
570
656
|
readonly repo: v.OptionalSchema<v.StringSchema<undefined>, "">;
|
|
657
|
+
readonly issue_repo: v.OptionalSchema<v.StringSchema<undefined>, "">;
|
|
571
658
|
readonly label_prefix: v.OptionalSchema<v.StringSchema<undefined>, "spf">;
|
|
572
659
|
readonly chain: v.OptionalSchema<v.StringSchema<undefined>, "plan-build-test">;
|
|
573
660
|
readonly base_branch: v.OptionalSchema<v.StringSchema<undefined>, "main">;
|
|
@@ -593,6 +680,7 @@ export declare const SFConfigSchema: v.ObjectSchema<{
|
|
|
593
680
|
issue_provider: "github" | "jira";
|
|
594
681
|
code_host: "bitbucket" | "github";
|
|
595
682
|
repo: string;
|
|
683
|
+
issue_repo: string;
|
|
596
684
|
label_prefix: string;
|
|
597
685
|
chain: string;
|
|
598
686
|
base_branch: string;
|
|
@@ -627,13 +715,24 @@ export declare const SFConfigSchema: v.ObjectSchema<{
|
|
|
627
715
|
name: string;
|
|
628
716
|
}[];
|
|
629
717
|
}>;
|
|
718
|
+
readonly review: v.OptionalSchema<v.ObjectSchema<{
|
|
719
|
+
readonly require_human_signoff: v.OptionalSchema<v.BooleanSchema<undefined>, false>;
|
|
720
|
+
readonly signoff_timeout_seconds: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.MinValueAction<number, 1, undefined>]>, 300>;
|
|
721
|
+
}, undefined>, () => {
|
|
722
|
+
require_human_signoff: boolean;
|
|
723
|
+
signoff_timeout_seconds: number;
|
|
724
|
+
}>;
|
|
630
725
|
}, undefined>;
|
|
631
726
|
export type SFConfig = v.InferOutput<typeof SFConfigSchema>;
|
|
727
|
+
/** The full set of event kinds any tracer.event()/makeEventRecord() call site emits — kept in sync with src/ui/shared/types.ts's EventType. */
|
|
728
|
+
export declare const EVENT_RECORD_TYPES: readonly ["phase_start", "agent_start", "tool_call", "handoff", "gate_pass", "gate_fail", "log", "agent_end", "phase_end", "error"];
|
|
729
|
+
export declare const EventRecordTypeSchema: v.PicklistSchema<readonly ["phase_start", "agent_start", "tool_call", "handoff", "gate_pass", "gate_fail", "log", "agent_end", "phase_end", "error"], undefined>;
|
|
730
|
+
export type EventRecordType = v.InferOutput<typeof EventRecordTypeSchema>;
|
|
632
731
|
/** One traced event, always logged against adw_id + phase. */
|
|
633
732
|
export interface EventRecord {
|
|
634
733
|
adw_id: string;
|
|
635
734
|
phase_id: string;
|
|
636
|
-
type:
|
|
735
|
+
type: EventRecordType;
|
|
637
736
|
name: string;
|
|
638
737
|
payload: Record<string, unknown>;
|
|
639
738
|
parent_id: string;
|
|
@@ -643,7 +742,7 @@ export interface EventRecord {
|
|
|
643
742
|
}
|
|
644
743
|
export declare function makeEventRecord(input: Partial<EventRecord> & {
|
|
645
744
|
adw_id: string;
|
|
646
|
-
type:
|
|
745
|
+
type: EventRecordType;
|
|
647
746
|
}): EventRecord;
|
|
648
747
|
/**
|
|
649
748
|
* Everything one coding-agent dispatch+read turn needs, whichever backend
|
|
@@ -675,6 +774,7 @@ export interface AgentRequest {
|
|
|
675
774
|
output_type_name: string;
|
|
676
775
|
cwd: string;
|
|
677
776
|
flue_db_path: string;
|
|
777
|
+
env?: Record<string, string>;
|
|
678
778
|
}
|
|
679
779
|
/**
|
|
680
780
|
* Tokens and the dollars they cost, per component, summed over a call.
|
package/dist/core/data_types.js
CHANGED
|
@@ -281,6 +281,15 @@ export const AgentConfigSchema = v.object({
|
|
|
281
281
|
// [...] -> only these. A trailing "/" means a directory prefix; a "*"
|
|
282
282
|
// makes it a glob; anything else is an exact path.
|
|
283
283
|
writes: v.optional(v.nullable(v.array(v.string()))),
|
|
284
|
+
// Opt-in env allowlist for this agent's subprocess/sandbox environment.
|
|
285
|
+
// undefined/null (the default) -> unrestricted: the full operator
|
|
286
|
+
// environment is passed through, byte-identical to before this field
|
|
287
|
+
// existed — same three-state shape as `writes` above, so `null` (the
|
|
288
|
+
// spelling config.md teaches for "unrestricted") parses instead of
|
|
289
|
+
// rejecting. Set -> only these keys, plus the baseline
|
|
290
|
+
// (PATH/HOME/USER/LANG/TERM/TMPDIR) that agent_flue.ts's local() sandbox
|
|
291
|
+
// would keep anyway.
|
|
292
|
+
env_allowlist: v.optional(v.nullable(v.array(v.string()))),
|
|
284
293
|
});
|
|
285
294
|
export const ConfigDefaultsSchema = v.object({
|
|
286
295
|
coding_agent: v.optional(v.picklist(["flue", "claude_code"]), "flue"),
|
|
@@ -296,9 +305,36 @@ export const ConfigDefaultsSchema = v.object({
|
|
|
296
305
|
protected_files: v.optional(v.array(v.string()), () => [".spf/", "spf.config.yaml"]),
|
|
297
306
|
data_dir: v.optional(v.string(), ".spf/data"),
|
|
298
307
|
});
|
|
308
|
+
/**
|
|
309
|
+
* OpenTelemetry span export — OFF unless this block exists, and `endpoint` is
|
|
310
|
+
* required BY THIS SCHEMA rather than defaulted, because presence of the block
|
|
311
|
+
* IS the activation switch. No ambient environment variable can turn export on
|
|
312
|
+
* (notably NOT `OTEL_EXPORTER_OTLP_ENDPOINT`): an unrelated shell variable must
|
|
313
|
+
* never become a data-egress switch. See `core/otel.ts`'s header for the full
|
|
314
|
+
* set of constraints, including the attribute allowlist that keeps repo source
|
|
315
|
+
* code (tool args, prompts, envelopes, the request text) off the wire.
|
|
316
|
+
*
|
|
317
|
+
* `endpoint` is URL-validated so a typo fails at config load rather than as a
|
|
318
|
+
* silent per-run export failure. Either the full OTLP traces path
|
|
319
|
+
* (`https://collector:4318/v1/traces`) or a bare origin (`/v1/traces` is
|
|
320
|
+
* appended — see `resolveTracesUrl`). `headers` is where a collector's auth
|
|
321
|
+
* token goes; its VALUES are treated as secrets and never logged.
|
|
322
|
+
*/
|
|
323
|
+
export const OTelConfigSchema = v.object({
|
|
324
|
+
endpoint: v.pipe(v.string(), v.url()),
|
|
325
|
+
headers: v.optional(v.record(v.string(), v.string()), undefined),
|
|
326
|
+
service_name: v.optional(v.string(), "spf"),
|
|
327
|
+
});
|
|
299
328
|
export const ObservabilityConfigSchema = v.object({
|
|
300
329
|
db: v.optional(v.string(), ".spf/data/spf.db"),
|
|
301
330
|
poll_ms: v.optional(v.number(), 500),
|
|
331
|
+
// Absent by default. `agents.ts`'s mergeRawConfig spreads `observability`
|
|
332
|
+
// field-by-field, so this nested object merges as a WHOLE-OBJECT replace on
|
|
333
|
+
// override — an override that sets `otel:` replaces the base's entirely,
|
|
334
|
+
// which is the semantics you want for an endpoint + its headers (a
|
|
335
|
+
// half-merged pair of the two would send tokens to the wrong collector).
|
|
336
|
+
// Pinned by a merge-survival test in src/test/data_types.test.ts.
|
|
337
|
+
otel: v.optional(OTelConfigSchema),
|
|
302
338
|
});
|
|
303
339
|
/**
|
|
304
340
|
* `spf watch`'s configuration. `issue_provider` (the tracker) and
|
|
@@ -315,8 +351,25 @@ export const ObservabilityConfigSchema = v.object({
|
|
|
315
351
|
* watch` startup, not here — an empty string parses fine (this schema has
|
|
316
352
|
* no opinion on whether watch is even configured), matching the same
|
|
317
353
|
* "fails loudly before anything spawns, not eagerly at parse time" pattern
|
|
318
|
-
* `quality:` already uses.
|
|
319
|
-
*
|
|
354
|
+
* `quality:` already uses. `repo` always names `code_host`'s own repo —
|
|
355
|
+
* its shape depends on `code_host`: "owner/name" for github,
|
|
356
|
+
* "workspace/repo_slug" for bitbucket. `resolveCodeHostProvider`
|
|
357
|
+
* (`cli/commands/watch.ts`) always reads it.
|
|
358
|
+
*
|
|
359
|
+
* `issue_repo` exists for exactly one combination where that single field
|
|
360
|
+
* stops being enough: `issue_provider: github` with `code_host: bitbucket`
|
|
361
|
+
* — GitHub-issues-against-a-Bitbucket-repo, a real setup this project
|
|
362
|
+
* explicitly supports, where the issue tracker and the code host are
|
|
363
|
+
* genuinely different repos in different systems, not the same repo worn
|
|
364
|
+
* two ways. Every other combination stays a single field: `issue_provider:
|
|
365
|
+
* github` + `code_host: github` is one repo by construction; `issue_provider:
|
|
366
|
+
* jira` never reads `repo` at all (it uses `jira.base_url`/`project_key`),
|
|
367
|
+
* so `repo` unambiguously belongs to whichever code host is configured.
|
|
368
|
+
* `resolveIssueProvider`'s github branch and `core/refine.ts`'s
|
|
369
|
+
* `resolveAuthoringProvider` (issue authoring always targets the issue
|
|
370
|
+
* tracker, never the code host) both read `issue_repo || repo` — leaving
|
|
371
|
+
* `issue_repo` unset is a complete no-op, so no existing config needs to
|
|
372
|
+
* change.
|
|
320
373
|
*/
|
|
321
374
|
export const WatchIssueProviderSchema = v.picklist(["github", "jira"]);
|
|
322
375
|
export const WatchCodeHostSchema = v.picklist(["github", "bitbucket"]);
|
|
@@ -344,6 +397,7 @@ export const WatchConfigSchema = v.object({
|
|
|
344
397
|
issue_provider: v.optional(WatchIssueProviderSchema, "github"),
|
|
345
398
|
code_host: v.optional(WatchCodeHostSchema, "github"),
|
|
346
399
|
repo: v.optional(v.string(), ""),
|
|
400
|
+
issue_repo: v.optional(v.string(), ""),
|
|
347
401
|
label_prefix: v.optional(v.string(), "spf"),
|
|
348
402
|
chain: v.optional(v.string(), "plan-build-test"),
|
|
349
403
|
base_branch: v.optional(v.string(), "main"),
|
|
@@ -381,6 +435,33 @@ export const NotificationsConfigSchema = v.object({
|
|
|
381
435
|
timeout_ms: v.optional(v.number(), 5_000),
|
|
382
436
|
channels: v.optional(v.array(NotifyChannelSchema), () => []),
|
|
383
437
|
});
|
|
438
|
+
/**
|
|
439
|
+
* `simple_sdlc`'s human-signoff gate — see the ACCEPTED ADVERSARIAL
|
|
440
|
+
* AMENDMENTS on the review-accountability thread. `simple_sdlc.ts`'s
|
|
441
|
+
* `commit_build` predicate is the ONLY place in this codebase where an AI
|
|
442
|
+
* reviewer's `approved` flag gates a commit; `decideSignoff` (same file)
|
|
443
|
+
* turns that PROPOSAL into a human's DISPOSAL wherever a human is at the
|
|
444
|
+
* keyboard, and reads these two knobs when there isn't one.
|
|
445
|
+
*
|
|
446
|
+
* `require_human_signoff` defaults to FALSE for this release, deliberately:
|
|
447
|
+
* flipping it to fail-closed-by-default would break every unattended
|
|
448
|
+
* `simple-sdlc` run (`spf watch`, CI) the day this shipped, before `spf
|
|
449
|
+
* watch` itself is signoff-aware (its own human gate today is the PR merge,
|
|
450
|
+
* now informed by the reviewer digest — see `cli/commands/watch.ts`). An
|
|
451
|
+
* unattended run instead proceeds on the AI verdict alone with a LOUD
|
|
452
|
+
* one-time warning (`decideSignoff`'s `AI_ONLY_SIGNOFF_WARNING`) until that
|
|
453
|
+
* changes. Set `true` and an unattended run fails the phase CLOSED instead —
|
|
454
|
+
* see `decideSignoff`'s fail-closed branch — rather than silently
|
|
455
|
+
* auto-approving because nobody typed at a prompt that was never shown.
|
|
456
|
+
*
|
|
457
|
+
* `signoff_timeout_seconds` bounds the interactive prompt itself: expiry
|
|
458
|
+
* means NOT accepted (the confirm's own default), never an unbounded stdin
|
|
459
|
+
* read inside `run.phase()` — see `cli/ask.ts`'s `confirm(..., {timeoutMs})`.
|
|
460
|
+
*/
|
|
461
|
+
export const ReviewConfigSchema = v.object({
|
|
462
|
+
require_human_signoff: v.optional(v.boolean(), false),
|
|
463
|
+
signoff_timeout_seconds: v.optional(v.pipe(v.number(), v.minValue(1)), 300),
|
|
464
|
+
});
|
|
384
465
|
export const SFConfigSchema = v.object({
|
|
385
466
|
defaults: v.optional(ConfigDefaultsSchema, () => v.parse(ConfigDefaultsSchema, {})),
|
|
386
467
|
observability: v.optional(ObservabilityConfigSchema, () => v.parse(ObservabilityConfigSchema, {})),
|
|
@@ -388,7 +469,23 @@ export const SFConfigSchema = v.object({
|
|
|
388
469
|
quality: v.optional(QualityConfigSchema, () => v.parse(QualityConfigSchema, {})),
|
|
389
470
|
watch: v.optional(WatchConfigSchema, () => v.parse(WatchConfigSchema, {})),
|
|
390
471
|
notifications: v.optional(NotificationsConfigSchema, () => v.parse(NotificationsConfigSchema, {})),
|
|
472
|
+
review: v.optional(ReviewConfigSchema, () => v.parse(ReviewConfigSchema, {})),
|
|
391
473
|
});
|
|
474
|
+
// ── Tracing ──────────────────────────────────────────────────────────────────
|
|
475
|
+
/** The full set of event kinds any tracer.event()/makeEventRecord() call site emits — kept in sync with src/ui/shared/types.ts's EventType. */
|
|
476
|
+
export const EVENT_RECORD_TYPES = [
|
|
477
|
+
"phase_start",
|
|
478
|
+
"agent_start",
|
|
479
|
+
"tool_call",
|
|
480
|
+
"handoff",
|
|
481
|
+
"gate_pass",
|
|
482
|
+
"gate_fail",
|
|
483
|
+
"log",
|
|
484
|
+
"agent_end",
|
|
485
|
+
"phase_end",
|
|
486
|
+
"error",
|
|
487
|
+
];
|
|
488
|
+
export const EventRecordTypeSchema = v.picklist(EVENT_RECORD_TYPES);
|
|
392
489
|
export function makeEventRecord(input) {
|
|
393
490
|
return {
|
|
394
491
|
phase_id: "",
|