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,226 @@
|
|
|
1
|
+
"""Producer adapter for the OpenAI ``codex`` CLI.
|
|
2
|
+
|
|
3
|
+
Symmetric to :class:`syncade.adapters.openai.CodexAdapter` but for
|
|
4
|
+
the fix-it subprocess. Real headless producer runs use ``yolo`` to
|
|
5
|
+
create git commits because the ``workspace-write`` sandbox blocks
|
|
6
|
+
``.git/index.lock`` writes. The output type is
|
|
7
|
+
:class:`syncade.adapters.producer.ProducerOutput` (just the narrative text),
|
|
8
|
+
not a structured
|
|
9
|
+
:class:`syncade.findings.ReviewerOutput`.
|
|
10
|
+
|
|
11
|
+
Built against the codex CLI's observed JSONL output.
|
|
12
|
+
``codex exec``'s JSONL extraction reuses
|
|
13
|
+
:meth:`CodexAdapter.extract_final_text` so the
|
|
14
|
+
shared envelope-parsing path stays one source of truth (this is
|
|
15
|
+
the same pattern used when the synthesizer reuses the
|
|
16
|
+
extractor with a different no-agent-message exception class).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from syncade.adapters.base import Invocation
|
|
24
|
+
from syncade.adapters.openai import (
|
|
25
|
+
_YOLO_FLAG,
|
|
26
|
+
CodexAdapter,
|
|
27
|
+
_check_codex_auth,
|
|
28
|
+
)
|
|
29
|
+
from syncade.adapters.producer import ProducerOutput
|
|
30
|
+
from syncade.auth_preflight import assert_codex_reality_honours_declaration
|
|
31
|
+
from syncade.config import ProducerConfig
|
|
32
|
+
from syncade.config_auth import apply_auth_to_env
|
|
33
|
+
from syncade.findings import ReviewerOutputError
|
|
34
|
+
from syncade.process import SubprocessResult
|
|
35
|
+
from syncade.worktree_env import worktree_scoped_env
|
|
36
|
+
|
|
37
|
+
_PRODUCER_PERMISSION_VALUES: tuple[str, ...] = ("yolo",)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class OpenAIProducerAdapter:
|
|
41
|
+
"""ProducerAdapter for the OpenAI ``codex`` CLI.
|
|
42
|
+
|
|
43
|
+
``build_invocation`` produces a ``codex exec --json`` argv that:
|
|
44
|
+
|
|
45
|
+
- pins the model from :attr:`ProducerConfig.model`
|
|
46
|
+
- sets reasoning effort via ``-c model_reasoning_effort=<level>``
|
|
47
|
+
(codex has no dedicated ``--effort`` flag — same discovery-doc
|
|
48
|
+
finding the reviewer adapter relies on)
|
|
49
|
+
- maps permissions: ``yolo`` →
|
|
50
|
+
``--dangerously-bypass-approvals-and-sandbox``; stale/sandboxed values
|
|
51
|
+
rejected at build time
|
|
52
|
+
- scopes file access to the producer worktree via ``-C`` and
|
|
53
|
+
``--add-dir``
|
|
54
|
+
|
|
55
|
+
``parse_output`` extracts the final ``agent_message`` text via
|
|
56
|
+
:meth:`CodexAdapter.extract_final_text`, passing
|
|
57
|
+
:class:`ReviewerOutputError` as the no-agent-message exception
|
|
58
|
+
class (the producer doesn't have a phase-specific exception
|
|
59
|
+
type, and exit 40 is the right bucket either way — a producer
|
|
60
|
+
that completed cleanly but emitted no agent message is
|
|
61
|
+
operationally similar to a reviewer that did the same).
|
|
62
|
+
|
|
63
|
+
``check_auth`` runs ``codex login status``. The producer and
|
|
64
|
+
reviewer adapters share auth state (same ``~/.codex/auth.json``), but the
|
|
65
|
+
method remains available for callers that drive the producer phase in
|
|
66
|
+
isolation.
|
|
67
|
+
|
|
68
|
+
The adapter never shells out itself in ``build_invocation`` or
|
|
69
|
+
``parse_output``. ``check_auth`` is the one exception (same
|
|
70
|
+
pattern as :class:`CodexAdapter.check_auth`).
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
name = "openai"
|
|
74
|
+
|
|
75
|
+
def check_auth(self) -> None:
|
|
76
|
+
_check_codex_auth(
|
|
77
|
+
binary_missing_message=(
|
|
78
|
+
"codex binary not found on PATH — install codex-cli to "
|
|
79
|
+
"run the producer subprocess (the OpenAI Codex CLI, not "
|
|
80
|
+
"a third-party fork)"
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def build_invocation(
|
|
85
|
+
self,
|
|
86
|
+
producer_config: ProducerConfig,
|
|
87
|
+
worktree_path: Path,
|
|
88
|
+
prompt: str,
|
|
89
|
+
) -> Invocation:
|
|
90
|
+
"""Construct the ``codex exec --json`` invocation for one
|
|
91
|
+
producer round.
|
|
92
|
+
|
|
93
|
+
Same argv shape as :meth:`CodexAdapter.build_invocation` (per
|
|
94
|
+
the codex CLI output notes) with the
|
|
95
|
+
:data:`syncade.config.ProducerPermissions` value-set (``yolo``).
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
ValueError: If ``producer_config.provider`` is not
|
|
99
|
+
``"openai"`` — defensive guard against misroute.
|
|
100
|
+
ValueError: If ``producer_config.permissions`` is
|
|
101
|
+
``"safe"`` — belt-and-braces guard for callers that
|
|
102
|
+
bypass the schema's :data:`ProducerPermissions`
|
|
103
|
+
Literal.
|
|
104
|
+
"""
|
|
105
|
+
self._validate_provider(producer_config.provider)
|
|
106
|
+
self._validate_permissions(producer_config.permissions)
|
|
107
|
+
# Structural backstop: refuse to spawn codex for a non-auto declaration the probed
|
|
108
|
+
# login does not honour, on any path that skipped the CLI auth gate. No-op on the
|
|
109
|
+
# gated CLI path. See syncade.auth_preflight.
|
|
110
|
+
assert_codex_reality_honours_declaration(producer_config)
|
|
111
|
+
|
|
112
|
+
argv: list[str] = [
|
|
113
|
+
"codex",
|
|
114
|
+
"exec",
|
|
115
|
+
"--json",
|
|
116
|
+
"--model",
|
|
117
|
+
producer_config.model,
|
|
118
|
+
"-c",
|
|
119
|
+
f"model_reasoning_effort={producer_config.thinking}",
|
|
120
|
+
]
|
|
121
|
+
argv.append(_YOLO_FLAG)
|
|
122
|
+
# The prompt goes on STDIN, never argv — see AnthropicAdapter.build_invocation.
|
|
123
|
+
# `codex exec` reads instructions from stdin when no positional PROMPT is given.
|
|
124
|
+
argv.extend(
|
|
125
|
+
[
|
|
126
|
+
"-C",
|
|
127
|
+
str(worktree_path),
|
|
128
|
+
"--add-dir",
|
|
129
|
+
str(worktree_path),
|
|
130
|
+
]
|
|
131
|
+
)
|
|
132
|
+
return Invocation(
|
|
133
|
+
argv=argv,
|
|
134
|
+
cwd=worktree_path,
|
|
135
|
+
env=apply_auth_to_env(worktree_scoped_env(worktree_path), producer_config),
|
|
136
|
+
stdin_text=prompt,
|
|
137
|
+
timeout_seconds=None,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
@staticmethod
|
|
141
|
+
def _validate_provider(provider: str) -> None:
|
|
142
|
+
"""Refuse a config whose ``provider`` is not ``"openai"``.
|
|
143
|
+
|
|
144
|
+
Mirrors :meth:`CodexAdapter._validate_provider`. The
|
|
145
|
+
producer registry routes by provider name; this guard
|
|
146
|
+
catches a misroute at build time instead of letting the
|
|
147
|
+
subprocess fail with a confusing codex-side error after
|
|
148
|
+
non-codex flags get spliced in.
|
|
149
|
+
"""
|
|
150
|
+
if provider != "openai":
|
|
151
|
+
raise ValueError(
|
|
152
|
+
f"OpenAIProducerAdapter received a ProducerConfig with "
|
|
153
|
+
f"provider={provider!r}; expected 'openai'. The "
|
|
154
|
+
f"producer registry should route configs to the adapter "
|
|
155
|
+
f"whose name matches the config's provider field."
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
@staticmethod
|
|
159
|
+
def _validate_permissions(permissions: str) -> None:
|
|
160
|
+
"""Refuse unsupported permissions at build time.
|
|
161
|
+
|
|
162
|
+
Same logic as the reviewer adapter's analogous guard plus
|
|
163
|
+
:meth:`AnthropicProducerAdapter._validate_permissions`. The
|
|
164
|
+
:data:`ProducerPermissions` Literal already rejects ``safe``
|
|
165
|
+
at config-load; this guard catches the case where a caller
|
|
166
|
+
constructs a :class:`ProducerConfig` programmatically and
|
|
167
|
+
bypasses the schema (typically in unit tests).
|
|
168
|
+
"""
|
|
169
|
+
if permissions == "safe":
|
|
170
|
+
raise ValueError(
|
|
171
|
+
"OpenAIProducerAdapter cannot run a producer with "
|
|
172
|
+
"permissions='safe' headlessly: the corresponding "
|
|
173
|
+
"`approval_policy=untrusted` mode prompts for tool use "
|
|
174
|
+
"and `codex exec` cannot answer prompts. Use 'yolo' for "
|
|
175
|
+
"headless producer commits."
|
|
176
|
+
)
|
|
177
|
+
if permissions not in _PRODUCER_PERMISSION_VALUES:
|
|
178
|
+
valid = "', '".join(_PRODUCER_PERMISSION_VALUES)
|
|
179
|
+
raise ValueError(
|
|
180
|
+
"OpenAIProducerAdapter received unsupported producer "
|
|
181
|
+
f"permissions={permissions!r}; expected one of '{valid}'."
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
def parse_output(self, result: SubprocessResult) -> ProducerOutput:
|
|
185
|
+
"""Parse a finished ``codex exec --json`` subprocess result.
|
|
186
|
+
|
|
187
|
+
Decision tree:
|
|
188
|
+
|
|
189
|
+
1. Extract the final ``agent_message`` text via
|
|
190
|
+
:meth:`CodexAdapter.extract_final_text`,
|
|
191
|
+
passing :class:`ReviewerOutputError` as the
|
|
192
|
+
no-agent-message exception class.
|
|
193
|
+
2. The extractor itself handles subprocess-side failures
|
|
194
|
+
(``turn.failed`` events, non-zero rc, unrecovered bare ``error``
|
|
195
|
+
streams, auth signatures) — it raises
|
|
196
|
+
:class:`ReviewerInvocationError` for those, which the
|
|
197
|
+
orchestrator's producer-phase exception handler maps to
|
|
198
|
+
exit 40 (subprocess failure).
|
|
199
|
+
3. On success, wrap the text in
|
|
200
|
+
:class:`ProducerOutput(narrative_text=...)`. No further
|
|
201
|
+
structured parsing — the producer doesn't emit a
|
|
202
|
+
schema-bound payload (unlike the reviewer adapter where
|
|
203
|
+
the next step is ``parse_reviewer_output`` against the
|
|
204
|
+
extracted text).
|
|
205
|
+
|
|
206
|
+
:meth:`CodexAdapter.extract_final_text` is
|
|
207
|
+
reused (rather than re-implemented in this module) for the
|
|
208
|
+
same reason the synthesizer uses it: codex's
|
|
209
|
+
JSONL stream-extraction logic is non-trivial, and a divergent
|
|
210
|
+
implementation in the producer adapter would have its own
|
|
211
|
+
edge cases to discover (turn.failed-vs-error event
|
|
212
|
+
precedence, auth-signature detection, empty-agent-message
|
|
213
|
+
handling).
|
|
214
|
+
"""
|
|
215
|
+
# Reuse the codex reviewer adapter's extraction helper via
|
|
216
|
+
# an internal CodexAdapter instance. The instance is
|
|
217
|
+
# stateless across calls, so allocating fresh per parse_output
|
|
218
|
+
# is cheap and avoids stuffing the helper into a static
|
|
219
|
+
# @classmethod (which would need to update CodexAdapter's
|
|
220
|
+
# surface unnecessarily).
|
|
221
|
+
codex_helper = CodexAdapter()
|
|
222
|
+
final_text = codex_helper.extract_final_text(
|
|
223
|
+
result,
|
|
224
|
+
empty_output_exception_class=ReviewerOutputError,
|
|
225
|
+
)
|
|
226
|
+
return ProducerOutput(narrative_text=final_text)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Provider-string-to-adapter lookup for the dispatcher.
|
|
2
|
+
|
|
3
|
+
The dispatcher takes a list of :class:`ReviewerConfig`s and
|
|
4
|
+
routes each to the adapter whose :attr:`ReviewerAdapter.name` matches
|
|
5
|
+
the config's ``provider`` field. This module is that routing table.
|
|
6
|
+
|
|
7
|
+
Intentionally a tiny dict and not a plugin system. Adding a new
|
|
8
|
+
provider lands by appending one line here; nothing dynamic until we
|
|
9
|
+
have evidence we need it.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from collections.abc import Callable
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from typing import Literal
|
|
17
|
+
|
|
18
|
+
from syncade.adapters.anthropic import AnthropicAdapter
|
|
19
|
+
from syncade.adapters.base import ReviewerAdapter
|
|
20
|
+
from syncade.adapters.openai import CodexAdapter
|
|
21
|
+
|
|
22
|
+
ProviderRole = Literal["reviewer", "producer"]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class UnknownProviderError(ValueError):
|
|
27
|
+
role: ProviderRole
|
|
28
|
+
requested: str
|
|
29
|
+
known: tuple[str, ...]
|
|
30
|
+
|
|
31
|
+
def __str__(self) -> str:
|
|
32
|
+
known = ", ".join(sorted(self.known))
|
|
33
|
+
return (
|
|
34
|
+
f"unknown {self.role} adapter {self.requested!r} in "
|
|
35
|
+
f".syncade/config.toml. Known adapters: {known}"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Mapping: provider string → zero-arg factory that constructs the
|
|
40
|
+
# adapter. Using factories (not pre-built instances) so each call to
|
|
41
|
+
# ``get_adapter`` returns a fresh adapter — keeps tests that mutate
|
|
42
|
+
# adapter state from leaking across calls.
|
|
43
|
+
_ADAPTER_FACTORIES: dict[str, Callable[[], ReviewerAdapter]] = {
|
|
44
|
+
"anthropic": AnthropicAdapter,
|
|
45
|
+
"openai": CodexAdapter,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_adapter(provider: str) -> ReviewerAdapter:
|
|
50
|
+
"""Return a fresh adapter instance for the given provider string.
|
|
51
|
+
|
|
52
|
+
The provider name must match a key in the registry. The known
|
|
53
|
+
providers are tied to ``ReviewerAdapter.name`` for each shipped
|
|
54
|
+
adapter; the dispatcher uses this lookup to route each
|
|
55
|
+
:class:`~syncade.config.ReviewerConfig` to its matching adapter.
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
ValueError: When ``provider`` is not a known key. The error
|
|
59
|
+
message names the bad input AND lists the known
|
|
60
|
+
providers, so a user with a typo in
|
|
61
|
+
``.syncade/config.toml`` (``provider = "anthrpic"``) sees
|
|
62
|
+
both their mistake and the fix at once.
|
|
63
|
+
"""
|
|
64
|
+
try:
|
|
65
|
+
factory = _ADAPTER_FACTORIES[provider]
|
|
66
|
+
except KeyError:
|
|
67
|
+
raise UnknownProviderError(
|
|
68
|
+
role="reviewer",
|
|
69
|
+
requested=provider,
|
|
70
|
+
known=tuple(_ADAPTER_FACTORIES),
|
|
71
|
+
) from None
|
|
72
|
+
return factory()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def known_providers() -> list[str]:
|
|
76
|
+
"""Return the sorted list of registered provider names.
|
|
77
|
+
|
|
78
|
+
Useful for surface-area assertions in tests and for any future
|
|
79
|
+
`syncade providers` CLI command that lists what's available.
|
|
80
|
+
"""
|
|
81
|
+
return sorted(_ADAPTER_FACTORIES)
|