1#[derive(Debug, Clone, Copy, PartialEq)]
5pub enum Kind {
6 Bool,
8 Int(i64, i64),
10 Num(f64, f64),
12 Str,
14 Choice(&'static [&'static str]),
16 Duration,
18 Argv,
20 ArgvList,
22 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#[derive(Debug, Clone, Copy)]
44pub struct Key {
45 pub key: &'static str,
47 pub kind: Kind,
49 pub default: Option<&'static str>,
51 pub required: bool,
53 pub about: &'static str,
55}
56
57#[derive(Debug, Clone, Copy)]
59pub struct Section {
60 pub name: &'static str,
62 pub title: &'static str,
64 pub about: &'static str,
66 pub named: bool,
68 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
97pub(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
107pub 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];