@gr8ful/spf 0.5.1 → 0.7.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 +168 -29
- package/assets/defaults/spf.config.yaml +68 -0
- package/assets/prompts/refiner/system.md +42 -6
- package/assets/prompts/refiner/user.md +46 -8
- package/assets/skill/SKILL.md +1 -0
- package/assets/skill/references/config.md +192 -5
- package/assets/templates/ts-flue-ollama.spf.config.yaml +25 -0
- package/assets/templates/ts.spf.config.yaml +12 -2
- 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 +8 -17
- package/dist/chains/steps.js +102 -14
- package/dist/cli/commands/doctor.js +112 -3
- 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 +23 -1
- package/dist/cli/commands/run.js +9 -2
- package/dist/cli/commands/trace.d.ts +18 -0
- package/dist/cli/commands/trace.js +22 -3
- package/dist/cli/commands/watch.d.ts +8 -0
- package/dist/cli/commands/watch.js +95 -33
- package/dist/cli/index.js +13 -1
- package/dist/cli/interview.js +9 -5
- package/dist/core/agents.d.ts +56 -0
- package/dist/core/agents.js +152 -1
- package/dist/core/data_types.d.ts +340 -5
- package/dist/core/data_types.js +185 -5
- 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 +48 -2
- 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 +46 -6
- package/dist/core/issues/github_provider.js +120 -5
- package/dist/core/issues/jira_provider.d.ts +83 -12
- package/dist/core/issues/jira_provider.js +110 -4
- package/dist/core/issues/provider.d.ts +124 -19
- package/dist/core/issues/provider.js +24 -7
- 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/refine.d.ts +45 -8
- package/dist/core/refine.js +98 -24
- 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 +174 -11
- package/dist/core/watch.js +531 -33
- 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
|
@@ -164,6 +164,20 @@ export type DocumentOutputT = v.InferOutput<typeof DocumentOutput.schema>;
|
|
|
164
164
|
* this list before publish ever runs (unique keys, resolvable references,
|
|
165
165
|
* no cycles, container/leaf kind agreement, at least one leaf).
|
|
166
166
|
*/
|
|
167
|
+
/**
|
|
168
|
+
* `p0` (drop everything) .. `p3` (someday) — see `assets/prompts/refiner/system.md`'s
|
|
169
|
+
* `## Priority` section for what each rung means. Two rules the schema alone
|
|
170
|
+
* can't enforce, checked instead by `gates.refinementWellFormed`
|
|
171
|
+
* (monotonicity: no node outranks its `parent`) and `core/refine.ts`'s
|
|
172
|
+
* `publish()` (the spec's own priority, when known, is a ceiling clamped onto
|
|
173
|
+
* every node): see both files' doc comments.
|
|
174
|
+
*/
|
|
175
|
+
export declare const RefinedPrioritySchema: v.PicklistSchema<["p0", "p1", "p2", "p3"], undefined>;
|
|
176
|
+
export type RefinedPriority = v.InferOutput<typeof RefinedPrioritySchema>;
|
|
177
|
+
/** Lower rank = more urgent. The one place both `gates.refinementWellFormed` (monotonicity) and `core/refine.ts`'s `publish()` (the spec-priority ceiling) get their ordering from — see `RefinedPrioritySchema`'s doc comment. */
|
|
178
|
+
export declare const PRIORITY_RANK: Record<RefinedPriority, number>;
|
|
179
|
+
/** `priority`, pulled down to `ceiling` if it outranks it — never raised. `ceiling` nullish (a bare `spf refine` with nothing to inherit from) is a no-op. */
|
|
180
|
+
export declare function clampPriority(priority: RefinedPriority, ceiling: RefinedPriority | null | undefined): RefinedPriority;
|
|
167
181
|
export declare const RefinedIssueSchema: v.ObjectSchema<{
|
|
168
182
|
readonly key: v.StringSchema<undefined>;
|
|
169
183
|
readonly kind: v.PicklistSchema<["epic", "feature", "story", "bug", "task"], undefined>;
|
|
@@ -171,8 +185,31 @@ export declare const RefinedIssueSchema: v.ObjectSchema<{
|
|
|
171
185
|
readonly body: v.StringSchema<undefined>;
|
|
172
186
|
readonly parent: v.OptionalSchema<v.StringSchema<undefined>, "">;
|
|
173
187
|
readonly blocked_by: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, () => never[]>;
|
|
188
|
+
readonly priority: v.OptionalSchema<v.PicklistSchema<["p0", "p1", "p2", "p3"], undefined>, "p2">;
|
|
174
189
|
}, undefined>;
|
|
175
190
|
export type RefinedIssue = v.InferOutput<typeof RefinedIssueSchema>;
|
|
191
|
+
/**
|
|
192
|
+
* One open question the refiner could not answer itself — material ambiguity
|
|
193
|
+
* (scope, data model, an external dependency choice, a UX contract, anything
|
|
194
|
+
* that would contradict an ADR) it is refusing to guess on, per
|
|
195
|
+
* `assets/prompts/refiner/system.md`'s "ask, don't decide" rule. `id` is
|
|
196
|
+
* stable within one round so a resumed run's answers can be matched back to
|
|
197
|
+
* the question they answer; `why_it_matters`/`options`/`recommendation`/
|
|
198
|
+
* `evidence` exist so a human can answer in one word ("go with your rec")
|
|
199
|
+
* instead of re-deriving the tradeoff the refiner already worked out.
|
|
200
|
+
* `gates.refinementWellFormed` requires `issues` to be empty whenever this is
|
|
201
|
+
* non-empty — escalating means publishing nothing THIS round, never a
|
|
202
|
+
* partial tree pinned to an unanswered question.
|
|
203
|
+
*/
|
|
204
|
+
export declare const RefineQuestionSchema: v.ObjectSchema<{
|
|
205
|
+
readonly id: v.StringSchema<undefined>;
|
|
206
|
+
readonly question: v.StringSchema<undefined>;
|
|
207
|
+
readonly why_it_matters: v.OptionalSchema<v.StringSchema<undefined>, "">;
|
|
208
|
+
readonly options: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, () => never[]>;
|
|
209
|
+
readonly recommendation: v.OptionalSchema<v.StringSchema<undefined>, "">;
|
|
210
|
+
readonly evidence: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, () => never[]>;
|
|
211
|
+
}, undefined>;
|
|
212
|
+
export type RefineQuestion = v.InferOutput<typeof RefineQuestionSchema>;
|
|
176
213
|
/** A product spec decomposed into a feature/story tree — see `steps.refine()` and `core/refine.ts`. */
|
|
177
214
|
export declare const RefineOutput: EnvelopeType<{
|
|
178
215
|
status: "fail" | "success";
|
|
@@ -186,6 +223,15 @@ export declare const RefineOutput: EnvelopeType<{
|
|
|
186
223
|
body: string;
|
|
187
224
|
parent: string;
|
|
188
225
|
blocked_by: string[];
|
|
226
|
+
priority: "p0" | "p1" | "p2" | "p3";
|
|
227
|
+
}[];
|
|
228
|
+
questions: {
|
|
229
|
+
id: string;
|
|
230
|
+
question: string;
|
|
231
|
+
why_it_matters: string;
|
|
232
|
+
options: string[];
|
|
233
|
+
recommendation: string;
|
|
234
|
+
evidence: string[];
|
|
189
235
|
}[];
|
|
190
236
|
}>;
|
|
191
237
|
export type RefineOutputT = v.InferOutput<typeof RefineOutput.schema>;
|
|
@@ -388,6 +434,50 @@ export declare const ConfigDefaultsSchema: v.ObjectSchema<{
|
|
|
388
434
|
readonly tools: v.OptionalSchema<v.NullableSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>, undefined>;
|
|
389
435
|
readonly protected_files: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, () => string[]>;
|
|
390
436
|
readonly data_dir: v.OptionalSchema<v.StringSchema<undefined>, ".spf/data">;
|
|
437
|
+
/**
|
|
438
|
+
* RUN BUDGET CEILINGS — the two knobs that bound what one adw_id may spend.
|
|
439
|
+
*
|
|
440
|
+
* BOTH ABSENT BY DEFAULT, and absence is a total no-op: `agents.ts`'s
|
|
441
|
+
* `assertRunBudget()` returns immediately when neither is set, so every
|
|
442
|
+
* existing config behaves byte-identically to before this field existed.
|
|
443
|
+
* There is no ambient environment variable and no implicit default — an
|
|
444
|
+
* unbounded run stays the default because a surprise mid-run failure on a
|
|
445
|
+
* ceiling nobody chose is worse than the spend.
|
|
446
|
+
*
|
|
447
|
+
* SCOPE IS THE RUN, NOT THE CALL. Enforced against the Run's ACCUMULATED
|
|
448
|
+
* usage (`run.tokens`/`run.cost` — the same totals `run.addUsage()` mirrors
|
|
449
|
+
* into the sessions row), checked BEFORE every agent dispatch including
|
|
450
|
+
* every JSON-repair retry and every gate correction, because the cost of
|
|
451
|
+
* the call about to happen is unknowable in advance. That makes these a
|
|
452
|
+
* hard cap on FURTHER spend rather than a post-hoc report: reaching the
|
|
453
|
+
* ceiling stops the next call, it does not merely note that the last one
|
|
454
|
+
* was expensive. A tripped ceiling fails the phase closed — see
|
|
455
|
+
* `agents.ts`'s `BudgetExceeded`.
|
|
456
|
+
*
|
|
457
|
+
* `max_run_cost` is USD (the same unit the provider's own usage.cost
|
|
458
|
+
* arrives in, summed by `UsageBreakdown`); `max_run_tokens` is TOTAL
|
|
459
|
+
* tokens, i.e. the spend number — every turn re-sends the whole
|
|
460
|
+
* conversation, so this counts cached re-reads too, exactly like the
|
|
461
|
+
* `total_tokens` column in `sessions` (see `ui/server/db.ts`'s `usage()`
|
|
462
|
+
* for why that number is much larger than "material moved").
|
|
463
|
+
*
|
|
464
|
+
* Both are `> 0`, not `>= 0`: a zero ceiling would mean "no agent may ever
|
|
465
|
+
* run", which is a config mistake, not a budget — it would fail the first
|
|
466
|
+
* phase of every chain with a budget message instead of saying what is
|
|
467
|
+
* actually wrong.
|
|
468
|
+
*
|
|
469
|
+
* THE BACK-FILL TRAP (see `agents.ts`'s `loadConfig`): that function copies
|
|
470
|
+
* a handful of `defaults` keys DOWN onto each agent that hasn't set them
|
|
471
|
+
* (coding_agent/model/thinking/color/tools/writes/env_allowlist). These two
|
|
472
|
+
* keys are deliberately NOT in that list and must never be added to it —
|
|
473
|
+
* they are RUN-scoped ceilings, not per-agent settings, and a per-agent
|
|
474
|
+
* copy would read as "each agent may spend this much", which is a
|
|
475
|
+
* different (and unenforced) feature. `mergeRawConfig`'s `defaults` spread
|
|
476
|
+
* is what carries them through config layering, key-by-key; pinned by
|
|
477
|
+
* `src/test/budget.test.ts`.
|
|
478
|
+
*/
|
|
479
|
+
readonly max_run_cost: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.GtValueAction<number, 0, undefined>]>, undefined>;
|
|
480
|
+
readonly max_run_tokens: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.GtValueAction<number, 0, undefined>]>, undefined>;
|
|
391
481
|
}, undefined>;
|
|
392
482
|
export type ConfigDefaults = v.InferOutput<typeof ConfigDefaultsSchema>;
|
|
393
483
|
/**
|
|
@@ -460,10 +550,50 @@ export declare const WatchIssueProviderSchema: v.PicklistSchema<["github", "jira
|
|
|
460
550
|
export type WatchIssueProviderKind = v.InferOutput<typeof WatchIssueProviderSchema>;
|
|
461
551
|
export declare const WatchCodeHostSchema: v.PicklistSchema<["github", "bitbucket"], undefined>;
|
|
462
552
|
export type WatchCodeHostKind = v.InferOutput<typeof WatchCodeHostSchema>;
|
|
463
|
-
/**
|
|
553
|
+
/**
|
|
554
|
+
* What each `RefinedIssue.kind` creates as, on Jira — Jira's create endpoint
|
|
555
|
+
* requires a real `issuetype`, and project setups vary (renamed types,
|
|
556
|
+
* non-English instances, custom schemes), so this is a name -> name map,
|
|
557
|
+
* not a hardcoded assumption. Every field defaults independently: a config
|
|
558
|
+
* that only sets `bug: Defect` still gets Epic/Epic/Story/Task for the
|
|
559
|
+
* other four. `jira_provider.ts`'s `createIssue`/`validateIssueTypes` are
|
|
560
|
+
* the readers; `spf watch init` and `spf watch`'s own startup check
|
|
561
|
+
* (`cli/commands/watch.ts`) both validate this against the real project
|
|
562
|
+
* before anything unattended runs on it.
|
|
563
|
+
*/
|
|
564
|
+
export declare const JiraIssueTypeMapSchema: v.ObjectSchema<{
|
|
565
|
+
readonly epic: v.OptionalSchema<v.StringSchema<undefined>, "Epic">;
|
|
566
|
+
readonly feature: v.OptionalSchema<v.StringSchema<undefined>, "Epic">;
|
|
567
|
+
readonly story: v.OptionalSchema<v.StringSchema<undefined>, "Story">;
|
|
568
|
+
readonly bug: v.OptionalSchema<v.StringSchema<undefined>, "Bug">;
|
|
569
|
+
readonly task: v.OptionalSchema<v.StringSchema<undefined>, "Task">;
|
|
570
|
+
}, undefined>;
|
|
571
|
+
export type JiraIssueTypeMap = v.InferOutput<typeof JiraIssueTypeMapSchema>;
|
|
572
|
+
/**
|
|
573
|
+
* Only consulted when `issue_provider: jira`. Auth is `JIRA_EMAIL` +
|
|
574
|
+
* `JIRA_API_TOKEN` env vars, checked at startup like `GITHUB_TOKEN`. Whole-
|
|
575
|
+
* object replace on config-file-layer merge, like `refine`/
|
|
576
|
+
* `observability.otel` (see `agents.ts`'s `mergeRawConfig`) — an override
|
|
577
|
+
* file that touches `watch.jira` at all must repeat `issue_types` too if it
|
|
578
|
+
* wants to keep a customized mapping, same caveat that already applies to
|
|
579
|
+
* `base_url`/`project_key` today.
|
|
580
|
+
*/
|
|
464
581
|
export declare const WatchJiraConfigSchema: v.ObjectSchema<{
|
|
465
582
|
readonly base_url: v.OptionalSchema<v.StringSchema<undefined>, "">;
|
|
466
583
|
readonly project_key: v.OptionalSchema<v.StringSchema<undefined>, "">;
|
|
584
|
+
readonly issue_types: v.OptionalSchema<v.ObjectSchema<{
|
|
585
|
+
readonly epic: v.OptionalSchema<v.StringSchema<undefined>, "Epic">;
|
|
586
|
+
readonly feature: v.OptionalSchema<v.StringSchema<undefined>, "Epic">;
|
|
587
|
+
readonly story: v.OptionalSchema<v.StringSchema<undefined>, "Story">;
|
|
588
|
+
readonly bug: v.OptionalSchema<v.StringSchema<undefined>, "Bug">;
|
|
589
|
+
readonly task: v.OptionalSchema<v.StringSchema<undefined>, "Task">;
|
|
590
|
+
}, undefined>, () => {
|
|
591
|
+
epic: string;
|
|
592
|
+
feature: string;
|
|
593
|
+
story: string;
|
|
594
|
+
bug: string;
|
|
595
|
+
task: string;
|
|
596
|
+
}>;
|
|
467
597
|
}, undefined>;
|
|
468
598
|
export type WatchJiraConfig = v.InferOutput<typeof WatchJiraConfigSchema>;
|
|
469
599
|
/**
|
|
@@ -471,10 +601,10 @@ export type WatchJiraConfig = v.InferOutput<typeof WatchJiraConfigSchema>;
|
|
|
471
601
|
* spec into a feature/story tree of real issues, instead of running
|
|
472
602
|
* `watch.chain` against it directly (a spec is not individually workable —
|
|
473
603
|
* see `core/refine.ts`). Off by default so an existing `watch:` config's
|
|
474
|
-
* behavior is unchanged by upgrading
|
|
475
|
-
* `
|
|
476
|
-
*
|
|
477
|
-
*
|
|
604
|
+
* behavior is unchanged by upgrading. Needs `issue_provider: github` or
|
|
605
|
+
* `"jira"` — both implement `IssueAuthoringProvider` (create/link/list) —
|
|
606
|
+
* any other value fails loudly at `spf watch` startup rather than running a
|
|
607
|
+
* refine lane that can never publish anything.
|
|
478
608
|
*/
|
|
479
609
|
export declare const WatchRefineConfigSchema: v.ObjectSchema<{
|
|
480
610
|
readonly enabled: v.OptionalSchema<v.BooleanSchema<undefined>, false>;
|
|
@@ -492,12 +622,48 @@ export declare const WatchConfigSchema: v.ObjectSchema<{
|
|
|
492
622
|
readonly base_branch: v.OptionalSchema<v.StringSchema<undefined>, "main">;
|
|
493
623
|
readonly poll_ms: v.OptionalSchema<v.NumberSchema<undefined>, 60000>;
|
|
494
624
|
readonly concurrency: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>]>, 2>;
|
|
625
|
+
/**
|
|
626
|
+
* Threaded straight through as `runChainDef`'s `options` argument for
|
|
627
|
+
* every unattended `spf watch` dispatch (build lane AND refine lane) —
|
|
628
|
+
* the exact same `Record<string, string>` shape an interactive `spf
|
|
629
|
+
* <chain> --suite <name>` builds in `cli/commands/run.ts`'s
|
|
630
|
+
* `dispatchChain` (e.g. `{suite: "strict"}`, `{agent: "..."}`). Fixes the
|
|
631
|
+
* KNOWN LIMITATION called out in PR #20: `cli/commands/watch.ts`'s
|
|
632
|
+
* `runChain`/`runRefine` wrappers used to call `runChainDef` with no
|
|
633
|
+
* options at all, so nothing --suite-shaped could ever reach a chain run
|
|
634
|
+
* `spf watch` dispatched — see `cli/commands/watch.ts`. Empty by default,
|
|
635
|
+
* so an existing `watch:` config's behavior is unchanged by upgrading.
|
|
636
|
+
* Whole-object replace on merge, like `jira`/`refine` above and
|
|
637
|
+
* `observability.otel` — see `agents.ts`'s `mergeRawConfig` and
|
|
638
|
+
* `data_types.test.ts`'s merge-survival test for this field.
|
|
639
|
+
*/
|
|
640
|
+
readonly chain_options: v.OptionalSchema<v.RecordSchema<v.StringSchema<undefined>, v.StringSchema<undefined>, undefined>, () => {}>;
|
|
495
641
|
readonly jira: v.OptionalSchema<v.ObjectSchema<{
|
|
496
642
|
readonly base_url: v.OptionalSchema<v.StringSchema<undefined>, "">;
|
|
497
643
|
readonly project_key: v.OptionalSchema<v.StringSchema<undefined>, "">;
|
|
644
|
+
readonly issue_types: v.OptionalSchema<v.ObjectSchema<{
|
|
645
|
+
readonly epic: v.OptionalSchema<v.StringSchema<undefined>, "Epic">;
|
|
646
|
+
readonly feature: v.OptionalSchema<v.StringSchema<undefined>, "Epic">;
|
|
647
|
+
readonly story: v.OptionalSchema<v.StringSchema<undefined>, "Story">;
|
|
648
|
+
readonly bug: v.OptionalSchema<v.StringSchema<undefined>, "Bug">;
|
|
649
|
+
readonly task: v.OptionalSchema<v.StringSchema<undefined>, "Task">;
|
|
650
|
+
}, undefined>, () => {
|
|
651
|
+
epic: string;
|
|
652
|
+
feature: string;
|
|
653
|
+
story: string;
|
|
654
|
+
bug: string;
|
|
655
|
+
task: string;
|
|
656
|
+
}>;
|
|
498
657
|
}, undefined>, () => {
|
|
499
658
|
base_url: string;
|
|
500
659
|
project_key: string;
|
|
660
|
+
issue_types: {
|
|
661
|
+
epic: string;
|
|
662
|
+
feature: string;
|
|
663
|
+
story: string;
|
|
664
|
+
bug: string;
|
|
665
|
+
task: string;
|
|
666
|
+
};
|
|
501
667
|
}>;
|
|
502
668
|
readonly refine: v.OptionalSchema<v.ObjectSchema<{
|
|
503
669
|
readonly enabled: v.OptionalSchema<v.BooleanSchema<undefined>, false>;
|
|
@@ -574,6 +740,64 @@ export declare const ReviewConfigSchema: v.ObjectSchema<{
|
|
|
574
740
|
readonly signoff_timeout_seconds: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.MinValueAction<number, 1, undefined>]>, 300>;
|
|
575
741
|
}, undefined>;
|
|
576
742
|
export type ReviewConfig = v.InferOutput<typeof ReviewConfigSchema>;
|
|
743
|
+
/**
|
|
744
|
+
* Risk-tiered per-role model routing — SPF #14. One rung of a ladder:
|
|
745
|
+
* `name` is what `tiering.roles` points at, `model` speaks EXACTLY the same
|
|
746
|
+
* vocabulary as an agent's own `model:` for that backend (provider/model-id
|
|
747
|
+
* for flue, Claude Code's bare alias/full-name for claude_code) — there is
|
|
748
|
+
* deliberately no per-provider table, since for flue the provider is
|
|
749
|
+
* already the first segment of the value.
|
|
750
|
+
*
|
|
751
|
+
* `coding_agent` declares WHICH BACKEND'S VOCABULARY this rung's `model`
|
|
752
|
+
* speaks — same picklist and same default as `AgentConfigSchema`'s own
|
|
753
|
+
* field (`:409`), reused rather than restated. A tier changes an agent's
|
|
754
|
+
* `model` and NOTHING else (`coding_agent` stays the agent's own, always),
|
|
755
|
+
* so a rung can only route roles whose `coding_agent` matches its own — see
|
|
756
|
+
* `core/tiering.ts`'s rule T, enforced by `agents.ts`'s `validate()`.
|
|
757
|
+
*/
|
|
758
|
+
export declare const TierSchema: v.ObjectSchema<{
|
|
759
|
+
readonly name: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
760
|
+
readonly coding_agent: v.OptionalSchema<v.PicklistSchema<["flue", "claude_code"], undefined>, "flue">;
|
|
761
|
+
readonly model: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
762
|
+
}, undefined>;
|
|
763
|
+
export type Tier = v.InferOutput<typeof TierSchema>;
|
|
764
|
+
/**
|
|
765
|
+
* OFF by default, and `enabled: false`/absent is a TOTAL no-op: every agent
|
|
766
|
+
* dispatches at exactly the model its roster entry names, byte-identical to
|
|
767
|
+
* before this key existed — same discipline `max_run_cost`/`max_run_tokens`
|
|
768
|
+
* hold themselves to (`ConfigDefaultsSchema`'s own comment above).
|
|
769
|
+
*
|
|
770
|
+
* TOP-LEVEL (`SFConfigSchema` below), deliberately NOT nested under
|
|
771
|
+
* `defaults:` — `agents.ts`'s `loadConfig` back-fill loop copies a FIXED
|
|
772
|
+
* list of `defaults` keys DOWN onto every agent that hasn't set them, and
|
|
773
|
+
* `AgentConfigSchema` is a non-strict `v.object`, so a stray copy would be
|
|
774
|
+
* silently STRIPPED at parse rather than rejected (see
|
|
775
|
+
* `ConfigDefaultsSchema`'s "THE BACK-FILL TRAP" comment — `max_run_cost`/
|
|
776
|
+
* `max_run_tokens` needed an explicit never-add-this-to-the-list comment to
|
|
777
|
+
* survive exactly that trap). A top-level key sits outside that loop
|
|
778
|
+
* entirely, so the trap cannot apply here at all.
|
|
779
|
+
*
|
|
780
|
+
* `tiers` is the ladder, WEAKEST FIRST: a risk level shifts every routed
|
|
781
|
+
* role UP or DOWN this list by the same step, so order is the whole
|
|
782
|
+
* semantics — a SEQUENCE states that unambiguously where a mapping's key
|
|
783
|
+
* order would be parser-dependent.
|
|
784
|
+
*
|
|
785
|
+
* `roles` is the baseline tier per ROLE (an agent name). Naming an agent
|
|
786
|
+
* here IS the operator's statement "route this one by tier" — so the
|
|
787
|
+
* resolved tier wins over that agent's own `model:`. An agent NOT named
|
|
788
|
+
* here is never retiered: its `model:` stands, untouched. See
|
|
789
|
+
* `core/tiering.ts`'s `resolveTiering` for the full precedence rule.
|
|
790
|
+
*/
|
|
791
|
+
export declare const TieringConfigSchema: v.ObjectSchema<{
|
|
792
|
+
readonly enabled: v.OptionalSchema<v.BooleanSchema<undefined>, false>;
|
|
793
|
+
readonly tiers: v.OptionalSchema<v.ArraySchema<v.ObjectSchema<{
|
|
794
|
+
readonly name: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
795
|
+
readonly coding_agent: v.OptionalSchema<v.PicklistSchema<["flue", "claude_code"], undefined>, "flue">;
|
|
796
|
+
readonly model: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
797
|
+
}, undefined>, undefined>, () => never[]>;
|
|
798
|
+
readonly roles: v.OptionalSchema<v.RecordSchema<v.StringSchema<undefined>, v.StringSchema<undefined>, undefined>, () => {}>;
|
|
799
|
+
}, undefined>;
|
|
800
|
+
export type TieringConfig = v.InferOutput<typeof TieringConfigSchema>;
|
|
577
801
|
export declare const SFConfigSchema: v.ObjectSchema<{
|
|
578
802
|
readonly defaults: v.OptionalSchema<v.ObjectSchema<{
|
|
579
803
|
readonly coding_agent: v.OptionalSchema<v.PicklistSchema<["flue", "claude_code"], undefined>, "flue">;
|
|
@@ -584,6 +808,50 @@ export declare const SFConfigSchema: v.ObjectSchema<{
|
|
|
584
808
|
readonly tools: v.OptionalSchema<v.NullableSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>, undefined>;
|
|
585
809
|
readonly protected_files: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, () => string[]>;
|
|
586
810
|
readonly data_dir: v.OptionalSchema<v.StringSchema<undefined>, ".spf/data">;
|
|
811
|
+
/**
|
|
812
|
+
* RUN BUDGET CEILINGS — the two knobs that bound what one adw_id may spend.
|
|
813
|
+
*
|
|
814
|
+
* BOTH ABSENT BY DEFAULT, and absence is a total no-op: `agents.ts`'s
|
|
815
|
+
* `assertRunBudget()` returns immediately when neither is set, so every
|
|
816
|
+
* existing config behaves byte-identically to before this field existed.
|
|
817
|
+
* There is no ambient environment variable and no implicit default — an
|
|
818
|
+
* unbounded run stays the default because a surprise mid-run failure on a
|
|
819
|
+
* ceiling nobody chose is worse than the spend.
|
|
820
|
+
*
|
|
821
|
+
* SCOPE IS THE RUN, NOT THE CALL. Enforced against the Run's ACCUMULATED
|
|
822
|
+
* usage (`run.tokens`/`run.cost` — the same totals `run.addUsage()` mirrors
|
|
823
|
+
* into the sessions row), checked BEFORE every agent dispatch including
|
|
824
|
+
* every JSON-repair retry and every gate correction, because the cost of
|
|
825
|
+
* the call about to happen is unknowable in advance. That makes these a
|
|
826
|
+
* hard cap on FURTHER spend rather than a post-hoc report: reaching the
|
|
827
|
+
* ceiling stops the next call, it does not merely note that the last one
|
|
828
|
+
* was expensive. A tripped ceiling fails the phase closed — see
|
|
829
|
+
* `agents.ts`'s `BudgetExceeded`.
|
|
830
|
+
*
|
|
831
|
+
* `max_run_cost` is USD (the same unit the provider's own usage.cost
|
|
832
|
+
* arrives in, summed by `UsageBreakdown`); `max_run_tokens` is TOTAL
|
|
833
|
+
* tokens, i.e. the spend number — every turn re-sends the whole
|
|
834
|
+
* conversation, so this counts cached re-reads too, exactly like the
|
|
835
|
+
* `total_tokens` column in `sessions` (see `ui/server/db.ts`'s `usage()`
|
|
836
|
+
* for why that number is much larger than "material moved").
|
|
837
|
+
*
|
|
838
|
+
* Both are `> 0`, not `>= 0`: a zero ceiling would mean "no agent may ever
|
|
839
|
+
* run", which is a config mistake, not a budget — it would fail the first
|
|
840
|
+
* phase of every chain with a budget message instead of saying what is
|
|
841
|
+
* actually wrong.
|
|
842
|
+
*
|
|
843
|
+
* THE BACK-FILL TRAP (see `agents.ts`'s `loadConfig`): that function copies
|
|
844
|
+
* a handful of `defaults` keys DOWN onto each agent that hasn't set them
|
|
845
|
+
* (coding_agent/model/thinking/color/tools/writes/env_allowlist). These two
|
|
846
|
+
* keys are deliberately NOT in that list and must never be added to it —
|
|
847
|
+
* they are RUN-scoped ceilings, not per-agent settings, and a per-agent
|
|
848
|
+
* copy would read as "each agent may spend this much", which is a
|
|
849
|
+
* different (and unenforced) feature. `mergeRawConfig`'s `defaults` spread
|
|
850
|
+
* is what carries them through config layering, key-by-key; pinned by
|
|
851
|
+
* `src/test/budget.test.ts`.
|
|
852
|
+
*/
|
|
853
|
+
readonly max_run_cost: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.GtValueAction<number, 0, undefined>]>, undefined>;
|
|
854
|
+
readonly max_run_tokens: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.GtValueAction<number, 0, undefined>]>, undefined>;
|
|
587
855
|
}, undefined>, () => {
|
|
588
856
|
coding_agent: "claude_code" | "flue";
|
|
589
857
|
model: string;
|
|
@@ -593,6 +861,8 @@ export declare const SFConfigSchema: v.ObjectSchema<{
|
|
|
593
861
|
tools?: string[] | null | undefined;
|
|
594
862
|
protected_files: string[];
|
|
595
863
|
data_dir: string;
|
|
864
|
+
max_run_cost?: number | undefined;
|
|
865
|
+
max_run_tokens?: number | undefined;
|
|
596
866
|
}>;
|
|
597
867
|
readonly observability: v.OptionalSchema<v.ObjectSchema<{
|
|
598
868
|
readonly db: v.OptionalSchema<v.StringSchema<undefined>, ".spf/data/spf.db">;
|
|
@@ -660,12 +930,48 @@ export declare const SFConfigSchema: v.ObjectSchema<{
|
|
|
660
930
|
readonly base_branch: v.OptionalSchema<v.StringSchema<undefined>, "main">;
|
|
661
931
|
readonly poll_ms: v.OptionalSchema<v.NumberSchema<undefined>, 60000>;
|
|
662
932
|
readonly concurrency: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>]>, 2>;
|
|
933
|
+
/**
|
|
934
|
+
* Threaded straight through as `runChainDef`'s `options` argument for
|
|
935
|
+
* every unattended `spf watch` dispatch (build lane AND refine lane) —
|
|
936
|
+
* the exact same `Record<string, string>` shape an interactive `spf
|
|
937
|
+
* <chain> --suite <name>` builds in `cli/commands/run.ts`'s
|
|
938
|
+
* `dispatchChain` (e.g. `{suite: "strict"}`, `{agent: "..."}`). Fixes the
|
|
939
|
+
* KNOWN LIMITATION called out in PR #20: `cli/commands/watch.ts`'s
|
|
940
|
+
* `runChain`/`runRefine` wrappers used to call `runChainDef` with no
|
|
941
|
+
* options at all, so nothing --suite-shaped could ever reach a chain run
|
|
942
|
+
* `spf watch` dispatched — see `cli/commands/watch.ts`. Empty by default,
|
|
943
|
+
* so an existing `watch:` config's behavior is unchanged by upgrading.
|
|
944
|
+
* Whole-object replace on merge, like `jira`/`refine` above and
|
|
945
|
+
* `observability.otel` — see `agents.ts`'s `mergeRawConfig` and
|
|
946
|
+
* `data_types.test.ts`'s merge-survival test for this field.
|
|
947
|
+
*/
|
|
948
|
+
readonly chain_options: v.OptionalSchema<v.RecordSchema<v.StringSchema<undefined>, v.StringSchema<undefined>, undefined>, () => {}>;
|
|
663
949
|
readonly jira: v.OptionalSchema<v.ObjectSchema<{
|
|
664
950
|
readonly base_url: v.OptionalSchema<v.StringSchema<undefined>, "">;
|
|
665
951
|
readonly project_key: v.OptionalSchema<v.StringSchema<undefined>, "">;
|
|
952
|
+
readonly issue_types: v.OptionalSchema<v.ObjectSchema<{
|
|
953
|
+
readonly epic: v.OptionalSchema<v.StringSchema<undefined>, "Epic">;
|
|
954
|
+
readonly feature: v.OptionalSchema<v.StringSchema<undefined>, "Epic">;
|
|
955
|
+
readonly story: v.OptionalSchema<v.StringSchema<undefined>, "Story">;
|
|
956
|
+
readonly bug: v.OptionalSchema<v.StringSchema<undefined>, "Bug">;
|
|
957
|
+
readonly task: v.OptionalSchema<v.StringSchema<undefined>, "Task">;
|
|
958
|
+
}, undefined>, () => {
|
|
959
|
+
epic: string;
|
|
960
|
+
feature: string;
|
|
961
|
+
story: string;
|
|
962
|
+
bug: string;
|
|
963
|
+
task: string;
|
|
964
|
+
}>;
|
|
666
965
|
}, undefined>, () => {
|
|
667
966
|
base_url: string;
|
|
668
967
|
project_key: string;
|
|
968
|
+
issue_types: {
|
|
969
|
+
epic: string;
|
|
970
|
+
feature: string;
|
|
971
|
+
story: string;
|
|
972
|
+
bug: string;
|
|
973
|
+
task: string;
|
|
974
|
+
};
|
|
669
975
|
}>;
|
|
670
976
|
readonly refine: v.OptionalSchema<v.ObjectSchema<{
|
|
671
977
|
readonly enabled: v.OptionalSchema<v.BooleanSchema<undefined>, false>;
|
|
@@ -686,9 +992,19 @@ export declare const SFConfigSchema: v.ObjectSchema<{
|
|
|
686
992
|
base_branch: string;
|
|
687
993
|
poll_ms: number;
|
|
688
994
|
concurrency: number;
|
|
995
|
+
chain_options: {
|
|
996
|
+
[x: string]: string;
|
|
997
|
+
};
|
|
689
998
|
jira: {
|
|
690
999
|
base_url: string;
|
|
691
1000
|
project_key: string;
|
|
1001
|
+
issue_types: {
|
|
1002
|
+
epic: string;
|
|
1003
|
+
feature: string;
|
|
1004
|
+
story: string;
|
|
1005
|
+
bug: string;
|
|
1006
|
+
task: string;
|
|
1007
|
+
};
|
|
692
1008
|
};
|
|
693
1009
|
refine: {
|
|
694
1010
|
enabled: boolean;
|
|
@@ -722,6 +1038,25 @@ export declare const SFConfigSchema: v.ObjectSchema<{
|
|
|
722
1038
|
require_human_signoff: boolean;
|
|
723
1039
|
signoff_timeout_seconds: number;
|
|
724
1040
|
}>;
|
|
1041
|
+
readonly tiering: v.OptionalSchema<v.ObjectSchema<{
|
|
1042
|
+
readonly enabled: v.OptionalSchema<v.BooleanSchema<undefined>, false>;
|
|
1043
|
+
readonly tiers: v.OptionalSchema<v.ArraySchema<v.ObjectSchema<{
|
|
1044
|
+
readonly name: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
1045
|
+
readonly coding_agent: v.OptionalSchema<v.PicklistSchema<["flue", "claude_code"], undefined>, "flue">;
|
|
1046
|
+
readonly model: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
1047
|
+
}, undefined>, undefined>, () => never[]>;
|
|
1048
|
+
readonly roles: v.OptionalSchema<v.RecordSchema<v.StringSchema<undefined>, v.StringSchema<undefined>, undefined>, () => {}>;
|
|
1049
|
+
}, undefined>, () => {
|
|
1050
|
+
enabled: boolean;
|
|
1051
|
+
tiers: {
|
|
1052
|
+
name: string;
|
|
1053
|
+
coding_agent: "claude_code" | "flue";
|
|
1054
|
+
model: string;
|
|
1055
|
+
}[];
|
|
1056
|
+
roles: {
|
|
1057
|
+
[x: string]: string;
|
|
1058
|
+
};
|
|
1059
|
+
}>;
|
|
725
1060
|
}, undefined>;
|
|
726
1061
|
export type SFConfig = v.InferOutput<typeof SFConfigSchema>;
|
|
727
1062
|
/** The full set of event kinds any tracer.event()/makeEventRecord() call site emits — kept in sync with src/ui/shared/types.ts's EventType. */
|