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.
- syncade/__init__.py +3 -0
- syncade/__main__.py +6 -0
- syncade/adapters/__init__.py +0 -0
- syncade/adapters/anthropic.py +457 -0
- syncade/adapters/base.py +221 -0
- syncade/adapters/fake.py +73 -0
- syncade/adapters/fake_common.py +29 -0
- syncade/adapters/fake_producer_audit_draft.py +460 -0
- syncade/adapters/fake_reviewer_synth.py +310 -0
- syncade/adapters/openai.py +484 -0
- syncade/adapters/openai_parsing.py +119 -0
- syncade/adapters/producer.py +221 -0
- syncade/adapters/producer_anthropic.py +300 -0
- syncade/adapters/producer_openai.py +226 -0
- syncade/adapters/registry.py +81 -0
- syncade/auth_check.py +554 -0
- syncade/auth_preflight.py +342 -0
- syncade/base_resolution.py +214 -0
- syncade/billing.py +141 -0
- syncade/checks_config.py +113 -0
- syncade/cli/__init__.py +546 -0
- syncade/cli/auth_gate.py +59 -0
- syncade/cli/config_keys.py +135 -0
- syncade/cli/config_list.py +82 -0
- syncade/cli/config_menu_rows.py +166 -0
- syncade/cli/config_mode.py +609 -0
- syncade/cli/config_overrides.py +122 -0
- syncade/cli/config_tui.py +476 -0
- syncade/cli/doctor_mode.py +72 -0
- syncade/cli/gc_mode.py +109 -0
- syncade/cli/install_skill.py +514 -0
- syncade/cli/metrics_mode.py +363 -0
- syncade/cli/modes.py +573 -0
- syncade/cli/parser.py +450 -0
- syncade/cli/parser_types.py +137 -0
- syncade/cli/paths.py +38 -0
- syncade/cli/preflight_paths.py +90 -0
- syncade/cli/resolve.py +116 -0
- syncade/cli/resume_mode.py +324 -0
- syncade/cli/toml_writer.py +410 -0
- syncade/cli/validate.py +421 -0
- syncade/config.py +478 -0
- syncade/config_auth.py +310 -0
- syncade/config_cold.py +209 -0
- syncade/config_gc.py +55 -0
- syncade/config_loader.py +182 -0
- syncade/config_loop.py +282 -0
- syncade/config_producer.py +222 -0
- syncade/config_retry.py +49 -0
- syncade/config_types.py +59 -0
- syncade/diff_filter.py +437 -0
- syncade/dispatcher.py +571 -0
- syncade/doctor.py +425 -0
- syncade/doctor_env.py +218 -0
- syncade/doctor_preview.py +524 -0
- syncade/doctor_types.py +28 -0
- syncade/exit_codes.py +82 -0
- syncade/findings.py +242 -0
- syncade/findings_json.py +456 -0
- syncade/gc.py +211 -0
- syncade/gc_execute.py +372 -0
- syncade/gc_protection.py +129 -0
- syncade/gc_types.py +50 -0
- syncade/gc_worktrees.py +200 -0
- syncade/git_object_id.py +12 -0
- syncade/git_preconditions.py +389 -0
- syncade/logging.py +289 -0
- syncade/metrics/__init__.py +32 -0
- syncade/metrics/aggregate.py +550 -0
- syncade/metrics/schema.py +221 -0
- syncade/orchestrator/__init__.py +61 -0
- syncade/orchestrator/_runs_dir.py +24 -0
- syncade/orchestrator/branch_advance.py +165 -0
- syncade/orchestrator/branch_guard.py +98 -0
- syncade/orchestrator/budget.py +107 -0
- syncade/orchestrator/escalation_coverage.py +81 -0
- syncade/orchestrator/loop.py +611 -0
- syncade/orchestrator/loop_dispatch_check.py +112 -0
- syncade/orchestrator/loop_finalize.py +404 -0
- syncade/orchestrator/loop_preflight.py +131 -0
- syncade/orchestrator/loop_resume.py +91 -0
- syncade/orchestrator/loop_rmtree.py +70 -0
- syncade/orchestrator/loop_round_step.py +599 -0
- syncade/orchestrator/prior_round.py +336 -0
- syncade/orchestrator/producer_phase.py +169 -0
- syncade/orchestrator/results.py +306 -0
- syncade/orchestrator/resume.py +96 -0
- syncade/orchestrator/resume_load.py +483 -0
- syncade/orchestrator/resume_plan.py +554 -0
- syncade/orchestrator/resume_target.py +215 -0
- syncade/orchestrator/resume_types.py +182 -0
- syncade/orchestrator/reviewer_template_failure.py +99 -0
- syncade/orchestrator/round.py +573 -0
- syncade/orchestrator/round_checks.py +91 -0
- syncade/orchestrator/round_no_changes.py +369 -0
- syncade/orchestrator/round_predispatch.py +212 -0
- syncade/orchestrator/verdict.py +279 -0
- syncade/persistence/__init__.py +189 -0
- syncade/persistence/_atomic.py +33 -0
- syncade/persistence/_clusters.py +70 -0
- syncade/persistence/_findings_verdict.py +201 -0
- syncade/persistence/_markdown.py +286 -0
- syncade/persistence/_validation.py +37 -0
- syncade/persistence/checks.py +249 -0
- syncade/persistence/decision_needed.py +289 -0
- syncade/persistence/findings_md.py +389 -0
- syncade/persistence/handoff.py +389 -0
- syncade/persistence/handoff_classify.py +196 -0
- syncade/persistence/last_reviewed.py +67 -0
- syncade/persistence/loop_manifest.py +165 -0
- syncade/persistence/loop_summary.py +352 -0
- syncade/persistence/loop_summary_text.py +428 -0
- syncade/persistence/producer.py +250 -0
- syncade/persistence/reviewer.py +198 -0
- syncade/persistence/round_manifest.py +238 -0
- syncade/persistence/run_init.py +153 -0
- syncade/persistence/run_summary.py +585 -0
- syncade/persistence/run_summary_next_steps.py +443 -0
- syncade/persistence/synth.py +242 -0
- syncade/persistence/test_run.py +152 -0
- syncade/presets.py +36 -0
- syncade/pricing_config.py +72 -0
- syncade/process.py +600 -0
- syncade/producer.py +189 -0
- syncade/producer_attempt.py +463 -0
- syncade/producer_escalation.py +146 -0
- syncade/producer_git.py +199 -0
- syncade/producer_result.py +205 -0
- syncade/prompts.py +448 -0
- syncade/prompts_loader.py +238 -0
- syncade/retry.py +159 -0
- syncade/run_inputs.py +40 -0
- syncade/run_status.py +198 -0
- syncade/selfcheck.py +471 -0
- syncade/skills/claude/README.md +221 -0
- syncade/skills/claude/SKILL.md +625 -0
- syncade/skills/codex/README.md +116 -0
- syncade/skills/codex/SKILL.md +574 -0
- syncade/snapshot.py +598 -0
- syncade/spec_audit.py +437 -0
- syncade/spec_audit_schema.py +190 -0
- syncade/spec_draft.py +423 -0
- syncade/spec_source.py +135 -0
- syncade/synthesis.py +428 -0
- syncade/synthesis_clusters.py +203 -0
- syncade/synthesis_repair.py +230 -0
- syncade/synthesis_schema.py +65 -0
- syncade/synthesizer/__init__.py +38 -0
- syncade/synthesizer/constants.py +33 -0
- syncade/synthesizer/driver.py +531 -0
- syncade/synthesizer/rendering.py +63 -0
- syncade/synthesizer/result.py +73 -0
- syncade/synthesizer/validation.py +421 -0
- syncade/synthesizer/workspace.py +208 -0
- syncade/templates/presets/balanced.toml +13 -0
- syncade/templates/presets/cheap.toml +12 -0
- syncade/templates/presets/thorough.toml +9 -0
- syncade/templates/producer.md +231 -0
- syncade/templates/reviewer.md +279 -0
- syncade/templates/reviewer_adversarial.md +164 -0
- syncade/templates/reviewer_codex.md +165 -0
- syncade/templates/spec_audit.md +168 -0
- syncade/templates/spec_draft.md +62 -0
- syncade/templates/synthesizer.md +204 -0
- syncade/test_runner.py +476 -0
- syncade/test_runner_classify.py +98 -0
- syncade/transcript.py +150 -0
- syncade/usage.py +407 -0
- syncade/worktree.py +497 -0
- syncade/worktree_env.py +133 -0
- syncade/worktree_paths.py +139 -0
- syncade-0.6.2.dist-info/METADATA +314 -0
- syncade-0.6.2.dist-info/RECORD +177 -0
- syncade-0.6.2.dist-info/WHEEL +5 -0
- syncade-0.6.2.dist-info/entry_points.txt +2 -0
- syncade-0.6.2.dist-info/licenses/LICENSE +202 -0
- 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
|
syncade/config_retry.py
ADDED
|
@@ -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
|
syncade/config_types.py
ADDED
|
@@ -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.
|