@gr8ful/spf 0.5.1 → 0.6.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 +61 -17
- package/assets/defaults/spf.config.yaml +68 -0
- package/assets/prompts/refiner/system.md +32 -6
- package/assets/prompts/refiner/user.md +37 -5
- package/assets/skill/SKILL.md +1 -0
- package/assets/skill/references/config.md +153 -4
- package/assets/templates/ts-flue-ollama.spf.config.yaml +25 -0
- package/assets/templates/ts.spf.config.yaml +6 -0
- package/dist/chains/index.d.ts +11 -0
- package/dist/chains/index.js +38 -3
- package/dist/chains/repo_chains.js +1 -0
- package/dist/chains/simple_sdlc.js +1 -1
- package/dist/chains/steps.d.ts +27 -9
- package/dist/chains/steps.js +82 -13
- package/dist/cli/commands/doctor.js +108 -1
- package/dist/cli/commands/estimate.d.ts +82 -0
- package/dist/cli/commands/estimate.js +317 -0
- package/dist/cli/commands/fanout.d.ts +40 -0
- package/dist/cli/commands/fanout.js +401 -0
- package/dist/cli/commands/init.js +19 -0
- package/dist/cli/commands/trace.d.ts +18 -0
- package/dist/cli/commands/trace.js +22 -3
- package/dist/cli/commands/watch.js +40 -26
- package/dist/cli/index.js +12 -0
- package/dist/core/agents.d.ts +56 -0
- package/dist/core/agents.js +152 -1
- package/dist/core/data_types.d.ts +232 -0
- package/dist/core/data_types.js +135 -0
- package/dist/core/fanout.d.ts +229 -0
- package/dist/core/fanout.js +313 -0
- package/dist/core/gates.d.ts +8 -0
- package/dist/core/gates.js +24 -1
- package/dist/core/git_helper.d.ts +98 -0
- package/dist/core/git_helper.js +127 -0
- package/dist/core/issues/github_provider.d.ts +7 -1
- package/dist/core/issues/github_provider.js +33 -2
- package/dist/core/issues/jira_provider.d.ts +5 -1
- package/dist/core/issues/jira_provider.js +26 -3
- package/dist/core/issues/provider.d.ts +53 -2
- package/dist/core/notify/channel.d.ts +1 -1
- package/dist/core/notify/notifier.d.ts +16 -0
- package/dist/core/notify/notifier.js +36 -0
- package/dist/core/otel.d.ts +64 -9
- package/dist/core/otel.js +78 -14
- package/dist/core/runner.d.ts +8 -0
- package/dist/core/runner.js +7 -0
- package/dist/core/session.d.ts +25 -0
- package/dist/core/session.js +97 -28
- package/dist/core/tiering.d.ts +145 -0
- package/dist/core/tiering.js +235 -0
- package/dist/core/tracer.d.ts +10 -0
- package/dist/core/tracer.js +12 -0
- package/dist/core/watch.d.ts +89 -9
- package/dist/core/watch.js +193 -19
- package/dist/ui/server/db.d.ts +39 -0
- package/dist/ui/server/db.js +61 -0
- package/package.json +2 -1
- package/dist/test/agent_cc.test.d.ts +0 -1
- package/dist/test/agent_cc.test.js +0 -95
- package/dist/test/agent_flue.test.d.ts +0 -1
- package/dist/test/agent_flue.test.js +0 -83
- package/dist/test/chains.test.d.ts +0 -12
- package/dist/test/chains.test.js +0 -92
- package/dist/test/data_types.test.d.ts +0 -10
- package/dist/test/data_types.test.js +0 -220
- package/dist/test/env_file.test.d.ts +0 -1
- package/dist/test/env_file.test.js +0 -74
- package/dist/test/fake_asker.d.ts +0 -23
- package/dist/test/fake_asker.js +0 -30
- package/dist/test/git_helper.test.d.ts +0 -1
- package/dist/test/git_helper.test.js +0 -59
- package/dist/test/hermetic_git.d.ts +0 -1
- package/dist/test/hermetic_git.js +0 -22
- package/dist/test/init_command.test.d.ts +0 -14
- package/dist/test/init_command.test.js +0 -136
- package/dist/test/interview.test.d.ts +0 -15
- package/dist/test/interview.test.js +0 -425
- package/dist/test/notify.test.d.ts +0 -1
- package/dist/test/notify.test.js +0 -174
- package/dist/test/ollama_provider.test.d.ts +0 -1
- package/dist/test/ollama_provider.test.js +0 -103
- package/dist/test/otel.test.d.ts +0 -26
- package/dist/test/otel.test.js +0 -512
- package/dist/test/paths.test.d.ts +0 -1
- package/dist/test/paths.test.js +0 -68
- package/dist/test/refine.test.d.ts +0 -1
- package/dist/test/refine.test.js +0 -189
- package/dist/test/repo_chains.test.d.ts +0 -21
- package/dist/test/repo_chains.test.js +0 -416
- package/dist/test/signoff.test.d.ts +0 -1
- package/dist/test/signoff.test.js +0 -329
- package/dist/test/ui_server.test.d.ts +0 -7
- package/dist/test/ui_server.test.js +0 -120
- package/dist/test/watch.test.d.ts +0 -1
- package/dist/test/watch.test.js +0 -687
package/dist/core/agents.d.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* with a correction — context intact, bounded retries. Agent proposes, code
|
|
8
8
|
* disposes.
|
|
9
9
|
*/
|
|
10
|
+
import { type TierResolution } from "./tiering.ts";
|
|
10
11
|
import { GateReport, makeEventRecord, type AgentCall, type AgentConfig, type EnvelopeBase, type Phase, type SFConfig } from "./data_types.ts";
|
|
11
12
|
/**
|
|
12
13
|
* `undefined` (no `env_allowlist` configured — the default) means "don't
|
|
@@ -18,6 +19,51 @@ import { GateReport, makeEventRecord, type AgentCall, type AgentConfig, type Env
|
|
|
18
19
|
export declare function agentEnv(agent: AgentConfig): Record<string, string> | undefined;
|
|
19
20
|
export declare class GateFailure extends Error {
|
|
20
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* A run that has reached (or passed) one of `defaults`' budget ceilings.
|
|
24
|
+
*
|
|
25
|
+
* Its own class, like `GateFailure`, so a caller can tell "this run was cut
|
|
26
|
+
* off on purpose" from "the agent broke" — thrown from `send()` below, which
|
|
27
|
+
* puts it inside `run.phase()`'s try/catch, so the phase records `fail` and
|
|
28
|
+
* the run exits non-zero. That is the intended outcome: fail CLOSED. A budget
|
|
29
|
+
* that logged a warning and kept spending would not be a budget.
|
|
30
|
+
*/
|
|
31
|
+
export declare class BudgetExceeded extends Error {
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* `$0.412` — three decimals, with a trailing zero trimmed so a round ceiling
|
|
35
|
+
* reads as the operator wrote it (`$0.40`, not `$0.400`). Cents are not
|
|
36
|
+
* enough precision here: a single cheap call is often sub-cent, and a budget
|
|
37
|
+
* message that says `$0.00 of max_run_cost $0.40` tells nobody anything.
|
|
38
|
+
*/
|
|
39
|
+
export declare function formatUsd(value: number): string;
|
|
40
|
+
/**
|
|
41
|
+
* The accumulating totals a budget check reads. Structurally a subset of
|
|
42
|
+
* `Run` (`core/runner.ts`) — `run.tokens`/`run.cost` are incremented by
|
|
43
|
+
* `run.addUsage()` after every send — so the real `Run` satisfies it with no
|
|
44
|
+
* adapter, and a test can pass a plain object with fake usage.
|
|
45
|
+
*/
|
|
46
|
+
export interface RunBudgetState {
|
|
47
|
+
cfg: SFConfig;
|
|
48
|
+
tokens: number;
|
|
49
|
+
cost: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Throw if this run has spent its budget. Called BEFORE every agent
|
|
53
|
+
* dispatch, never after.
|
|
54
|
+
*
|
|
55
|
+
* WHY BEFORE, AND WHY `>=`: the cost of the call about to happen is
|
|
56
|
+
* unknowable until it finishes, so a ceiling can only ever be enforced as
|
|
57
|
+
* "no further calls" — checked after the fact it would be a report, not a
|
|
58
|
+
* cap. `>=` follows from that: a run that has already spent exactly its
|
|
59
|
+
* ceiling has nothing left to spend, and letting one more (unbounded) call
|
|
60
|
+
* through would make the ceiling soft by exactly one call — which, on a
|
|
61
|
+
* chain whose last phase is the expensive one, is the whole overrun.
|
|
62
|
+
*
|
|
63
|
+
* Both ceilings absent (the default) returns immediately: zero behavior
|
|
64
|
+
* change, no arithmetic, nothing to get wrong.
|
|
65
|
+
*/
|
|
66
|
+
export declare function assertRunBudget(run: RunBudgetState): void;
|
|
21
67
|
/**
|
|
22
68
|
* Load and merge every existing path in `configPaths`, in order — later
|
|
23
69
|
* paths override earlier ones. Built-in defaults first, an optional `.spf/`
|
|
@@ -35,6 +81,9 @@ export declare function validate(cfg: SFConfig, required: string[], requiredSuit
|
|
|
35
81
|
interface RunForAgents {
|
|
36
82
|
cfg: SFConfig;
|
|
37
83
|
adw_id: string;
|
|
84
|
+
/** Run-total tokens/cost so far, mirrored by `addUsage` below — read by `assertRunBudget` before every send. */
|
|
85
|
+
tokens: number;
|
|
86
|
+
cost: number;
|
|
38
87
|
repo_root: string;
|
|
39
88
|
spf_dir: string | null;
|
|
40
89
|
data_dir: string;
|
|
@@ -45,6 +94,13 @@ interface RunForAgents {
|
|
|
45
94
|
model: string;
|
|
46
95
|
coding_agent: string;
|
|
47
96
|
}>;
|
|
97
|
+
/**
|
|
98
|
+
* Set once by `startRun` (`src/chains/steps.ts`), null until then. Optional
|
|
99
|
+
* here — a structural interface must not force every present and future
|
|
100
|
+
* test fake to carry it — and `effectiveAgent`'s own `?? base.model`
|
|
101
|
+
* fallback already handles absence. See `core/tiering.ts`.
|
|
102
|
+
*/
|
|
103
|
+
tiering?: TierResolution | null;
|
|
48
104
|
tracer: {
|
|
49
105
|
event: (record: ReturnType<typeof makeEventRecord>) => string;
|
|
50
106
|
processStart: (adwId: string, kind: string, name: string, pid: number, command: string) => void;
|
package/dist/core/agents.js
CHANGED
|
@@ -16,6 +16,7 @@ import * as agentFlue from "./agent_flue.js";
|
|
|
16
16
|
import * as paths from "./paths.js";
|
|
17
17
|
import * as permissions from "./permissions.js";
|
|
18
18
|
import * as prompts from "./prompts.js";
|
|
19
|
+
import { effectiveAgent } from "./tiering.js";
|
|
19
20
|
import { GateReport, UsageBreakdown, makeEventRecord, SFConfigSchema, } from "./data_types.js";
|
|
20
21
|
import { newId, operatorEnv } from "./utils.js";
|
|
21
22
|
const JSON_FIX_ATTEMPTS = 2; // continue-with-correction attempts for malformed JSON
|
|
@@ -45,6 +46,55 @@ export function agentEnv(agent) {
|
|
|
45
46
|
}
|
|
46
47
|
export class GateFailure extends Error {
|
|
47
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* A run that has reached (or passed) one of `defaults`' budget ceilings.
|
|
51
|
+
*
|
|
52
|
+
* Its own class, like `GateFailure`, so a caller can tell "this run was cut
|
|
53
|
+
* off on purpose" from "the agent broke" — thrown from `send()` below, which
|
|
54
|
+
* puts it inside `run.phase()`'s try/catch, so the phase records `fail` and
|
|
55
|
+
* the run exits non-zero. That is the intended outcome: fail CLOSED. A budget
|
|
56
|
+
* that logged a warning and kept spending would not be a budget.
|
|
57
|
+
*/
|
|
58
|
+
export class BudgetExceeded extends Error {
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* `$0.412` — three decimals, with a trailing zero trimmed so a round ceiling
|
|
62
|
+
* reads as the operator wrote it (`$0.40`, not `$0.400`). Cents are not
|
|
63
|
+
* enough precision here: a single cheap call is often sub-cent, and a budget
|
|
64
|
+
* message that says `$0.00 of max_run_cost $0.40` tells nobody anything.
|
|
65
|
+
*/
|
|
66
|
+
export function formatUsd(value) {
|
|
67
|
+
const fixed = value.toFixed(3);
|
|
68
|
+
return `$${fixed.endsWith("0") ? fixed.slice(0, -1) : fixed}`;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Throw if this run has spent its budget. Called BEFORE every agent
|
|
72
|
+
* dispatch, never after.
|
|
73
|
+
*
|
|
74
|
+
* WHY BEFORE, AND WHY `>=`: the cost of the call about to happen is
|
|
75
|
+
* unknowable until it finishes, so a ceiling can only ever be enforced as
|
|
76
|
+
* "no further calls" — checked after the fact it would be a report, not a
|
|
77
|
+
* cap. `>=` follows from that: a run that has already spent exactly its
|
|
78
|
+
* ceiling has nothing left to spend, and letting one more (unbounded) call
|
|
79
|
+
* through would make the ceiling soft by exactly one call — which, on a
|
|
80
|
+
* chain whose last phase is the expensive one, is the whole overrun.
|
|
81
|
+
*
|
|
82
|
+
* Both ceilings absent (the default) returns immediately: zero behavior
|
|
83
|
+
* change, no arithmetic, nothing to get wrong.
|
|
84
|
+
*/
|
|
85
|
+
export function assertRunBudget(run) {
|
|
86
|
+
const { max_run_cost: maxCost, max_run_tokens: maxTokens } = run.cfg.defaults;
|
|
87
|
+
if (maxCost === undefined && maxTokens === undefined)
|
|
88
|
+
return;
|
|
89
|
+
if (maxCost !== undefined && run.cost >= maxCost) {
|
|
90
|
+
throw new BudgetExceeded(`run budget exceeded: ${formatUsd(run.cost)} of max_run_cost ${formatUsd(maxCost)} — ` +
|
|
91
|
+
`raise defaults.max_run_cost or split the work`);
|
|
92
|
+
}
|
|
93
|
+
if (maxTokens !== undefined && run.tokens >= maxTokens) {
|
|
94
|
+
throw new BudgetExceeded(`run budget exceeded: ${run.tokens.toLocaleString("en-US")} tokens of max_run_tokens ` +
|
|
95
|
+
`${maxTokens.toLocaleString("en-US")} — raise defaults.max_run_tokens or split the work`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
48
98
|
/**
|
|
49
99
|
* A ValiError's own `.message` is only its FIRST issue — fine for a quick
|
|
50
100
|
* console line, not for something a human has to act on or a model has to
|
|
@@ -99,6 +149,15 @@ function mergeRawConfig(base, override) {
|
|
|
99
149
|
defaults: { ...(base.defaults || {}), ...(override.defaults || {}) },
|
|
100
150
|
observability: { ...(base.observability || {}), ...(override.observability || {}) },
|
|
101
151
|
quality: { ...(base.quality || {}), ...(override.quality || {}) },
|
|
152
|
+
// `watch` is spread WHOLE — no per-sub-key enumeration inside it — so
|
|
153
|
+
// any WatchConfigSchema field, present or future (jira, refine,
|
|
154
|
+
// chain_options, ...), already passes through this line untouched.
|
|
155
|
+
// The silent-drop trap this function's own doc comment warns about is
|
|
156
|
+
// about the TOP-LEVEL keys named here, not watch's own fields; the
|
|
157
|
+
// actual boundary for one of those is WatchConfigSchema itself (an
|
|
158
|
+
// unknown key there is stripped at `v.parse`, same failure mode, just a
|
|
159
|
+
// different gate) — see `chain_options`'s merge-survival test in
|
|
160
|
+
// `data_types.test.ts` for the check that actually matters here.
|
|
102
161
|
watch: { ...(base.watch || {}), ...(override.watch || {}) },
|
|
103
162
|
// channels is a whole-array replace on override, same as quality.checks —
|
|
104
163
|
// you don't want an override's channels appended to the built-in's.
|
|
@@ -106,6 +165,15 @@ function mergeRawConfig(base, override) {
|
|
|
106
165
|
// review.require_human_signoff / review.signoff_timeout_seconds — see
|
|
107
166
|
// data_types.ts's ReviewConfigSchema doc comment for why this key exists.
|
|
108
167
|
review: { ...(base.review || {}), ...(override.review || {}) },
|
|
168
|
+
// tiering.enabled / .tiers / .roles — key-by-key at this level, so a repo
|
|
169
|
+
// that only flips `enabled` keeps the base's tiers/roles. `tiers` is a
|
|
170
|
+
// whole-LIST replace on override (like quality.checks and
|
|
171
|
+
// notifications.channels): a repo that declares its own ladder replaces
|
|
172
|
+
// the packaged one wholesale rather than getting an unordered splice of
|
|
173
|
+
// both. `roles` is a whole-OBJECT replace for the same reason — a
|
|
174
|
+
// half-merged role map would route some agents by the base's ladder and
|
|
175
|
+
// some by the override's. Pinned by src/test/data_types.test.ts.
|
|
176
|
+
tiering: { ...(base.tiering || {}), ...(override.tiering || {}) },
|
|
109
177
|
agents: mergeAgentLists(base.agents || [], override.agents || []),
|
|
110
178
|
};
|
|
111
179
|
}
|
|
@@ -128,6 +196,15 @@ export function loadConfig(configPaths) {
|
|
|
128
196
|
raw = mergeRawConfig(raw, parsed);
|
|
129
197
|
}
|
|
130
198
|
const defaults = raw.defaults || {};
|
|
199
|
+
// THE BACK-FILL LIST IS PER-AGENT SETTINGS ONLY. Every key here is copied
|
|
200
|
+
// DOWN onto each agent that didn't set it, so only fields that mean
|
|
201
|
+
// something about ONE agent belong. Run-scoped `defaults` keys must stay
|
|
202
|
+
// out: `data_dir`, `protected_files`, and `max_run_cost`/`max_run_tokens`
|
|
203
|
+
// are properties of the RUN, and a per-agent copy of a run budget would
|
|
204
|
+
// read as "each agent may spend this much" — a different, unenforced
|
|
205
|
+
// feature. (AgentConfigSchema is a non-strict v.object, so a stray copy
|
|
206
|
+
// would be silently STRIPPED at parse rather than rejected: the mistake
|
|
207
|
+
// would look like it worked. See ConfigDefaultsSchema's own note.)
|
|
131
208
|
for (const agent of raw.agents || []) {
|
|
132
209
|
for (const key of ["coding_agent", "model", "thinking", "color", "tools", "writes", "env_allowlist"]) {
|
|
133
210
|
if (key in defaults && !(key in agent))
|
|
@@ -216,13 +293,81 @@ export function validate(cfg, required, requiredSuites = [], cwd) {
|
|
|
216
293
|
}
|
|
217
294
|
}
|
|
218
295
|
}
|
|
296
|
+
// Tiering (SPF #14) — every check below lives inside this ONE guard. A
|
|
297
|
+
// disabled ladder is not a config error, it is a config that is off: none
|
|
298
|
+
// of this fires for `enabled: false`, no matter what `tiers`/`roles` say —
|
|
299
|
+
// the same reason the packaged ladder + roles map surviving key-by-key
|
|
300
|
+
// merge into a repo that never opted in must not fail every chain.
|
|
301
|
+
if (cfg.tiering.enabled) {
|
|
302
|
+
// cfg-global: every declared rung must be well-formed for its OWN
|
|
303
|
+
// declared backend — the same branch this function already runs for
|
|
304
|
+
// agent.model, just keyed off the tier's coding_agent instead.
|
|
305
|
+
for (const tier of cfg.tiering.tiers) {
|
|
306
|
+
if (tier.coding_agent === "flue") {
|
|
307
|
+
try {
|
|
308
|
+
agentFlue.resolveModel(tier.model);
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
problems.push(`tiering.tiers[${JSON.stringify(tier.name)}]: ${error.message}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
else if (!tier.model.trim()) {
|
|
315
|
+
problems.push(`tiering.tiers[${JSON.stringify(tier.name)}]: model is empty`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// cfg-global: `enabled: true` with nothing to route is a silent no-op —
|
|
319
|
+
// the same reason an unconfigured quality.suites entry above is an
|
|
320
|
+
// error rather than a quiet pass-through.
|
|
321
|
+
if (cfg.tiering.tiers.length === 0) {
|
|
322
|
+
problems.push("tiering.enabled is true but tiering.tiers is empty — declare at least one rung or set tiering.enabled: false");
|
|
323
|
+
}
|
|
324
|
+
if (Object.keys(cfg.tiering.roles).length === 0) {
|
|
325
|
+
problems.push("tiering.enabled is true but tiering.roles is empty — name at least one role or set tiering.enabled: false");
|
|
326
|
+
}
|
|
327
|
+
// Scoped to `required` — same precedent as the per-agent loop above
|
|
328
|
+
// (`:256`/`:277-288`): a `roles` key naming a role no phase in this run
|
|
329
|
+
// will dispatch is neither routed nor validated. This is what lets the
|
|
330
|
+
// packaged six-name `roles` map survive key-by-key merge into a repo
|
|
331
|
+
// with a pruned/renamed roster without failing every chain.
|
|
332
|
+
for (const name of required) {
|
|
333
|
+
const tierName = cfg.tiering.roles[name];
|
|
334
|
+
if (tierName === undefined)
|
|
335
|
+
continue; // precedence: not named in roles -> untouched, nothing to check
|
|
336
|
+
const tier = cfg.tiering.tiers.find((t) => t.name === tierName);
|
|
337
|
+
if (!tier) {
|
|
338
|
+
problems.push(`tiering.roles.${name} names tier ${JSON.stringify(tierName)}, which is not declared in tiering.tiers`);
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
const agent = cfg.agents.find((a) => a.name === name);
|
|
342
|
+
if (!agent)
|
|
343
|
+
continue; // already reported above by the per-agent loop's own resolve() failure
|
|
344
|
+
// Rule T — the backend-compatibility rule (§4.4A). A tier changes an
|
|
345
|
+
// agent's `model` and NOTHING else, so a role is routable by a tier
|
|
346
|
+
// only when their backends agree; a mismatch is a named error, never
|
|
347
|
+
// a silent skip, because a config that says "route this role by tier"
|
|
348
|
+
// and then quietly does not is the failure mode this codebase already
|
|
349
|
+
// refuses for suites.
|
|
350
|
+
if (agent.coding_agent !== tier.coding_agent) {
|
|
351
|
+
problems.push(`agent ${JSON.stringify(name)} (coding_agent: ${agent.coding_agent}) is routed by tiering.roles to tier ${JSON.stringify(tier.name)} ` +
|
|
352
|
+
`(coding_agent: ${tier.coding_agent}) — a tier's model only speaks its own backend's vocabulary; declare a ` +
|
|
353
|
+
`${agent.coding_agent} rung for ${JSON.stringify(name)} or remove it from tiering.roles`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
219
357
|
if (problems.length > 0) {
|
|
220
358
|
throw new Error("config validation failed:\n- " + problems.join("\n- "));
|
|
221
359
|
}
|
|
222
360
|
}
|
|
223
361
|
/** One agent call: render prompts -> pi run -> typed parse -> gates -> envelope. */
|
|
224
362
|
export async function execute(run, phase, call) {
|
|
225
|
-
|
|
363
|
+
// The single dispatch-site change tiering makes: one effective AgentConfig,
|
|
364
|
+
// `model` and ONLY `model` possibly overridden (rule T — `coding_agent` is
|
|
365
|
+
// never touched here). Every downstream reader below (`agent_start`'s
|
|
366
|
+
// payload, `run.console.agentStarted`, the actual `AgentRequest`,
|
|
367
|
+
// `agentSessionId`'s reuse comparison, the traced `agent_sessions.model`
|
|
368
|
+
// column, `run.saveAgentMap`) becomes consistent for free. See
|
|
369
|
+
// `core/tiering.ts`'s `effectiveAgent`.
|
|
370
|
+
const agent = effectiveAgent(run, resolve(run.cfg, phase.params.owner));
|
|
226
371
|
const agentDir = path.join(run.session_dir, agent.name);
|
|
227
372
|
mkdirSync(agentDir, { recursive: true });
|
|
228
373
|
const variables = {
|
|
@@ -258,6 +403,12 @@ export async function execute(run, phase, call) {
|
|
|
258
403
|
let latest = null;
|
|
259
404
|
const spent = new UsageBreakdown();
|
|
260
405
|
async function send(promptText) {
|
|
406
|
+
// The ONE dispatch point for this phase — the first prompt, every
|
|
407
|
+
// JSON-repair retry, and every gate correction all funnel through here,
|
|
408
|
+
// so checking the run's ceilings here is what makes them a cap on the
|
|
409
|
+
// WHOLE run rather than on the first call of each phase. See
|
|
410
|
+
// `assertRunBudget` for why it is checked before, not after.
|
|
411
|
+
assertRunBudget(run);
|
|
261
412
|
const request = {
|
|
262
413
|
prompt: promptText,
|
|
263
414
|
system_prompt: systemText,
|
|
@@ -173,6 +173,28 @@ export declare const RefinedIssueSchema: v.ObjectSchema<{
|
|
|
173
173
|
readonly blocked_by: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, () => never[]>;
|
|
174
174
|
}, undefined>;
|
|
175
175
|
export type RefinedIssue = v.InferOutput<typeof RefinedIssueSchema>;
|
|
176
|
+
/**
|
|
177
|
+
* One open question the refiner could not answer itself — material ambiguity
|
|
178
|
+
* (scope, data model, an external dependency choice, a UX contract, anything
|
|
179
|
+
* that would contradict an ADR) it is refusing to guess on, per
|
|
180
|
+
* `assets/prompts/refiner/system.md`'s "ask, don't decide" rule. `id` is
|
|
181
|
+
* stable within one round so a resumed run's answers can be matched back to
|
|
182
|
+
* the question they answer; `why_it_matters`/`options`/`recommendation`/
|
|
183
|
+
* `evidence` exist so a human can answer in one word ("go with your rec")
|
|
184
|
+
* instead of re-deriving the tradeoff the refiner already worked out.
|
|
185
|
+
* `gates.refinementWellFormed` requires `issues` to be empty whenever this is
|
|
186
|
+
* non-empty — escalating means publishing nothing THIS round, never a
|
|
187
|
+
* partial tree pinned to an unanswered question.
|
|
188
|
+
*/
|
|
189
|
+
export declare const RefineQuestionSchema: v.ObjectSchema<{
|
|
190
|
+
readonly id: v.StringSchema<undefined>;
|
|
191
|
+
readonly question: v.StringSchema<undefined>;
|
|
192
|
+
readonly why_it_matters: v.OptionalSchema<v.StringSchema<undefined>, "">;
|
|
193
|
+
readonly options: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, () => never[]>;
|
|
194
|
+
readonly recommendation: v.OptionalSchema<v.StringSchema<undefined>, "">;
|
|
195
|
+
readonly evidence: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, () => never[]>;
|
|
196
|
+
}, undefined>;
|
|
197
|
+
export type RefineQuestion = v.InferOutput<typeof RefineQuestionSchema>;
|
|
176
198
|
/** A product spec decomposed into a feature/story tree — see `steps.refine()` and `core/refine.ts`. */
|
|
177
199
|
export declare const RefineOutput: EnvelopeType<{
|
|
178
200
|
status: "fail" | "success";
|
|
@@ -187,6 +209,14 @@ export declare const RefineOutput: EnvelopeType<{
|
|
|
187
209
|
parent: string;
|
|
188
210
|
blocked_by: string[];
|
|
189
211
|
}[];
|
|
212
|
+
questions: {
|
|
213
|
+
id: string;
|
|
214
|
+
question: string;
|
|
215
|
+
why_it_matters: string;
|
|
216
|
+
options: string[];
|
|
217
|
+
recommendation: string;
|
|
218
|
+
evidence: string[];
|
|
219
|
+
}[];
|
|
190
220
|
}>;
|
|
191
221
|
export type RefineOutputT = v.InferOutput<typeof RefineOutput.schema>;
|
|
192
222
|
export declare const QualityAreaSchema: v.PicklistSchema<["frontend", "backend"], undefined>;
|
|
@@ -388,6 +418,50 @@ export declare const ConfigDefaultsSchema: v.ObjectSchema<{
|
|
|
388
418
|
readonly tools: v.OptionalSchema<v.NullableSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>, undefined>;
|
|
389
419
|
readonly protected_files: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, () => string[]>;
|
|
390
420
|
readonly data_dir: v.OptionalSchema<v.StringSchema<undefined>, ".spf/data">;
|
|
421
|
+
/**
|
|
422
|
+
* RUN BUDGET CEILINGS — the two knobs that bound what one adw_id may spend.
|
|
423
|
+
*
|
|
424
|
+
* BOTH ABSENT BY DEFAULT, and absence is a total no-op: `agents.ts`'s
|
|
425
|
+
* `assertRunBudget()` returns immediately when neither is set, so every
|
|
426
|
+
* existing config behaves byte-identically to before this field existed.
|
|
427
|
+
* There is no ambient environment variable and no implicit default — an
|
|
428
|
+
* unbounded run stays the default because a surprise mid-run failure on a
|
|
429
|
+
* ceiling nobody chose is worse than the spend.
|
|
430
|
+
*
|
|
431
|
+
* SCOPE IS THE RUN, NOT THE CALL. Enforced against the Run's ACCUMULATED
|
|
432
|
+
* usage (`run.tokens`/`run.cost` — the same totals `run.addUsage()` mirrors
|
|
433
|
+
* into the sessions row), checked BEFORE every agent dispatch including
|
|
434
|
+
* every JSON-repair retry and every gate correction, because the cost of
|
|
435
|
+
* the call about to happen is unknowable in advance. That makes these a
|
|
436
|
+
* hard cap on FURTHER spend rather than a post-hoc report: reaching the
|
|
437
|
+
* ceiling stops the next call, it does not merely note that the last one
|
|
438
|
+
* was expensive. A tripped ceiling fails the phase closed — see
|
|
439
|
+
* `agents.ts`'s `BudgetExceeded`.
|
|
440
|
+
*
|
|
441
|
+
* `max_run_cost` is USD (the same unit the provider's own usage.cost
|
|
442
|
+
* arrives in, summed by `UsageBreakdown`); `max_run_tokens` is TOTAL
|
|
443
|
+
* tokens, i.e. the spend number — every turn re-sends the whole
|
|
444
|
+
* conversation, so this counts cached re-reads too, exactly like the
|
|
445
|
+
* `total_tokens` column in `sessions` (see `ui/server/db.ts`'s `usage()`
|
|
446
|
+
* for why that number is much larger than "material moved").
|
|
447
|
+
*
|
|
448
|
+
* Both are `> 0`, not `>= 0`: a zero ceiling would mean "no agent may ever
|
|
449
|
+
* run", which is a config mistake, not a budget — it would fail the first
|
|
450
|
+
* phase of every chain with a budget message instead of saying what is
|
|
451
|
+
* actually wrong.
|
|
452
|
+
*
|
|
453
|
+
* THE BACK-FILL TRAP (see `agents.ts`'s `loadConfig`): that function copies
|
|
454
|
+
* a handful of `defaults` keys DOWN onto each agent that hasn't set them
|
|
455
|
+
* (coding_agent/model/thinking/color/tools/writes/env_allowlist). These two
|
|
456
|
+
* keys are deliberately NOT in that list and must never be added to it —
|
|
457
|
+
* they are RUN-scoped ceilings, not per-agent settings, and a per-agent
|
|
458
|
+
* copy would read as "each agent may spend this much", which is a
|
|
459
|
+
* different (and unenforced) feature. `mergeRawConfig`'s `defaults` spread
|
|
460
|
+
* is what carries them through config layering, key-by-key; pinned by
|
|
461
|
+
* `src/test/budget.test.ts`.
|
|
462
|
+
*/
|
|
463
|
+
readonly max_run_cost: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.GtValueAction<number, 0, undefined>]>, undefined>;
|
|
464
|
+
readonly max_run_tokens: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.GtValueAction<number, 0, undefined>]>, undefined>;
|
|
391
465
|
}, undefined>;
|
|
392
466
|
export type ConfigDefaults = v.InferOutput<typeof ConfigDefaultsSchema>;
|
|
393
467
|
/**
|
|
@@ -492,6 +566,22 @@ export declare const WatchConfigSchema: v.ObjectSchema<{
|
|
|
492
566
|
readonly base_branch: v.OptionalSchema<v.StringSchema<undefined>, "main">;
|
|
493
567
|
readonly poll_ms: v.OptionalSchema<v.NumberSchema<undefined>, 60000>;
|
|
494
568
|
readonly concurrency: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>]>, 2>;
|
|
569
|
+
/**
|
|
570
|
+
* Threaded straight through as `runChainDef`'s `options` argument for
|
|
571
|
+
* every unattended `spf watch` dispatch (build lane AND refine lane) —
|
|
572
|
+
* the exact same `Record<string, string>` shape an interactive `spf
|
|
573
|
+
* <chain> --suite <name>` builds in `cli/commands/run.ts`'s
|
|
574
|
+
* `dispatchChain` (e.g. `{suite: "strict"}`, `{agent: "..."}`). Fixes the
|
|
575
|
+
* KNOWN LIMITATION called out in PR #20: `cli/commands/watch.ts`'s
|
|
576
|
+
* `runChain`/`runRefine` wrappers used to call `runChainDef` with no
|
|
577
|
+
* options at all, so nothing --suite-shaped could ever reach a chain run
|
|
578
|
+
* `spf watch` dispatched — see `cli/commands/watch.ts`. Empty by default,
|
|
579
|
+
* so an existing `watch:` config's behavior is unchanged by upgrading.
|
|
580
|
+
* Whole-object replace on merge, like `jira`/`refine` above and
|
|
581
|
+
* `observability.otel` — see `agents.ts`'s `mergeRawConfig` and
|
|
582
|
+
* `data_types.test.ts`'s merge-survival test for this field.
|
|
583
|
+
*/
|
|
584
|
+
readonly chain_options: v.OptionalSchema<v.RecordSchema<v.StringSchema<undefined>, v.StringSchema<undefined>, undefined>, () => {}>;
|
|
495
585
|
readonly jira: v.OptionalSchema<v.ObjectSchema<{
|
|
496
586
|
readonly base_url: v.OptionalSchema<v.StringSchema<undefined>, "">;
|
|
497
587
|
readonly project_key: v.OptionalSchema<v.StringSchema<undefined>, "">;
|
|
@@ -574,6 +664,64 @@ export declare const ReviewConfigSchema: v.ObjectSchema<{
|
|
|
574
664
|
readonly signoff_timeout_seconds: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.MinValueAction<number, 1, undefined>]>, 300>;
|
|
575
665
|
}, undefined>;
|
|
576
666
|
export type ReviewConfig = v.InferOutput<typeof ReviewConfigSchema>;
|
|
667
|
+
/**
|
|
668
|
+
* Risk-tiered per-role model routing — SPF #14. One rung of a ladder:
|
|
669
|
+
* `name` is what `tiering.roles` points at, `model` speaks EXACTLY the same
|
|
670
|
+
* vocabulary as an agent's own `model:` for that backend (provider/model-id
|
|
671
|
+
* for flue, Claude Code's bare alias/full-name for claude_code) — there is
|
|
672
|
+
* deliberately no per-provider table, since for flue the provider is
|
|
673
|
+
* already the first segment of the value.
|
|
674
|
+
*
|
|
675
|
+
* `coding_agent` declares WHICH BACKEND'S VOCABULARY this rung's `model`
|
|
676
|
+
* speaks — same picklist and same default as `AgentConfigSchema`'s own
|
|
677
|
+
* field (`:409`), reused rather than restated. A tier changes an agent's
|
|
678
|
+
* `model` and NOTHING else (`coding_agent` stays the agent's own, always),
|
|
679
|
+
* so a rung can only route roles whose `coding_agent` matches its own — see
|
|
680
|
+
* `core/tiering.ts`'s rule T, enforced by `agents.ts`'s `validate()`.
|
|
681
|
+
*/
|
|
682
|
+
export declare const TierSchema: v.ObjectSchema<{
|
|
683
|
+
readonly name: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
684
|
+
readonly coding_agent: v.OptionalSchema<v.PicklistSchema<["flue", "claude_code"], undefined>, "flue">;
|
|
685
|
+
readonly model: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
686
|
+
}, undefined>;
|
|
687
|
+
export type Tier = v.InferOutput<typeof TierSchema>;
|
|
688
|
+
/**
|
|
689
|
+
* OFF by default, and `enabled: false`/absent is a TOTAL no-op: every agent
|
|
690
|
+
* dispatches at exactly the model its roster entry names, byte-identical to
|
|
691
|
+
* before this key existed — same discipline `max_run_cost`/`max_run_tokens`
|
|
692
|
+
* hold themselves to (`ConfigDefaultsSchema`'s own comment above).
|
|
693
|
+
*
|
|
694
|
+
* TOP-LEVEL (`SFConfigSchema` below), deliberately NOT nested under
|
|
695
|
+
* `defaults:` — `agents.ts`'s `loadConfig` back-fill loop copies a FIXED
|
|
696
|
+
* list of `defaults` keys DOWN onto every agent that hasn't set them, and
|
|
697
|
+
* `AgentConfigSchema` is a non-strict `v.object`, so a stray copy would be
|
|
698
|
+
* silently STRIPPED at parse rather than rejected (see
|
|
699
|
+
* `ConfigDefaultsSchema`'s "THE BACK-FILL TRAP" comment — `max_run_cost`/
|
|
700
|
+
* `max_run_tokens` needed an explicit never-add-this-to-the-list comment to
|
|
701
|
+
* survive exactly that trap). A top-level key sits outside that loop
|
|
702
|
+
* entirely, so the trap cannot apply here at all.
|
|
703
|
+
*
|
|
704
|
+
* `tiers` is the ladder, WEAKEST FIRST: a risk level shifts every routed
|
|
705
|
+
* role UP or DOWN this list by the same step, so order is the whole
|
|
706
|
+
* semantics — a SEQUENCE states that unambiguously where a mapping's key
|
|
707
|
+
* order would be parser-dependent.
|
|
708
|
+
*
|
|
709
|
+
* `roles` is the baseline tier per ROLE (an agent name). Naming an agent
|
|
710
|
+
* here IS the operator's statement "route this one by tier" — so the
|
|
711
|
+
* resolved tier wins over that agent's own `model:`. An agent NOT named
|
|
712
|
+
* here is never retiered: its `model:` stands, untouched. See
|
|
713
|
+
* `core/tiering.ts`'s `resolveTiering` for the full precedence rule.
|
|
714
|
+
*/
|
|
715
|
+
export declare const TieringConfigSchema: v.ObjectSchema<{
|
|
716
|
+
readonly enabled: v.OptionalSchema<v.BooleanSchema<undefined>, false>;
|
|
717
|
+
readonly tiers: v.OptionalSchema<v.ArraySchema<v.ObjectSchema<{
|
|
718
|
+
readonly name: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
719
|
+
readonly coding_agent: v.OptionalSchema<v.PicklistSchema<["flue", "claude_code"], undefined>, "flue">;
|
|
720
|
+
readonly model: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
721
|
+
}, undefined>, undefined>, () => never[]>;
|
|
722
|
+
readonly roles: v.OptionalSchema<v.RecordSchema<v.StringSchema<undefined>, v.StringSchema<undefined>, undefined>, () => {}>;
|
|
723
|
+
}, undefined>;
|
|
724
|
+
export type TieringConfig = v.InferOutput<typeof TieringConfigSchema>;
|
|
577
725
|
export declare const SFConfigSchema: v.ObjectSchema<{
|
|
578
726
|
readonly defaults: v.OptionalSchema<v.ObjectSchema<{
|
|
579
727
|
readonly coding_agent: v.OptionalSchema<v.PicklistSchema<["flue", "claude_code"], undefined>, "flue">;
|
|
@@ -584,6 +732,50 @@ export declare const SFConfigSchema: v.ObjectSchema<{
|
|
|
584
732
|
readonly tools: v.OptionalSchema<v.NullableSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>, undefined>;
|
|
585
733
|
readonly protected_files: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, () => string[]>;
|
|
586
734
|
readonly data_dir: v.OptionalSchema<v.StringSchema<undefined>, ".spf/data">;
|
|
735
|
+
/**
|
|
736
|
+
* RUN BUDGET CEILINGS — the two knobs that bound what one adw_id may spend.
|
|
737
|
+
*
|
|
738
|
+
* BOTH ABSENT BY DEFAULT, and absence is a total no-op: `agents.ts`'s
|
|
739
|
+
* `assertRunBudget()` returns immediately when neither is set, so every
|
|
740
|
+
* existing config behaves byte-identically to before this field existed.
|
|
741
|
+
* There is no ambient environment variable and no implicit default — an
|
|
742
|
+
* unbounded run stays the default because a surprise mid-run failure on a
|
|
743
|
+
* ceiling nobody chose is worse than the spend.
|
|
744
|
+
*
|
|
745
|
+
* SCOPE IS THE RUN, NOT THE CALL. Enforced against the Run's ACCUMULATED
|
|
746
|
+
* usage (`run.tokens`/`run.cost` — the same totals `run.addUsage()` mirrors
|
|
747
|
+
* into the sessions row), checked BEFORE every agent dispatch including
|
|
748
|
+
* every JSON-repair retry and every gate correction, because the cost of
|
|
749
|
+
* the call about to happen is unknowable in advance. That makes these a
|
|
750
|
+
* hard cap on FURTHER spend rather than a post-hoc report: reaching the
|
|
751
|
+
* ceiling stops the next call, it does not merely note that the last one
|
|
752
|
+
* was expensive. A tripped ceiling fails the phase closed — see
|
|
753
|
+
* `agents.ts`'s `BudgetExceeded`.
|
|
754
|
+
*
|
|
755
|
+
* `max_run_cost` is USD (the same unit the provider's own usage.cost
|
|
756
|
+
* arrives in, summed by `UsageBreakdown`); `max_run_tokens` is TOTAL
|
|
757
|
+
* tokens, i.e. the spend number — every turn re-sends the whole
|
|
758
|
+
* conversation, so this counts cached re-reads too, exactly like the
|
|
759
|
+
* `total_tokens` column in `sessions` (see `ui/server/db.ts`'s `usage()`
|
|
760
|
+
* for why that number is much larger than "material moved").
|
|
761
|
+
*
|
|
762
|
+
* Both are `> 0`, not `>= 0`: a zero ceiling would mean "no agent may ever
|
|
763
|
+
* run", which is a config mistake, not a budget — it would fail the first
|
|
764
|
+
* phase of every chain with a budget message instead of saying what is
|
|
765
|
+
* actually wrong.
|
|
766
|
+
*
|
|
767
|
+
* THE BACK-FILL TRAP (see `agents.ts`'s `loadConfig`): that function copies
|
|
768
|
+
* a handful of `defaults` keys DOWN onto each agent that hasn't set them
|
|
769
|
+
* (coding_agent/model/thinking/color/tools/writes/env_allowlist). These two
|
|
770
|
+
* keys are deliberately NOT in that list and must never be added to it —
|
|
771
|
+
* they are RUN-scoped ceilings, not per-agent settings, and a per-agent
|
|
772
|
+
* copy would read as "each agent may spend this much", which is a
|
|
773
|
+
* different (and unenforced) feature. `mergeRawConfig`'s `defaults` spread
|
|
774
|
+
* is what carries them through config layering, key-by-key; pinned by
|
|
775
|
+
* `src/test/budget.test.ts`.
|
|
776
|
+
*/
|
|
777
|
+
readonly max_run_cost: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.GtValueAction<number, 0, undefined>]>, undefined>;
|
|
778
|
+
readonly max_run_tokens: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.GtValueAction<number, 0, undefined>]>, undefined>;
|
|
587
779
|
}, undefined>, () => {
|
|
588
780
|
coding_agent: "claude_code" | "flue";
|
|
589
781
|
model: string;
|
|
@@ -593,6 +785,8 @@ export declare const SFConfigSchema: v.ObjectSchema<{
|
|
|
593
785
|
tools?: string[] | null | undefined;
|
|
594
786
|
protected_files: string[];
|
|
595
787
|
data_dir: string;
|
|
788
|
+
max_run_cost?: number | undefined;
|
|
789
|
+
max_run_tokens?: number | undefined;
|
|
596
790
|
}>;
|
|
597
791
|
readonly observability: v.OptionalSchema<v.ObjectSchema<{
|
|
598
792
|
readonly db: v.OptionalSchema<v.StringSchema<undefined>, ".spf/data/spf.db">;
|
|
@@ -660,6 +854,22 @@ export declare const SFConfigSchema: v.ObjectSchema<{
|
|
|
660
854
|
readonly base_branch: v.OptionalSchema<v.StringSchema<undefined>, "main">;
|
|
661
855
|
readonly poll_ms: v.OptionalSchema<v.NumberSchema<undefined>, 60000>;
|
|
662
856
|
readonly concurrency: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>]>, 2>;
|
|
857
|
+
/**
|
|
858
|
+
* Threaded straight through as `runChainDef`'s `options` argument for
|
|
859
|
+
* every unattended `spf watch` dispatch (build lane AND refine lane) —
|
|
860
|
+
* the exact same `Record<string, string>` shape an interactive `spf
|
|
861
|
+
* <chain> --suite <name>` builds in `cli/commands/run.ts`'s
|
|
862
|
+
* `dispatchChain` (e.g. `{suite: "strict"}`, `{agent: "..."}`). Fixes the
|
|
863
|
+
* KNOWN LIMITATION called out in PR #20: `cli/commands/watch.ts`'s
|
|
864
|
+
* `runChain`/`runRefine` wrappers used to call `runChainDef` with no
|
|
865
|
+
* options at all, so nothing --suite-shaped could ever reach a chain run
|
|
866
|
+
* `spf watch` dispatched — see `cli/commands/watch.ts`. Empty by default,
|
|
867
|
+
* so an existing `watch:` config's behavior is unchanged by upgrading.
|
|
868
|
+
* Whole-object replace on merge, like `jira`/`refine` above and
|
|
869
|
+
* `observability.otel` — see `agents.ts`'s `mergeRawConfig` and
|
|
870
|
+
* `data_types.test.ts`'s merge-survival test for this field.
|
|
871
|
+
*/
|
|
872
|
+
readonly chain_options: v.OptionalSchema<v.RecordSchema<v.StringSchema<undefined>, v.StringSchema<undefined>, undefined>, () => {}>;
|
|
663
873
|
readonly jira: v.OptionalSchema<v.ObjectSchema<{
|
|
664
874
|
readonly base_url: v.OptionalSchema<v.StringSchema<undefined>, "">;
|
|
665
875
|
readonly project_key: v.OptionalSchema<v.StringSchema<undefined>, "">;
|
|
@@ -686,6 +896,9 @@ export declare const SFConfigSchema: v.ObjectSchema<{
|
|
|
686
896
|
base_branch: string;
|
|
687
897
|
poll_ms: number;
|
|
688
898
|
concurrency: number;
|
|
899
|
+
chain_options: {
|
|
900
|
+
[x: string]: string;
|
|
901
|
+
};
|
|
689
902
|
jira: {
|
|
690
903
|
base_url: string;
|
|
691
904
|
project_key: string;
|
|
@@ -722,6 +935,25 @@ export declare const SFConfigSchema: v.ObjectSchema<{
|
|
|
722
935
|
require_human_signoff: boolean;
|
|
723
936
|
signoff_timeout_seconds: number;
|
|
724
937
|
}>;
|
|
938
|
+
readonly tiering: v.OptionalSchema<v.ObjectSchema<{
|
|
939
|
+
readonly enabled: v.OptionalSchema<v.BooleanSchema<undefined>, false>;
|
|
940
|
+
readonly tiers: v.OptionalSchema<v.ArraySchema<v.ObjectSchema<{
|
|
941
|
+
readonly name: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
942
|
+
readonly coding_agent: v.OptionalSchema<v.PicklistSchema<["flue", "claude_code"], undefined>, "flue">;
|
|
943
|
+
readonly model: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
944
|
+
}, undefined>, undefined>, () => never[]>;
|
|
945
|
+
readonly roles: v.OptionalSchema<v.RecordSchema<v.StringSchema<undefined>, v.StringSchema<undefined>, undefined>, () => {}>;
|
|
946
|
+
}, undefined>, () => {
|
|
947
|
+
enabled: boolean;
|
|
948
|
+
tiers: {
|
|
949
|
+
name: string;
|
|
950
|
+
coding_agent: "claude_code" | "flue";
|
|
951
|
+
model: string;
|
|
952
|
+
}[];
|
|
953
|
+
roles: {
|
|
954
|
+
[x: string]: string;
|
|
955
|
+
};
|
|
956
|
+
}>;
|
|
725
957
|
}, undefined>;
|
|
726
958
|
export type SFConfig = v.InferOutput<typeof SFConfigSchema>;
|
|
727
959
|
/** The full set of event kinds any tracer.event()/makeEventRecord() call site emits — kept in sync with src/ui/shared/types.ts's EventType. */
|