syncade 0.6.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. syncade/__init__.py +3 -0
  2. syncade/__main__.py +6 -0
  3. syncade/adapters/__init__.py +0 -0
  4. syncade/adapters/anthropic.py +457 -0
  5. syncade/adapters/base.py +221 -0
  6. syncade/adapters/fake.py +73 -0
  7. syncade/adapters/fake_common.py +29 -0
  8. syncade/adapters/fake_producer_audit_draft.py +460 -0
  9. syncade/adapters/fake_reviewer_synth.py +310 -0
  10. syncade/adapters/openai.py +484 -0
  11. syncade/adapters/openai_parsing.py +119 -0
  12. syncade/adapters/producer.py +221 -0
  13. syncade/adapters/producer_anthropic.py +300 -0
  14. syncade/adapters/producer_openai.py +226 -0
  15. syncade/adapters/registry.py +81 -0
  16. syncade/auth_check.py +554 -0
  17. syncade/auth_preflight.py +342 -0
  18. syncade/base_resolution.py +214 -0
  19. syncade/billing.py +141 -0
  20. syncade/checks_config.py +113 -0
  21. syncade/cli/__init__.py +546 -0
  22. syncade/cli/auth_gate.py +59 -0
  23. syncade/cli/config_keys.py +135 -0
  24. syncade/cli/config_list.py +82 -0
  25. syncade/cli/config_menu_rows.py +166 -0
  26. syncade/cli/config_mode.py +609 -0
  27. syncade/cli/config_overrides.py +122 -0
  28. syncade/cli/config_tui.py +476 -0
  29. syncade/cli/doctor_mode.py +72 -0
  30. syncade/cli/gc_mode.py +109 -0
  31. syncade/cli/install_skill.py +514 -0
  32. syncade/cli/metrics_mode.py +363 -0
  33. syncade/cli/modes.py +573 -0
  34. syncade/cli/parser.py +450 -0
  35. syncade/cli/parser_types.py +137 -0
  36. syncade/cli/paths.py +38 -0
  37. syncade/cli/preflight_paths.py +90 -0
  38. syncade/cli/resolve.py +116 -0
  39. syncade/cli/resume_mode.py +324 -0
  40. syncade/cli/toml_writer.py +410 -0
  41. syncade/cli/validate.py +421 -0
  42. syncade/config.py +478 -0
  43. syncade/config_auth.py +310 -0
  44. syncade/config_cold.py +209 -0
  45. syncade/config_gc.py +55 -0
  46. syncade/config_loader.py +182 -0
  47. syncade/config_loop.py +282 -0
  48. syncade/config_producer.py +222 -0
  49. syncade/config_retry.py +49 -0
  50. syncade/config_types.py +59 -0
  51. syncade/diff_filter.py +437 -0
  52. syncade/dispatcher.py +571 -0
  53. syncade/doctor.py +425 -0
  54. syncade/doctor_env.py +218 -0
  55. syncade/doctor_preview.py +524 -0
  56. syncade/doctor_types.py +28 -0
  57. syncade/exit_codes.py +82 -0
  58. syncade/findings.py +242 -0
  59. syncade/findings_json.py +456 -0
  60. syncade/gc.py +211 -0
  61. syncade/gc_execute.py +372 -0
  62. syncade/gc_protection.py +129 -0
  63. syncade/gc_types.py +50 -0
  64. syncade/gc_worktrees.py +200 -0
  65. syncade/git_object_id.py +12 -0
  66. syncade/git_preconditions.py +389 -0
  67. syncade/logging.py +289 -0
  68. syncade/metrics/__init__.py +32 -0
  69. syncade/metrics/aggregate.py +550 -0
  70. syncade/metrics/schema.py +221 -0
  71. syncade/orchestrator/__init__.py +61 -0
  72. syncade/orchestrator/_runs_dir.py +24 -0
  73. syncade/orchestrator/branch_advance.py +165 -0
  74. syncade/orchestrator/branch_guard.py +98 -0
  75. syncade/orchestrator/budget.py +107 -0
  76. syncade/orchestrator/escalation_coverage.py +81 -0
  77. syncade/orchestrator/loop.py +611 -0
  78. syncade/orchestrator/loop_dispatch_check.py +112 -0
  79. syncade/orchestrator/loop_finalize.py +404 -0
  80. syncade/orchestrator/loop_preflight.py +131 -0
  81. syncade/orchestrator/loop_resume.py +91 -0
  82. syncade/orchestrator/loop_rmtree.py +70 -0
  83. syncade/orchestrator/loop_round_step.py +599 -0
  84. syncade/orchestrator/prior_round.py +336 -0
  85. syncade/orchestrator/producer_phase.py +169 -0
  86. syncade/orchestrator/results.py +306 -0
  87. syncade/orchestrator/resume.py +96 -0
  88. syncade/orchestrator/resume_load.py +483 -0
  89. syncade/orchestrator/resume_plan.py +554 -0
  90. syncade/orchestrator/resume_target.py +215 -0
  91. syncade/orchestrator/resume_types.py +182 -0
  92. syncade/orchestrator/reviewer_template_failure.py +99 -0
  93. syncade/orchestrator/round.py +573 -0
  94. syncade/orchestrator/round_checks.py +91 -0
  95. syncade/orchestrator/round_no_changes.py +369 -0
  96. syncade/orchestrator/round_predispatch.py +212 -0
  97. syncade/orchestrator/verdict.py +279 -0
  98. syncade/persistence/__init__.py +189 -0
  99. syncade/persistence/_atomic.py +33 -0
  100. syncade/persistence/_clusters.py +70 -0
  101. syncade/persistence/_findings_verdict.py +201 -0
  102. syncade/persistence/_markdown.py +286 -0
  103. syncade/persistence/_validation.py +37 -0
  104. syncade/persistence/checks.py +249 -0
  105. syncade/persistence/decision_needed.py +289 -0
  106. syncade/persistence/findings_md.py +389 -0
  107. syncade/persistence/handoff.py +389 -0
  108. syncade/persistence/handoff_classify.py +196 -0
  109. syncade/persistence/last_reviewed.py +67 -0
  110. syncade/persistence/loop_manifest.py +165 -0
  111. syncade/persistence/loop_summary.py +352 -0
  112. syncade/persistence/loop_summary_text.py +428 -0
  113. syncade/persistence/producer.py +250 -0
  114. syncade/persistence/reviewer.py +198 -0
  115. syncade/persistence/round_manifest.py +238 -0
  116. syncade/persistence/run_init.py +153 -0
  117. syncade/persistence/run_summary.py +585 -0
  118. syncade/persistence/run_summary_next_steps.py +443 -0
  119. syncade/persistence/synth.py +242 -0
  120. syncade/persistence/test_run.py +152 -0
  121. syncade/presets.py +36 -0
  122. syncade/pricing_config.py +72 -0
  123. syncade/process.py +600 -0
  124. syncade/producer.py +189 -0
  125. syncade/producer_attempt.py +463 -0
  126. syncade/producer_escalation.py +146 -0
  127. syncade/producer_git.py +199 -0
  128. syncade/producer_result.py +205 -0
  129. syncade/prompts.py +448 -0
  130. syncade/prompts_loader.py +238 -0
  131. syncade/retry.py +159 -0
  132. syncade/run_inputs.py +40 -0
  133. syncade/run_status.py +198 -0
  134. syncade/selfcheck.py +471 -0
  135. syncade/skills/claude/README.md +221 -0
  136. syncade/skills/claude/SKILL.md +625 -0
  137. syncade/skills/codex/README.md +116 -0
  138. syncade/skills/codex/SKILL.md +574 -0
  139. syncade/snapshot.py +598 -0
  140. syncade/spec_audit.py +437 -0
  141. syncade/spec_audit_schema.py +190 -0
  142. syncade/spec_draft.py +423 -0
  143. syncade/spec_source.py +135 -0
  144. syncade/synthesis.py +428 -0
  145. syncade/synthesis_clusters.py +203 -0
  146. syncade/synthesis_repair.py +230 -0
  147. syncade/synthesis_schema.py +65 -0
  148. syncade/synthesizer/__init__.py +38 -0
  149. syncade/synthesizer/constants.py +33 -0
  150. syncade/synthesizer/driver.py +531 -0
  151. syncade/synthesizer/rendering.py +63 -0
  152. syncade/synthesizer/result.py +73 -0
  153. syncade/synthesizer/validation.py +421 -0
  154. syncade/synthesizer/workspace.py +208 -0
  155. syncade/templates/presets/balanced.toml +13 -0
  156. syncade/templates/presets/cheap.toml +12 -0
  157. syncade/templates/presets/thorough.toml +9 -0
  158. syncade/templates/producer.md +231 -0
  159. syncade/templates/reviewer.md +279 -0
  160. syncade/templates/reviewer_adversarial.md +164 -0
  161. syncade/templates/reviewer_codex.md +165 -0
  162. syncade/templates/spec_audit.md +168 -0
  163. syncade/templates/spec_draft.md +62 -0
  164. syncade/templates/synthesizer.md +204 -0
  165. syncade/test_runner.py +476 -0
  166. syncade/test_runner_classify.py +98 -0
  167. syncade/transcript.py +150 -0
  168. syncade/usage.py +407 -0
  169. syncade/worktree.py +497 -0
  170. syncade/worktree_env.py +133 -0
  171. syncade/worktree_paths.py +139 -0
  172. syncade-0.6.2.dist-info/METADATA +314 -0
  173. syncade-0.6.2.dist-info/RECORD +177 -0
  174. syncade-0.6.2.dist-info/WHEEL +5 -0
  175. syncade-0.6.2.dist-info/entry_points.txt +2 -0
  176. syncade-0.6.2.dist-info/licenses/LICENSE +202 -0
  177. syncade-0.6.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,222 @@
