Skip to main content

speccoder_runtime/settings/
catalogue.rs

1//! Every key of `speccoder.toml`: its type, default and meaning.
2
3/// What a key holds.
4#[derive(Debug, Clone, Copy, PartialEq)]
5pub enum Kind {
6    /// `true` or `false`.
7    Bool,
8    /// A whole number in a range.
9    Int(i64, i64),
10    /// A number in a range.
11    Num(f64, f64),
12    /// Text.
13    Str,
14    /// One of these.
15    Choice(&'static [&'static str]),
16    /// `30m`, `12h`, `7d`.
17    Duration,
18    /// A program and its arguments.
19    Argv,
20    /// A list of programs with arguments.
21    ArgvList,
22    /// A list of strings.
23    StrList,
24}
25
26impl Kind {
27    pub(super) const fn name(self) -> &'static str {
28        match self {
29            Self::Bool => "bool",
30            Self::Int(..) => "integer",
31            Self::Num(..) => "number",
32            Self::Str => "string",
33            Self::Choice(_) => "choice",
34            Self::Duration => "duration",
35            Self::Argv => "argv",
36            Self::ArgvList => "argv_list",
37            Self::StrList => "string_list",
38        }
39    }
40}
41
42/// One key of the catalogue.
43#[derive(Debug, Clone, Copy)]
44pub struct Key {
45    /// The key, dotted below a sub-table (`sigstore.identity`).
46    pub key: &'static str,
47    /// What it holds.
48    pub kind: Kind,
49    /// Its default as JSON, when it has one.
50    pub default: Option<&'static str>,
51    /// The section is refused without it.
52    pub required: bool,
53    /// What it does.
54    pub about: &'static str,
55}
56
57/// One section of the catalogue.
58#[derive(Debug, Clone, Copy)]
59pub struct Section {
60    /// The table name.
61    pub name: &'static str,
62    /// For people.
63    pub title: &'static str,
64    /// What it decides.
65    pub about: &'static str,
66    /// `[name.<table>]` sections.
67    pub named: bool,
68    /// Its keys.
69    pub keys: &'static [Key],
70}
71
72pub(super) const fn k(
73    key: &'static str,
74    kind: Kind,
75    default: Option<&'static str>,
76    about: &'static str,
77) -> Key {
78    Key {
79        key,
80        kind,
81        default,
82        required: false,
83        about,
84    }
85}
86
87pub(super) const fn req(key: &'static str, kind: Kind, about: &'static str) -> Key {
88    Key {
89        key,
90        kind,
91        default: None,
92        required: true,
93        about,
94    }
95}
96
97/// The spec formats, by the names `[spec] format` takes — from the format
98/// model, not spelled here.
99pub(super) const FORMATS: &[&str] = &[
100    speccoder_core::spec::FormatKind::ALL[0].name(),
101    speccoder_core::spec::FormatKind::ALL[1].name(),
102    speccoder_core::spec::FormatKind::ALL[2].name(),
103];
104
105pub(super) const PRICE: Kind = Kind::Num(0.0, 100_000.0);
106
107/// Every key of `speccoder.toml`, in the reference's order. A test holds it
108/// to the parsers and to the configuration reference.
109pub const CATALOGUE: &[Section] = &[
110    Section {
111        name: "model",
112        title: "Model",
113        about: "The model that makes the attempts. The key comes from ANTHROPIC_API_KEY or OPENAI_API_KEY, Bedrock's credentials from AWS — never from this file.",
114        named: false,
115        keys: &[
116            req(
117                "provider",
118                Kind::Choice(&["anthropic", "openai", "openai-compatible", "bedrock"]),
119                "Who serves the model; openai-compatible covers Ollama, vLLM, llama.cpp, LM Studio and others; bedrock is Amazon Bedrock, credentials from AWS",
120            ),
121            req(
122                "model",
123                Kind::Str,
124                "The model, for example claude-sonnet-5-5",
125            ),
126            k(
127                "region",
128                Kind::Str,
129                None,
130                "Bedrock only: the AWS region calls go to (the journal records it)",
131            ),
132            k(
133                "reasoning_summary",
134                Kind::Bool,
135                Some("true"),
136                "Show the model's summary of its reasoning (a claim, not evidence)",
137            ),
138            k(
139                "max_output_tokens",
140                Kind::Int(1, 1_000_000),
141                Some("4096"),
142                "Output tokens per model turn",
143            ),
144            k(
145                "max_run_tokens",
146                Kind::Int(1, 100_000_000),
147                Some("500000"),
148                "Tokens per attempt",
149            ),
150            k(
151                "cache_ttl",
152                Kind::Choice(&["5m", "1h"]),
153                Some("\"5m\""),
154                "Anthropic's prompt-cache lifetime",
155            ),
156            k(
157                "base_url",
158                Kind::Str,
159                None,
160                "The server, required for openai-compatible (e.g. http://localhost:11434/v1)",
161            ),
162            k(
163                "api_key_env",
164                Kind::Str,
165                None,
166                "openai-compatible only: the variable holding the server's key",
167            ),
168            k(
169                "structured",
170                Kind::Choice(&["native", "tool"]),
171                Some("\"native\""),
172                "openai-compatible only: native structured output, or a forced tool call",
173            ),
174            k(
175                "price_input_per_mtok",
176                PRICE,
177                None,
178                "USD per million input tokens, for cost reports",
179            ),
180            k(
181                "price_output_per_mtok",
182                PRICE,
183                None,
184                "USD per million output tokens",
185            ),
186            k(
187                "price_cache_read_per_mtok",
188                PRICE,
189                None,
190                "USD per million tokens read from the cache",
191            ),
192            k(
193                "price_cache_write_per_mtok",
194                PRICE,
195                None,
196                "USD per million tokens written to the five-minute cache",
197            ),
198            k(
199                "price_cache_write_1h_per_mtok",
200                PRICE,
201                None,
202                "USD per million tokens written to the one-hour cache",
203            ),
204        ],
205    },
206    Section {
207        name: "checks",
208        title: "Checks",
209        about: "The commands that decide whether nothing regressed. Each check's command up to its first option is also something the model may run.",
210        named: true,
211        keys: &[
212            req(
213                "argv",
214                Kind::Argv,
215                "Program and arguments, run without a shell",
216            ),
217            k(
218                "cwd",
219                Kind::Str,
220                Some("\".\""),
221                "Working directory, relative to the project root",
222            ),
223            k(
224                "when_exists",
225                Kind::Str,
226                None,
227                "Run only if this path exists; otherwise skipped, never passed",
228            ),
229            k(
230                "tier",
231                Kind::Choice(&["fast", "full"]),
232                Some("\"fast\""),
233                "fast judges every task; full runs once after the feature's last task",
234            ),
235            k(
236                "timeout_secs",
237                Kind::Int(1, 86_400),
238                Some("600"),
239                "Deadline for the whole process group",
240            ),
241            k(
242                "junit",
243                Kind::Str,
244                None,
245                "A JUnit XML report the check writes, for per-test results",
246            ),
247            k(
248                "list",
249                Kind::Argv,
250                None,
251                "A command that lists the tests the check would run (one test id or file path per line); speccoder runs it when it cannot otherwise tell",
252            ),
253        ],
254    },
255    Section {
256        name: "budget",
257        title: "Budget",
258        about: "How much an attempt may do, how often a task is tried, and what a run may spend.",
259        named: false,
260        keys: &[
261            k(
262                "max_turns",
263                Kind::Int(1, 1000),
264                Some("30"),
265                "Model turns per attempt",
266            ),
267            k(
268                "max_tool_calls",
269                Kind::Int(1, 10_000),
270                Some("100"),
271                "Tool calls per attempt",
272            ),
273            k(
274                "max_attempts",
275                Kind::Int(1, 20),
276                Some("2"),
277                "Attempts per task run; a failed verdict is retried with the verifier's facts",
278            ),
279            k(
280                "best_of",
281                Kind::Int(1, 10),
282                Some("1"),
283                "Independent samples per task, the best by evidence kept",
284            ),
285            k(
286                "cap",
287                Kind::Str,
288                None,
289                "What a run may spend: tokens (\"2M\", \"500k\") or dollars (\"$5\", at [model]'s prices); `run --cap` overrides it",
290            ),
291        ],
292    },
293    Section {
294        name: "commands",
295        title: "Commands",
296        about: "What the model may run without asking, besides the checks. `allow` is exact; a prefix in `allow_prefix` runs whatever follows it, so it is saved only from a preview of what it would newly allow.",
297        named: false,
298        keys: &[
299            k(
300                "allow",
301                Kind::ArgvList,
302                Some("[]"),
303                "Exact commands that run without asking, e.g. [\"cargo\", \"check\"]; nothing after them",
304            ),
305            k(
306                "allow_prefix",
307                Kind::ArgvList,
308                Some("[]"),
309                "Command prefixes (two elements or more) whose every extension runs without asking; saved only from the coverage preview",
310            ),
311        ],
312    },
313    Section {
314        name: "approvals",
315        title: "Approvals",
316        about: "What runs without asking, how long a call waits for a person, and how often a run may ask.",
317        named: false,
318        keys: &[
319            k(
320                "contained",
321                Kind::Choice(&["allow", "ask"]),
322                Some("\"allow\""),
323                "Commands the OS sandbox confines (writes only here, no network beyond loopback) run without asking; \"ask\" asks for each",
324            ),
325            k(
326                "wait",
327                Kind::Duration,
328                Some("\"7d\""),
329                "How long a call waits before it is denied as expired (m, h or d)",
330            ),
331            k(
332                "budget",
333                Kind::Int(0, 100),
334                Some("12"),
335                "Prompts a run shows you; further calls wait together in Needs You",
336            ),
337        ],
338    },
339    Section {
340        name: "tests",
341        title: "Tests",
342        about: "What the model may do with tests that existed before an attempt.",
343        named: false,
344        keys: &[k(
345            "frozen",
346            Kind::Choice(&["ask", "additions"]),
347            Some("\"ask\""),
348            "ask before any change, or allow new lines without asking",
349        )],
350    },
351    Section {
352        name: "spec",
353        title: "Spec format",
354        about: "Which spec format the project uses; detected when unset.",
355        named: false,
356        keys: &[k(
357            "format",
358            Kind::Choice(FORMATS),
359            None,
360            "The spec format; --format and SPECCODER_FORMAT override it",
361        )],
362    },
363    Section {
364        name: "context",
365        title: "Instructions",
366        about: "The repository's instructions for agents, given to every attempt.",
367        named: false,
368        keys: &[k(
369            "instructions",
370            Kind::StrList,
371            Some("[\"AGENTS.md\"]"),
372            "File names read per directory, the first that exists; none to read none",
373        )],
374    },
375    Section {
376        name: "acceptance",
377        title: "Acceptance",
378        about: "Tests derived from the spec, and how strong they are.",
379        named: false,
380        keys: &[
381            k(
382                "generate",
383                Kind::Bool,
384                Some("true"),
385                "Derive acceptance tests from the spec: a story's oracle before its first code task, a convergence gap's own test",
386            ),
387            k(
388                "held_out",
389                Kind::Bool,
390                Some("true"),
391                "Also write held-out tests the implementer never sees; a story's last code task needs them to pass",
392            ),
393            k(
394                "strength",
395                Kind::Bool,
396                Some("true"),
397                "Measure the acceptance strength of every verified task",
398            ),
399            k(
400                "min_strength",
401                Kind::Num(0.0, 1.0),
402                None,
403                "Below it the set is strengthened once, then the task stops for you",
404            ),
405            k(
406                "near_miss",
407                Kind::Int(0, 3),
408                Some("2"),
409                "Wrong variants to try",
410            ),
411            k(
412                "permitted",
413                Kind::Int(0, 2),
414                Some("1"),
415                "Spec-permitted alternatives to try; 0 turns the over-strict check off",
416            ),
417            k(
418                "strength_timeout_secs",
419                Kind::Int(1, 86_400),
420                Some("900"),
421                "Budget for the whole strength measurement",
422            ),
423        ],
424    },
425    Section {
426        name: "bug",
427        title: "Bug fixes",
428        about: "`speccoder bug`: how hard reproduce tries before the bug goes to you as cannot reproduce. A project setting only: a value in your user config is ignored.",
429        named: false,
430        keys: &[
431            k(
432                "reproduce_tries",
433                Kind::Int(1, 5),
434                Some("3"),
435                "Tries of the reproduction author before cannot reproduce (a rejection counts as one)",
436            ),
437            k(
438                "test",
439                Kind::Argv,
440                None,
441                "How one named test (`--test PATH::NAME`) runs, `{file}` and `{name}` filled in; default: the toolchain's own runner",
442            ),
443        ],
444    },
445    Section {
446        name: "ask",
447        title: "Ask",
448        about: "The read-only conversation beside every view: its model and its limits.",
449        named: false,
450        keys: &[
451            k(
452                "model",
453                Kind::Str,
454                None,
455                "Ask's model; [model]'s when unset (same provider)",
456            ),
457            k(
458                "max_turn_tokens",
459                Kind::Int(1_000, 10_000_000),
460                Some("100000"),
461                "Tokens one question may use, cache reads apart",
462            ),
463            k(
464                "daily_tokens",
465                Kind::Int(0, 1_000_000_000),
466                Some("1000000"),
467                "Tokens Ask may use in a UTC day; runs are never counted",
468            ),
469        ],
470    },
471    Section {
472        name: "ladder",
473        title: "Promotion",
474        about: "When a quick change or bug that grew is offered as a spec of its own.",
475        named: false,
476        keys: &[k(
477            "promote_files",
478            Kind::Int(1, 10_000),
479            Some("8"),
480            "More files changed than this offers promotion",
481        )],
482    },
483    Section {
484        name: "converge",
485        title: "Convergence",
486        about: "How often a feature may be brought back to its spec.",
487        named: false,
488        keys: &[k(
489            "max_rounds",
490            Kind::Int(1, 20),
491            Some("3"),
492            "Convergence phases a feature may get",
493        )],
494    },
495    Section {
496        name: "review",
497        title: "Spec review",
498        about: "Whether a run waits for every requirement to be approved, and how tests are written at review.",
499        named: false,
500        keys: &[
501            k(
502                "required",
503                Kind::Bool,
504                Some("false"),
505                "A run waits until every requirement is approved on its current wording",
506            ),
507            k(
508                "oracle_samples",
509                Kind::Int(1, 2),
510                Some("2"),
511                "Independent author calls Write Tests makes; two are compared for ambiguity",
512            ),
513        ],
514    },
515    Section {
516        name: "monitor",
517        title: "Gaming monitor",
518        about: "A second model that reviews every passing attempt for gaming.",
519        named: false,
520        keys: &[k(
521            "enabled",
522            Kind::Bool,
523            Some("false"),
524            "Review every passing attempt",
525        )],
526    },
527    Section {
528        name: "git",
529        title: "Git",
530        about: "How passed tasks are recorded.",
531        named: false,
532        keys: &[k(
533            "commit",
534            Kind::Choice(&["per-task", "none"]),
535            Some("\"per-task\""),
536            "Commit each passed task, or leave changes uncommitted",
537        )],
538    },
539    Section {
540        name: "format",
541        title: "Formatter",
542        about: "A formatter run after each attempt; its changes are part of the attempt and audited.",
543        named: false,
544        keys: &[k(
545            "argv",
546            Kind::Argv,
547            None,
548            "The formatter, e.g. [\"cargo\", \"fmt\"]",
549        )],
550    },
551    Section {
552        name: "sandbox",
553        title: "Sandbox",
554        about: "Where commands the model runs may write and connect.",
555        named: false,
556        keys: &[
557            k(
558                "mode",
559                Kind::Choice(&["auto", "required", "off"]),
560                Some("\"auto\""),
561                "auto sandboxes where the OS can; required refuses to run unsandboxed",
562            ),
563            k(
564                "network",
565                Kind::Choice(&["off", "loopback", "on"]),
566                Some("\"loopback\""),
567                "Network for commands; loopback lets local test servers work",
568            ),
569            k(
570                "network_allow",
571                Kind::ArgvList,
572                Some("[]"),
573                "Exact commands that get the network, e.g. [\"cargo\", \"fetch\"]",
574            ),
575            k(
576                "network_allow_prefix",
577                Kind::ArgvList,
578                Some("[]"),
579                "Prefixes (two elements or more) whose extensions get the network; saved only from the coverage preview",
580            ),
581            k(
582                "writable",
583                Kind::StrList,
584                Some("[]"),
585                "Extra writable paths (~ is your home), e.g. ~/.m2",
586            ),
587        ],
588    },
589    Section {
590        name: "worktree",
591        title: "Worktree",
592        about: "What a run's worktree gets that git never holds: dependency folders (node_modules, .venv) and git-ignored files such as .env.",
593        named: false,
594        keys: &[
595            k(
596                "prepare",
597                Kind::Choice(&["auto", "off"]),
598                Some("\"auto\""),
599                "auto installs Node and uv dependencies (locked, offline) before the baseline; off never",
600            ),
601            k(
602                "include",
603                Kind::StrList,
604                Some("[]"),
605                "Git-ignored files copied from your checkout, .gitignore patterns, e.g. .env (with .worktreeinclude's); never committed, secret-named values masked",
606            ),
607        ],
608    },
609    Section {
610        name: "attest",
611        title: "Attestations",
612        about: "Signed statements of every tick, as git notes.",
613        named: false,
614        keys: &[
615            k(
616                "enabled",
617                Kind::Bool,
618                Some("true"),
619                "Write the statements on each tick commit",
620            ),
621            k(
622                "signer",
623                Kind::Choice(&["git", "sigstore", "none"]),
624                Some("\"git\""),
625                "Your git signing key, keyless sigstore, or unsigned",
626            ),
627            k(
628                "sign_at",
629                Kind::Choice(&["tick", "later"]),
630                Some("\"tick\""),
631                "Sign at each tick, or later with speccoder attest sign",
632            ),
633            k(
634                "sigstore.identity",
635                Kind::Str,
636                None,
637                "The identity you sign with, suggested to reviewers",
638            ),
639            k("sigstore.issuer", Kind::Str, None, "Its OIDC issuer"),
640        ],
641    },
642    Section {
643        name: "mutation",
644        title: "Mutation testing",
645        about: "Mutants run against the suite after a passing verdict.",
646        named: false,
647        keys: &[
648            req(
649                "tool",
650                Kind::Choice(&["cargo-mutants", "command"]),
651                "cargo-mutants, or command for any tool writing the mutation-testing report",
652            ),
653            k(
654                "argv",
655                Kind::Argv,
656                None,
657                "For command: the program; may use {diff}, {out}, {files}, {files_csv}",
658            ),
659            k("report", Kind::Str, None, "For command: the report file"),
660            k(
661                "mode",
662                Kind::Choice(&["report", "require"]),
663                Some("\"report\""),
664                "report records; require fails the verdict when mutants survive",
665            ),
666            k(
667                "timeout_secs",
668                Kind::Int(1, 86_400),
669                Some("900"),
670                "Limit for the whole mutation run",
671            ),
672            k(
673                "suite",
674                Kind::Bool,
675                Some("true"),
676                "Run mutants against the whole suite; false uses them for strength only",
677            ),
678            k(
679                "acceptance_argv",
680                Kind::Argv,
681                None,
682                "For command: runs the mutants against the acceptance tests alone",
683            ),
684        ],
685    },
686    Section {
687        name: "executor",
688        title: "Executor",
689        about: "Who makes the attempts: speccoder's own loop, or an external agent over ACP whose calls speccoder decides.",
690        named: false,
691        keys: &[
692            k(
693                "agent",
694                Kind::Choice(&["copilot", "opencode", "claude", "codex", "gemini"]),
695                None,
696                "GitHub Copilot, OpenCode, Claude Code, Codex or Gemini CLI, over ACP",
697            ),
698            k(
699                "kind",
700                Kind::Choice(&["native", "acp", "command"]),
701                None,
702                "Without an agent: native, any ACP agent (acp) or any command, by argv",
703            ),
704            k(
705                "model",
706                Kind::Str,
707                None,
708                "The agent's model, where it offers a choice",
709            ),
710            k(
711                "mode",
712                Kind::Str,
713                None,
714                "The agent's own permission mode; unset, speccoder asks for the one that asks first",
715            ),
716            k(
717                "timeout_secs",
718                Kind::Int(1, 86_400),
719                Some("1800"),
720                "How long one attempt may take",
721            ),
722            k(
723                "argv",
724                Kind::Argv,
725                None,
726                "For acp: the agent's command; for command: the program, which reads the task on stdin",
727            ),
728        ],
729    },
730    Section {
731        name: "mcp",
732        title: "MCP servers",
733        about: "Stdio MCP servers started in the worktree for each task; their tools appear as mcp__<server>__<tool>.",
734        named: true,
735        keys: &[
736            req("command", Kind::Argv, "The server's program and arguments"),
737            k(
738                "allow",
739                Kind::StrList,
740                Some("[]"),
741                "Tools called without approval",
742            ),
743            k("deny", Kind::StrList, Some("[]"), "Tools never offered"),
744        ],
745    },
746    Section {
747        name: "hooks",
748        title: "Hooks",
749        about: "Your own commands around a run: a gate (at) stops a tick or a pull request unless it exits 0; an observer (on) hears ticks and stops. Each runs only after you trusted its exact bytes.",
750        named: true,
751        keys: &[
752            k(
753                "at",
754                Kind::Choice(&["tick", "pr"]),
755                None,
756                "A gate: before every tick, or before a pull request is pushed",
757            ),
758            k(
759                "on",
760                Kind::StrList,
761                None,
762                "An observer: task.ticked, task.stopped, or \"*\"",
763            ),
764            k(
765                "argv",
766                Kind::Argv,
767                None,
768                "The command; an observer gets the event on stdin",
769            ),
770            k(
771                "url",
772                Kind::Str,
773                None,
774                "An observer's webhook: https (http only to localhost)",
775            ),
776            k(
777                "secret_env",
778                Kind::Str,
779                None,
780                "The variable with the webhook's signing key (Standard Webhooks)",
781            ),
782            k(
783                "timeout",
784                Kind::Int(1, 600),
785                None,
786                "Seconds: 60 for a gate, 10 for an observer",
787            ),
788            k(
789                "env",
790                Kind::StrList,
791                Some("[]"),
792                "Variables passed through, by name",
793            ),
794            k(
795                "network",
796                Kind::Bool,
797                Some("false"),
798                "Whether a gate may reach the network",
799            ),
800            k(
801                "files",
802                Kind::StrList,
803                Some("[]"),
804                "More files it depends on (its configuration): trusted with it, never written by the model",
805            ),
806        ],
807    },
808];