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
syncade/logging.py
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""Small stdout/stderr progress logger for syncade runs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import TYPE_CHECKING, Literal
|
|
8
|
+
|
|
9
|
+
from syncade.billing import from_usages as _billing_from_usages
|
|
10
|
+
from syncade.findings import ReviewerOutputError
|
|
11
|
+
from syncade.retry import is_usage_limit_error
|
|
12
|
+
from syncade.synthesis import SynthesizerOutputError
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from collections.abc import Iterator
|
|
16
|
+
|
|
17
|
+
from syncade.orchestrator import RunResult
|
|
18
|
+
|
|
19
|
+
LogLevel = Literal["quiet", "normal"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _timestamp() -> str:
|
|
23
|
+
return datetime.now().strftime("[%H:%M:%S]")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _is_budget_stop(run_result: RunResult) -> bool:
|
|
27
|
+
return getattr(run_result, "termination_reason", None) == "budget_exceeded"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _budget_stop_line(run_result: RunResult) -> str:
|
|
31
|
+
"""The budget-abort notice (PR-v2-11), shown even under ``--quiet``.
|
|
32
|
+
|
|
33
|
+
Rewritten in PR-h-field-06 to answer the four questions the operator actually has, the
|
|
34
|
+
same shape :func:`_usage_limit_stop_line` uses: what was spent against what ceiling, that
|
|
35
|
+
the completed work survived, and the exact command to continue. It previously pointed at
|
|
36
|
+
a file for the tally, which is one indirection too many for a terminal condition — and it
|
|
37
|
+
matters more now that a ceiling exists BY DEFAULT, so the first time most operators meet
|
|
38
|
+
this message they will not have chosen the number it names.
|
|
39
|
+
|
|
40
|
+
Tokens are translated to their API-equivalent dollar figure, flagged as a VALUATION: the
|
|
41
|
+
numbers come from the enforcement tally carried on the result, so this line, the
|
|
42
|
+
loop-summary Budget section and ``--metrics`` cannot disagree.
|
|
43
|
+
"""
|
|
44
|
+
tokens = sum(u.total_tokens for u in run_result.budget_usages)
|
|
45
|
+
# Use billing.from_usages() — the SAME rule as loop-summary.md and --metrics — so that
|
|
46
|
+
# the lower-bound warning fires whenever cost_incomplete_tokens > 0 (which includes any
|
|
47
|
+
# Usage with cost_source="unknown", even when cost_usd is non-None). Checking
|
|
48
|
+
# `cost_usd is not None` alone misses that case and contradicts the artifact.
|
|
49
|
+
b = _billing_from_usages(run_result.budget_usages)
|
|
50
|
+
run_id = run_result.artifacts.run_dir.name
|
|
51
|
+
|
|
52
|
+
if run_result.budget_ceiling == "budget_usd":
|
|
53
|
+
limit = run_result.budget_usd
|
|
54
|
+
crossed = f"cost ceiling (budget_usd = ${limit:,.2f})" if limit else "cost ceiling"
|
|
55
|
+
else:
|
|
56
|
+
tokens_limit = run_result.budget_tokens
|
|
57
|
+
crossed = (
|
|
58
|
+
f"token ceiling (budget_tokens = {tokens_limit:,})" if tokens_limit else "token ceiling"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if b.total_unpriced_tokens:
|
|
62
|
+
valuation = (
|
|
63
|
+
f"${b.api_equiv:,.2f} (LOWER BOUND — {b.total_unpriced_tokens:,} tokens unpriced)"
|
|
64
|
+
)
|
|
65
|
+
elif b.api_equiv > 0:
|
|
66
|
+
valuation = f"${b.api_equiv:,.2f}"
|
|
67
|
+
else:
|
|
68
|
+
valuation = "unpriced"
|
|
69
|
+
if run_result.budget_ceiling == "budget_usd":
|
|
70
|
+
remedy = "Raise the ceiling (or set `[loop] budget_usd = 0` for none)"
|
|
71
|
+
else:
|
|
72
|
+
remedy = "Raise the ceiling (or set `[loop] budget_tokens = 0` for none)"
|
|
73
|
+
return (
|
|
74
|
+
f"[syncade] stopped early: this run crossed your {crossed}.\n"
|
|
75
|
+
f" Spent: {tokens:,} tokens (API-equivalent {valuation} — a valuation, not "
|
|
76
|
+
f"billed money).\n"
|
|
77
|
+
f" Completed rounds and their findings are preserved; nothing was interrupted "
|
|
78
|
+
f"mid-call.\n"
|
|
79
|
+
f" {remedy}, then: "
|
|
80
|
+
f"syncade --resume {run_id}"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _approaching_budget_line(ceiling: str, usages: list, loop: object) -> str:
|
|
85
|
+
"""The 80%-of-ceiling heads-up. Advisory: it never changes a verdict or an exit code.
|
|
86
|
+
|
|
87
|
+
Says what the remaining headroom IS rather than only that a limit is near, because the
|
|
88
|
+
decision it supports is "let this run finish or stop it myself", and that needs a number.
|
|
89
|
+
"""
|
|
90
|
+
if ceiling == "budget_usd":
|
|
91
|
+
spent = sum(u.cost_usd for u in usages if u.cost_usd is not None)
|
|
92
|
+
limit = loop.budget_usd
|
|
93
|
+
return (
|
|
94
|
+
f"[syncade] approaching your cost ceiling: ${spent:,.2f} of ${limit:,.2f} "
|
|
95
|
+
f"(API-equivalent, a valuation not billed money). The run stops at the ceiling and "
|
|
96
|
+
f"is resumable."
|
|
97
|
+
)
|
|
98
|
+
spent = sum(u.total_tokens for u in usages)
|
|
99
|
+
limit = loop.budget_tokens
|
|
100
|
+
return (
|
|
101
|
+
f"[syncade] approaching your token ceiling: {spent:,} of {limit:,} tokens "
|
|
102
|
+
f"({limit - spent:,} left). The run stops at the ceiling and is resumable; raise "
|
|
103
|
+
f"`[loop] budget_tokens` or set it to 0 for none."
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _quota_candidates(
|
|
108
|
+
run_result: RunResult,
|
|
109
|
+
) -> Iterator[tuple[BaseException | None, str | None]]:
|
|
110
|
+
"""Every actor that could have been the one refused, with its provider.
|
|
111
|
+
|
|
112
|
+
A quota is ACCOUNT-wide, so whichever actor reaches the provider first discovers it — and
|
|
113
|
+
after the routing fix that is as often the judge or the producer as a reviewer. Scanning
|
|
114
|
+
only the reviewers left both of those printing a bare "the provider", which is the one
|
|
115
|
+
thing this line exists to avoid saying.
|
|
116
|
+
"""
|
|
117
|
+
for r in run_result.dispatch_result.results:
|
|
118
|
+
yield r.error, r.provider
|
|
119
|
+
synth = run_result.synth_result
|
|
120
|
+
if synth is not None:
|
|
121
|
+
yield synth.error, synth.provider
|
|
122
|
+
for rnd in reversed(run_result.rounds):
|
|
123
|
+
producer = rnd.producer_result
|
|
124
|
+
if producer is not None:
|
|
125
|
+
yield producer.error, producer.provider
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _usage_limit_stop_line(run_result: RunResult) -> str:
|
|
129
|
+
"""The provider-quota abort notice (PR-h-field-02), shown even under ``--quiet``.
|
|
130
|
+
|
|
131
|
+
Three field runs hit this and the operator saw NOTHING — no verdict, no error, no exit code.
|
|
132
|
+
A terminal condition the operator can act on is worth four lines: what happened, that their
|
|
133
|
+
completed work survived, the remedy, and the exact command to continue.
|
|
134
|
+
|
|
135
|
+
Names the PROVIDER because a mixed panel makes "which one ran out" the first question, and
|
|
136
|
+
names the run id because the resume command is useless without it.
|
|
137
|
+
"""
|
|
138
|
+
provider = next(
|
|
139
|
+
(p for err, p in _quota_candidates(run_result) if p and is_usage_limit_error(err)),
|
|
140
|
+
"the provider",
|
|
141
|
+
)
|
|
142
|
+
run_id = run_result.artifacts.run_dir.name
|
|
143
|
+
return (
|
|
144
|
+
f"[syncade] stopped early: {provider}'s usage limit reached — not your configured "
|
|
145
|
+
f"budget, "
|
|
146
|
+
f"and not a fault in the code under review.\n"
|
|
147
|
+
f" Completed rounds and their findings are preserved; nothing was interrupted "
|
|
148
|
+
f"mid-call.\n"
|
|
149
|
+
f" Remedy: consume a reset (run `codex`, then /usage) or wait for the window — "
|
|
150
|
+
f"retrying now fails the same way.\n"
|
|
151
|
+
f" Resume with: syncade --resume {run_id}"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _is_usage_limit_stop(run_result: RunResult) -> bool:
|
|
156
|
+
return getattr(run_result, "termination_reason", None) == "provider_usage_limit"
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class Logger:
|
|
160
|
+
"""Timestamped stdout/stderr logger with quiet-mode suppression."""
|
|
161
|
+
|
|
162
|
+
def __init__(self, level: LogLevel = "normal") -> None:
|
|
163
|
+
self.level = level
|
|
164
|
+
|
|
165
|
+
def _emit(self, line: str) -> None:
|
|
166
|
+
if self.level != "quiet":
|
|
167
|
+
print(f"{_timestamp()} {line}", flush=True)
|
|
168
|
+
|
|
169
|
+
def _emit_err(self, line: str) -> None:
|
|
170
|
+
print(f"{_timestamp()} {line}", file=sys.stderr, flush=True)
|
|
171
|
+
|
|
172
|
+
def _emit_warning(self, line: str) -> None:
|
|
173
|
+
if self.level != "quiet":
|
|
174
|
+
print(f"{_timestamp()} {line}", file=sys.stderr, flush=True)
|
|
175
|
+
|
|
176
|
+
def warning(self, message: str) -> None:
|
|
177
|
+
self._emit_warning(f"warning: {message}")
|
|
178
|
+
|
|
179
|
+
def safety(self, message: str) -> None:
|
|
180
|
+
"""A disclosure about the DISPOSITION OF COMMITTED WORK — never suppressed (PR-h-13).
|
|
181
|
+
|
|
182
|
+
`--quiet` is for progress, not for safety. The class this exists for is narrow and
|
|
183
|
+
answerable in one sentence: *where did the producer's commits go, and is my tree
|
|
184
|
+
consistent with that?* A branch that did or did not advance, a commit stranded on a
|
|
185
|
+
detached HEAD, a non-descendant ending SHA, a failed `update-ref` — and above all the
|
|
186
|
+
working tree left holding a fully STAGED REVERT of the producer's work, where the
|
|
187
|
+
operator's next `git commit` silently undoes the run.
|
|
188
|
+
|
|
189
|
+
That last one is not hypothetical: it happened twice in one session on 2026-08-10, with
|
|
190
|
+
the warning visible and an experienced operator driving. Under `--quiet` there would
|
|
191
|
+
have been nothing at all. The auth block and the default-branch disclosure already
|
|
192
|
+
bypass quiet for the same reason; this gives that rule a name instead of a hand-rolled
|
|
193
|
+
`print` at each site.
|
|
194
|
+
|
|
195
|
+
Deliberately NOT this class: dirty-tree and untracked-file notes, resume-drift notes,
|
|
196
|
+
auto-prune failures. They describe the run, not the fate of work already committed.
|
|
197
|
+
"""
|
|
198
|
+
self._emit_err(f"warning: {message}")
|
|
199
|
+
|
|
200
|
+
def event(self, message: str, *, error: bool = False) -> None:
|
|
201
|
+
if error:
|
|
202
|
+
self._emit_err(message)
|
|
203
|
+
else:
|
|
204
|
+
self._emit(message)
|
|
205
|
+
|
|
206
|
+
def summary(self, run_result: RunResult) -> None:
|
|
207
|
+
"""Print the final run summary, including parse-error artifact pointers."""
|
|
208
|
+
artifacts = run_result.artifacts
|
|
209
|
+
round_dir = artifacts.round_dir
|
|
210
|
+
output_error_reviewers = [
|
|
211
|
+
r
|
|
212
|
+
for r in run_result.dispatch_result.results
|
|
213
|
+
if isinstance(r.error, ReviewerOutputError)
|
|
214
|
+
]
|
|
215
|
+
synth_result = run_result.synth_result
|
|
216
|
+
synth_output_error = synth_result is not None and isinstance(
|
|
217
|
+
synth_result.error, SynthesizerOutputError
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
budget_note = _budget_stop_line(run_result) if _is_budget_stop(run_result) else None
|
|
221
|
+
if budget_note is None and _is_usage_limit_stop(run_result):
|
|
222
|
+
budget_note = _usage_limit_stop_line(run_result)
|
|
223
|
+
|
|
224
|
+
if self.level == "quiet":
|
|
225
|
+
print(
|
|
226
|
+
f"[syncade] run complete — exit {run_result.exit_code}, "
|
|
227
|
+
f"summary at {artifacts.summary_path}",
|
|
228
|
+
flush=True,
|
|
229
|
+
)
|
|
230
|
+
if budget_note is not None:
|
|
231
|
+
print(budget_note, flush=True)
|
|
232
|
+
for r in output_error_reviewers:
|
|
233
|
+
print(
|
|
234
|
+
f"[syncade] {r.reviewer_name} raw response: "
|
|
235
|
+
f"{round_dir / f'{r.reviewer_name}.stdout'}; parse exception: "
|
|
236
|
+
f"{round_dir / f'{r.reviewer_name}.error.txt'}",
|
|
237
|
+
flush=True,
|
|
238
|
+
)
|
|
239
|
+
if synth_output_error:
|
|
240
|
+
print(
|
|
241
|
+
f"[syncade] synthesizer raw response: "
|
|
242
|
+
f"{round_dir / 'synthesizer.stdout'}; parse exception: "
|
|
243
|
+
f"{round_dir / 'synthesizer.error.txt'}",
|
|
244
|
+
flush=True,
|
|
245
|
+
)
|
|
246
|
+
return
|
|
247
|
+
|
|
248
|
+
lines = [
|
|
249
|
+
f"{_timestamp()} run complete",
|
|
250
|
+
f" run id: {artifacts.run_dir.name}",
|
|
251
|
+
f" exit code: {run_result.exit_code}",
|
|
252
|
+
]
|
|
253
|
+
for r in run_result.dispatch_result.results:
|
|
254
|
+
if r.output is not None:
|
|
255
|
+
lines.append(f" - {r.reviewer_name} ({r.provider}): {r.output.verdict}")
|
|
256
|
+
else:
|
|
257
|
+
err_cls = type(r.error).__name__ if r.error else "Unknown"
|
|
258
|
+
lines.append(f" - {r.reviewer_name} ({r.provider}): FAILED ({err_cls})")
|
|
259
|
+
lines.append(f" artifacts: {artifacts.run_dir}")
|
|
260
|
+
lines.append(f" summary: {artifacts.summary_path}")
|
|
261
|
+
if artifacts.findings_md_path is not None:
|
|
262
|
+
lines.append(f" findings: {artifacts.findings_md_path}")
|
|
263
|
+
if budget_note is not None:
|
|
264
|
+
lines.append(budget_note)
|
|
265
|
+
|
|
266
|
+
for r in output_error_reviewers:
|
|
267
|
+
lines.append(
|
|
268
|
+
f" For {r.reviewer_name}: raw response preserved at "
|
|
269
|
+
f"{round_dir / f'{r.reviewer_name}.stdout'}; parse exception at "
|
|
270
|
+
f"{round_dir / f'{r.reviewer_name}.error.txt'}."
|
|
271
|
+
)
|
|
272
|
+
lines.append(
|
|
273
|
+
" The reviewer ran successfully but its output didn't parse — the verdict "
|
|
274
|
+
"and findings may still be readable in the raw .stdout file."
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
if synth_output_error:
|
|
278
|
+
lines.append(
|
|
279
|
+
f" For synthesizer: raw response preserved at "
|
|
280
|
+
f"{round_dir / 'synthesizer.stdout'}; parse exception at "
|
|
281
|
+
f"{round_dir / 'synthesizer.error.txt'}."
|
|
282
|
+
)
|
|
283
|
+
lines.append(
|
|
284
|
+
" The synthesizer ran successfully but its output didn't parse — common "
|
|
285
|
+
"shapes are invented findings, unanimous-blocker deactivation attempts, or "
|
|
286
|
+
"missing required fields."
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
print("\n".join(lines), flush=True)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Metrics sink for syncade runs (PR-v2-03).
|
|
2
|
+
|
|
3
|
+
A derived, rebuildable sqlite view over the ``.syncade/runs/`` artifact corpus.
|
|
4
|
+
Read-only over run artifacts; stdlib ``sqlite3`` only (no new dependency).
|
|
5
|
+
|
|
6
|
+
Public surface is re-exported here, but monkeypatches/tests should target the
|
|
7
|
+
concrete submodule that owns the lookup (CLAUDE.md Decomposition Rule).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from syncade.metrics.schema import (
|
|
13
|
+
ActorStatRow,
|
|
14
|
+
ReviewerStatRow,
|
|
15
|
+
RunRow,
|
|
16
|
+
fetch_runs,
|
|
17
|
+
open_db,
|
|
18
|
+
upsert_actor_stat,
|
|
19
|
+
upsert_reviewer_stat,
|
|
20
|
+
upsert_run,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"ActorStatRow",
|
|
25
|
+
"ReviewerStatRow",
|
|
26
|
+
"RunRow",
|
|
27
|
+
"fetch_runs",
|
|
28
|
+
"open_db",
|
|
29
|
+
"upsert_actor_stat",
|
|
30
|
+
"upsert_reviewer_stat",
|
|
31
|
+
"upsert_run",
|
|
32
|
+
]
|