Skip to main content

supercode_harness/
presets.rs

1//! §4 "Presets" (`docs/composable-harness/COMPOSABLE-HARNESS-DESIGN.md`) —
2//! P2 of the composable-harness migration (design §5.2, phase **P2**).
3//!
4//! The six reserved built-in presets, compiled in as TOML consts, transcribed
5//! faithfully from the design doc's §4.1-§4.5 TOML blocks (and §4's intro
6//! paragraph for the sixth, `supercode-default`, which the doc defines by
7//! prose rather than a TOML block — S10 fix: "`pi-core` MINUS `{trust,
8//! session_tree, session_share, server, plugins}`", not "`pi-core` plus
9//! extras").
10//!
11//! **Syntax fix (P2 judgment call).** The design doc's `[capabilities.X]
12//! { enabled = true, ... }` lines combine a TOML table-HEADER and an
13//! inline-table VALUE on one line, which is not valid TOML (verified
14//! empirically against the `toml` crate: `invalid table header, expected
15//! newline`). Naively rewriting them as dotted-key assignments
16//! (`capabilities.X = { ... }`) is also unsafe wherever such a line appears
17//! *after* an already-open `[capabilities.permissions]` table (cc-parity,
18//! cx-parity, oc-parity all have this): a dotted key inside an open table is
19//! relative to the CURRENT table, so `capabilities.permissions.sandbox = {..}`
20//! written while inside `[capabilities.permissions]` nests as
21//! `capabilities.permissions.capabilities.permissions.sandbox`, silently
22//! corrupting the structure. The transcription below instead expands every
23//! `[capabilities.X] { k = v, ... }` shorthand into the equivalent explicit
24//! form — a real `[capabilities.X]` table header (always root-absolute,
25//! never context-relative) followed by `k = v` lines — which is safe
26//! regardless of surrounding context and preserves the exact same resolved
27//! structure. Every block below is verified to parse into `HarnessConfig`
28//! in this module's tests, and each preset's resolved shape is golden-tested
29//! against §4.6's per-preset verdicts in
30//! `crates/harness/tests/composable_presets.rs`.
31//!
32//! Comments from the design doc are preserved verbatim inside each TOML
33//! block for traceability back to the source section.
34
35/// `pi-core` — design §4.1.
36pub const PI_CORE_TOML: &str = r#"# built-in preset: pi-core — the §1 core with pi's exact defaults, plus pi's four kept extras.
37schema_version = 1
38
39[core]
40effort = "medium"                       # pi default thinking level (pi§3, src:core/defaults.ts:3)
41max_tool_output_bytes = 51200           # pi's shared truncation policy: 50KB / 2000 lines (pi§1, truncate.ts)
42project_context = true                  # AGENTS.md/CLAUDE.md global + ancestor walk (pi§2 "Context files")
43env_context = true                      # pi appends Current date + cwd to the prompt (pi§2, system-prompt.ts:88-170)
44
45[core.retry]                            # pi agent-level auto-retry (pi§3)
46enabled = true
47max_retries = 3
48base_delay_ms = 2000
49
50[core.tools]
51enabled = ["read_file", "bash", "edit_file", "write_file"]  # pi's default-ACTIVE four (pi§1, src:core/sdk.ts:245)
52schema_tier = "full"
53[core.tools.read_file]
54multimodal = true                       # pi read returns images as attachments (pi§1, read.ts)
55
56[core.skills]
57enabled = true                          # agentskills.io discovery + progressive disclosure (pi§2; D-7 met by read_file)
58                                        # No `harness` here: `supercode-default` extends this preset and is DEFINED as
59                                        # today's unfiltered default stack (§4 intro), which discovers no SKILL.md
60                                        # package and offers no `skill` tool. Naming a root table here would change what
61                                        # `supercode` with no config file at all does.
62
63[core.compaction]
64enabled = true
65after_messages = 0                      # pi's trigger is token pressure, never message count (pi§2)
66reserve_tokens = 16384                  # compaction.reserveTokens default (pi§2, pi§6)
67keep_recent_tokens = 20000              # compaction.keepRecentTokens default (pi§2)
68summarize = true                        # structured Goal/Constraints/Progress/… summary (pi§2, compaction.md)
69
70[core.steering]
71steering_mode = "one-at-a-time"         # pi delivery-mode defaults (pi§3 "Message queue", pi§6)
72follow_up_mode = "one-at-a-time"
73
74# ---- modules ON (each is on pi's kept-list, pi§10 closing) ----
75[capabilities.trust]
76enabled = true
77default = "ask"
78# pi's ONE built-in gate (pi§4, trust-manager.ts; defaultProjectTrust "ask")
79[capabilities.session_tree]
80enabled = true
81branch_summaries = true
82labels = true
83# THE core pi feature (pi§10; D5)
84[capabilities.session_share]
85enabled = true
86# /share gist public link (pi§8); human /export HTML is core now (§1.6, S6) and on regardless
87[capabilities.server]
88enabled = true
89# --mode json / --mode rpc embedding ladder (pi§8, pi§10)
90[capabilities.plugins]
91enabled = true
92# everything-is-an-extension (pi§7; D-10 dep satisfied by trust above)
93[capabilities.tui]
94enabled = true
95# §1.9 recorded deviation, default-on in parity presets
96
97# ---- notable OFFs (each a pi FIRST-PARTY omission, pi§10 / catalog §3) ----
98[capabilities.tools_search]
99enabled = false
100# grep/find/ls exist but are OPT-IN even in pi (pi§1 "--tools"); one line re-enables
101[capabilities.mcp]
102enabled = false
103# "intentionally does not include built-in MCP" (pi§7)
104[capabilities.subagents]
105enabled = false
106# example extension only (pi§3 "NO subagents")
107[capabilities.permissions]
108enabled = false
109approval = "never"
110sandbox = "danger_full_access"
111                                # pi has NO popups/rules/sandbox (pi§4; README "Permissions & containerization").
112                                # C3 fires its MANDATORY warning here BY DESIGN — pi's own docs say containers, not trust in the harness.
113[capabilities.plan_mode]
114enabled = false
115# example ext only (pi§10)
116[capabilities.todos]
117enabled = false
118# example ext only (pi§10)
119[capabilities.tools_background]
120enabled = false
121# "tmux instead" (pi§10)
122[capabilities.tools_web]
123enabled = false
124# web search ships as a SKILL in pi (pi§10)
125[capabilities.checkpoint]
126enabled = false
127# git-checkpoint example ext only (pi§5)
128[capabilities.memory]
129enabled = false
130# no memory subsystem (pi§2)
131[capabilities.hooks]
132enabled = false
133# pi's "hooks" are code extensions, not config-registered commands
134[capabilities.deferred_tools]
135enabled = false
136[capabilities.cache]
137enabled = false
138[capabilities.reduction]
139enabled = false
140# supercode-only OPTIONAL policies off; A7 truncation + rehydrate stay always-on core regardless (§1.13, S1/S7 — no longer a D-8/D-8-error risk)
141[capabilities.model_catalog]
142enabled = false
143[capabilities.model_oauth]
144enabled = false
145# pi HAS /login OAuth (pi§9) — deferred module 27, recorded gap
146"#;
147
148/// `cc-parity` — design §4.2.
149pub const CC_PARITY_TOML: &str = r#"# built-in preset: cc-parity — Claude Code's default surface, composed.
150schema_version = 1
151
152[core]
153model = "anthropic/claude-opus-4-8"     # CC account-default Opus 4.8 (cc§9 "Account-type defaults")
154effort = "medium"
155env_context = true                      # CC startup context: cwd/git status (cc§2 "Startup context"); BP-4 adds the approval/sandbox policy line the catalog:90 semantics name, re-emitted per turn on change
156context_injections = true               # BP-4 (catalog:91): CC splices ambient reminder blocks into every session (hook `additionalContext`, `<system-reminder>` blocks, cc§2) — arms `crate::context_injection`'s built-in blocks + the runtime splice seam
157project_context = true                  # CLAUDE.md tiers + directory walk (cc§2); global tier included (§1.4)
158nested_instructions = true               # S6/S12 home: subdir CLAUDE.md loaded on demand, CC default (catalog:84) — closes a gap-ledger row
159instruction_imports  = true              # S6/S12 home: `@path` imports, depth 4, CC default (catalog:85) — closes a gap-ledger row
160project_root_markers = [".git"]          # BP-4: bounds the ANCESTOR walk (catalog:81) — CC walks UP from cwd and concatenates root→cwd, closest read last (cc§2 "Directory-walk loading")
161project_doc_max_bytes = 0                # BP-4 (catalog:87): CC documents NO byte cap on CLAUDE.md — its hygiene levers are the two below — and §3.1 spells "uncapped" as 0. Stated explicitly rather than left absent so the preset says what CC does instead of leaving it to a default.
162project_doc_excludes = []                # BP-4 (catalog:87): CC's `claudeMdExcludes` (cc§2) — glob/absolute-path list of CLAUDE.md files to skip; CC ships it EMPTY, same as the rule sets below
163project_doc_strip_comments = true        # BP-4 (catalog:87): CC strips block-level `<!-- … -->` from CLAUDE.md before injection so maintainer notes cost no tokens (cc§2 "HTML comment stripping")
164parallel_tool_calls = true               # BP-2: CC runs an assistant turn's independent sibling calls as a concurrent batch (cc§1, catalog:59) — the gated batch path in `Agent::run_tools_concurrently`, armed
165tool_output_spill = true                 # BP-2: CC's Bash-overflow → session file recovery door (catalog:58): a capped tool result keeps its full bytes in a per-session spill file the notice names, readable with `read_file` — no `capabilities.reduction` required
166file_mentions = true                    # BP-5 (catalog D2 "@-file mentions / attachments"): `@` in the prompt injects that file's context (cc§2 "`@`-file mentions"). Deny-rule aware, as CC documents it — a mention resolving to a protected path (see `permissions.protected_paths` below) inlines the refusal, never the bytes.
167output_style = "default"                # BP-5 (catalog D2 "Output style / personality module"): CC's `outputStyle` setting at the value CC itself ships — the Default style, which appends nothing to the base prompt (cc§7 "Output styles"). The LAYER is what this arms: naming any other style (`explanatory`, `learning`, or a `~/.claude/output-styles/<name>.md` of the user's own) swaps the response-style instructions without touching `system_prompt`.
168path_rules = true                       # BP-5 (catalog D2 "Path-scoped rules"): CC's `.claude/rules/*.md` (cc§2). A rule with no `paths:` joins the instruction blob at startup; a rule WITH `paths:` waits and is injected the first time a tool touches a matching file — CC's own "loads only when Claude touches matching files".
169
170[core.model_switch]
171allow_switch = true                     # BP-13 (catalog D9 "Mid-session model switching"): CC's `/model` + Alt+P change the model without losing the session (cc§9). Arms the governed switch — reasoning-artifact filtering (dep 8) plus a persisted `model_change` record in the session journal.
172                                        # `notice` is deliberately NOT set: CC switches SILENTLY (no injected switch instructions); that is Codex's behaviour, and cx-parity sets it there.
173
174[core.tools]
175# BP-3: `current_time`/`sleep` join the optional default-tool names (the
176# `view_image` precedent, §1.2's "fifth optional default-tool name, not a new
177# module"). The catalog's clock/sleep row is `✓*` for CC — ScheduleWakeup
178# paces the loop but neither reports the time nor pauses it — so cc-parity
179# supplies the capability itself rather than the footnote.
180enabled = ["read_file", "bash", "edit_file", "write_file", "current_time", "sleep"]
181schema_tier = "full"
182[core.tools.read_file]
183multimodal = true                       # CC Read renders images/PDFs/notebooks (cc§1 Read)
184line_numbers = true                     # BP-2: CC Read's `cat -n` gutter, numbered from `offset` (cc§1 Read, catalog:26)
185[core.tools.edit_file]
186require_read_before_edit = true         # S6/S12 home: CC Edit refuses unless the file was read this conversation (catalog:32) — closes the cc-parity gap-ledger row
187notebook_aware = true                   # S6/S12 home: NotebookEdit cell-level replace/insert/delete (catalog:40) — closes the cc-parity gap-ledger row
188[core.tools.bash]
189timeout_secs = 120                      # CC default 2 min, model-raisable (cc§1 Bash)
190
191[core.skills]
192enabled = true                          # SKILL.md dirs + commands, descriptions-only until invoked (cc§7 Skills)
193harness = "claude-code"                 # BP-6: the loop discovers SKILL.md from CC's own documented roots and precedence — enterprise/managed > `~/.claude/skills` > plugin bundles (`plugin:skill`) > project `.claude/skills`, plus nested subdirectory skills as `dir:skill` (cc§7 "Skill locations & precedence")
194shell_injection = true                  # BP-5 (catalog D2 "Shell-output injection in templates/skills"): CC executes `` !`cmd` `` inline and ```` ```! ```` blocks inside a skill/command body AT LOAD TIME (cc§7 "Dynamic context injection"), disabled org-wide with `disableSkillShellExecution` — this key is that switch stated positively. Every extracted command is decided by the one permissions engine with THIS preset's rules and protected paths; under `approval = "untrusted"` a bare `bash` is `Ask`, so a body that wants its own command run declares it in `allowed-tools`, exactly as CC requires.
195
196[core.compaction]
197enabled = true
198summarize = true                        # CC auto-compaction near limit + /compact [instructions] (cc§2). BP-4: this key now arms the CORE span-summary side-call too — design §1.5 makes "an LLM summary of the compacted span" part of obligation 5, and §3.1 annotates this very key "SpanSummary side-call … D-9 small-model fallback"
199reserve_tokens = 16384                  # CC's threshold is pct-based (CLAUDE_CODE_AUTOCOMPACT_PCT_OVERRIDE, cc§2); reserve is our §1.5 equivalent
200focus_instructions = ""                 # BP-4 (catalog:98): CC has NO standing compaction focus — steering is per invocation, `/compact [instructions]` (cc§2). Stated empty so the preset says that, rather than leaving the knob to a default, and so the mechanism is armed at the one place it belongs: `Agent::compact_now(focus)`.
201
202[core.session]
203append_only = true                      # BP-8 (catalog:150): CC flushes every event to its session JSONL as the turn runs (cc§5), so a crash mid-turn keeps the turn. Arms `crate::session_journal` — supercode's own store otherwise rewrites `<name>.jsonl` only at end-of-turn.
204queue_persist = true                    # BP-8 (catalog:154): CC writes QUEUE-OPERATION records for prompts typed while it is busy (cc§5), so a pending input survives a restart. The journal armed above is where those records go.
205auto_title = true                       # BP-7 (catalog:150): CC writes `ai-title` records — a cheap-model title for every session (cc§5). The titler, its small-model routing and its `SessionStore::set_title` write were all built and CLI-wired at P4b; this preset never set the gate, so under cc-parity no title was ever generated. `small_model` is pinned below (`capabilities.model_catalog`), so this runs on Haiku, not Opus.
206
207[core.prompts]
208# BP-7 (catalog §4a "Review mode"): CC's `/code-review` + `/security-review` are
209# purpose-built review TURNS with a fixed report format, not a separate agent
210# (cc§10). This template IS that format; `Agent::review` sends it as an ordinary
211# turn of this same session, so the review inherits the session's tools,
212# permissions, transcript and records.
213code-review = """Review the current code changes as a dedicated review turn. {args}
214Inspect the diff and the files it touches with the available tools before judging anything.
215Report in exactly these sections, omitting a section only when it is genuinely empty:
2161. Correctness — defects, in severity order (blocker / major / minor), each with file:line and the failing case.
2172. Security — untrusted input reaching a trust boundary, secrets, injection, permission widening.
2183. Reuse and simplification — existing code the change should have used; code the change makes dead.
2194. Verdict — one line: SHIP, SHIP WITH FIXES, or DO NOT SHIP, and why."""
220
221# ---- modules ON ----
222[capabilities.tools_search]
223enabled = true
224glob = true
225content_search = true
226list_dir = false
227                               # Glob + Grep, promptless read-class (cc§1); list_dir OFF (S15 fix) — CC lists dirs via Bash/Glob, Read rejects directories (catalog:33 fn²), so the module's dir-listing sub-tool would be a capability CC users never see
228[capabilities.tools_web]
229enabled = true
230fetch = true
231search = true
232# WebFetch + WebSearch (cc§1)
233[capabilities.tools_question]
234enabled = true
235# AskUserQuestion (cc§1)
236[capabilities.todos]
237enabled = true
238persist = true
239goals = true
240# Task*/TodoWrite checklist, persists across compaction (cc§1, cc§3)
241# BP-7 (catalog:138): `goals` is this module's persistent-objective variant (design §2 module 7) — CC's `/goal`, a standing condition restated at the tail of every request and persisted as `<session>.goal.json`, distinct from the per-stretch `update_plan` checklist above.
242[capabilities.plan_mode]
243enabled = true
244effort = "high"                         # BP-13 (catalog D9, the plan-mode half of "Reasoning effort / thinking budgets"): planning is the phase that most rewards deeper reasoning, and CC's plan mode is where a session does its thinking (cc§3). While the mode is live the request carries this level instead of `[core] effort`; leaving the mode restores it.
245# Shift+Tab / EnterPlanMode read-only mode (cc§3); dep met by permissions.rules below
246[capabilities.subagents]
247enabled = true
248max_depth = 2
249background = true
250background_prompts = "parent"
251                               # Agent tool; background-by-default v2.1.198+, nested allowed (cc§1, cc§3)
252                               # C6 resolved via the schema key (S2 fix, not prose): background_prompts = "parent" — background children surface prompts in the parent session (cc§3, claude-code.md:98)
253[capabilities.tools_background]
254enabled = true
255# run_in_background + Ctrl+B (cc§1, cc§8); C6: same parent-surfaced queue as subagents.background_prompts above
256
257[capabilities.permissions]
258enabled = true
259approval = "untrusted"                  # CC tiered default: read-only never prompts, Bash/edits prompt first-use (cc§4 "Tiered defaults")
260                                        # No C3: approval != never.
261auto_approved_tools = ["read_file", "glob", "search", "ask_user", "current_time", "sleep", "enter_plan_mode", "exit_plan_mode", "update_plan"]
262                                        # CC read-only tier (cc§4); `list_dir` removed (S15 — module tool is off above)
263                                        # BP-3: the new no-side-effect tools join that tier — CC never prompts before
264                                        # AskUserQuestion/EnterPlanMode/ExitPlanMode, and under `approval = "untrusted"`
265                                        # every tool NOT listed here asks first, which would put a permission prompt in
266                                        # front of the question prompt (and would refuse both outright in a headless run).
267                                        # `exit_plan_mode` carries its own explicit plan approval, so the generic gate in
268                                        # front of it is pure double-prompting.
269                                        # BP-8 (catalog:156): `update_plan` joins the same tier — CC's TodoWrite is a
270                                        # checklist write with no side effect outside the session and never prompts
271                                        # (cc§1/cc§3). Under `approval = "untrusted"` it otherwise asks on every plan
272                                        # update, which in a headless run refuses the plan outright — i.e. the plan row's
273                                        # own behaviour would be unreachable under this preset.
274# module 12 in table form (S5): OS sandbox OFF (CC's `/sandbox` is opt-in, cc§4), fs tier unconfined.
275# One key `permissions.sandbox` — the table form, not the bare-scalar shorthand, so no collision.
276[capabilities.permissions.sandbox]
277enabled = false
278tier = "danger_full_access"
279env_policy = "inherit"                  # BP-10 (catalog "Child-process env sanitization", cc `✓*`): CC's env control is `sandbox.credentials.envVars` — an OPT-IN of the opt-in `/sandbox` (cc§4). With the OS sandbox off above, a CC Bash call inherits the user's environment, so the preset SAYS "inherit" rather than leaving the knob to a default that happens to agree; cx-parity states the other posture on the same key.
280escalation = "ask"                      # BP-10 (catalog "Sandbox-escalation path", cc `✓*` allowUnsandboxedCommands): with `enabled = false` nothing is confined, so this never fires under cc-parity today — it is the answer for the `/sandbox` session, where CC asks before running a command outside the sandbox rather than refusing it outright.
281[capabilities.permissions.sandbox.network]
282enabled = false                         # BP-10: CC's own default. `/sandbox` is opt-in and its network proxy with it (cc§4 `sandbox.network.*`); stated so the preset carries CC's posture on the key instead of defaulting to it silently.
283[capabilities.permissions.rules]
284enabled = true                          # deny→ask→allow first-match IS the CC algebra — native, no translation (C5 decision; cc§4 "Rule sets & evaluation")
285deny  = []
286ask   = []
287allow = []                              # CC ships empty rule sets; "don't ask again" persists into allow at runtime (cc§4)
288[capabilities.permissions.protected_paths]
289enabled = true                          # never-auto-approved set (cc§4 "Protected paths")
290# Rule-layer floor: file-tools + bash redirect targets + apply_patch + known
291# argv-writers (tee/dd/cp/mv/install/sed -i/truncate/ln); an opaque or
292# dynamic bash write is forced to Ask. Complete OS-level write confinement
293# is `permissions.sandbox`'s job (module 10), not this table's — see
294# `crate::permissions` module doc / `Config::permissions_protected_paths`.
295paths = [".git/**", ".env*", ".claude/**", ".vscode/**", ".idea/**", "~/.claude/settings*"]
296[capabilities.permissions.approvals]
297persist = true                          # BP-10 (catalog "Session approval caching"): CC records "don't ask again" PER PROJECT + command, not per process (cc§4) — the grant is still there tomorrow. Stored beside the session's other per-project records ($SUPERCODE_HOME/approvals/<project tag>.json) and reversible: delete it (or `ApprovalCache::clear`) and the next matching call asks again.
298
299[capabilities.trust]
300enabled = true
301default = "ask"
302# workspace trust gates project allow-rules (cc§4)
303[capabilities.mcp]
304enabled = true
305# stdio/HTTP/OAuth, resources, prompts-as-commands (cc§7)
306[capabilities.deferred_tools]
307enabled = true
308core = ["read_file", "bash", "edit_file", "write_file", "glob", "search", "update_plan", "ask_user", "enter_plan_mode", "exit_plan_mode", "current_time", "sleep"]
309                               # CC defers MCP tool definitions BY DEFAULT behind ToolSearch (cc§7 "Tool search"); builtins stay eager
310                               # BP-3: the new built-ins are eager for the same reason the older ones are — CC advertises AskUserQuestion/EnterPlanMode/ExitPlanMode up front, and a question tool the model must first tool_search for is not the same capability
311[capabilities.hooks]
312enabled = true
313# config-registered lifecycle hooks (cc§7: 30 events; module ships the CC-compatible subset first)
314[capabilities.memory]
315enabled = true
316# auto memory MEMORY.md + topic files (cc§2); D-9 dep → model_catalog below
317[capabilities.checkpoint]
318enabled = true
319# per-prompt file-history-snapshot → /rewind (cc§5)
320[capabilities.session_tree]
321enabled = true
322branch_summaries = false
323labels = false
324                               # CC has the tree DATA MODEL (uuid/parentUuid, cc§5) + /rewind; summaries/labels are pi-isms
325[capabilities.model_catalog]
326enabled = true
327small_model = "anthropic/claude-haiku-4-5"
328fallback = []                           # CC ships NO fallback model; `--fallback-model` (≤3) is per-invocation (cc§9). The chain is EXECUTED by the loop when one is given — see `Agent::complete_with_fallback`.
329provider = "anthropic"                  # BP-13: which provider's alias scope is in force — CC's friendly names resolve per provider/account (cc§9)
330account = "max"                         # BP-13 (cc§9 "Account-type defaults"): on Max, `default`/`best` mean Opus; another plan's scope would mean something else
331service_tier = "auto"                   # BP-13 (catalog D9 "Fast mode / service tiers"): CC's standard tier; `/fast` moves the session to `priority` and warns about the cache churn (cc§9)
332allowed_models = []                     # BP-13 (catalog D9 "Org model allowlists"): CC ships availableModels EMPTY — unrestricted. Stated so the preset says CC's posture rather than leaving it defaulted.
333denied_models = []
334                               # aliases + ANTHROPIC_SMALL_FAST_MODEL + fallback chains (cc§9)
335[capabilities.model_catalog.aliases]
336"*[1m]" = "{}[1m]"                      # BP-13: CC's 1M-context SUFFIX form (cc§9 `sonnet[1m]`) — a pattern alias whose captured stem is itself alias-resolved, so `sonnet[1m]` lands on `anthropic/claude-sonnet-4-6[1m]`
337[capabilities.model_catalog.providers.anthropic.aliases]
338fast = "anthropic/claude-haiku-4-5"     # BP-13: provider-scoped — `fast` means Haiku only while this session talks to Anthropic
339[capabilities.model_catalog.providers.anthropic.accounts.max.aliases]
340default = "anthropic/claude-opus-4-8"   # BP-13 (cc§9): the Max plan's account defaults
341best = "anthropic/claude-opus-4-8"
342[capabilities.model_catalog.models."anthropic/claude-haiku-4-5"]
343max_effort = "low"                      # BP-13: the per-model effort TIER — the small/fast model is not asked for deep reasoning, whatever `[core] effort` says
344[capabilities.tui]
345enabled = true
346
347# ---- notable OFFs ----
348[capabilities.tools_apply_patch]
349enabled = false
350# CC is edit-only (C1; catalog §5 conflict 1)
351[capabilities.tools_persistent_shell]
352enabled = false
353[capabilities.lsp]
354enabled = false
355# CC's LSP is inactive until a plugin installs it (cc§1) — off matches default
356[capabilities.formatters]
357enabled = false
358[capabilities.session_share]
359enabled = false
360# no PUBLIC share links in CC (D5 OC+PI-only row); `/export`+`/copy` are core now (§1.6 `export_format`, S6) and stay on regardless
361[capabilities.server]
362enabled = false
363# CC has no local HTTP server surface; SDK is in-process
364[capabilities.reduction]
365enabled = false
366[capabilities.cache]
367enabled = true
368plan = "imported_prefix"
369warnings = true
370# BP-4 deviation from §4.2's own `enabled = false` line, recorded here rather than silently:
371# that line's reason ("CC caching is provider-automatic") does not survive the catalog's own
372# grading of the same behavior. catalog:110 "Cache-aware context architecture" marks CC ✓ with
373# the cache-action matrix, cache-preserving `/cd` and TTL switches (cc§2) — i.e. CC deliberately
374# SHAPES the cached prefix and warns when an action would churn it, which is a harness behavior,
375# not a provider one (Anthropic prompt caching is driven by explicit breakpoints, and something
376# has to place them). supercode's equivalent is exactly `CachePlan::ImportedPrefix` + the
377# imported-prefix compaction clamp + `AgentEvent::CacheWarning`, all already implemented and
378# wired — with the module off they simply never fired under this preset, which is the gap the
379# ledger row named. `warnings = true` is C2's referee, and now reaches `Config::cache_warnings`.
380[capabilities.structured_output]
381enabled = false
382# --json-schema is headless-only surface; enable per-run
383[capabilities.model_oauth]
384enabled = false
385# recorded gap: CC's DEFAULT auth is subscription OAuth (cc§9) — module 27 deferred
386"#;
387
388/// `cx-parity` — design §4.3.
389pub const CX_PARITY_TOML: &str = r#"# built-in preset: cx-parity — Codex's default surface, composed.
390schema_version = 1
391
392[core]
393effort = "medium"                       # model_reasoning_effort default tier (cx§6, cx§9)
394env_context = true                      # <environment_context> block: cwd/sandbox/approval (cx§2) — BP-4 supplies the approval/sandbox line this comment already claims, and re-emits the block on change (cx§2 "re-emitted on change")
395context_injections = true               # BP-4 (catalog:91): Codex's whole `context/` library (~25 block types) is spliced at assembly time (cx§2) — arms `crate::context_injection`'s built-in blocks + the runtime splice seam
396project_context = true                  # AGENTS.md hierarchy, root-down concat, 32KiB cap (cx§2)
397project_root_markers = [".git"]         # BP-4: cx's own `project_root_markers` default (cx§6 config census) — the git root the AGENTS.md walk climbs to before descending root→cwd (cx§2)
398project_doc_max_bytes = 32768           # BP-4 (catalog:87): cx's documented `project_doc_max_bytes` default, 32 KiB (cx§2, cx§6 "project_doc_max_bytes (default 32768)") — the cap the `project_context` line above already claims but nothing enforced
399project_doc_excludes = []               # BP-4: Codex has no exclude list (that is CC's `claudeMdExcludes`); stated empty so the preset's hygiene posture is complete rather than defaulted
400project_doc_strip_comments = false      # BP-4: Codex strips nothing from AGENTS.md — the HTML-comment strip is CC-only (catalog:87)
401# P2 placement fix: §4.3's own TOML block places `shell_env_snapshot` under
402# `[core.tools]`, but §3.1's schema (the "annotated, exhaustive" canonical
403# definition, line ~598) defines it as a direct `[core]` scalar, not a
404# `core.tools.*` key — `CoreToolsConfig` has no such field, so a literal
405# under-`[core.tools]` placement would silently parse-and-drop it. Moved
406# here to match §3.1 (the schema doc doesn't have this key twice with two
407# different homes; §4.3 is corrected to agree with it).
408shell_env_snapshot = true               # S6/S12 home: shell-env snapshotting, cx stable-on feature (catalog:338) — closes a gap-ledger row
409parallel_tool_calls = true              # BP-2: Codex runs sibling tool calls concurrently behind its RwLock gate (cx§1, catalog:59) — the same batch path, armed
410tool_output_spill = true                # BP-2: Codex token-caps a tool result with no spill file of its own (catalog:58 `✓*`); supercode's capped result names a per-session spill file the model reads back with `cat` (cx's own read pathway), so the truncation is recoverable rather than lossy
411file_mentions = true                    # BP-5 (catalog D2 "@-file mentions / attachments"): Codex's `@`-mention popup and `/mention` insert a path into the prompt (cx§2 "`@`-mentions (files)"); the path then has to become CONTEXT, which is what this key does. Same permissions-engine read check the cc side gets — cx's `.codex/**` protected paths refuse in place.
412output_style = "none"                   # BP-5 (catalog D2 "Output style / personality module"): Codex's `personality` key at its neutral value (cx§6 `personality (none|friendly|pragmatic)`, cx§2 "Personality layer", `/personality`). `friendly`/`pragmatic` are the swaps; `none` is the selection Codex makes when nobody has chosen, and it appends nothing.
413# C4 (catalog §5 conflict 4): Codex's base prompt VARIES BY APPROVAL MODE (cx§2: "proactively run
414# tests only under never"). This preset pins prompt + approval together; when CONTINUING an
415# imported rollout, the emulate path replays the rollout's own persisted base_instructions
416# verbatim (session_meta carries them — cx§2:101; supercode SessionMeta.system_prompt), which is
417# exact prompt parity by construction rather than imitation.
418
419[core.tools]
420# BP-3: five more optional default-tool names (the `view_image` precedent),
421# each closing a gap-ledger row the catalog scores `✓` for Codex:
422#   `request_user_input` — cx's own experimental spelling of the question
423#      tool, registered as an ALIAS of the same tool object `ask_user` is, so
424#      a continued Codex session's calls keep resolving (catalog:45).
425#   `current_time` + `sleep` — cx's clock/sleep features (catalog:53).
426#   `get_context_remaining` + `new_context` — cx's token_budget feature
427#      (catalog:54).
428#   `image_gen` — cx's image feature (catalog:52); the tool posts to the
429#      SESSION's own provider `/v1/images/generations` and reports
430#      unsupported_action when that provider has no image route.
431enabled = ["bash", "view_image", "request_user_input", "current_time", "sleep", "get_context_remaining", "new_context", "image_gen"]
432                                        # Codex has NO read/write/edit/glob/grep function tools:
433                                        # reads via shell (cat, rg), writes via apply_patch (cx§1 "File reads/writes"; D1 footnote ¹).
434                                        # Disabling edit/write advertising is ALSO the C1 resolution.
435                                        # `view_image` (S6/S12 home, catalog:28) closes the gap-ledger row: with `read_file` off, cx-parity
436                                        # would otherwise have NO image-input pathway at all, unlike stock Codex's dedicated tool.
437schema_tier = "full"
438
439[core.skills]
440enabled = true                          # SKILL.md discovery, $skill mentions (cx§7 Skills).
441harness = "codex"                       # BP-6: the loop discovers SKILL.md from Codex's own documented roots — admin `/etc/codex/skills`, the bundled `$CODEX_HOME/skills/.system` cache, user `~/.agents/skills` + `$CODEX_HOME/skills`, repo `.agents/skills` from cwd to the repo root (cx§7) — invoked by `$slug` mention or the `skill` tool
442implicit_match = false                  # cx§7 also matches a skill IMPLICITLY from its description; off here, so only an explicit `$slug`/`/skill:` invocation or a `skill` tool call ever spends a body's tokens
443                                        # D-7 (S3-amended, no longer a judgment call): the read pathway is bash (`cat`) in the codex
444                                        # shape — §2.1's D-7 now names read_file|bash explicitly; the resolver warns, doesn't error.
445
446[core.compaction]
447enabled = true
448summarize = true                        # /compact + auto-compaction at model_auto_compact_token_limit (cx§2). BP-4: also arms the CORE span-summary side-call (design §1.5 obligation 5; §3.1 "SpanSummary side-call")
449focus_instructions = ""                 # BP-4 (catalog:98): Codex has no standing compaction focus either — `/compact [instructions]` steers per invocation (cx§2). Empty states that explicitly.
450reserve_tokens = 16384                  # BP-1: §4.3's own TOML block armed NEITHER trigger, so `Agent::maybe_compact`
451                                        # returned false on its `threshold.is_none() && reserve_tokens.is_none()` guard
452                                        # and cx-parity could never compact at all — contradicting the `summarize` line's
453                                        # own comment ("auto-compaction at model_auto_compact_token_limit"). That limit is
454                                        # an absolute TOKEN limit, i.e. §1.5's context-window-pressure trigger, not a
455                                        # message count — so the pressure knob is the one to arm (pi-core's `after_messages
456                                        # = 0` comment states the same reading: "the trigger is token pressure, never
457                                        # message count"). Value transcribed from cc-parity/pi-core's own 16384, the §1.5
458                                        # `reserve_tokens` equivalent this schema expresses a foreign auto-compact
459                                        # threshold as.
460
461[core.model_switch]
462allow_switch = true                     # BP-13 (catalog D9 "Mid-session model switching"): Codex's `/model` changes the model without losing the thread (cx§9). Arms the governed switch — dep-8 reasoning-artifact filtering plus a persisted `model_change` record in the session journal.
463notice = true                           # BP-13: Codex additionally INJECTS switch instructions into the conversation on a mid-session change (cx§9), so the incoming model reads the handoff instead of inferring it. CC does not, which is why cc-parity leaves this unset.
464
465[core.session]
466append_only = true                      # BP-8 (catalog:150): Codex appends every rollout line and flushes per line, with a retry (cx§5) — a crash mid-turn keeps the turn. Arms `crate::session_journal`.
467                                        # `queue_persist` is deliberately NOT set: catalog:154 is `—` for Codex (no queue-operation records), and a cx-parity session must not gain an input-durability guarantee stock Codex does not have.
468auto_title = true                       # BP-7 (catalog:150, cx `✓*`): Codex derives a title/preview for every rollout into its SQLite index and offers `/title` for a manual override (cx§5). supercode's equivalent is the same small-model titler cc-parity uses; the VARIANT the catalog footnotes is that cx derives its default from the first message rather than a model call, which is why this row is `✓*` for cx and not `✓`.
469
470[core.prompts]
471# BP-7 (catalog §4a "Review mode"): Codex ships `/review` AND a `codex review`
472# subcommand with a `review_model` of its own (cx§3). The template below is the
473# report format; the model choice stays this session's model, since cx-parity
474# pins no `review_model` (upstream leaves it unset by default too).
475code-review = """Review the current code changes as a dedicated review turn. {args}
476Read the diff and the files it touches with the shell before judging anything.
477Report in exactly these sections, omitting a section only when it is genuinely empty:
4781. Correctness — defects, in severity order (blocker / major / minor), each with file:line and the failing case.
4792. Security — untrusted input reaching a trust boundary, secrets, injection, permission widening.
4803. Reuse and simplification — existing code the change should have used; code the change makes dead.
4814. Verdict — one line: SHIP, SHIP WITH FIXES, or DO NOT SHIP, and why."""
482
483# ---- modules ON ----
484[capabilities.tools_persistent_shell]
485enabled = true
486# exec_command/write_stdin PTY unified exec (cx§1); supercode has it (builtins.rs:981-984)
487[capabilities.tools_apply_patch]
488enabled = true
489per_model = true
490                               # freeform envelope, default write path (cx§1); per_model honors C1 via model_catalog bits (cx§9)
491[capabilities.todos]
492enabled = true
493persist = true
494goals = true
495# update_plan is ALWAYS registered (cx§1); goals (S6/S12 home, catalog:138, cx `/goal`) is the persistent-objective variant of this same module — closes a gap-ledger row
496# BP-7: the `goals` key above is that variant, armed. Codex keeps its goal in `goals_1.sqlite`; supercode keeps the same single record in `<session>.goal.json`, restated at the tail of every request while it stands.
497[capabilities.tools_web]
498enabled = true
499fetch = false
500search = true
501                               # Codex has hosted web_search but NO web-fetch tool (cx§1); cached mode default
502[capabilities.tools_background]
503enabled = true
504# background terminals, /ps //stop (cx§1); C6 (S8-corrected defense): under `model_requested`, tools run sandboxed WITHOUT prompting unless the model itself escalates — from a background task's perspective that's an auto-run default, satisfying C6's auto-policy requirement without needing a separate allow-list
505[capabilities.subagents]
506enabled = true
507max_depth = 1
508background = true
509background_prompts = "auto_policy"
510                               # multi_agent default-on, agents.max_depth default 1 (cx§1, cx§6)
511                               # BP-7 (catalog:135 "Background subagents + resume", cx `✓ v2 mailbox
512                               # (send_message/wait/interrupt)`): §4.3's own line read
513                               # `background = false`, which contradicted the very column it was
514                               # transcribing — Codex's multi-agent v2 detaches children and talks to
515                               # them through a mailbox. On, with `background_prompts = "auto_policy"`
516                               # (C6's required companion, and the honest one for cx: under
517                               # `approval = "model_requested"` tools run sandboxed without prompting
518                               # unless the MODEL escalates, so a detached child has no interactive
519                               # prompt to surface to a parent — the same reasoning
520                               # `capabilities.tools_background`'s own C6 comment above already gives).
521[capabilities.tools_question]
522enabled = true
523# BP-3 (flipped from `false`): the catalog scores cx `✓*` for the structured
524# user-question tool — `request_user_input` EXISTS at the pin, behind an
525# experimental flag — and the parity ledger's denominator is that column, so
526# cx-parity has to supply the capability rather than the footnote. The module
527# registers the tool; `[core.tools] enabled` above additionally registers
528# Codex's own spelling as an alias. §2.1's `tools_question → tui|server` dep is
529# met by `[capabilities.tui]` below.
530[capabilities.deferred_tools]
531enabled = true
532core = ["bash", "shell", "apply_patch", "update_plan", "ask_user", "request_user_input", "current_time", "sleep", "get_context_remaining", "new_context", "image_gen"]
533                               # ToolExposure::Deferred + native tool_search is Codex's own mechanism (cx§1)
534                               # BP-3: the new built-ins stay eager — a feature tool the model must tool_search for first is not the same capability Codex ships
535[capabilities.structured_output]
536enabled = true
537# --output-schema final-response contract (cx§8); module 33, Config.response_format
538
539[capabilities.permissions]
540enabled = true
541approval = "model_requested"            # S8 fix: Codex `on-request` default is "the MODEL decides when to ask" (cx§4, protocol.rs:921-924) — NOT supercode's `OnRequest` (client-side allowlist check, config.rs:39-40, 299-302); using the wrong enum value would prompt on every non-allowlisted tool call where stock Codex prompts almost never. `model_requested` is the NEW distinct mode (§3.2) the module must re-implement escalation-initiated-by-the-model for.
542[capabilities.permissions.sandbox]
543tier = "workspace_write"                # writes in cwd + tmp, no network (cx§4); RECOMMENDED POSTURE (S16 fix), not upstream's labeled default — codex.md names no sandbox mode "(default)" (unlike approval); this is upstream's own steered guidance ("prefer --sandbox workspace-write", the deprecated --full-auto warning)
544                                        # BP-10: the TABLE form of the same key (§3.1 defines `sandbox = "X"` as identical to `sandbox = { tier = "X" }`), so the three knobs below can be stated. `enabled` is deliberately left UNSET, exactly as the bare form left it — `crate::sandbox::os_sandbox_active` then keeps the tier-driven trigger this preset already had.
545env_policy = "filtered"                 # BP-10 (catalog "Child-process env sanitization", cx ✓): Codex's `shell_environment_policy` filters secrets from a spawned shell BY DEFAULT (cx§4). Until this line, cx-parity resolved to `Inherit` and no sanitization happened under the preset at all — the mechanism existed and nothing armed it.
546escalation = "ask"                      # BP-10 (catalog "Sandbox-escalation path", cx ✓): when a confining tier cannot be enforced on this host, Codex does not silently run unconfined — the user is asked. No handler installed still denies (fail-closed, `crate::sandbox::resolve_escalation`).
547[capabilities.permissions.sandbox.network]
548enabled = true                          # BP-10 (catalog "Network sandbox / domain rules", cx ✓): `workspace_write` cuts subprocess network (cx§4). Real on this host: macOS seatbelt `(deny network*)`, Linux a fresh network namespace. Per-DOMAIN filtering of arbitrary subprocess traffic is the remaining gap — see the ledger row's note.
549[capabilities.permissions.rules]
550enabled = true                          # execpolicy .rules allow/prompt/forbidden → translated into deny→ask→allow (C5)
551deny  = []
552ask   = []
553allow = []
554[capabilities.permissions.protected_paths]
555enabled = true
556# Rule-layer floor (file-tools + bash redirect targets + apply_patch + known
557# argv-writers; opaque/dynamic bash writes forced to Ask) — NOT the same as
558# cx's `workspace_write` OS sandbox read-only mount above; see
559# `crate::permissions` module doc for exactly what is/isn't covered here.
560paths = [".git/**", ".codex/**"]        # read-only even inside writable roots (cx§4 workspace-write)
561[capabilities.permissions.approvals]
562persist = true                          # BP-10: cx's `with_cached_approval` grants are saved as prefix rules that outlive the process (cx§4). Same per-project store and the same one-file reversibility as cc-parity.
563
564# BP-10 (catalog "Named permission profiles", cx §4 `[permissions.<name>]`
565# Beta: "extends, fs+net rules"). Reusable, INHERITABLE bundles, selectable
566# per run without editing config — `supercode --permission-profile <name>`
567# (sugar for `-c capabilities.permissions.profile=<name>`, so the selection
568# goes through the same resolver every other key does). The three bundles
569# below are Codex's own three postures expressed in this schema; no
570# `profile` key is set, so cx-parity's own top-level permission keys stand
571# until a run names one.
572# Each bundle carries the SANDBOX TIER + RULES cx§4 names ("extends, fs+net
573# rules"), deliberately not `approval`: the approval mode is coupled to the
574# base prompt (C4) and to §2.2 C6's background-prompt dependency, so a
575# bundle that silently changed it would make a permission switch a
576# loop-shape switch. The mechanism accepts `approval` from a user's own
577# bundle; these shipped three do not use it.
578[capabilities.permissions.profiles.read-only]
579sandbox = "read_only"
580[capabilities.permissions.profiles.workspace-write]
581sandbox = "workspace_write"
582[capabilities.permissions.profiles.locked-down]
583extends = "read-only"                   # the inheritance half of the row: this bundle IS read-only, plus a shell floor
584rules = { deny = ["bash", "shell", "background_exec"] }
585
586[capabilities.trust]
587enabled = true
588default = "ask"
589# [projects] trust_level gate + hook hash-trust (cx§4:153, cx§7)
590[capabilities.mcp]
591enabled = true
592serve = true
593# full client stack (cx§7); serve = codex mcp-server analog (module 16)
594[capabilities.hooks]
595enabled = true
596# CC-compatible 10-event shape, hash-trusted (cx§7 "Lifecycle hooks")
597[capabilities.model_catalog]
598enabled = true
599provider = "openai"                     # BP-13: the alias scope in force for this session (cx§9)
600service_tier = "auto"                   # BP-13 (catalog D9 "Fast mode / service tiers"): cx's `model_service_tier` default; `/fast` moves the session to `priority` (cx§9)
601allowed_models = []                     # BP-13 (catalog D9): Codex pins features/profiles by requirements, not by a model allowlist — stated empty so the preset says so
602denied_models = []
603# capability bits (apply_patch_tool_type, supports_search_tool) drive
604                               # per-model tool swaps — the C1 resolution machinery (cx§9 "Model catalog")
605# BP-5 (catalog D2 "Per-model-family base-prompt selection", cx§2 "Per-model
606# base instructions"): Codex selects its system prompt PER MODEL FAMILY from
607# bundled markdown — gpt_5_codex_prompt.md, gpt-5.1-codex-max_prompt.md,
608# gpt-5.2-codex_prompt.md, gpt_5_1_prompt.md, gpt_5_2_prompt.md, and
609# prompt_with_apply_patch_instructions.md, the variant that appends the full
610# apply_patch tutorial. The FUNCTIONAL split across that set is exactly that
611# tutorial: a codex-tuned family is taught the patch envelope, a general
612# family is not. These two entries are that split, in supercode own words —
613# never a copy of upstream prompt text. A model matching neither keeps
614# core.system_prompt, so this table narrows nothing.
615# Keys are model-id globs and the MOST SPECIFIC (longest) match wins:
616# openai/gpt-5.2-codex takes *codex*, openai/gpt-5.1 takes *gpt-5*.
617[capabilities.model_catalog.base_prompts]
618"*gpt-5*" = """
619You are a coding agent running in a terminal. Work through the shell: read
620with cat/rg, change files by writing them out, and verify with the project own
621commands before reporting anything as done. Prefer the smallest change that
622fixes the problem, and say plainly what you did and what you did not check.
623"""
624"*codex*" = """
625You are a coding agent running in a terminal. Work through the shell: read
626with cat/rg, change files by writing them out, and verify with the project own
627commands before reporting anything as done. Prefer the smallest change that
628fixes the problem, and say plainly what you did and what you did not check.
629
630File edits go through the apply_patch envelope. One envelope may carry several
631operations, and every path is relative to the working directory:
632
633*** Begin Patch
634*** Add File: path/to/new.rs
635+the whole new file, one + per line
636*** Update File: path/to/existing.rs
637@@ context line locating the hunk
638-the exact line being replaced
639+its replacement
640*** Delete File: path/to/gone.rs
641*** End Patch
642
643Context lines carry a leading space, removals a -, additions a +. An update
644whose context does not appear verbatim in the file is rejected whole, so read
645the file first and quote it exactly.
646"""
647[capabilities.model_catalog.models."openai/gpt-5*"]
648apply_patch = true                      # BP-13: the freeform apply_patch envelope IS this family's write path (cx§1) — so `edit_file`/`write_file` are never co-advertised to it
649search_tool = true
650[capabilities.model_catalog.models."openai/gpt-4*"]
651apply_patch = false                     # BP-13: a family the catalog marks as NOT taking the envelope gets `edit_file`/`write_file` instead — the tool SURFACE adapts to the model, inside `ToolRegistry::from_config`'s own selection
652[capabilities.tui]
653enabled = true
654
655# ---- notable OFFs ----
656[capabilities.tools_search]
657enabled = false
658# no glob/grep tools; "prefer rg" via shell is prompt guidance (cx§2)
659[capabilities.plan_mode]
660enabled = false
661# /plan is effort-tier steering, not a CC/OC restriction mode (cx§6; catalog D1 CC+OC)
662[capabilities.memory]
663enabled = false
664# [features].memories = false default (cx§6, cx§7)
665[capabilities.checkpoint]
666enabled = true
667restore = false
668# BP-7 (catalog:112 "Turn diff tracking", cx `✓ turn_diff_tracker`): §4.3's own
669# line read `enabled = false` with the reason "no shadow-git; ghost_snapshot is a
670# legacy no-op (cx§6)". That reason is about the RESTORE half, and it still
671# stands — `restore = false` states it as a key rather than as prose, and
672# `CheckpointObserver::restore` refuses under it. But Codex genuinely DOES track
673# each turn's cumulative file diff (cx§3 `turn_diff_tracker`), and this module is
674# where supercode captures per-turn pre-images, so leaving the whole table off
675# meant cx-parity captured no turn-diff data at all — the gap the ledger row
676# named. On: tracking without restoring, which is exactly Codex's shape.
677# `file-checkpointing-code-restore` is `—` for cx and stays a cc-only row.
678[capabilities.session_tree]
679enabled = false
680# rollout is STRICTLY LINEAR (C7); fork = truncate+copy (D5 footnote ¹³)
681[capabilities.session_share]
682enabled = false
683[capabilities.lsp]
684enabled = false
685[capabilities.formatters]
686enabled = false
687[capabilities.server]
688enabled = false
689# app-server parity is out of preset scope — see gaps
690[capabilities.reduction]
691enabled = false
692[capabilities.cache]
693enabled = false
694[capabilities.model_oauth]
695enabled = false
696# ChatGPT-subscription login (cx§9) — module 27 deferred
697"#;
698
699/// `oc-parity` — design §4.4.
700pub const OC_PARITY_TOML: &str = r#"# built-in preset: oc-parity — opencode's default surface, composed.
701schema_version = 1
702
703[core]
704env_context = true
705project_context = true                  # AGENTS.md + instructions[] concat (oc§6)
706nested_instructions = true              # S6/S12 home: nested AGENTS.md auto-attached only for touched-file dirs, oc default (catalog:84; opencode.md:34 "nested-AGENTS.md") — closes an oc-parity gap-ledger row
707instruction_imports  = true             # S6/S12 home: `instructions[]` config imports, oc default (catalog:85; opencode.md:367) — closes an oc-parity gap-ledger row
708max_tool_output_bytes = 51200           # tool_output.max_bytes default 51200 / 2000 lines (oc§1 Truncate service)
709
710[core.session]
711auto_title = true                       # S6/S12 home: hidden title+summary agents (deny-all utility agents) on small_model, oc default (catalog:150; opencode.md:169-170,235) — closes an oc-parity gap-ledger row; small_model is "" below so this falls back to the main model per D-9 until a cheap model is configured
712
713[core.tools]
714enabled = ["read_file", "bash", "edit_file", "write_file"]  # oc registry core (oc§1; read subsumes ls)
715schema_tier = "full"
716[core.tools.read_file]
717multimodal = true                       # images/PDFs as attachments (oc§1 read)
718[core.tools.bash]
719timeout_secs = 120                      # flags.bashDefaultTimeoutMs default 120000 (oc§1 bash)
720
721[core.skills]
722enabled = true                          # skill tool + .opencode/skills + remote registries (oc§7)
723harness = "opencode"                    # BP-6: the loop reads opencode's own roots — `{skill,skills}` under the global config dir and every `.opencode` dir (oc§7)
724
725[core.compaction]
726enabled = true
727summarize = true                        # compaction{auto,prune,…} (oc§6)
728
729# ---- modules ON ----
730[capabilities.tools_search]
731enabled = true
732# glob + grep via ripgrep (oc§1)
733[capabilities.todos]
734enabled = true
735persist = true
736# todowrite → SQLite todo table (oc§1)
737[capabilities.tools_web]
738enabled = true
739fetch = true
740search = false
741                               # webfetch is default; websearch only under the Zen provider / exa flags (oc§1 "webSearchEnabled")
742[capabilities.subagents]
743enabled = true
744max_depth = 2
745background = false
746                               # task tool → child session via parentID, resumable task_id (oc§1); background is env-gated experimental → off
747[capabilities.tools_apply_patch]
748enabled = true
749per_model = true
750                               # THE C1 precedent: swapped in (edit/write out) for gpt-* models (oc§1 apply_patch; catalog §5 conflict 1)
751[capabilities.plan_mode]
752enabled = false
753# S18 fix (flipped from `true`): opencode's plan_enter/plan_exit TOOLS — exactly what this module is defined by (§2 module 8) — are DENY-BY-DEFAULT at the pin (opencode.md:251), and this preset's own translated rule set below denies them. What oc actually runs by default is the LEGACY generation: the plan agent is a permission-ruleset agent (edit denied) — already expressible as an agent-scoped `permissions.rules` restriction, not the tool-based `plan_mode` module. Enabling `plan_mode` here would contradict oc's own deny-default; off is the honest reading.
754
755[capabilities.permissions]
756enabled = true
757approval = "on_request"                 # ask-flow with once|always|reject replies (oc§4 "Ask/approve flow")
758sandbox  = "danger_full_access"         # opencode has NO OS sandbox (catalog D4: sandbox is CC+CX only)
759[capabilities.permissions.rules]
760enabled = true
761# opencode's default policy, TRANSLATED per the C5 decision (last-match-wins → deny→ask→allow
762# first-match). Source policy (oc§4 "Default policy"): {"*": allow} with carve-outs
763# doom_loop: ask, external_directory: ask, question: deny, plan_enter/plan_exit: deny,
764# read {*.env: ask, *.env.*: ask, *.env.example: allow}.
765#
766# S4 fix — this is NOT "the same fixed point" as oc's last-match algebra, and is recorded honestly
767# as THREE NAMED DEVIATIONS rather than claimed as exact parity:
768#   1. `.env.example` → ASK here, not ALLOW. Under first-match deny→ask→allow, a read of
769#      `.env.example` matches the ask-rule `read_file(*.env.*)` (glob matches) BEFORE the allow
770#      list is ever consulted, so it asks where stock opencode allows. The engine's rule grammar
771#      has no specificity/negation to express "ask unless a more-specific allow" — fixing this
772#      would require adding that to the grammar (not done here); the deviation is in the SAFE
773#      direction (stricter) and is named, not hidden.
774#   2. `doom_loop` is NOT a rule-language pattern at all — it's a repetition TRIGGER (same tool
775#      call repeated), not a tool/path match. Routed instead to its actual mechanism: the P4
776#      doom-loop breaker (a call-repetition counter + PreToolHook default, §5.2 P4) — no rule
777#      entry for it below.
778#   3. `external_directory` is an oc PERMISSION CATEGORY (any tool touching paths outside the
779#      worktree), not a tool name — routed instead to its actual permission category: `[core]
780#      additional_dirs` (Config.additional_dirs, config.rs:169) governs which extra roots are
781#      writable at all; paths outside cwd AND outside `additional_dirs` are simply not reachable,
782#      which is a stricter (not equivalent) reading of oc's ask-by-default.
783deny  = ["plan_enter", "plan_exit"]      # matches module 8's off-by-default above (S18) and oc's own "plan_enter/plan_exit: deny"
784ask   = ["read_file(*.env)", "read_file(*.env.*)"]   # includes .env.example per deviation 1 above (glob matches before any allow)
785allow = ["*"]
786[capabilities.permissions.protected_paths]
787enabled = false
788# oc does .env protection through rules (above), not a path module
789
790[capabilities.trust]
791enabled = true
792default = "ask"
793# DELIBERATE SAFETY DEVIATION: opencode LACKS a project trust gate (catalog §3 closing) yet loads
794# .opencode/ plugins/tools/commands from the repo. Our resolver treats plugins→trust as a HARD dep
795# (D-10: "config-borne code execution without a trust gate is an injection hole") — so oc-parity
796# ships the gate ON. This only NARROWS behavior (§3.3 monotonic-tightening spirit); recorded, not hidden.
797
798[capabilities.mcp]
799enabled = true
800# local/remote/OAuth servers (oc§7)
801[capabilities.plugins]
802enabled = true
803# .opencode/plugin + npm specs (oc§7); dep on trust satisfied above
804[capabilities.lsp]
805enabled = true
806# 38 auto-spawned servers; diagnostics into edit/write results (oc§7, oc§10)
807[capabilities.formatters]
808enabled = true
809diff_back = true
810# ~27 format-on-write formatters; diff_back honors C10 (oc§7; oc§10; catalog §5 conflict 10)
811[capabilities.checkpoint]
812enabled = true
813# shadow-git snapshots + revert/unrevert (oc§4 "Snapshots"/"Revert")
814[capabilities.session_share]
815enabled = true
816# share manual|auto|disabled, default manual (oc§5, oc§6)
817[capabilities.server]
818enabled = true
819# the client/server split: every frontend is an HTTP client (oc§8)
820[capabilities.model_catalog]
821enabled = true
822small_model = ""
823# models.dev catalog + small_model config key (oc§6, oc§9)
824[capabilities.tui]
825enabled = true
826
827# ---- notable OFFs ----
828[capabilities.tools_question]
829enabled = false
830# question tool is DENY-by-default outside build/plan agents (oc§1, oc§4)
831[capabilities.tools_background]
832enabled = false
833# background subagents are env-gated experimental at the pin (oc§1)
834[capabilities.session_tree]
835enabled = false
836# oc sessions are parent/child linear, no in-place tree (D5; C7)
837[capabilities.memory]
838enabled = false
839[capabilities.hooks]
840enabled = false
841# no config-registered hooks; the plugin API is the interception layer (oc§7)
842[capabilities.deferred_tools]
843enabled = false
844# opencode advertises eagerly (D1: deferred is CC+CX)
845[capabilities.cache]
846enabled = false
847[capabilities.reduction]
848enabled = false
849# oc "prune" is the LOSSY analog (catalog §1 UNIQUE OC note); ours stays off to match, mechanism on per §1.13
850[capabilities.structured_output]
851enabled = false
852[capabilities.model_oauth]
853enabled = false
854# provider /login flows (oc§9) — module 27 deferred
855"#;
856
857/// `token-saver` — design §4.5.
858pub const TOKEN_SAVER_TOML: &str = r#"# built-in preset: token-saver — the reduction spine over the minimal core.
859schema_version = 1
860extends = "pi-core"                     # smallest surface = cheapest surface; every knob below overrides it
861
862[core.tools]
863schema_tier = "minimal"                 # TR-8/T5 schema tiering (config.rs:219-225)
864# C9 (catalog §5 conflict 9): a GLOBAL minimal tier is a footgun for models trained on exact
865# schemas. Per-tool override survives the global — pin any load-bearing tool back:
866[core.tools.edit_file]
867schema_tier = "full"                    # exact-string edit is the least forgiving schema; keep it verbatim
868
869[core.compaction]
870enabled = true
871reserve_tokens = 24576                  # trigger earlier than pi's 16384 — spend the summary, save the window
872keep_recent_tokens = 10000              # aggressive: half of pi's keep budget (recall traded — see caveats)
873summarize = true                        # SpanSummary side-call (reduce.rs:274-289) → small_model below (D-9)
874
875[capabilities.reduction]                # module 23 — ALL genuinely-optional passes on (≡ CLI reduce=true, userconfig.rs:33-38)
876enabled = true
877# NOTE (S7): no `truncation` key here — A7 ToolOutputTruncated (reduce.rs:95-103) is always-on core
878# plumbing (§1.13), never a `[capabilities.reduction]` toggle, in token-saver same as every other preset.
879stale_reads = true                      # A8 FileReadElided (reduce.rs:104-111)
880diff_reads = true                       # TR-3 FileReadDiffed (reduce.rs:202-217)
881duplicates = true                       # TR-2 DuplicateOutput (reduce.rs:228-234)
882supersede = true                        # TR-6 Superseded (reduce/supersede.rs)
883tool_input_elision = true               # TR-10 ToolInputElided (reduce.rs:148-169)
884normalize_output = true                 # T30 OutputNormalized (reduce/normalize.rs)
885image_redaction = true                  # A9 ImageRedacted (reduce.rs:112-116) — ON here, off everywhere else
886span_summaries = true                   # TR-7 (reduce/summarize.rs; D-9)
887handoff = true                          # reduce/handoff.rs — smallest-faithful-context model handoff
888
889[capabilities.deferred_tools]           # module 24 — the FLAGSHIP lever (SPEC.md B6)
890enabled = true
891core = ["read_file", "bash", "edit_file", "write_file"]  # builtins stay eager; everything else behind tool_search
892
893[capabilities.cache]                    # module 25 — the C2 referee
894enabled = true
895plan = "imported_prefix"                # CachePlan::ImportedPrefix (config.rs:88-96)
896warnings = true                         # cache_warnings (config.rs:227-239): every prefix-churning feature must answer to this
897
898[capabilities.model_catalog]            # module 26 — D-9 consumer
899enabled = true
900small_model = "anthropic/claude-haiku-4-5"  # compaction summaries + span summaries route here, not the main model
901"#;
902
903/// `supercode-default` — design §4 intro (S10 fix).
904pub const SUPERCODE_DEFAULT_TOML: &str = r#"# built-in preset: supercode-default — pi-core MINUS {trust, session_tree,
905# session_share, server, plugins}, PLUS the six extra with_builtins() builtins
906# ON, notify available, reduction off (design §4 intro paragraph, S10 fix: NOT
907# "pi-core plus extras" — pi-core itself turns those five modules ON to match
908# pi's kept-list, so this preset is pi-core's core knobs UNCHANGED with a
909# capability delta). This is what `supercode` resolves to with NO config file
910# at all — "today's defaults, named and warned" rather than implicit
911# (design:958-960).
912schema_version = 1
913extends = "pi-core"
914
915# core knobs identical to pi-core (§4.1's [core]/[core.retry]/[core.tools]/
916# [core.skills]/[core.compaction]/[core.steering] blocks) — unchanged, nothing
917# to override here; inherited verbatim via `extends`.
918
919# ---- the S10 delta over pi-core: OFF (today's CLI has none of these — "—"
920# across the board in §2's Today column) ----
921[capabilities.trust]
922enabled = false
923[capabilities.session_tree]
924enabled = false
925[capabilities.session_share]
926enabled = false
927[capabilities.server]
928enabled = false
929[capabilities.plugins]
930enabled = false
931
932# ---- the six extra with_builtins() builtins (tools/mod.rs:179-192), ON ----
933# list_dir/glob/search -> tools_search; apply_patch -> tools_apply_patch;
934# persistent_shell -> tools_persistent_shell; update_plan -> todos.
935[capabilities.tools_search]
936enabled = true
937[capabilities.tools_apply_patch]
938enabled = true
939# NOT per_model: with_builtins() registers every tool struct unconditionally
940# with no per-model filtering at all (§4.6 "faithful to today's actual
941# unfiltered default stack") — this is what makes C1's warning fire here,
942# honestly, rather than suppressing it with a `per_model` bit today's CLI
943# doesn't actually have.
944[capabilities.tools_persistent_shell]
945enabled = true
946[capabilities.todos]
947enabled = true
948
949# notify available (today's CLI already ships full notify support end to end
950# — userconfig.rs:61-71 — unlike pi-core, which doesn't mention it at all).
951[capabilities.notify]
952enabled = true
953
954# reduction off (policies only; A7 truncation + rehydrate stay always-on core
955# regardless, §1.13) — already off by inheritance from pi-core; restated for
956# clarity per the design intro's explicit "reduction off" callout.
957[capabilities.reduction]
958enabled = false
959
960# permissions stays off too (approval = never, sandbox = danger_full_access)
961# — identical to pi-core's own values (config.rs:36-38, tools/mod.rs:40-42);
962# restated verbatim so the C3 mandatory warning fires here by the same
963# mechanism as pi-core's, naming today's actual default stack rather than
964# leaving it implicit (design:971-974).
965[capabilities.permissions]
966enabled = false
967approval = "never"
968sandbox = "danger_full_access"
969"#;
970
971/// The six reserved built-in preset names (design §4, opening paragraph).
972pub const RESERVED_PRESET_NAMES: &[&str] = &[
973    "pi-core",
974    "cc-parity",
975    "cx-parity",
976    "oc-parity",
977    "token-saver",
978    "supercode-default",
979];
980
981/// Look up a built-in preset's compiled-in TOML text by name. Returns `None`
982/// for anything not one of the six [`RESERVED_PRESET_NAMES`] — the resolver
983/// (`configfile.rs` §3.5) falls back to treating the name as a file path in
984/// that case (user/global layer only, §3.3).
985pub fn lookup(name: &str) -> Option<&'static str> {
986    match name {
987        "pi-core" => Some(PI_CORE_TOML),
988        "cc-parity" => Some(CC_PARITY_TOML),
989        "cx-parity" => Some(CX_PARITY_TOML),
990        "oc-parity" => Some(OC_PARITY_TOML),
991        "token-saver" => Some(TOKEN_SAVER_TOML),
992        "supercode-default" => Some(SUPERCODE_DEFAULT_TOML),
993        _ => None,
994    }
995}
996
997#[cfg(test)]
998mod tests {
999    use super::*;
1000    use crate::configfile::{resolve, HarnessConfig, ResolveOptions};
1001    use crate::modules::ModuleId;
1002    use crate::tools::ToolRegistry;
1003
1004    /// Resolve a built-in preset exactly the way `supercode harness parity`
1005    /// does — the real resolver, strict, no `[experimental]` table at all.
1006    fn resolved(name: &str) -> crate::configfile::Resolved {
1007        let toml = lookup(name).unwrap();
1008        resolve(toml, None, &ResolveOptions { strict: true })
1009            .unwrap_or_else(|e| panic!("preset `{name}` failed to resolve: {e}"))
1010    }
1011
1012    fn registry_names(config: &crate::Config) -> Vec<String> {
1013        ToolRegistry::from_config(config)
1014            .iter()
1015            .map(|t| t.name().to_string())
1016            .collect()
1017    }
1018
1019    /// Every reserved preset's compiled-in TOML must be valid TOML that
1020    /// parses into a `HarnessConfig` — the design's own claim ("they were
1021    /// made TOML-valid in the final design commit") verified mechanically
1022    /// rather than trusted, since the doc's literal `[capabilities.X] { .. }`
1023    /// shorthand is NOT valid TOML as written (see the module doc comment).
1024    #[test]
1025    fn every_reserved_preset_parses() {
1026        for name in RESERVED_PRESET_NAMES {
1027            let toml = lookup(name).unwrap_or_else(|| panic!("no TOML for preset `{name}`"));
1028            HarnessConfig::from_toml_str(toml)
1029                .unwrap_or_else(|e| panic!("preset `{name}` failed to parse: {e}"));
1030        }
1031    }
1032
1033    /// `lookup` returns `None` for anything not a reserved name (the
1034    /// resolver's built-in-vs-path branch point, §3.5 step 1).
1035    #[test]
1036    fn lookup_returns_none_for_non_preset_names() {
1037        assert!(lookup("not-a-real-preset").is_none());
1038        assert!(lookup("./some/path.toml").is_none());
1039        assert!(lookup("").is_none());
1040    }
1041
1042    /// BP-1 AC1 (cc side). The RESOLVED `cc-parity` preset — no
1043    /// `[experimental]` table anywhere — must actually register the two
1044    /// tools its `[capabilities.tools_web] { fetch = true, search = true }`
1045    /// block declares. Before BP-1 `ToolRegistry::from_config` bailed to
1046    /// `with_builtins()` unless `[experimental] module_registry = true`,
1047    /// which no preset sets, so this block resolved, was golden-tested,
1048    /// and then had no effect on the tool surface at all.
1049    ///
1050    /// "Advertised" is `Config::tool_enabled` (the filter
1051    /// `Agent::tool_schemas` applies to the registry), not the schema
1052    /// array itself: cc-parity also turns `deferred_tools` on, so a
1053    /// non-core tool legitimately reaches the model through `tool_search`
1054    /// rather than the eager array — that deferral is CC's own behavior
1055    /// (cc§7 "Tool search"), not a gap.
1056    #[test]
1057    fn cc_parity_registers_and_advertises_the_web_tools() {
1058        let r = resolved("cc-parity");
1059        assert!(
1060            r.config.module_registry,
1061            "a resolved preset must drive the module registry with no experimental flag"
1062        );
1063        let names = registry_names(&r.config);
1064        for tool in ["web_fetch", "web_search"] {
1065            assert!(
1066                names.contains(&tool.to_string()),
1067                "cc-parity must register `{tool}`; registry = {names:?}"
1068            );
1069            assert!(
1070                r.config.tool_enabled(tool),
1071                "cc-parity must advertise `{tool}`"
1072            );
1073        }
1074    }
1075
1076    /// BP-1 AC1 (cc side, MCP). `mcp_module_on` in the CLI is exactly
1077    /// "is `ModuleId::McpClient` in the resolved activation set" now, and
1078    /// that predicate is what gates MCP resource-tool registration,
1079    /// prompts-as-commands, server instructions, and the http/sse
1080    /// transports in `attach_mcp`. Under cc-parity it must be true.
1081    #[test]
1082    fn cc_parity_activates_the_mcp_client_module() {
1083        let r = resolved("cc-parity");
1084        assert!(r.config.module_activation.is_active(ModuleId::McpClient));
1085        assert_eq!(r.modules.get("mcp"), Some(&true));
1086    }
1087
1088    /// BP-1 AC1 (cx side). `cx-parity`'s `[core.tools] enabled = ["bash",
1089    /// "view_image"]` is the preset's statement that Codex has no
1090    /// read/write/edit function tools at all (reads go through `cat`,
1091    /// writes through `apply_patch`) — and it must now be the registry's
1092    /// statement too.
1093    #[test]
1094    fn cx_parity_registers_view_image_and_no_file_tools() {
1095        let r = resolved("cx-parity");
1096        let names = registry_names(&r.config);
1097        assert!(
1098            names.contains(&"view_image".to_string()),
1099            "cx-parity must register `view_image` (its only image pathway); registry = {names:?}"
1100        );
1101        for tool in ["read_file", "write_file", "edit_file"] {
1102            assert!(
1103                !names.contains(&tool.to_string()),
1104                "cx-parity must NOT register `{tool}`; registry = {names:?}"
1105            );
1106        }
1107        // The write path Codex actually uses is still there.
1108        assert!(names.contains(&"apply_patch".to_string()));
1109        assert!(names.contains(&"bash".to_string()));
1110    }
1111
1112    /// BP-3: the tool surface `cc-parity` claims, checked against the
1113    /// registry the RESOLVED preset actually builds — the same predicate
1114    /// `supercode harness parity`'s `tool` evidence resolves through.
1115    /// `enter_plan_mode`/`exit_plan_mode` come from
1116    /// `[capabilities.plan_mode]`, `ask_user` from
1117    /// `[capabilities.tools_question]`, `current_time`/`sleep` from
1118    /// `[core.tools] enabled`.
1119    #[test]
1120    fn cc_parity_registers_the_new_core_tools() {
1121        let r = resolved("cc-parity");
1122        let names = registry_names(&r.config);
1123        for tool in [
1124            "ask_user",
1125            "enter_plan_mode",
1126            "exit_plan_mode",
1127            "current_time",
1128            "sleep",
1129        ] {
1130            assert!(
1131                names.contains(&tool.to_string()),
1132                "cc-parity must register `{tool}`; registry = {names:?}"
1133            );
1134            assert!(
1135                r.config.tool_enabled(tool),
1136                "cc-parity must advertise `{tool}`"
1137            );
1138        }
1139        // Codex-only rows stay out of the CC surface (the catalog scores
1140        // both `—` for cc), and so does cx's own question spelling.
1141        for tool in [
1142            "request_user_input",
1143            "image_gen",
1144            "new_context",
1145            "get_context_remaining",
1146        ] {
1147            assert!(
1148                !names.contains(&tool.to_string()),
1149                "cc-parity must NOT register `{tool}`; registry = {names:?}"
1150            );
1151        }
1152    }
1153
1154    /// BP-3: the same check for `cx-parity`, including Codex's own
1155    /// `request_user_input` spelling registered ALONGSIDE `ask_user` (one
1156    /// tool object, two registered names) and the deliberate absence of the
1157    /// plan-mode tools — cx's `/plan` is user-driven steering, so the
1158    /// module stays off there and only the mode itself is available.
1159    #[test]
1160    fn cx_parity_registers_the_new_core_tools_including_the_codex_spelling() {
1161        let r = resolved("cx-parity");
1162        let names = registry_names(&r.config);
1163        for tool in [
1164            "ask_user",
1165            "request_user_input",
1166            "current_time",
1167            "sleep",
1168            "get_context_remaining",
1169            "new_context",
1170            "image_gen",
1171        ] {
1172            assert!(
1173                names.contains(&tool.to_string()),
1174                "cx-parity must register `{tool}`; registry = {names:?}"
1175            );
1176            assert!(
1177                r.config.tool_enabled(tool),
1178                "cx-parity must advertise `{tool}`"
1179            );
1180        }
1181        for tool in ["enter_plan_mode", "exit_plan_mode"] {
1182            assert!(
1183                !names.contains(&tool.to_string()),
1184                "cx-parity keeps `[capabilities.plan_mode]` off, so `{tool}` must not be \
1185                 registered; registry = {names:?}"
1186            );
1187        }
1188    }
1189
1190    /// BP-3: the new built-ins are advertised EAGERLY under both presets'
1191    /// `deferred_tools` — a question tool the model must first `tool_search`
1192    /// for is not the capability CC/CX ship.
1193    #[test]
1194    fn the_new_core_tools_are_eager_under_both_parity_presets() {
1195        for (preset, tools) in [
1196            (
1197                "cc-parity",
1198                &[
1199                    "ask_user",
1200                    "enter_plan_mode",
1201                    "exit_plan_mode",
1202                    "current_time",
1203                    "sleep",
1204                ][..],
1205            ),
1206            (
1207                "cx-parity",
1208                &[
1209                    "ask_user",
1210                    "request_user_input",
1211                    "current_time",
1212                    "sleep",
1213                    "get_context_remaining",
1214                    "new_context",
1215                    "image_gen",
1216                ][..],
1217            ),
1218        ] {
1219            let r = resolved(preset);
1220            let crate::config::ToolAdvertising::Deferred { core } = &r.config.tool_advertising
1221            else {
1222                panic!("{preset} enables `deferred_tools`, so advertising must be Deferred");
1223            };
1224            for tool in tools {
1225                assert!(
1226                    core.iter().any(|c| c == tool),
1227                    "{preset} must advertise `{tool}` eagerly; core = {core:?}"
1228                );
1229            }
1230        }
1231    }
1232
1233    /// BP-3 (§2 module 6): the question tool, driven end to end over the
1234    /// RESOLVED preset — resolve, build the registry, call the tool, and
1235    /// let a mock frontend answer it. This is the behaviour the ledger row
1236    /// claims; registration alone would not be.
1237    #[tokio::test]
1238    async fn cc_parity_ask_user_is_answered_by_a_mock_frontend() {
1239        struct Frontend;
1240        #[async_trait::async_trait]
1241        impl crate::mcp::McpElicitationHandler for Frontend {
1242            async fn handle(
1243                &self,
1244                request: &crate::mcp::ElicitationRequest,
1245            ) -> crate::mcp::ElicitationResponse {
1246                // The frontend sees the real question text and the answer
1247                // schema, exactly as it would off the broker.
1248                assert!(request.message.contains("Which database?"), "{request:?}");
1249                assert_eq!(
1250                    request.requested_schema["properties"]["q1"]["type"],
1251                    "string"
1252                );
1253                crate::mcp::ElicitationResponse {
1254                    action: crate::mcp::ElicitationAction::Accept,
1255                    content: Some(serde_json::json!({"q1": "sqlite"})),
1256                }
1257            }
1258        }
1259
1260        let r = resolved("cc-parity");
1261        let registry = ToolRegistry::from_config(&r.config);
1262        let tool = registry.get("ask_user").expect("cc-parity registers it");
1263        let mut ctx = crate::tools::ToolContext::new(std::env::temp_dir());
1264
1265        // Deny-default first: with no frontend attached the tool refuses
1266        // rather than hanging.
1267        let args = serde_json::json!({"questions": [{
1268            "question": "Which database?",
1269            "header": "Database",
1270            "options": [{"label": "postgres"}, {"label": "sqlite"}]
1271        }]});
1272        let refused = tool.execute(args.clone(), &ctx).await;
1273        assert!(
1274            refused.is_err(),
1275            "headless must be deny-default, got {refused:?}"
1276        );
1277
1278        ctx.question_handler = Some(crate::tools::UserQuestionHandler(std::sync::Arc::new(
1279            Frontend,
1280        )));
1281        let answer = tool
1282            .execute(args, &ctx)
1283            .await
1284            .expect("the frontend answers");
1285        assert!(answer.contains("Database: sqlite"), "{answer}");
1286    }
1287
1288    /// BP-3 (§2 module 8): `exit_plan_mode` over the RESOLVED `cc-parity`
1289    /// preset — the plan is presented on the session's approval door, a
1290    /// refusal keeps the mode on, and only an approval clears it.
1291    #[tokio::test]
1292    async fn cc_parity_plan_exit_needs_the_approval_door() {
1293        struct Door(std::sync::atomic::AtomicBool);
1294        impl crate::permissions::PermissionsApprovalHandler for Door {
1295            fn ask(
1296                &self,
1297                req: &crate::permissions::ApprovalRequest,
1298            ) -> crate::permissions::ApprovalOutcome {
1299                assert_eq!(req.tool, "exit_plan_mode");
1300                assert!(
1301                    req.subject
1302                        .is_some_and(|s| s.contains("rewrite the parser")),
1303                    "the approval must carry the plan: {:?}",
1304                    req.subject
1305                );
1306                // Refuse the first time, approve the second.
1307                if self.0.swap(true, std::sync::atomic::Ordering::SeqCst) {
1308                    crate::permissions::ApprovalOutcome::Allow
1309                } else {
1310                    crate::permissions::ApprovalOutcome::Deny
1311                }
1312            }
1313        }
1314
1315        let r = resolved("cc-parity");
1316        let registry = ToolRegistry::from_config(&r.config);
1317        let enter = registry.get("enter_plan_mode").expect("registered");
1318        let exit = registry.get("exit_plan_mode").expect("registered");
1319        let mut ctx = crate::tools::ToolContext::new(std::env::temp_dir());
1320        ctx.approval_handler = Some(crate::tools::ToolApprovalHandler(std::sync::Arc::new(
1321            Door(std::sync::atomic::AtomicBool::new(false)),
1322        )));
1323
1324        enter
1325            .execute(serde_json::json!({}), &ctx)
1326            .await
1327            .expect("entering plan mode");
1328        assert!(ctx.plan_mode.is_active());
1329
1330        let refused = exit
1331            .execute(serde_json::json!({"plan": "rewrite the parser"}), &ctx)
1332            .await
1333            .expect("a refusal is a result, not an error");
1334        assert!(refused.contains("did NOT approve"), "{refused}");
1335        assert!(
1336            ctx.plan_mode.is_active(),
1337            "a refused exit must keep the mode on"
1338        );
1339
1340        let approved = exit
1341            .execute(serde_json::json!({"plan": "rewrite the parser"}), &ctx)
1342            .await
1343            .expect("approved exit");
1344        assert!(approved.contains("APPROVED"), "{approved}");
1345        assert!(!ctx.plan_mode.is_active());
1346    }
1347
1348    /// BP-1 AC2. `[core.compaction] summarize` was parsed into
1349    /// `CoreCompactionConfig` and dropped; it now reaches `Config`. Every
1350    /// preset sets it (directly or by inheriting `pi-core`).
1351    #[test]
1352    fn compaction_summarize_reaches_config_for_every_preset() {
1353        for name in RESERVED_PRESET_NAMES {
1354            let r = resolved(name);
1355            assert!(
1356                r.config.compaction_summarize,
1357                "preset `{name}` sets `core.compaction.summarize = true`; it must reach Config"
1358            );
1359        }
1360    }
1361
1362    /// BP-1 AC2 (cx trigger). `cx-parity` armed NEITHER compaction trigger,
1363    /// so `Agent::maybe_compact` returned false on its
1364    /// `threshold.is_none() && compaction_reserve_tokens.is_none()` guard
1365    /// and the preset could never compact. The design's own comment on that
1366    /// block names an auto-compact TOKEN limit, i.e. §1.5's pressure
1367    /// trigger.
1368    #[test]
1369    fn cx_parity_arms_the_compaction_pressure_trigger() {
1370        let r = resolved("cx-parity");
1371        assert!(r.config.compaction_enabled);
1372        assert_eq!(r.config.compaction_reserve_tokens, Some(16384));
1373    }
1374
1375    /// The explicit OPT-OUT survives: `[experimental] module_registry =
1376    /// false` pins a resolved config back to the unfiltered
1377    /// `with_builtins()` stack.
1378    #[test]
1379    fn module_registry_false_is_an_explicit_opt_out() {
1380        let toml = format!("{CC_PARITY_TOML}\n[experimental]\nmodule_registry = false\n");
1381        let r = resolve(&toml, None, &ResolveOptions::default()).expect("resolves");
1382        assert!(!r.config.module_registry);
1383        let names = registry_names(&r.config);
1384        let builtins: Vec<String> = ToolRegistry::with_builtins()
1385            .iter()
1386            .map(|t| t.name().to_string())
1387            .collect();
1388        assert_eq!(names, builtins);
1389    }
1390
1391    /// `pi-core` has no `extends` (it is a root); the other five all resolve
1392    /// somewhere (four are roots too, `token-saver`/`supercode-default`
1393    /// extend `pi-core`) — sanity-checking the chain shape golden tests will
1394    /// exercise in full.
1395    #[test]
1396    fn token_saver_and_supercode_default_extend_pi_core() {
1397        let ts = HarnessConfig::from_toml_str(TOKEN_SAVER_TOML).unwrap();
1398        assert_eq!(ts.extends.as_deref(), Some("pi-core"));
1399        let sd = HarnessConfig::from_toml_str(SUPERCODE_DEFAULT_TOML).unwrap();
1400        assert_eq!(sd.extends.as_deref(), Some("pi-core"));
1401        let pc = HarnessConfig::from_toml_str(PI_CORE_TOML).unwrap();
1402        assert_eq!(pc.extends, None);
1403    }
1404
1405    // ---- BP-2: tool fidelity under the resolved parity presets ----------
1406
1407    /// BP-2 helper: the tool context an `Agent` built from `config` would
1408    /// hand every tool call, rooted at `cwd` — the resolved preset's own
1409    /// `[core.tools.*]` knobs, not a hand-assembled context.
1410    fn preset_ctx(config: &crate::Config) -> crate::tools::ToolContext {
1411        let (ctx, _, _) = crate::agent::build_tool_context(config);
1412        ctx
1413    }
1414
1415    fn tmp_dir(tag: &str) -> std::path::PathBuf {
1416        let dir = std::env::temp_dir().join(format!(
1417            "supercode-bp2-{tag}-{}-{}",
1418            std::process::id(),
1419            std::time::SystemTime::now()
1420                .duration_since(std::time::UNIX_EPOCH)
1421                .map(|d| d.as_nanos())
1422                .unwrap_or(0)
1423        ));
1424        std::fs::create_dir_all(&dir).unwrap();
1425        dir
1426    }
1427
1428    async fn run_tool(
1429        config: &crate::Config,
1430        tool: &str,
1431        args: serde_json::Value,
1432    ) -> crate::error::Result<String> {
1433        let registry = ToolRegistry::from_config(config);
1434        let t = registry
1435            .get(tool)
1436            .unwrap_or_else(|| panic!("`{tool}` is not registered under this preset"));
1437        t.execute(args, &preset_ctx(config)).await
1438    }
1439
1440    /// BP-2 row `file-read-tool-paged-line-numbered` (cc). Claude Code's
1441    /// Read returns a `cat -n` gutter and its model cites those numbers;
1442    /// supercode's `read_file` returned the raw slice. Under the RESOLVED
1443    /// cc-parity preset it must now number, and number from `offset`.
1444    #[tokio::test]
1445    async fn cc_parity_read_file_numbers_lines_cat_n_style() {
1446        let mut r = resolved("cc-parity");
1447        assert!(
1448            r.config.read_file_line_numbers,
1449            "cc-parity must set `[core.tools.read_file] line_numbers`"
1450        );
1451        let dir = tmp_dir("readnum");
1452        std::fs::write(dir.join("sample.txt"), "alpha\nbeta\ngamma\n").unwrap();
1453        r.config.cwd = dir.clone();
1454
1455        let whole = run_tool(
1456            &r.config,
1457            "read_file",
1458            serde_json::json!({"path": "sample.txt"}),
1459        )
1460        .await
1461        .unwrap();
1462        assert_eq!(
1463            whole, "     1\talpha\n     2\tbeta\n     3\tgamma\n",
1464            "cc-parity read_file must emit a `cat -n` gutter"
1465        );
1466
1467        let sliced = run_tool(
1468            &r.config,
1469            "read_file",
1470            serde_json::json!({"path": "sample.txt", "offset": 2, "limit": 2}),
1471        )
1472        .await
1473        .unwrap();
1474        assert_eq!(
1475            sliced, "     2\tbeta\n     3\tgamma",
1476            "an `offset` read must number from the offset, not from 1"
1477        );
1478    }
1479
1480    /// BP-2 (same row, the other direction): pi-core keeps the unnumbered
1481    /// raw slice — the gutter is Claude Code's behavior, and a preset that
1482    /// does not claim it must not silently acquire it.
1483    #[tokio::test]
1484    async fn pi_core_read_file_keeps_the_raw_unnumbered_slice() {
1485        let mut r = resolved("pi-core");
1486        assert!(!r.config.read_file_line_numbers);
1487        let dir = tmp_dir("readraw");
1488        std::fs::write(dir.join("sample.txt"), "alpha\nbeta\n").unwrap();
1489        r.config.cwd = dir.clone();
1490        let out = run_tool(
1491            &r.config,
1492            "read_file",
1493            serde_json::json!({"path": "sample.txt"}),
1494        )
1495        .await
1496        .unwrap();
1497        assert_eq!(out, "alpha\nbeta\n");
1498    }
1499
1500    /// BP-2 row `multimodal-read-images-pdf-notebook` (cc). The multimodal
1501    /// branch used to be images-only, so a PDF or a notebook came back as
1502    /// UTF-8-lossy soup. Under the resolved cc-parity preset a PDF returns
1503    /// its extracted text pages and an `.ipynb` returns its cells WITH
1504    /// their outputs.
1505    #[tokio::test]
1506    async fn cc_parity_read_file_renders_pdf_text_and_notebook_cells() {
1507        let mut r = resolved("cc-parity");
1508        assert!(r.config.read_file_multimodal);
1509        let dir = tmp_dir("multimodal");
1510        r.config.cwd = dir.clone();
1511
1512        std::fs::write(
1513            dir.join("doc.pdf"),
1514            b"%PDF-1.4\n1 0 obj\n<< /Type /Page >>\nendobj\n2 0 obj\n<< >>\nstream\nBT (parity page text) Tj ET\nendstream\nendobj\n%%EOF\n"
1515                .as_slice(),
1516        )
1517        .unwrap();
1518        let pdf = run_tool(
1519            &r.config,
1520            "read_file",
1521            serde_json::json!({"path": "doc.pdf"}),
1522        )
1523        .await
1524        .unwrap();
1525        assert!(pdf.contains("--- page 1 ---"), "{pdf}");
1526        assert!(pdf.contains("parity page text"), "{pdf}");
1527
1528        let nb = serde_json::json!({
1529            "metadata": {"kernelspec": {"display_name": "Python 3"}},
1530            "cells": [{
1531                "cell_type": "code", "execution_count": 2, "source": ["print('hi')\n"],
1532                "outputs": [{"output_type": "stream", "name": "stdout", "text": ["hi\n"]}]
1533            }]
1534        });
1535        std::fs::write(dir.join("nb.ipynb"), nb.to_string()).unwrap();
1536        let out = run_tool(
1537            &r.config,
1538            "read_file",
1539            serde_json::json!({"path": "nb.ipynb"}),
1540        )
1541        .await
1542        .unwrap();
1543        assert!(out.contains("--- cell 0 (code) [2] ---"), "{out}");
1544        assert!(out.contains("print('hi')"), "{out}");
1545        assert!(out.contains("[stdout]\nhi"), "{out}");
1546    }
1547
1548    /// BP-2 row `parallel-tool-call-execution`. The concurrent batch path
1549    /// existed but neither parity preset armed it, so sibling calls always
1550    /// ran sequentially under cc-parity/cx-parity — both harnesses run
1551    /// them concurrently. pi-core keeps the sequential path.
1552    #[test]
1553    fn both_parity_presets_arm_parallel_tool_calls() {
1554        for name in ["cc-parity", "cx-parity"] {
1555            assert!(
1556                resolved(name).config.parallel_tool_calls,
1557                "`{name}` must set `core.parallel_tool_calls`"
1558            );
1559        }
1560        assert!(
1561            !resolved("pi-core").config.parallel_tool_calls,
1562            "pi-core keeps the sequential path"
1563        );
1564    }
1565
1566    /// BP-2 row `read-before-edit-enforcement` (cc). The refusal used to be
1567    /// path-only — a file read once and then modified behind the model's
1568    /// back still edited cleanly. Under the resolved cc-parity preset the
1569    /// "and unchanged" half must hold, and a Bash view must satisfy the
1570    /// rule the way CC's does.
1571    ///
1572    /// Driven through ONE `ToolContext`, the way an `Agent` does: the read
1573    /// record lives on the context, so a per-call context would be testing
1574    /// nothing.
1575    #[tokio::test]
1576    async fn cc_parity_edit_refuses_unread_and_stale_files_and_accepts_a_bash_view() {
1577        let mut r = resolved("cc-parity");
1578        assert!(r.config.edit_file_require_read_before_edit);
1579        let dir = tmp_dir("staleedit");
1580        r.config.cwd = dir.clone();
1581        let path = dir.join("code.txt");
1582        std::fs::write(&path, "alpha\n").unwrap();
1583
1584        let ctx = preset_ctx(&r.config);
1585        let registry = ToolRegistry::from_config(&r.config);
1586        let read = registry.get("read_file").unwrap();
1587        let edit = registry.get("edit_file").unwrap();
1588        let bash = registry.get("bash").unwrap();
1589        let edit_call = |old: &str, new: &str| serde_json::json!({"path": "code.txt", "old_string": old, "new_string": new});
1590
1591        // 1. never read → refused, naming the read requirement.
1592        let never = edit
1593            .execute(edit_call("alpha", "beta"), &ctx)
1594            .await
1595            .expect_err("an unread file must be refused");
1596        assert!(
1597            never.to_string().contains("must be read with `read_file`"),
1598            "{never}"
1599        );
1600
1601        // 2. read, then edited → accepted; and the model's own second edit
1602        //    of the file it just wrote is still accepted (its view is the
1603        //    bytes it wrote, not a stale one).
1604        read.execute(serde_json::json!({"path": "code.txt"}), &ctx)
1605            .await
1606            .unwrap();
1607        edit.execute(edit_call("alpha", "beta"), &ctx)
1608            .await
1609            .expect("a read file edits");
1610        edit.execute(edit_call("beta", "gamma"), &ctx)
1611            .await
1612            .expect("the model's own consecutive edit is not stale");
1613
1614        // 3. changed on disk behind the model's back → refused as stale.
1615        std::fs::write(&path, "one\ntwo\n").unwrap();
1616        read.execute(serde_json::json!({"path": "code.txt"}), &ctx)
1617            .await
1618            .unwrap();
1619        std::fs::write(&path, "one\ntwo\nthree (someone else)\n").unwrap();
1620        let stale = edit
1621            .execute(edit_call("one", "1"), &ctx)
1622            .await
1623            .expect_err("an edit against a changed file must be refused");
1624        assert!(
1625            stale
1626                .to_string()
1627                .contains("has changed on disk since it was read"),
1628            "{stale}"
1629        );
1630
1631        // 4. a single-file Bash view satisfies the rule (CC's exemption).
1632        bash.execute(serde_json::json!({"command": "cat code.txt"}), &ctx)
1633            .await
1634            .unwrap();
1635        edit.execute(edit_call("two", "2"), &ctx)
1636            .await
1637            .expect("a `cat` view satisfies read-before-edit");
1638    }
1639
1640    /// BP-2: the Bash-view exemption is exactly the inventory's narrow rule
1641    /// — a composed or transformed view is NOT a view of the file.
1642    #[test]
1643    fn bash_view_exemption_is_narrow() {
1644        use crate::tools::bash_view_target;
1645        assert_eq!(
1646            bash_view_target("cat src/lib.rs").as_deref(),
1647            Some("src/lib.rs")
1648        );
1649        assert_eq!(bash_view_target("/bin/cat x.txt").as_deref(), Some("x.txt"));
1650        assert_eq!(
1651            bash_view_target("head -n 20 x.txt").as_deref(),
1652            Some("x.txt")
1653        );
1654        assert_eq!(
1655            bash_view_target("sed -n '1,5p' x.txt").as_deref(),
1656            Some("x.txt")
1657        );
1658        assert_eq!(bash_view_target("grep foo x.txt").as_deref(), Some("x.txt"));
1659        // Not exemptions: pipes, redirects, composition, multi-file,
1660        // a non-viewer, and `sed` without `-n` (which prints edited output).
1661        assert_eq!(bash_view_target("cat x.txt | head -5"), None);
1662        assert_eq!(bash_view_target("cat x.txt > y.txt"), None);
1663        assert_eq!(bash_view_target("cat x.txt; rm x.txt"), None);
1664        assert_eq!(bash_view_target("cat a.txt b.txt"), None);
1665        assert_eq!(bash_view_target("echo hi"), None);
1666        assert_eq!(bash_view_target("sed 's/a/b/' x.txt"), None);
1667        assert_eq!(bash_view_target("cat $(ls)"), None);
1668    }
1669
1670    /// BP-2: a one-shot local HTTP server (the same offline pattern the
1671    /// P4c web tests use) — no test in this crate touches the real network.
1672    async fn one_shot_http(body: &str, content_type: &str) -> std::net::SocketAddr {
1673        use tokio::io::{AsyncReadExt, AsyncWriteExt};
1674        let response = format!(
1675            "HTTP/1.1 200 OK\r\nContent-Type: {content_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
1676            body.len()
1677        );
1678        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1679        let addr = listener.local_addr().unwrap();
1680        tokio::spawn(async move {
1681            if let Ok((mut sock, _)) = listener.accept().await {
1682                let mut buf = [0u8; 4096];
1683                let _ = sock.read(&mut buf).await;
1684                let _ = sock.write_all(response.as_bytes()).await;
1685                let _ = sock.flush().await;
1686            }
1687        });
1688        addr
1689    }
1690
1691    /// Serializes the two tests that set process-wide web env vars.
1692    static WEB_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
1693
1694    /// BP-2 row `web-fetch-tool` (cc). The tool returned the raw response
1695    /// body with no markdown conversion and no cache, where CC's WebFetch
1696    /// does both. Under the resolved cc-parity preset a fetched HTML page
1697    /// must come back as markdown, and a second fetch of the same URL must
1698    /// be served from the on-disk cache — proven by the server being
1699    /// one-shot: a live second fetch could not succeed.
1700    #[tokio::test]
1701    async fn cc_parity_web_fetch_converts_html_to_markdown_and_caches_it() {
1702        let _guard = WEB_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
1703        let cache = tmp_dir("webcache");
1704        std::env::set_var(crate::tools::WEB_CACHE_DIR_ENV, &cache);
1705
1706        let html = "<html><head><title>t</title><style>b{}</style></head><body>\
1707            <h1>Parity</h1><p>Fetched <strong>page</strong>.</p>\
1708            <a href=\"https://example.com/next\">next</a></body></html>";
1709        let addr = one_shot_http(html, "text/html; charset=utf-8").await;
1710        let url = format!("http://127.0.0.1:{}/page", addr.port());
1711
1712        let r = resolved("cc-parity");
1713        let first = run_tool(&r.config, "web_fetch", serde_json::json!({"url": url}))
1714            .await
1715            .unwrap();
1716        assert!(first.contains("markdown"), "{first}");
1717        assert!(first.contains("# Parity"), "{first}");
1718        assert!(first.contains("Fetched **page**."), "{first}");
1719        assert!(
1720            first.contains("[next](https://example.com/next)"),
1721            "{first}"
1722        );
1723        assert!(
1724            !first.contains("<h1>"),
1725            "raw markup reached the model: {first}"
1726        );
1727
1728        // The one-shot server is finished; only the cache can answer.
1729        let second = run_tool(&r.config, "web_fetch", serde_json::json!({"url": url}))
1730            .await
1731            .expect("the second fetch must be served from the cache");
1732        assert!(second.starts_with("[web_fetch: cached "), "{second}");
1733        assert!(second.contains("# Parity"), "{second}");
1734
1735        std::env::remove_var(crate::tools::WEB_CACHE_DIR_ENV);
1736    }
1737
1738    /// BP-2 row `web-search-tool`. supercode bundled no backend at all: with
1739    /// no operator URL the tool returned a configuration error, where CC and
1740    /// Codex both search out of the box. The default endpoint is now a
1741    /// documented public one, the operator override still wins, and a
1742    /// results page is rendered as titles/urls/snippets rather than dumped.
1743    ///
1744    /// Offline: the override points at a local one-shot server serving a
1745    /// results page in the shape the default backend returns.
1746    #[tokio::test]
1747    async fn parity_presets_web_search_renders_results_from_its_backend() {
1748        let _guard = WEB_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
1749        assert_eq!(
1750            crate::tools::DEFAULT_WEB_SEARCH_URL,
1751            "https://html.duckduckgo.com/html/",
1752            "the built-in backend must need no operator configuration"
1753        );
1754        let page = "<html><body><div class=\"result\">\
1755            <a class=\"result__a\" href=\"//duckduckgo.com/l/?uddg=https%3A%2F%2Fexample.com%2Fdoc\">Example doc</a>\
1756            <a class=\"result__snippet\">What the page says.</a></div></body></html>";
1757        let addr = one_shot_http(page, "text/html").await;
1758        std::env::set_var(
1759            crate::tools::WEB_SEARCH_URL_ENV,
1760            format!("http://127.0.0.1:{}/search", addr.port()),
1761        );
1762        let r = resolved("cx-parity");
1763        let out = run_tool(
1764            &r.config,
1765            "web_search",
1766            serde_json::json!({"query": "example"}),
1767        )
1768        .await
1769        .unwrap();
1770        std::env::remove_var(crate::tools::WEB_SEARCH_URL_ENV);
1771        assert!(out.contains("1 results"), "{out}");
1772        assert!(
1773            out.contains("1. Example doc — https://example.com/doc"),
1774            "{out}"
1775        );
1776        assert!(out.contains("What the page says."), "{out}");
1777    }
1778}