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,524 @@
|
|
|
1
|
+
"""``syncade --doctor`` run-plan + cost preview (PR-v2-12 check 7).
|
|
2
|
+
|
|
3
|
+
Split out of :mod:`syncade.doctor` so the readiness checks and this data-heavier preview
|
|
4
|
+
(which reaches into base-resolution, the diff snapshot, and the metrics corpus) each stay
|
|
5
|
+
under the file-length cap. Both build :class:`~syncade.doctor_types.DoctorCheck` rows; the
|
|
6
|
+
shared type lives in :mod:`syncade.doctor_types` to avoid an import cycle.
|
|
7
|
+
|
|
8
|
+
- **plan:** resolves the diff base the SAME way the CLI does (``--base`` / ``--scope``,
|
|
9
|
+
honoring ``--max-rounds``); an unresolvable scope or bad base is red, matching the real
|
|
10
|
+
run's exit-60 refusal. Reports diff size, the actor set, and the round budget.
|
|
11
|
+
- **cost:** a FORWARD estimate for the planned run — reviewers + judge cost per round (they
|
|
12
|
+
run every round) scaled by the round budget, from the local corpus. The producer is NOT
|
|
13
|
+
folded into the per-round figure (it runs only on NO-SHIP rounds); its extra cost is noted
|
|
14
|
+
separately. Runs whose reviewer/judge cost is priced from INCOMPLETE token data are
|
|
15
|
+
excluded, so the figure is not falsely precise. No history → a VERY ROUGH list-price
|
|
16
|
+
fallback. ``cost_usd`` is an API-equivalent VALUATION, not billed money.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import math
|
|
22
|
+
import sqlite3
|
|
23
|
+
from collections import Counter
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from syncade.base_resolution import BaseResolutionError, resolve_scope
|
|
27
|
+
from syncade.config import SyncadeConfig
|
|
28
|
+
from syncade.diff_filter import (
|
|
29
|
+
concealed_destinations,
|
|
30
|
+
elide_binary_hunks,
|
|
31
|
+
filter_diff_for_reviewer,
|
|
32
|
+
unidentifiable_sections,
|
|
33
|
+
)
|
|
34
|
+
from syncade.doctor_types import _OK, _RED, DoctorCheck
|
|
35
|
+
from syncade.findings import get_findings_schema_string
|
|
36
|
+
from syncade.metrics.aggregate import backfill
|
|
37
|
+
from syncade.metrics.schema import fetch_actor_stats, fetch_runs, open_db
|
|
38
|
+
from syncade.orchestrator.branch_guard import current_branch_name
|
|
39
|
+
from syncade.orchestrator.round_no_changes import _CODEX_CHAR_CEILING
|
|
40
|
+
from syncade.persistence import read_last_reviewed
|
|
41
|
+
from syncade.prompts import load_reviewer_template_for, render_reviewer_prompt
|
|
42
|
+
from syncade.snapshot import SnapshotError, take_snapshot
|
|
43
|
+
|
|
44
|
+
# Coarse fallback ONLY (no local history): price one round from list prices at a NOMINAL
|
|
45
|
+
# per-actor token budget. Calibrated to observed runs (~$1-3/round) so the figure is the
|
|
46
|
+
# right order of magnitude, but it is a guess — flagged VERY ROUGH — because real cost scales
|
|
47
|
+
# with the diff the reviewers actually read.
|
|
48
|
+
_NOMINAL_INPUT_TOK = 200_000
|
|
49
|
+
_NOMINAL_OUTPUT_TOK = 40_000
|
|
50
|
+
|
|
51
|
+
# The actor roles that spend on EVERY review round (unlike the producer, which runs only on
|
|
52
|
+
# NO-SHIP rounds). The cost estimate is built from these so producer spend is not folded into
|
|
53
|
+
# the per-round figure.
|
|
54
|
+
_PER_ROUND_ROLES = ("reviewer", "synthesizer")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _diff_size(diff_text: str) -> tuple[int, int]:
|
|
58
|
+
"""(files, changed lines) in a unified diff — ``diff --git`` headers and +/- body
|
|
59
|
+
lines (excluding the ``+++``/``---`` file headers)."""
|
|
60
|
+
files = changed = 0
|
|
61
|
+
for line in diff_text.splitlines():
|
|
62
|
+
if line.startswith("diff --git "):
|
|
63
|
+
files += 1
|
|
64
|
+
elif line.startswith(("+", "-")) and not line.startswith(("+++", "---")):
|
|
65
|
+
changed += 1
|
|
66
|
+
return files, changed
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def reviewer_facing_bytes(diff_text: str, config: SyncadeConfig) -> int:
|
|
70
|
+
"""UTF-8 size of what a reviewer would actually be handed.
|
|
71
|
+
|
|
72
|
+
The SAME two transforms the round applies, in the same order — repo-context stripping
|
|
73
|
+
then binary elision — so doctor's prediction and `[loop] max_diff_bytes`'s enforcement
|
|
74
|
+
cannot disagree about what "the diff" means. Doctor's row IS the prediction here: it has
|
|
75
|
+
no `run_review` downstream to be authoritative for it, so a false green sends the
|
|
76
|
+
operator on to spend the live auth and producer-commit legs.
|
|
77
|
+
"""
|
|
78
|
+
stripped = filter_diff_for_reviewer(diff_text, config.review.strip_repo_context_files)
|
|
79
|
+
return len(elide_binary_hunks(stripped)[0].encode("utf-8"))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def based_diff_classify(
|
|
83
|
+
repo_root: Path,
|
|
84
|
+
config: SyncadeConfig,
|
|
85
|
+
*,
|
|
86
|
+
base_ref: str | None,
|
|
87
|
+
scope: str | None,
|
|
88
|
+
two_dot: bool,
|
|
89
|
+
) -> str:
|
|
90
|
+
"""Classify a based/scoped diff as ``'dispatch'``, ``'no_changes'``, or ``'malformed'``.
|
|
91
|
+
|
|
92
|
+
- ``'dispatch'``: reviewers will run; commit-safety guards apply.
|
|
93
|
+
- ``'no_changes'``: diff is known-empty; real run exits 0 before dispatch, no commit.
|
|
94
|
+
- ``'malformed'``: diff has unidentifiable headers; real run exits 60 (diff_malformed),
|
|
95
|
+
no commit — but this is a failure, not a benign no-op.
|
|
96
|
+
- ``'too_large'``: reviewer-facing diff exceeds ``[loop] max_diff_bytes``; real run
|
|
97
|
+
exits 60 (diff_too_large) before dispatch, no commit. Same shape as malformed.
|
|
98
|
+
- ``'prompt_too_large'``: assembled reviewer prompt exceeds the provider character
|
|
99
|
+
ceiling; real run exits 60 (prompt_too_large) before dispatch, no commit. Rendered
|
|
100
|
+
with a placeholder PR doc ref, so the size is a LOWER BOUND: this classification is
|
|
101
|
+
never wrong when it fires, and its absence promises nothing.
|
|
102
|
+
|
|
103
|
+
Returns ``'dispatch'`` (conservative: guard applies) on any resolution or snapshot error
|
|
104
|
+
— the plan check catches those failures with its own red; the branch check must not
|
|
105
|
+
double-fire."""
|
|
106
|
+
try:
|
|
107
|
+
if scope is not None:
|
|
108
|
+
branch = current_branch_name(repo_root)
|
|
109
|
+
last = read_last_reviewed(repo_root, branch) if branch else None
|
|
110
|
+
base = resolve_scope(repo_root, scope, last_reviewed_sha=last).base_sha
|
|
111
|
+
else:
|
|
112
|
+
base = base_ref
|
|
113
|
+
if base is None:
|
|
114
|
+
return "dispatch" # full-HEAD path (no diff base), always dispatches
|
|
115
|
+
snap = take_snapshot(repo_root, base_ref=base, three_dot=not two_dot)
|
|
116
|
+
except Exception:
|
|
117
|
+
return "dispatch" # cannot determine; be conservative
|
|
118
|
+
if config.review.strip_repo_context_files and unidentifiable_sections(snap.diff_text):
|
|
119
|
+
return "malformed"
|
|
120
|
+
_filtered = filter_diff_for_reviewer(snap.diff_text, config.review.strip_repo_context_files)
|
|
121
|
+
_filtered_elided, _ = elide_binary_hunks(_filtered)
|
|
122
|
+
if len(_filtered_elided.encode("utf-8")) > config.loop.max_diff_bytes:
|
|
123
|
+
return "too_large"
|
|
124
|
+
if (
|
|
125
|
+
snap.base_oid is not None
|
|
126
|
+
and not _filtered_elided
|
|
127
|
+
and not concealed_destinations(snap.diff_text, config.review.strip_repo_context_files)
|
|
128
|
+
):
|
|
129
|
+
return "no_changes"
|
|
130
|
+
# Prompt-size check: render the full prompt for each reviewer and see if it exceeds
|
|
131
|
+
# the provider character ceiling. Uses the placeholder "<pr-doc>" (same as check_plan
|
|
132
|
+
# when pr_doc_path is unknown), so this can undercount if the template repeats
|
|
133
|
+
# {pr_doc_path} — but without it the branch check would fire for prompt_too_large runs.
|
|
134
|
+
_diff_text = _filtered_elided or (
|
|
135
|
+
"(diff not provided; review against the full repo state at HEAD)"
|
|
136
|
+
)
|
|
137
|
+
_json_schema = get_findings_schema_string()
|
|
138
|
+
for _reviewer in config.reviewers:
|
|
139
|
+
try:
|
|
140
|
+
_tmpl = load_reviewer_template_for(
|
|
141
|
+
repo_root, provider=_reviewer.provider, template=_reviewer.template
|
|
142
|
+
)
|
|
143
|
+
_rendered = render_reviewer_prompt(
|
|
144
|
+
_tmpl,
|
|
145
|
+
pr_doc_path="<pr-doc>",
|
|
146
|
+
diff=_diff_text,
|
|
147
|
+
master_plan_path=None,
|
|
148
|
+
json_schema=_json_schema,
|
|
149
|
+
adversarial_lens=_reviewer.adversarial_lens,
|
|
150
|
+
bug_class_sweep=_reviewer.bug_class_sweep,
|
|
151
|
+
)
|
|
152
|
+
except Exception: # noqa: BLE001 — fail open; check_plan catches template errors
|
|
153
|
+
continue
|
|
154
|
+
if len(_rendered) > _CODEX_CHAR_CEILING:
|
|
155
|
+
return "prompt_too_large"
|
|
156
|
+
return "dispatch"
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def based_diff_will_dispatch(
|
|
160
|
+
repo_root: Path,
|
|
161
|
+
config: SyncadeConfig,
|
|
162
|
+
*,
|
|
163
|
+
base_ref: str | None,
|
|
164
|
+
scope: str | None,
|
|
165
|
+
two_dot: bool,
|
|
166
|
+
) -> bool:
|
|
167
|
+
"""Whether a based/scoped diff will dispatch reviewers.
|
|
168
|
+
|
|
169
|
+
Delegates to :func:`based_diff_classify`; ``'dispatch'`` → True, anything else → False."""
|
|
170
|
+
return (
|
|
171
|
+
based_diff_classify(repo_root, config, base_ref=base_ref, scope=scope, two_dot=two_dot)
|
|
172
|
+
== "dispatch"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def check_plan(
|
|
177
|
+
repo_root: Path,
|
|
178
|
+
config: SyncadeConfig,
|
|
179
|
+
*,
|
|
180
|
+
base_ref: str | None,
|
|
181
|
+
scope: str | None,
|
|
182
|
+
two_dot: bool = False,
|
|
183
|
+
max_rounds: int | None,
|
|
184
|
+
) -> DoctorCheck:
|
|
185
|
+
"""Preview the plan a real run would execute (C1): the resolved diff base + its size,
|
|
186
|
+
the exact actor set (producer runs ONLY on NO-SHIP), and the round budget. Resolves the
|
|
187
|
+
base the same way the CLI does — an unresolvable ``--scope`` or a bad ``--base`` is red,
|
|
188
|
+
matching the real run's exit-60 refusal. Read-only git, so inert.
|
|
189
|
+
|
|
190
|
+
**The prompt-size portion is a LOWER BOUND, and does not claim otherwise.** doctor
|
|
191
|
+
cannot know the PR doc: ``--doctor`` is a one-shot mode and the CLI rejects it beside a
|
|
192
|
+
PR_DOC positional, so the reference is rendered as a placeholder. A template that
|
|
193
|
+
repeats ``{pr_doc_path}`` therefore renders longer in the real run than here.
|
|
194
|
+
|
|
195
|
+
That asymmetry is why the check is still worth having: exceeding the ceiling on a lower
|
|
196
|
+
bound means the real prompt certainly exceeds it, so a RED is always a true positive.
|
|
197
|
+
The converse is NOT claimed — a green plan row does not promise the prompt will fit, and
|
|
198
|
+
the real run refuses cheaply before provisioning if it does not. Four dogfood rounds
|
|
199
|
+
tried to make this exact (estimate -> render -> real PR-doc path -> unreachable from the
|
|
200
|
+
CLI); saying what it is beats a fourth attempt at what it cannot be."""
|
|
201
|
+
if base_ref == "":
|
|
202
|
+
return DoctorCheck(
|
|
203
|
+
"plan",
|
|
204
|
+
_RED,
|
|
205
|
+
"--base was provided as an empty string — pass a valid commit ref",
|
|
206
|
+
fix="pass a non-empty --base <ref>, or omit --base for a full-HEAD review",
|
|
207
|
+
)
|
|
208
|
+
effective = max_rounds if max_rounds is not None else config.loop.max_rounds
|
|
209
|
+
will_commit = effective > 1
|
|
210
|
+
try:
|
|
211
|
+
if scope is not None:
|
|
212
|
+
branch = current_branch_name(repo_root)
|
|
213
|
+
last = read_last_reviewed(repo_root, branch) if branch else None
|
|
214
|
+
base = resolve_scope(repo_root, scope, last_reviewed_sha=last).base_sha
|
|
215
|
+
else:
|
|
216
|
+
base = base_ref # may be None: full-HEAD review with no diff base
|
|
217
|
+
except BaseResolutionError as exc:
|
|
218
|
+
return DoctorCheck(
|
|
219
|
+
"plan",
|
|
220
|
+
_RED,
|
|
221
|
+
f"--scope {scope!r} cannot resolve: {exc}",
|
|
222
|
+
fix="pass an explicit --base <ref>",
|
|
223
|
+
)
|
|
224
|
+
try:
|
|
225
|
+
if base is not None:
|
|
226
|
+
snap = take_snapshot(repo_root, base_ref=base, three_dot=not two_dot)
|
|
227
|
+
# A malformed diff exits 60 before dispatch in the real run — surface it as RED
|
|
228
|
+
# so the cheap-red gate skips live spend (auth/producer-commit). Only applies
|
|
229
|
+
# when strip targets are configured, matching the _diff_will_dispatch condition.
|
|
230
|
+
if config.review.strip_repo_context_files and unidentifiable_sections(snap.diff_text):
|
|
231
|
+
return DoctorCheck(
|
|
232
|
+
"plan",
|
|
233
|
+
_RED,
|
|
234
|
+
f"diff against {base!r} has an ambiguous path header"
|
|
235
|
+
" — real run exits 60 (diff_malformed)",
|
|
236
|
+
fix=(
|
|
237
|
+
"use --two-dot if paths contain ' b/';"
|
|
238
|
+
" check strip_repo_context_files config"
|
|
239
|
+
),
|
|
240
|
+
)
|
|
241
|
+
# The cap refuses before dispatch in the real run — RED, same as malformed,
|
|
242
|
+
# so the cheap-red gate skips the live auth/producer-commit spend.
|
|
243
|
+
_reviewed = reviewer_facing_bytes(snap.diff_text, config)
|
|
244
|
+
if _reviewed > config.loop.max_diff_bytes:
|
|
245
|
+
return DoctorCheck(
|
|
246
|
+
"plan",
|
|
247
|
+
_RED,
|
|
248
|
+
f"reviewer-facing diff is {_reviewed:,} bytes, over "
|
|
249
|
+
f"[loop] max_diff_bytes ({config.loop.max_diff_bytes:,})"
|
|
250
|
+
" — real run exits 60 (diff_too_large)",
|
|
251
|
+
fix=(
|
|
252
|
+
"narrow --base to a smaller range, split the PR, or raise "
|
|
253
|
+
"[loop] max_diff_bytes"
|
|
254
|
+
),
|
|
255
|
+
)
|
|
256
|
+
# Exact assembled-prompt check: render the full round-0 prompt and measure it.
|
|
257
|
+
# The former template+diff estimate omitted the JSON schema (~517 chars) and the
|
|
258
|
+
# adversarial-lens block (~3,316 chars) — a lower bound that let doctor report OK
|
|
259
|
+
# for a run that exits 60 (prompt_too_large). Fails open on template errors.
|
|
260
|
+
_filtered_text = filter_diff_for_reviewer(
|
|
261
|
+
snap.diff_text, config.review.strip_repo_context_files
|
|
262
|
+
)
|
|
263
|
+
_filtered_text, _ = elide_binary_hunks(_filtered_text)
|
|
264
|
+
_diff_for_render = (
|
|
265
|
+
_filtered_text
|
|
266
|
+
if _filtered_text
|
|
267
|
+
else "(diff not provided; review against the full repo state at HEAD)"
|
|
268
|
+
)
|
|
269
|
+
# A PLACEHOLDER ref, because doctor cannot know the real one: `--doctor` is a
|
|
270
|
+
# one-shot mode and the CLI rejects it alongside a PR_DOC positional. That makes
|
|
271
|
+
# the rendered size a LOWER BOUND — a template repeating `{pr_doc_path}` renders
|
|
272
|
+
# longer in the real run than here.
|
|
273
|
+
#
|
|
274
|
+
# A lower bound is still worth checking, because the error is one-directional:
|
|
275
|
+
# if even the lower bound exceeds the ceiling, the real prompt certainly does, so
|
|
276
|
+
# a RED here is always a true positive. The converse does not hold and is not
|
|
277
|
+
# claimed — passing this check does not promise the run will fit. Same idiom the
|
|
278
|
+
# budget surfaces use ("a strict lower bound"), and the honest alternative to
|
|
279
|
+
# predicting exactly, which four dogfood rounds showed doctor cannot do.
|
|
280
|
+
_pr_doc_ref = "<pr-doc>"
|
|
281
|
+
_json_schema = get_findings_schema_string()
|
|
282
|
+
for _reviewer in config.reviewers:
|
|
283
|
+
try:
|
|
284
|
+
_tmpl = load_reviewer_template_for(
|
|
285
|
+
repo_root, provider=_reviewer.provider, template=_reviewer.template
|
|
286
|
+
)
|
|
287
|
+
_rendered = render_reviewer_prompt(
|
|
288
|
+
_tmpl,
|
|
289
|
+
pr_doc_path=_pr_doc_ref,
|
|
290
|
+
diff=_diff_for_render,
|
|
291
|
+
master_plan_path=None,
|
|
292
|
+
json_schema=_json_schema,
|
|
293
|
+
adversarial_lens=_reviewer.adversarial_lens,
|
|
294
|
+
bug_class_sweep=_reviewer.bug_class_sweep,
|
|
295
|
+
)
|
|
296
|
+
except Exception: # noqa: BLE001 — template errors are checked live
|
|
297
|
+
continue
|
|
298
|
+
_prompt_chars = len(_rendered)
|
|
299
|
+
if _prompt_chars > _CODEX_CHAR_CEILING:
|
|
300
|
+
return DoctorCheck(
|
|
301
|
+
"plan",
|
|
302
|
+
_RED,
|
|
303
|
+
f"assembled prompt for reviewer {_reviewer.name!r} is "
|
|
304
|
+
f"{_prompt_chars:,} chars, over the provider "
|
|
305
|
+
f"ceiling of {_CODEX_CHAR_CEILING:,}"
|
|
306
|
+
" — real run exits 60 (prompt_too_large)",
|
|
307
|
+
fix=(
|
|
308
|
+
"trim the reviewer template in .syncade/templates/, narrow --base, "
|
|
309
|
+
"or lower [loop] max_diff_bytes"
|
|
310
|
+
),
|
|
311
|
+
)
|
|
312
|
+
files, changed = _diff_size(snap.diff_text)
|
|
313
|
+
# Name the OID the diff was actually taken against, not the ref the
|
|
314
|
+
# operator typed. Under three-dot they differ whenever the branch is
|
|
315
|
+
# behind its base — exactly the phantom-deletion case PR-h-02
|
|
316
|
+
# increment B fixes — so `diff vs main` read as the advanced tip and
|
|
317
|
+
# understated the preview in the one scenario it most matters.
|
|
318
|
+
origin = base if two_dot else f"branch point of {base}"
|
|
319
|
+
actual = snap.base_oid[:7] if snap.base_oid else base
|
|
320
|
+
diffdesc = (
|
|
321
|
+
f"diff from {actual} ({origin}): {files} file(s), {changed} changed line(s), "
|
|
322
|
+
f"{_reviewed:,}B reviewed of {config.loop.max_diff_bytes:,}B allowed"
|
|
323
|
+
)
|
|
324
|
+
else:
|
|
325
|
+
# No-diff-base path still probes snapshot to catch a corrupt git index that
|
|
326
|
+
# would fail the real run's take_snapshot before dispatch.
|
|
327
|
+
take_snapshot(repo_root)
|
|
328
|
+
diffdesc = "no diff base (reviewers see full HEAD)"
|
|
329
|
+
except SnapshotError as exc:
|
|
330
|
+
return DoctorCheck(
|
|
331
|
+
"plan",
|
|
332
|
+
_RED,
|
|
333
|
+
f"cannot diff against base {base!r}: {exc}",
|
|
334
|
+
fix="pass a valid --base <ref>",
|
|
335
|
+
)
|
|
336
|
+
actors = f"{len(config.reviewers)} reviewer(s) + judge"
|
|
337
|
+
if will_commit:
|
|
338
|
+
actors += ", producer on non-final NO-SHIP"
|
|
339
|
+
if config.loop.test_command:
|
|
340
|
+
actors += ", test-leg"
|
|
341
|
+
if config.checks:
|
|
342
|
+
actors += f", {len(config.checks)} check(s)"
|
|
343
|
+
return DoctorCheck("plan", _OK, f"{diffdesc}; {actors}; up to {effective} round(s)")
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _review_actors(config: SyncadeConfig) -> list:
|
|
347
|
+
"""The every-round actors whose cost the estimate is built from: each reviewer + the
|
|
348
|
+
judge. The producer is excluded (it runs only on NO-SHIP rounds and is noted separately);
|
|
349
|
+
the drafter/auditor belong to other modes and never run here."""
|
|
350
|
+
return [*config.reviewers, config.synthesizer]
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _unpriced_models(config: SyncadeConfig) -> list[str]:
|
|
354
|
+
"""Every-round models with no ``[pricing]`` entry, first-seen order. Their cost reads as
|
|
355
|
+
*unknown*, never $0 — the PR-v2-24 valuation-honesty ethic applied forward."""
|
|
356
|
+
unpriced: list[str] = []
|
|
357
|
+
for actor in _review_actors(config):
|
|
358
|
+
if actor.model not in unpriced and config.pricing.price_for(actor.model) is None:
|
|
359
|
+
unpriced.append(actor.model)
|
|
360
|
+
return unpriced
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _coarse_round_estimate(config: SyncadeConfig) -> float | None:
|
|
364
|
+
"""List-price cost of ONE reviewers + judge round at nominal per-actor token volumes, or
|
|
365
|
+
``None`` if any every-round actor is unpriced (then no honest figure exists)."""
|
|
366
|
+
total = 0.0
|
|
367
|
+
for actor in _review_actors(config):
|
|
368
|
+
price = config.pricing.price_for(actor.model)
|
|
369
|
+
if price is None:
|
|
370
|
+
return None
|
|
371
|
+
total += (
|
|
372
|
+
_NOMINAL_INPUT_TOK / 1e6 * price.input_per_mtok
|
|
373
|
+
+ _NOMINAL_OUTPUT_TOK / 1e6 * price.output_per_mtok
|
|
374
|
+
)
|
|
375
|
+
return total
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _planned_roster(config: SyncadeConfig) -> Counter:
|
|
379
|
+
"""The planned every-round roster as a multiset of ``(role, model)`` — each reviewer's
|
|
380
|
+
model plus the judge's. A historical run's cost is comparable only if its own per-round
|
|
381
|
+
roster matches this exactly."""
|
|
382
|
+
roster: Counter = Counter()
|
|
383
|
+
for reviewer in config.reviewers:
|
|
384
|
+
roster[("reviewer", reviewer.model)] += 1
|
|
385
|
+
roster[("synthesizer", config.synthesizer.model)] += 1
|
|
386
|
+
return roster
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _reviewer_judge_per_round(
|
|
390
|
+
runs, actor_stats, *, expected_roster: Counter
|
|
391
|
+
) -> tuple[float | None, int]:
|
|
392
|
+
"""(reviewers + judge cost per round, n matching runs) from history, or ``(None, 0)``.
|
|
393
|
+
|
|
394
|
+
Sums only the :data:`_PER_ROUND_ROLES` actors (they run every round; the producer is
|
|
395
|
+
excluded). A run is counted only when its per-round ``(role, model)`` multiset EXACTLY
|
|
396
|
+
matches ``expected_roster`` (the planned reviewers + judge). Matching on identity — not a
|
|
397
|
+
bare actor COUNT — is what excludes:
|
|
398
|
+
|
|
399
|
+
- unrelated old-model history (a run whose roster changed models — e.g. a prior gpt-5.6
|
|
400
|
+
panel counted toward a gpt-5.5 estimate),
|
|
401
|
+
- partial coverage (a run missing a planned reviewer),
|
|
402
|
+
- a duplicate reviewer row standing in for a missing different-model one, and
|
|
403
|
+
- a reviewer-only run with no judge (its multiset lacks the ``synthesizer`` entry).
|
|
404
|
+
|
|
405
|
+
Runs with any unknown-cost (``cost_usd is None``) or incomplete-token
|
|
406
|
+
(``cost_incomplete_tokens > 0``) per-round actor are also excluded, so the figure is never
|
|
407
|
+
a falsely-precise number built on missing data.
|
|
408
|
+
"""
|
|
409
|
+
rounds = {r.run_id: r.rounds_executed for r in runs}
|
|
410
|
+
per_run: dict[str, list] = {} # run_id -> [cost_sum, has_incomplete, roster_counter]
|
|
411
|
+
for actor in actor_stats:
|
|
412
|
+
if actor.role not in _PER_ROUND_ROLES:
|
|
413
|
+
continue
|
|
414
|
+
entry = per_run.setdefault(actor.run_id, [0.0, False, Counter()])
|
|
415
|
+
entry[2][(actor.role, actor.model)] += 1
|
|
416
|
+
if actor.cost_usd is None:
|
|
417
|
+
entry[1] = True # unknown cost is incomplete, not $0
|
|
418
|
+
else:
|
|
419
|
+
entry[0] += actor.cost_usd
|
|
420
|
+
if actor.cost_incomplete_tokens > 0:
|
|
421
|
+
entry[1] = True
|
|
422
|
+
# If this actor has per-round usage tracking and appeared in fewer rounds than the
|
|
423
|
+
# run executed, the run's cost average would be understated (the missing rounds are
|
|
424
|
+
# treated as $0 contribution). Exclude such runs. Legacy rows (rounds_with_usage=0)
|
|
425
|
+
# bypass this check so pre-schema-12 history is not silently dropped.
|
|
426
|
+
expected = rounds.get(actor.run_id, 0)
|
|
427
|
+
if actor.rounds_with_usage > 0 and actor.rounds_with_usage != expected:
|
|
428
|
+
entry[1] = True
|
|
429
|
+
total_cost = 0.0
|
|
430
|
+
total_rounds = 0
|
|
431
|
+
n = 0
|
|
432
|
+
for run_id, (cost, incomplete, roster) in per_run.items():
|
|
433
|
+
executed = rounds.get(run_id, 0)
|
|
434
|
+
if incomplete or executed <= 0 or roster != expected_roster:
|
|
435
|
+
continue
|
|
436
|
+
total_cost += cost
|
|
437
|
+
total_rounds += executed
|
|
438
|
+
n += 1
|
|
439
|
+
if not n:
|
|
440
|
+
return None, 0
|
|
441
|
+
return total_cost / total_rounds, n
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def check_cost(config: SyncadeConfig, repo_root: Path, *, max_rounds: int | None) -> DoctorCheck:
|
|
445
|
+
"""Forward cost estimate for the PLANNED run (C4), scaled by the round budget. Built from
|
|
446
|
+
the reviewers + judge cost-per-round in the local corpus (they run every round); the
|
|
447
|
+
producer is NOT folded in (it runs only on NO-SHIP rounds) — its extra cost is noted for a
|
|
448
|
+
committing run. With no clean history, falls back to a VERY ROUGH list-price estimate.
|
|
449
|
+
Always green — a preview must not gate a run — and reads an IN-MEMORY DB so the on-disk
|
|
450
|
+
``metrics.db`` is never written (inert). Any every-round model absent from ``[pricing]``
|
|
451
|
+
is named, never silently $0. Costs are an API-equivalent VALUATION, not billed money."""
|
|
452
|
+
effective = max_rounds if max_rounds is not None else config.loop.max_rounds
|
|
453
|
+
producer_note = "; the producer adds more on non-final NO-SHIP rounds" if effective > 1 else ""
|
|
454
|
+
unpriced = _unpriced_models(config)
|
|
455
|
+
if effective > 1:
|
|
456
|
+
# Producer can run in loop mode — name it if its model is also unpriced.
|
|
457
|
+
pmodel = config.producer.model
|
|
458
|
+
if pmodel not in unpriced and config.pricing.price_for(pmodel) is None:
|
|
459
|
+
unpriced.append(pmodel)
|
|
460
|
+
note = (
|
|
461
|
+
f"; unpriced model(s): {', '.join(unpriced)} (cost would read unknown)" if unpriced else ""
|
|
462
|
+
)
|
|
463
|
+
try:
|
|
464
|
+
conn = open_db(":memory:") # ephemeral: reads the corpus, writes no file
|
|
465
|
+
try:
|
|
466
|
+
backfill(conn, repo_root / ".syncade" / "runs")
|
|
467
|
+
runs = fetch_runs(conn)
|
|
468
|
+
actor_stats = fetch_actor_stats(conn)
|
|
469
|
+
finally:
|
|
470
|
+
conn.close()
|
|
471
|
+
except (sqlite3.Error, OSError) as exc:
|
|
472
|
+
return DoctorCheck("cost", _OK, f"metrics unavailable ({exc}); no forward estimate{note}")
|
|
473
|
+
per_round, n = _reviewer_judge_per_round(
|
|
474
|
+
runs, actor_stats, expected_roster=_planned_roster(config)
|
|
475
|
+
)
|
|
476
|
+
if per_round is not None:
|
|
477
|
+
return DoctorCheck(
|
|
478
|
+
"cost",
|
|
479
|
+
_OK,
|
|
480
|
+
f"~${per_round * effective:.2f} for up to {effective} round(s) — reviewers + judge "
|
|
481
|
+
f"(est. ${per_round:.2f}/round over {n} prior run(s), API-equivalent; ~$0 marginal "
|
|
482
|
+
f"on a subscription{producer_note}){note}",
|
|
483
|
+
)
|
|
484
|
+
coarse = _coarse_round_estimate(config)
|
|
485
|
+
if coarse is None:
|
|
486
|
+
return DoctorCheck(
|
|
487
|
+
"cost",
|
|
488
|
+
_OK,
|
|
489
|
+
f"no clean run history and an unpriced model — cannot estimate{note}",
|
|
490
|
+
)
|
|
491
|
+
nominal = coarse * effective
|
|
492
|
+
# Coarse range: floor(0.5×) to ceil(3×), minimum $1 for the low bound.
|
|
493
|
+
# A single two-decimal amount falsely implies precision we don't have without history.
|
|
494
|
+
low = max(1, math.floor(nominal * 0.5))
|
|
495
|
+
high = max(low + 1, math.ceil(nominal * 3.0))
|
|
496
|
+
return DoctorCheck(
|
|
497
|
+
"cost",
|
|
498
|
+
_OK,
|
|
499
|
+
f"~${low}–${high} for up to {effective} round(s) — reviewers + judge (VERY "
|
|
500
|
+
f"ROUGH: no local history, coarse range at list price ~{_NOMINAL_INPUT_TOK // 1000}K in/"
|
|
501
|
+
f"{_NOMINAL_OUTPUT_TOK // 1000}K out per actor{producer_note}){note}",
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def check_budget(config: SyncadeConfig) -> DoctorCheck:
|
|
506
|
+
"""Active token/dollar ceiling preview (PR-h-field-06 item 1).
|
|
507
|
+
|
|
508
|
+
Named in ``--doctor`` output so a first-run operator knows the default stop condition is
|
|
509
|
+
active and how to opt out. Always green — a ceiling is not an error. Reports both
|
|
510
|
+
ceiling types when both are configured."""
|
|
511
|
+
parts = []
|
|
512
|
+
if config.loop.budget_tokens:
|
|
513
|
+
parts.append(
|
|
514
|
+
f"token ceiling: {config.loop.budget_tokens:,} "
|
|
515
|
+
"(set `[loop] budget_tokens = 0` to disable)"
|
|
516
|
+
)
|
|
517
|
+
if config.loop.budget_usd:
|
|
518
|
+
parts.append(
|
|
519
|
+
f"cost ceiling: ${config.loop.budget_usd:.2f} "
|
|
520
|
+
"(API-equivalent; not billed money on a subscription)"
|
|
521
|
+
)
|
|
522
|
+
if parts:
|
|
523
|
+
return DoctorCheck("budget", _OK, "; ".join(parts))
|
|
524
|
+
return DoctorCheck("budget", _OK, "no token or cost ceiling configured")
|
syncade/doctor_types.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Shared types for ``syncade --doctor`` (PR-v2-12).
|
|
2
|
+
|
|
3
|
+
A LEAF module so the check *engine* (:mod:`syncade.doctor`) and the run-plan + cost *preview*
|
|
4
|
+
(:mod:`syncade.doctor_preview`) can both build :class:`DoctorCheck` rows without an import
|
|
5
|
+
cycle (doctor imports doctor_preview for the preview checks; both import this).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
|
|
12
|
+
_OK = "ok"
|
|
13
|
+
_RED = "red"
|
|
14
|
+
_SKIP = "skip"
|
|
15
|
+
|
|
16
|
+
_STATUS_GLYPH = {_OK: "✓", _RED: "✗", _SKIP: "–"} # ✓ ✗ –
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class DoctorCheck:
|
|
21
|
+
"""One preflight check's outcome. ``status`` is one of ``ok`` / ``red`` / ``skip``;
|
|
22
|
+
only ``red`` fails doctor's exit code (a ``skip`` means doctor could not run the check,
|
|
23
|
+
not that it passed). ``fix`` is an operator-facing remediation shown under a red row."""
|
|
24
|
+
|
|
25
|
+
name: str
|
|
26
|
+
status: str
|
|
27
|
+
detail: str
|
|
28
|
+
fix: str | None = None
|
syncade/exit_codes.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Exit codes for the ``syncade`` CLI.
|
|
2
|
+
|
|
3
|
+
These integer values are the CLI contract:
|
|
4
|
+
|
|
5
|
+
SUCCESS = 0
|
|
6
|
+
Clean run. With ``[loop] test_command`` unset, the synthesizer found no
|
|
7
|
+
active blockers. With ``test_command`` set, the clean worktree test command
|
|
8
|
+
also exited 0.
|
|
9
|
+
|
|
10
|
+
CLI_USAGE_ERROR = 2
|
|
11
|
+
Invalid command shape or argument syntax, matching argparse's usage-error
|
|
12
|
+
convention.
|
|
13
|
+
|
|
14
|
+
CLARIFICATION_NEEDED = 10
|
|
15
|
+
Clarification needed. Three producers, all of which write an operator-facing
|
|
16
|
+
``decision-needed.md`` (except the first, which is its own report):
|
|
17
|
+
|
|
18
|
+
- ``--spec-audit`` found blocking ambiguity in the brief;
|
|
19
|
+
- a producer escalation covering every active blocker;
|
|
20
|
+
- **two or more distinct reviewers each raised a blocker and the
|
|
21
|
+
synthesizer deactivated every one of them** (PR-h-01 increment D). Not a
|
|
22
|
+
defect claim — independent corroboration is the strongest signal syncade
|
|
23
|
+
produces, and discarding all of it may be right but is not a call a
|
|
24
|
+
machine should make silently. The document quotes each reviewer verbatim
|
|
25
|
+
next to what the synthesizer did with it.
|
|
26
|
+
|
|
27
|
+
MAX_ROUNDS_REACHED = 20
|
|
28
|
+
Max rounds reached before convergence. Latest findings are written for the
|
|
29
|
+
user to inspect.
|
|
30
|
+
|
|
31
|
+
BUDGET_EXCEEDED = 25
|
|
32
|
+
The per-run token/dollar ceiling (``--budget-tokens`` / ``--budget-usd`` or
|
|
33
|
+
the ``[loop]`` twins) was crossed at a dispatch boundary, so the loop aborted
|
|
34
|
+
before spending more. Distinct from 20 so a caller can tell "ran out of
|
|
35
|
+
rounds" (raise ``--max-rounds``) from "hit the cost ceiling" (raise the
|
|
36
|
+
budget). Whatever findings the completed rounds produced are preserved. The
|
|
37
|
+
budget gates FURTHER dispatch only: a terminal round keeps its own verdict —
|
|
38
|
+
a converged SHIP stays 0, a max-rounds/single-pass NO-SHIP stays 20/30 — and
|
|
39
|
+
is never relabeled 25, so this code means "stopped early to save spend," not
|
|
40
|
+
"a finished run happened to cost a lot."
|
|
41
|
+
|
|
42
|
+
The code is SHARED with a second cause the operator did not configure: a
|
|
43
|
+
provider refusing to serve because the account's usage window is exhausted
|
|
44
|
+
(``termination_reason="provider_usage_limit"``). Both stop cleanly at a phase
|
|
45
|
+
boundary and both resume, which is why they share a code — but they are told
|
|
46
|
+
apart by ``termination_reason``, and they want opposite responses: raise the
|
|
47
|
+
ceiling versus wait for the window to reset. Anything reporting this code to a
|
|
48
|
+
person should read the reason rather than assume the budget.
|
|
49
|
+
|
|
50
|
+
FINDINGS_PRESENT = 30
|
|
51
|
+
Findings present. A synthesizer blocker remains, the configured test command
|
|
52
|
+
failed, or producer/branch promotion could not complete safely.
|
|
53
|
+
|
|
54
|
+
REVIEWER_FAILURE = 40
|
|
55
|
+
Reviewer OR synthesizer subprocess failure. This also covers producer and
|
|
56
|
+
test-leg subprocess failures such as missing binaries, timeouts, process
|
|
57
|
+
crashes, provider/network errors, and model refusals.
|
|
58
|
+
|
|
59
|
+
CONFIG_ERROR = 50
|
|
60
|
+
Configuration error. ``.syncade/config.toml`` failed to parse or failed
|
|
61
|
+
schema validation.
|
|
62
|
+
|
|
63
|
+
WORKTREE_ERROR = 60
|
|
64
|
+
Worktree provisioning error. ``git worktree add`` or equivalent setup failed
|
|
65
|
+
before the requested phase could run.
|
|
66
|
+
|
|
67
|
+
REVIEWER_OUTPUT_UNPARSEABLE = 70
|
|
68
|
+
Reviewer OR synthesizer output unparseable. The raw response is preserved in
|
|
69
|
+
the matching ``.stdout`` artifact and the parse exception is preserved in the
|
|
70
|
+
matching ``.error.txt`` artifact.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
SUCCESS: int = 0
|
|
74
|
+
CLI_USAGE_ERROR: int = 2
|
|
75
|
+
CLARIFICATION_NEEDED: int = 10
|
|
76
|
+
MAX_ROUNDS_REACHED: int = 20
|
|
77
|
+
BUDGET_EXCEEDED: int = 25
|
|
78
|
+
FINDINGS_PRESENT: int = 30
|
|
79
|
+
REVIEWER_FAILURE: int = 40
|
|
80
|
+
CONFIG_ERROR: int = 50
|
|
81
|
+
WORKTREE_ERROR: int = 60
|
|
82
|
+
REVIEWER_OUTPUT_UNPARSEABLE: int = 70
|