1
+ """Producer subprocess configuration, and the harness detection that drives its
2
+ defaults.
3
+
4
+ Split out of ``config.py`` (PR-v2-23): that module was 462 LOC against a **blocking**
5
+ 500 cap, and the judge/drafter blocks this PR adds would have pushed it over. Everything
6
+ producer-shaped lives here — the harness probe, the provider→model pairing, and
7
+ ``ProducerConfig`` itself. ``config.py`` re-exports these names, so every existing
8
+ import path keeps working.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import math
14
+ import os
15
+ from typing import Literal
16
+
17
+ from pydantic import ConfigDict, Field, field_validator, model_validator
18
+
19
+ from syncade.config_auth import AuthedActor
20
+ from syncade.config_types import Thinking
21
+
22
+ Harness = Literal["claude-code", "codex"]
23
+ """Ambient coding harness syncade was invoked from.
24
+
25
+ Inferred from the process environment, not from the syncade CLI itself:
26
+ Claude Code sets ``CLAUDE_CODE_SESSION_ID`` and Codex sets
27
+ ``CODEX_THREAD_ID`` in the shell they hand to their tools."""
28
+
29
+
30
+ def _invoking_harness() -> Harness:
31
+ """The ambient coding harness, for producer default selection.
32
+
33
+ Claude Code wins if both markers are present — it is the more specific
34
+ signal (a Codex thread id can linger in an exported environment).
35
+
36
+ With neither marker (plain terminal, CI, cron) we fall back to the Codex
37
+ shape. That is the safe side: the reviewers and the judge are OpenAI
38
+ regardless of harness, so codex auth is required to run syncade at all,
39
+ while Anthropic auth is only implied when Claude Code is the harness.
40
+ """
41
+ if os.getenv("CLAUDE_CODE_SESSION_ID"):
42
+ return "claude-code"
43
+ if os.getenv("CODEX_THREAD_ID"):
44
+ return "codex"
45
+ return "codex"
46
+
47
+
48
+ # The producer follows the harness the operator is already coding in; the
49
+ # reviewers and the judge do not (see _default_reviewers / SYNTHESIZER_MODEL).
50
+ # provider → the model that provider's producer runs. Kept as a pair because
51
+ # the two must agree: handing claude-sonnet-4-6 to ``codex exec`` is an
52
+ # unknown-model failure at dispatch, not a config error.
53
+ _PRODUCER_MODELS: dict[str, str] = {
54
+ "anthropic": "claude-sonnet-4-6",
55
+ "openai": "gpt-5.6-terra",
56
+ }
57
+
58
+
59
+ def _default_producer_provider() -> str:
60
+ return "anthropic" if _invoking_harness() == "claude-code" else "openai"
61
+
62
+
63
+ def _default_producer_model() -> str:
64
+ return _PRODUCER_MODELS[_default_producer_provider()]
65
+
66
+
67
+ # ---------------------------------------------------------------------------
68
+ # Sub-models
69
+ # ---------------------------------------------------------------------------
70
+
71
+
72
+ ProducerProvider = Literal["anthropic", "openai"]
73
+ """Provider names the producer adapter registry recognizes.
74
+ The producer subprocess is a fresh LLM (claude or codex) — not the
75
+ operator's Claude Code session. Same provider-string vocabulary as
76
+ ``ReviewerConfig.provider`` so the same adapter routing applies."""
77
+
78
+ ProducerPermissions = Literal["yolo"]
79
+ """Tool-permission tier accepted by the producer adapters.
80
+
81
+ Real headless producer smokes require ``yolo`` to create git commits
82
+ unattended. Sandboxed modes either prompt for bash (Claude) or block
83
+ ``.git/index.lock`` writes (Codex), so they are rejected at config-load
84
+ instead of stalling mid-run."""
85
+
86
+
87
+ class ProducerConfig(AuthedActor):
88
+ """Producer subprocess configuration.
89
+
90
+ The producer is the fresh LLM subprocess that receives
91
+ ``findings.md`` after a NO-SHIP round and makes the fix. Symmetric
92
+ to :class:`ReviewerConfig`'s adapter-routing surface (``provider``
93
+ + ``model`` + ``thinking`` + ``permissions``) but with
94
+ code-writing defaults rather than auditing defaults.
95
+
96
+ Same provider-name keys as ``[[reviewers]]`` so the adapter
97
+ registry can route both via the same vocabulary. The empty
98
+ ``[producer]`` section (or absent section) in
99
+ ``.syncade/config.toml`` instantiates this with all defaults;
100
+ operators wanting codex as producer write
101
+ ``[producer]\\nprovider = "openai"\\nmodel = "gpt-5-codex"\\n``.
102
+
103
+ Defaults follow the invoking harness (:func:`_invoking_harness`) —
104
+ ``anthropic`` / ``claude-sonnet-4-6`` under Claude Code, ``openai`` /
105
+ ``gpt-5.6-terra`` otherwise — with ``thinking="medium"`` /
106
+ ``permissions="yolo"`` either way. The producer is the one role that
107
+ tracks the operator's toolchain; the reviewers and the judge stay pinned
108
+ to a cold OpenAI tier so verdicts stay comparable across harnesses. Other
109
+ permission values are rejected at the schema level (see
110
+ :data:`ProducerPermissions`).
111
+
112
+ The producer's ``timeout_seconds`` is per-round wall-clock; when
113
+ unset (the default), the orchestrator reuses
114
+ :attr:`LoopConfig.timeout_seconds` (i.e. the reviewer timeout).
115
+ The same :func:`math.isfinite` guard also applies to
116
+ :attr:`LoopConfig.timeout_seconds` applies here so a runaway
117
+ operator config can't produce an unkillable producer subprocess.
118
+
119
+ The producer is a fresh ``claude -p`` or ``codex exec`` subprocess for each
120
+ round. It receives prior producer context as replayed prompt input from
121
+ persisted artifacts, not as a resumable process session."""
122
+
123
+ model_config = ConfigDict(extra="forbid")
124
+
125
+ provider: ProducerProvider = Field(
126
+ default_factory=_default_producer_provider,
127
+ description=(
128
+ "Producer adapter provider. ``anthropic`` routes to "
129
+ ":class:`AnthropicProducerAdapter` (``claude -p`` with "
130
+ "``--permission-mode bypassPermissions``); ``openai`` routes "
131
+ "to :class:`OpenAIProducerAdapter` (``codex exec``). "
132
+ "Defaults to the invoking harness (see :func:`_invoking_harness`): "
133
+ "``anthropic`` under Claude Code, ``openai`` otherwise. "
134
+ "Uses the same provider names as ``ReviewerConfig.provider``, "
135
+ "but producer and reviewer adapters are registered separately; "
136
+ "a future provider must add each supported role explicitly."
137
+ ),
138
+ )
139
+ model: str = Field(
140
+ default_factory=_default_producer_model,
141
+ description=(
142
+ "Model identifier within the provider. Defaults to the model that "
143
+ "matches ``provider`` (see :data:`_PRODUCER_MODELS`): "
144
+ "``claude-sonnet-4-6`` for ``anthropic``, ``gpt-5.6-terra`` for "
145
+ "``openai``. Setting ``provider`` alone re-derives this, so the "
146
+ "pair cannot disagree; set ``model`` explicitly to pin a "
147
+ "different tier within the provider."
148
+ ),
149
+ )
150
+ thinking: Thinking = Field(
151
+ default="medium",
152
+ description=(
153
+ "Reasoning effort budget. ``medium`` is the default — the "
154
+ "producer is applying the synthesizer's consolidated findings "
155
+ "to code it can see, which is a narrower task than the "
156
+ "reviewers' open-ended audit or the judge's cross-reviewer "
157
+ "consolidation. Accepted values are enumerated by the "
158
+ ":data:`syncade.config.Thinking` Literal — see "
159
+ ":attr:`ReviewerConfig.thinking` for the canonical-source "
160
+ "note."
161
+ ),
162
+ )
163
+
164
+ @model_validator(mode="before")
165
+ @classmethod
166
+ def _keep_provider_and_model_paired(cls, data: object) -> object:
167
+ """An explicit ``provider`` with no ``model`` re-derives the model.
168
+
169
+ Without this, ``[producer]\\nprovider = "openai"`` on a Claude Code
170
+ box would inherit the harness's ``claude-sonnet-4-6`` default and hand
171
+ it to ``codex exec`` — an unknown-model failure at dispatch, and one
172
+ that only reproduces on that harness. The pair moves together.
173
+ """
174
+ if not isinstance(data, dict):
175
+ return data
176
+ provider = data.get("provider")
177
+ if provider and "model" not in data and provider in _PRODUCER_MODELS:
178
+ data = {**data, "model": _PRODUCER_MODELS[provider]}
179
+ return data
180
+
181
+ permissions: ProducerPermissions = Field(
182
+ default="yolo",
183
+ description=(
184
+ "Tool-permission tier. ``yolo`` (default) bypasses sandbox "
185
+ "and approval prompts entirely (claude's "
186
+ "``bypassPermissions`` / codex's "
187
+ "``--dangerously-bypass-approvals-and-sandbox``). This is "
188
+ "the only live-verified mode where both real headless "
189
+ "producer CLIs can create git commits; sandboxed modes are "
190
+ "rejected at the schema level because they stall or block "
191
+ "the commit path."
192
+ ),
193
+ )
194
+ timeout_seconds: float | None = Field(
195
+ default=None,
196
+ gt=0,
197
+ description=(
198
+ "Per-producer-round wall-clock timeout in seconds. "
199
+ "``None`` (default) reuses :attr:`LoopConfig.timeout_seconds` "
200
+ "(the reviewer timeout). The orchestrator does that "
201
+ "resolution; keeping the field nullable means 'use the "
202
+ "default' is distinguishable from 'explicitly set to "
203
+ "the same value as ``timeout_seconds``'. Must be > 0 "
204
+ "and finite when set; NaN and infinity are rejected via "
205
+ "a ``field_validator`` (same ``math.isfinite`` guard "
206
+ "this applies to :attr:`LoopConfig.timeout_seconds`)."
207
+ ),
208
+ )
209
+
210
+ @field_validator("timeout_seconds")
211
+ @classmethod
212
+ def _timeout_seconds_isfinite(cls, value: float | None) -> float | None:
213
+ """Reject NaN / inf — pydantic's ``gt=0`` admits both unaided.
214
+ Same field-validator pattern as
215
+ :meth:`LoopConfig._test_timeout_seconds_isfinite`. Without
216
+ this, a producer subprocess that hits an unkillable timeout
217
+ would hang the orchestrator's loop indefinitely."""
218
+ if value is not None and not math.isfinite(value):
219
+ raise ValueError( # GENERIC_ERR_OK: Pydantic validator expects ValueError.
220
+ f"producer.timeout_seconds must be a finite positive number; got {value!r}"
221
+ )
222
+ return value
@@ -0,0 +1,49 @@
1
+ """``[retry]`` config block (PR-v2-9): the shared transient-retry bound.
2
+
3
+ A LEAF module — pydantic only. It deliberately does NOT import :mod:`syncade.retry`: that module
4
+ pulls in ``adapters.base`` → ``config`` → back here, so importing it would form a cycle. Instead the
5
+ default is a LITERAL mirror of :data:`syncade.retry.MAX_RETRIES`, kept honest by a drift test
6
+ (``tests/config/test_retry_config.py::test_default_reproduces_the_runtime_bound_exactly``) that
7
+ fails loudly if the two ever diverge — the same leaf-plus-drift-guard idiom as ``config_cold``.
8
+
9
+ The runtime bound is ``retry.MAX_RETRIES``; this exposes it as config so an operator can raise it (a
10
+ flaky provider) or drop it to 0 (fail fast, no retries) without editing code. The value is threaded
11
+ to the three legs that retry a transient provider error — reviewer dispatch, cold synthesizer, and
12
+ producer — so they cannot drift on the bound.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
18
+
19
+ # Drift-guarded literal mirror of syncade.retry.MAX_RETRIES (see module docstring).
20
+ _DEFAULT_MAX_RETRIES = 2
21
+
22
+
23
+ class RetryConfig(BaseModel):
24
+ model_config = ConfigDict(extra="forbid")
25
+
26
+ max_retries: int = Field(
27
+ default=_DEFAULT_MAX_RETRIES,
28
+ ge=0,
29
+ description=(
30
+ "Extra attempts (AFTER the first) each model leg — reviewer, synthesizer, producer — "
31
+ "rides out a TRANSIENT provider error (429/5xx/dropped socket) before failing. Default "
32
+ f"{_DEFAULT_MAX_RETRIES}. 0 disables retries (fail fast); raise for a flaky provider. "
33
+ "Only transient errors retry — timeouts, parse failures, non-transient errors never do."
34
+ ),
35
+ )
36
+
37
+ @field_validator("max_retries", mode="before")
38
+ @classmethod
39
+ def _strict_int(cls, value: object) -> object:
40
+ # Pydantic's lax mode coerces a quoted number ("5"→5), an exact float (5.0→5), and a
41
+ # boolean (bool subclasses int: True→1) silently. In a TOML integer knob each is a config
42
+ # mistake that would quietly change retry behavior — reject them (exit 50), accept only a
43
+ # plain int.
44
+ if isinstance(value, bool) or not isinstance(value, int):
45
+ raise ValueError(
46
+ f"max_retries must be a plain integer (got {value!r}); quoted numbers, floats, "
47
+ "and booleans are rejected"
48
+ )
49
+ return value
@@ -0,0 +1,59 @@
1
+ """Enum-like Literals shared by every config module.
2
+
3
+ Lives alone so ``config.py`` and ``config_producer.py`` can both import them without a
4
+ cycle (``config`` re-exports ``config_producer``'s names, so the dependency must run one
5
+ way only). Any value not listed here fails validation — which is the point.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Literal
11
+
12
+ Thinking = Literal["low", "medium", "high", "xhigh", "max"]
13
+ """Reasoning effort budget passed to the underlying model adapter.
14
+
15
+ ``"xhigh"`` is the higher reasoning tier above
16
+ ``"high"``. BOTH providers accept it at the CLI level:
17
+
18
+ - Codex via ``-c model_reasoning_effort=xhigh`` (verified live
19
+ against codex 0.134.0; the operator's own
20
+ ``~/.codex/config.toml`` uses xhigh as the default).
21
+ - Claude via ``--effort xhigh`` (verified live against claude
22
+ 2.1.152, where ``claude --help`` lists ``--effort`` accepting
23
+ ``low | medium | high | xhigh | max``).
24
+
25
+ All four adapters (:class:`~syncade.adapters.openai.CodexAdapter`,
26
+ :class:`~syncade.adapters.producer_openai.OpenAIProducerAdapter`,
27
+ :class:`~syncade.adapters.anthropic.AnthropicAdapter`,
28
+ :class:`~syncade.adapters.producer_anthropic.AnthropicProducerAdapter`)
29
+ pass the value through verbatim — no per-provider rejection. The
30
+ brief's initial claim that ``claude --effort`` only supported
31
+ ``low/medium/high`` was unverified speculation that the first
32
+ validation codex-reviewer caught.
33
+
34
+ ``"max"`` was added when Claude Opus 4.8 dropped (2026-05-28).
35
+ Claude CLI lists it as the highest tier (above ``xhigh``); Codex's
36
+ ``-c model_reasoning_effort=max`` is accepted as a pass-through
37
+ (verified live). Adapters pass the value through verbatim; no
38
+ per-provider mapping update needed.
39
+ """
40
+
41
+ Permissions = Literal["safe", "trusted-execute", "yolo"]
42
+ """Tool-permission tier for an agent process. ``yolo`` grants full
43
+ auto-approval; ``safe`` requires explicit user approval for every action.
44
+
45
+ ``trusted-execute`` is the provider-symmetric "no prompts, full
46
+ worktree access" reviewer mode: it maps to ``bypassPermissions`` on
47
+ Anthropic and the unchanged ``-s workspace-write -c approval_policy=never``
48
+ on Codex. The old ``trusted`` value was provider-asymmetric and is no longer
49
+ accepted."""
50
+
51
+ ReviewerPermissions = Literal["trusted-execute", "yolo"]
52
+ """Reviewer permission tiers — the runnable subset of :data:`Permissions`. ``safe`` is excluded:
53
+ the real reviewer adapters refuse it (it prompts, so it hangs a headless subprocess), so it is
54
+ rejected at config-load rather than only at dispatch — mirroring
55
+ :data:`~syncade.config_producer.ProducerPermissions`. The adapters keep their own ``safe`` guard as
56
+ belt-and-braces for callers that bypass the schema (e.g. ``model_construct``)."""
57
+
58
+ # LoopConfig moved to config_loop; it is imported above for
59
+ # SyncadeConfig's ``loop`` field.