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,306 @@
|
|
|
1
|
+
"""Result dataclasses + categorical Literals.
|
|
2
|
+
|
|
3
|
+
Holds the immutable per-round and per-run result dataclasses plus the two
|
|
4
|
+
``Literal`` types they reference.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Literal
|
|
12
|
+
|
|
13
|
+
from syncade.dispatcher import DispatchResult
|
|
14
|
+
from syncade.persistence import (
|
|
15
|
+
ProducerArtifactPaths,
|
|
16
|
+
SynthesizerArtifactPaths,
|
|
17
|
+
TestRunArtifactPaths,
|
|
18
|
+
)
|
|
19
|
+
from syncade.producer import ProducerResult
|
|
20
|
+
from syncade.snapshot import Snapshot
|
|
21
|
+
from syncade.synthesizer import SynthesizerResult
|
|
22
|
+
from syncade.test_runner import TestRunResult
|
|
23
|
+
from syncade.usage import Usage
|
|
24
|
+
from syncade.worktree import WorktreeError
|
|
25
|
+
|
|
26
|
+
TestSkipReason = Literal[
|
|
27
|
+
"test_command_unset",
|
|
28
|
+
"reviewer_failed",
|
|
29
|
+
"synth_failed",
|
|
30
|
+
"synth_blocker",
|
|
31
|
+
"test_worktree_error",
|
|
32
|
+
]
|
|
33
|
+
"""Why the test re-run leg was skipped on a given round.
|
|
34
|
+
See the original ``TestSkipReason`` docstring for the per-
|
|
35
|
+
value semantics; this carries the type unchanged."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
TerminationReason = Literal[
|
|
39
|
+
"ship",
|
|
40
|
+
"no_changes_to_review",
|
|
41
|
+
"producer_emptied_diff",
|
|
42
|
+
"findings_present",
|
|
43
|
+
"max_rounds_reached",
|
|
44
|
+
"budget_exceeded",
|
|
45
|
+
"provider_usage_limit",
|
|
46
|
+
"producer_stalled",
|
|
47
|
+
"producer_subprocess_error",
|
|
48
|
+
"reviewer_failure",
|
|
49
|
+
"synth_failure",
|
|
50
|
+
"test_subprocess_error",
|
|
51
|
+
"check_subprocess_error",
|
|
52
|
+
"decision_needed",
|
|
53
|
+
"blockers_all_deactivated",
|
|
54
|
+
"worktree_error",
|
|
55
|
+
"diff_malformed",
|
|
56
|
+
"diff_too_large",
|
|
57
|
+
"prompt_too_large",
|
|
58
|
+
"parse_failure",
|
|
59
|
+
"config_error",
|
|
60
|
+
]
|
|
61
|
+
"""Why the loop terminated.
|
|
62
|
+
|
|
63
|
+
- ``ship`` — a round produced exit 0 (synth-clean + tests passed,
|
|
64
|
+
if configured). The loop terminates with SUCCESS.
|
|
65
|
+
- ``no_changes_to_review`` — the diff resolved to empty before any
|
|
66
|
+
reviewer was dispatched in round 0: the base resolved (``base_oid``
|
|
67
|
+
set) but the reviewer-facing diff was empty (either truly no changes,
|
|
68
|
+
or every section was legitimate repo-context stripped by the filter —
|
|
69
|
+
D1 and D3 in PR-h-02d). Zero subprocesses are dispatched for the whole
|
|
70
|
+
run. Loop terminates with SUCCESS (exit 0). Distinct from ``ship`` so
|
|
71
|
+
callers can tell "reviewed and approved" from "nothing to approve".
|
|
72
|
+
- ``producer_emptied_diff`` — same empty-diff condition as
|
|
73
|
+
``no_changes_to_review``, but fired in a later round (round 1+) after
|
|
74
|
+
prior rounds already dispatched reviewers and a producer. The producer's
|
|
75
|
+
commits reduced all reviewable changes to nothing. Loop terminates with
|
|
76
|
+
SUCCESS (exit 0). Distinct from ``no_changes_to_review`` because model
|
|
77
|
+
work WAS spent in prior rounds.
|
|
78
|
+
- ``findings_present`` — a single-pass run produced NO-SHIP. The loop ends with
|
|
79
|
+
exit 30 without implying a retry budget was exhausted.
|
|
80
|
+
- ``max_rounds_reached`` — the last round was NO-SHIP and
|
|
81
|
+
``round_idx + 1 == max_rounds``. Loop ends with exit 20.
|
|
82
|
+
- ``budget_exceeded`` — the running token/dollar tally crossed a configured
|
|
83
|
+
``[loop]`` budget at a phase boundary (before starting the next round, or
|
|
84
|
+
before a NO-SHIP round's producer). Loop ends with exit 25 (PR-v2-11).
|
|
85
|
+
- ``producer_stalled`` — a NO-SHIP round's producer subprocess
|
|
86
|
+
exited cleanly but didn't move HEAD. The next round would see
|
|
87
|
+
identical input. Loop ends with exit 30 + this reason. Since
|
|
88
|
+
this also covers a producer escalation that was NOT honored
|
|
89
|
+
(its ``finding_indices`` left an active blocker uncovered, or
|
|
90
|
+
referenced a non-blocker / out-of-range index) — a rejected
|
|
91
|
+
escalation is treated as an ordinary stall, not ``decision_needed``.
|
|
92
|
+
- ``producer_subprocess_error`` — a producer subprocess failed
|
|
93
|
+
(timeout, binary missing, adapter ReviewerInvocationError,
|
|
94
|
+
ReviewerOutputError). Loop ends with exit 40.
|
|
95
|
+
- ``reviewer_failure`` — a reviewer subprocess failed; the
|
|
96
|
+
synthesizer was skipped per the no-silent-degradation rule;
|
|
97
|
+
the loop terminates with the appropriate reviewer-failure exit
|
|
98
|
+
code (40, 50, 70 depending on failure shape).
|
|
99
|
+
- ``synth_failure`` — every reviewer succeeded but the
|
|
100
|
+
synthesizer subprocess failed; loop terminates with the
|
|
101
|
+
appropriate synth-failure exit code (40 or 70).
|
|
102
|
+
- ``test_subprocess_error`` — synth was clean but the test leg's
|
|
103
|
+
subprocess failed (binary missing, timeout); loop terminates
|
|
104
|
+
with exit 40.
|
|
105
|
+
- ``check_subprocess_error`` — every reviewer + the synth
|
|
106
|
+
succeeded, but a BLOCKING mechanical check's subprocess couldn't
|
|
107
|
+
run (binary missing, timeout); loop terminates with exit 40.
|
|
108
|
+
- ``decision_needed`` — a NO-SHIP
|
|
109
|
+
round's producer ESCALATED a finding as an operator decision (a
|
|
110
|
+
spec/design conflict it cannot fix in code) rather than committing
|
|
111
|
+
or stalling silently. The loop honors the escalation — checkpoints
|
|
112
|
+
and terminates with exit 10 (CLARIFICATION_NEEDED), writing
|
|
113
|
+
``decision-needed.md`` — ONLY when the escalation's
|
|
114
|
+
``finding_indices`` cover EVERY active blocker in the round
|
|
115
|
+
(mechanical set-containment via
|
|
116
|
+
``escalation_coverage.escalation_covers_active_blockers``). An
|
|
117
|
+
escalation that leaves an active blocker uncovered, or references a
|
|
118
|
+
non-blocker / out-of-range index, is NOT honored: it maps to
|
|
119
|
+
``producer_stalled`` (exit 30) and ``decision-needed.md`` is not
|
|
120
|
+
written. Either way the mechanical verdict is unchanged (still
|
|
121
|
+
NO-SHIP); when honored, the operator records a decision and resumes.
|
|
122
|
+
- ``worktree_error`` — a worktree could not be provisioned for
|
|
123
|
+
the reviewer or test phase; loop terminates with exit 60.
|
|
124
|
+
- ``diff_malformed`` — the reviewer-facing diff contained sections
|
|
125
|
+
whose headers could not be identified (unparseable, malformed
|
|
126
|
+
escapes, or invalid UTF-8). The run is refused before any
|
|
127
|
+
dispatcher is invoked (D2, PR-h-02d); loop terminates with exit
|
|
128
|
+
60. Distinct from ``worktree_error`` so tooling can tell "reviewer
|
|
129
|
+
worktree failed" from "diff could not be safely filtered".
|
|
130
|
+
- ``diff_too_large`` — the reviewer-facing diff (after stripping and
|
|
131
|
+
binary elision) exceeded ``[loop] max_diff_bytes``. The run is
|
|
132
|
+
refused before any dispatcher is invoked; loop terminates with exit
|
|
133
|
+
60 (PR-h-field-01 item 3 / PR-h-02e D2). The refusal message names
|
|
134
|
+
the measured size, the cap, and three ways to reduce the diff.
|
|
135
|
+
- ``prompt_too_large`` — the assembled reviewer prompt (diff + spec +
|
|
136
|
+
template overhead) exceeded the hard PROVIDER ceiling. There is no
|
|
137
|
+
``[loop] max_prompt_bytes`` knob and this must not imply one: the diff
|
|
138
|
+
is what an operator controls (``[loop] max_diff_bytes``), while the
|
|
139
|
+
prompt ceiling belongs to the provider (``codex exec`` refuses over
|
|
140
|
+
1,048,576 characters) and is not ours to relax. The run is refused
|
|
141
|
+
before any dispatcher is invoked; loop terminates with exit 60.
|
|
142
|
+
- ``parse_failure`` — reviewer or synthesizer output couldn't be
|
|
143
|
+
parsed; loop terminates with exit 70.
|
|
144
|
+
- ``config_error`` — an adapter-lookup failure (e.g. unknown
|
|
145
|
+
provider) surfaced during dispatch; loop terminates with exit
|
|
146
|
+
50.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@dataclass(frozen=True)
|
|
151
|
+
class RoundArtifacts:
|
|
152
|
+
"""Per-round artifact paths.
|
|
153
|
+
|
|
154
|
+
One :class:`RoundArtifacts` per round in the loop. The
|
|
155
|
+
aggregate :class:`RunArtifacts` carries
|
|
156
|
+
``rounds: list[RoundArtifacts]``; back-compat flat fields
|
|
157
|
+
(``manifest_path``, ``summary_path``, etc.) on
|
|
158
|
+
:class:`RunArtifacts` point at the LAST round's entry.
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
round_idx: int
|
|
162
|
+
round_dir: Path
|
|
163
|
+
manifest_path: Path
|
|
164
|
+
summary_path: Path
|
|
165
|
+
findings_md_path: Path | None = None
|
|
166
|
+
synthesizer_paths: SynthesizerArtifactPaths | None = None
|
|
167
|
+
test_run_paths: TestRunArtifactPaths | None = None
|
|
168
|
+
producer_paths: ProducerArtifactPaths | None = None
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@dataclass(frozen=True)
|
|
172
|
+
class RoundResult:
|
|
173
|
+
"""Outcome of one round of the loop.
|
|
174
|
+
|
|
175
|
+
Preserves per-round state on the aggregate :class:`RunResult.rounds` list.
|
|
176
|
+
The orchestrator's flat fields on :class:`RunResult` (``dispatch_result``,
|
|
177
|
+
``synth_result``, ``test_result``, etc.) point at the LAST round's entry;
|
|
178
|
+
for ``max_rounds=1`` runs that's round 0.
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
round_idx: int
|
|
182
|
+
snapshot: Snapshot
|
|
183
|
+
dispatch_result: DispatchResult
|
|
184
|
+
synth_result: SynthesizerResult | None
|
|
185
|
+
test_result: TestRunResult | None
|
|
186
|
+
test_skip_reason: TestSkipReason | None
|
|
187
|
+
test_worktree_error: WorktreeError | None
|
|
188
|
+
producer_result: ProducerResult | None
|
|
189
|
+
round_exit_code: int
|
|
190
|
+
artifacts: RoundArtifacts
|
|
191
|
+
check_results: list[TestRunResult] = field(default_factory=list)
|
|
192
|
+
# Set by _no_changes_to_review_result so renderers can distinguish
|
|
193
|
+
# "reviewed and approved" (round_exit_code=0, this=False) from
|
|
194
|
+
# "nothing dispatched because diff was empty" (round_exit_code=0, this=True).
|
|
195
|
+
no_changes_to_review: bool = False
|
|
196
|
+
# UTF-8 byte count of the reviewer-facing (filtered) diff, carried so the
|
|
197
|
+
# producer-phase manifest rewrite can reproduce what round.py measured without
|
|
198
|
+
# a second computation. None when not measured (e.g. diff_malformed refusal).
|
|
199
|
+
filtered_diff_bytes: int | None = None
|
|
200
|
+
# UTF-8 byte count of the raw diff (before repo-context stripping), carried so
|
|
201
|
+
# the producer-phase manifest rewrite and the no-changes path can supply the
|
|
202
|
+
# measured value without re-deriving it from the snapshot (which carries a
|
|
203
|
+
# sentinel on resume, making derivation fabricate a byte count).
|
|
204
|
+
# None when not measured (diff_malformed refusal; resume paths).
|
|
205
|
+
raw_diff_bytes: int | None = None
|
|
206
|
+
# Set by _fail_closed_refusal_result (D2, PR-h-02d). Non-None means the run
|
|
207
|
+
# was refused because these diff headers could not be identified. Allows
|
|
208
|
+
# verdict.py to return "diff_malformed" instead of the generic "worktree_error".
|
|
209
|
+
fail_closed_headers: list[str] | None = None
|
|
210
|
+
# Set by _diff_too_large_result (PR-h-02e D2, landed in PR-h-field-01). Non-None means the
|
|
211
|
+
# run was refused because the REVIEWER-FACING diff exceeded [loop] max_diff_bytes;
|
|
212
|
+
# it carries the measured size. Lets verdict.py return "diff_too_large" rather than
|
|
213
|
+
# the generic "worktree_error", so the manifest names the cause.
|
|
214
|
+
oversize_diff_bytes: int | None = None
|
|
215
|
+
# Set by _assembled_prompt_too_large_result. Non-None means the run was refused
|
|
216
|
+
# because an assembled reviewer prompt exceeded the provider's character ceiling;
|
|
217
|
+
# it carries the oversized char count. Lets verdict.py return "prompt_too_large".
|
|
218
|
+
oversize_prompt_chars: int | None = None
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@dataclass(frozen=True)
|
|
222
|
+
class RunArtifacts:
|
|
223
|
+
"""Aggregate artifact paths for one syncade run.
|
|
224
|
+
|
|
225
|
+
Flat fields (``round_dir``, ``manifest_path``, ``summary_path``,
|
|
226
|
+
``findings_md_path``, ``synthesizer_paths``, ``test_run_paths``) point at
|
|
227
|
+
the LAST round's artifacts. For ``max_rounds=1`` (single-pass), these are
|
|
228
|
+
the paths under ``round-0``.
|
|
229
|
+
|
|
230
|
+
Additional fields:
|
|
231
|
+
|
|
232
|
+
- :attr:`rounds`: list of :class:`RoundArtifacts`, one per round
|
|
233
|
+
that executed. ``len(rounds)`` is the number of rounds the loop
|
|
234
|
+
ran. For per-round inspection, traverse this list rather than
|
|
235
|
+
the flat fields.
|
|
236
|
+
- :attr:`loop_summary_path`: top-level ``loop-summary.md``
|
|
237
|
+
written at ``<run-id>/`` rolling up every round + final
|
|
238
|
+
verdict. ``None`` when the loop never reached the summary-
|
|
239
|
+
writing step (very early failures).
|
|
240
|
+
- :attr:`producer_paths` (): list of
|
|
241
|
+
:class:`ProducerArtifactPaths` | ``None``, one per round.
|
|
242
|
+
``producer_paths[N]`` is ``None`` when round N didn't run a
|
|
243
|
+
producer (the SHIP round; the final round under
|
|
244
|
+
max_rounds_reached; producer-stalled before a producer fired
|
|
245
|
+
— etc.). Parallel to the brief's documented shape; mirrors
|
|
246
|
+
``rounds[].producer_paths`` for callers that want the flat
|
|
247
|
+
per-round access pattern without traversing the nested list.
|
|
248
|
+
- :attr:`run_root_findings_md_path` (): top-level
|
|
249
|
+
``<run_dir>/findings.md`` — copy of the latest round's
|
|
250
|
+
``findings.md``. ``None`` when no round produced findings.md
|
|
251
|
+
(synth failed every round).
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
run_dir: Path
|
|
255
|
+
round_dir: Path
|
|
256
|
+
manifest_path: Path
|
|
257
|
+
summary_path: Path
|
|
258
|
+
findings_md_path: Path | None = None
|
|
259
|
+
synthesizer_paths: SynthesizerArtifactPaths | None = None
|
|
260
|
+
test_run_paths: TestRunArtifactPaths | None = None
|
|
261
|
+
rounds: list[RoundArtifacts] = field(default_factory=list)
|
|
262
|
+
loop_summary_path: Path | None = None
|
|
263
|
+
producer_paths: list[ProducerArtifactPaths | None] = field(default_factory=list)
|
|
264
|
+
run_root_findings_md_path: Path | None = None
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
@dataclass(frozen=True)
|
|
268
|
+
class RunResult:
|
|
269
|
+
"""Outcome of one orchestrator invocation.
|
|
270
|
+
|
|
271
|
+
Flat fields (``snapshot``, ``dispatch_result``, ``synth_result``,
|
|
272
|
+
``test_result``, ``test_skip_reason``, ``test_worktree_error``) point at the
|
|
273
|
+
LAST round's entry. For ``max_rounds=1`` (single-pass), these are the round
|
|
274
|
+
0 values.
|
|
275
|
+
|
|
276
|
+
Additional fields:
|
|
277
|
+
|
|
278
|
+
- :attr:`rounds`: list of :class:`RoundResult`, one per round
|
|
279
|
+
executed. ``len(rounds) <= config.loop.max_rounds``.
|
|
280
|
+
- :attr:`final_round`: 0-indexed round that terminated the loop.
|
|
281
|
+
Equals ``len(rounds) - 1`` on every termination path.
|
|
282
|
+
- :attr:`termination_reason`: categorical label for tooling
|
|
283
|
+
(see :data:`TerminationReason` for values).
|
|
284
|
+
"""
|
|
285
|
+
|
|
286
|
+
artifacts: RunArtifacts
|
|
287
|
+
snapshot: Snapshot
|
|
288
|
+
dispatch_result: DispatchResult
|
|
289
|
+
exit_code: int
|
|
290
|
+
synth_result: SynthesizerResult | None = None
|
|
291
|
+
test_result: TestRunResult | None = None
|
|
292
|
+
test_skip_reason: TestSkipReason | None = None
|
|
293
|
+
test_worktree_error: WorktreeError | None = None
|
|
294
|
+
rounds: list[RoundResult] = field(default_factory=list)
|
|
295
|
+
final_round: int = 0
|
|
296
|
+
termination_reason: TerminationReason | None = None
|
|
297
|
+
# The budget ENFORCEMENT tally and the ceiling that crossed, carried so the terminal
|
|
298
|
+
# notice can state the numbers that actually tripped (PR-h-field-06). Deliberately not
|
|
299
|
+
# recomputed from `rounds`: on a --resume the enforcement tally is the FRESH resumed
|
|
300
|
+
# spend, not the rehydrated originals, so a recomputation would print a different number
|
|
301
|
+
# from loop-summary.md and --metrics — the "one number, three surfaces" rule.
|
|
302
|
+
budget_usages: list[Usage] = field(default_factory=list)
|
|
303
|
+
budget_ceiling: str | None = None
|
|
304
|
+
# The CONFIGURED ceilings, so the notice can name the number the operator would change.
|
|
305
|
+
budget_tokens: int | None = None
|
|
306
|
+
budget_usd: float | None = None
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Resume detection + eligibility helpers.
|
|
2
|
+
|
|
3
|
+
Pure-function layer: takes a ``runs_root`` (or a specific ``run_dir``)
|
|
4
|
+
and returns resume plans without invoking any syncade subprocess.
|
|
5
|
+
The CLI handler (``_run_resume`` in :mod:`syncade.cli`) wires these
|
|
6
|
+
into ``run_review`` after resolving the target run-id.
|
|
7
|
+
|
|
8
|
+
Eligibility rule:
|
|
9
|
+
|
|
10
|
+
- ``run-init.json`` present AND ``loop-manifest.json`` MISSING →
|
|
11
|
+
ELIGIBLE (operator Ctrl-C, crash, SIGTERM — the loop terminator
|
|
12
|
+
never ran).
|
|
13
|
+
- ``run-init.json`` present AND ``loop-manifest.json`` malformed/unreadable →
|
|
14
|
+
ELIGIBLE enough to surface during resume-target resolution; the planner
|
|
15
|
+
reports the specific corruption.
|
|
16
|
+
- ``run-init.json`` present AND ``loop-manifest.json`` present AND
|
|
17
|
+
``final_exit_code in {10, 25, 40, 60, 70}`` → ELIGIBLE (decision-needed
|
|
18
|
+
checkpoint, a budget stop (25), or environment failure mid-loop —
|
|
19
|
+
subprocess error, worktree provisioning, parse failure).
|
|
20
|
+
- ``loop-manifest.json`` present AND ``final_exit_code in (0, 20, 30)``
|
|
21
|
+
→ NOT ELIGIBLE (loop completed normally; operator's next step is
|
|
22
|
+
a fresh run).
|
|
23
|
+
- ``run-init.json`` MISSING → NOT ELIGIBLE (not a syncade run, or missing the
|
|
24
|
+
required resume artifact).
|
|
25
|
+
|
|
26
|
+
Within an eligible run, the "round to retry" is determined by
|
|
27
|
+
walking ``round-0/``, ``round-1/``, ... up to ``max_rounds`` and
|
|
28
|
+
stopping at the FIRST round that didn't complete cleanly.
|
|
29
|
+
|
|
30
|
+
this module was decomposed into ``resume_types`` (shared
|
|
31
|
+
constants + the ``ResumeError`` / ``ResumePlan`` / ``TreeDriftError``
|
|
32
|
+
types), ``resume_target`` (find/resolve eligible runs), ``resume_plan``
|
|
33
|
+
(``plan_resume`` + the phase-failure classifier + decision read + SHA
|
|
34
|
+
derivation), and ``resume_load`` (tree-drift checks + completed-round
|
|
35
|
+
rehydration). This module is now a thin re-export shim so every
|
|
36
|
+
``syncade.orchestrator.resume.<name>`` import path is unchanged.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
from syncade.persistence import read_operator_decision as read_operator_decision
|
|
42
|
+
|
|
43
|
+
from .resume_load import (
|
|
44
|
+
_current_head_branch,
|
|
45
|
+
_current_head_sha,
|
|
46
|
+
_safe_read,
|
|
47
|
+
check_tree_drift,
|
|
48
|
+
load_completed_round,
|
|
49
|
+
)
|
|
50
|
+
from .resume_plan import (
|
|
51
|
+
_expected_sha_for_resumed_round,
|
|
52
|
+
_round_manifest_indicates_phase_failure,
|
|
53
|
+
plan_resume,
|
|
54
|
+
read_resume_decision,
|
|
55
|
+
)
|
|
56
|
+
from .resume_target import (
|
|
57
|
+
_read_operator_branch,
|
|
58
|
+
find_resumable_runs,
|
|
59
|
+
resolve_resume_target,
|
|
60
|
+
)
|
|
61
|
+
from .resume_types import (
|
|
62
|
+
_GIT_TIMEOUT_SECONDS,
|
|
63
|
+
_RESUMABLE_EXIT_CODES,
|
|
64
|
+
LOOP_MANIFEST_FILENAME,
|
|
65
|
+
ROUND_MANIFEST_FILENAME,
|
|
66
|
+
ResumeError,
|
|
67
|
+
ResumePlan,
|
|
68
|
+
TreeDriftError,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
__all__ = [
|
|
72
|
+
# types + constants
|
|
73
|
+
"ResumeError",
|
|
74
|
+
"ResumePlan",
|
|
75
|
+
"TreeDriftError",
|
|
76
|
+
"LOOP_MANIFEST_FILENAME",
|
|
77
|
+
"ROUND_MANIFEST_FILENAME",
|
|
78
|
+
"_RESUMABLE_EXIT_CODES",
|
|
79
|
+
"_GIT_TIMEOUT_SECONDS",
|
|
80
|
+
# target resolution
|
|
81
|
+
"find_resumable_runs",
|
|
82
|
+
"resolve_resume_target",
|
|
83
|
+
"_read_operator_branch",
|
|
84
|
+
# planning
|
|
85
|
+
"plan_resume",
|
|
86
|
+
"read_operator_decision",
|
|
87
|
+
"read_resume_decision",
|
|
88
|
+
"_round_manifest_indicates_phase_failure",
|
|
89
|
+
"_expected_sha_for_resumed_round",
|
|
90
|
+
# drift + rehydration
|
|
91
|
+
"check_tree_drift",
|
|
92
|
+
"load_completed_round",
|
|
93
|
+
"_current_head_sha",
|
|
94
|
+
"_current_head_branch",
|
|
95
|
+
"_safe_read",
|
|
96
|
+
]
|