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,443 @@
|
|
|
1
|
+
"""Per-exit-code "Next steps" guidance for summary.md.
|
|
2
|
+
|
|
3
|
+
Holds the static next-steps content blocks and the two resolvers
|
|
4
|
+
(``_resolve_next_steps`` for non-producer rounds, ``_resolve_next_steps_with_producer``
|
|
5
|
+
for rounds where a producer ran).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from syncade.producer import ProducerResult
|
|
11
|
+
from syncade.synthesizer import SynthesizerResult
|
|
12
|
+
from syncade.test_runner import TestRunResult
|
|
13
|
+
|
|
14
|
+
from .checks import check_aware_next_steps
|
|
15
|
+
from .producer import PRODUCER_NAME
|
|
16
|
+
|
|
17
|
+
# Per-exit-code "Next steps" guidance for summary.md. This rewired
|
|
18
|
+
# the SHIP/NO-SHIP guidance around the new findings.md artifact and
|
|
19
|
+
# added synth-specific pointers for the synthesizer-failure exit
|
|
20
|
+
# codes. Keyed on the exit codes run_review can actually emit
|
|
21
|
+
# (0/30/40/50/60/70); 10/20 aren't reachable from a single pass
|
|
22
|
+
# but get the generic fallback.
|
|
23
|
+
_NEXT_STEPS: dict[int, str] = {
|
|
24
|
+
0: (
|
|
25
|
+
"- Read `findings.md` (the consolidated review across both\n"
|
|
26
|
+
" reviewers, written by the synthesizer) for the headline\n"
|
|
27
|
+
" narrative. `summary.md` is the run-level dashboard. Ship it."
|
|
28
|
+
),
|
|
29
|
+
30: (
|
|
30
|
+
"- Read `findings.md` FIRST — it lists the active blockers\n"
|
|
31
|
+
" (synthesizer's consolidated view, with per-reviewer\n"
|
|
32
|
+
" provenance). Each finding names the original reviewer(s)\n"
|
|
33
|
+
" and their severity calls. Then `summary.md` for run-level\n"
|
|
34
|
+
" context. The per-reviewer `.parsed.json` files have the raw\n"
|
|
35
|
+
" structured outputs for deeper inspection."
|
|
36
|
+
),
|
|
37
|
+
# Exit 40 has TWO meaningful subcases:
|
|
38
|
+
# - reviewer-subprocess-failed → look at the per-reviewer files first
|
|
39
|
+
# - all-reviewers-succeeded + synth-subprocess-failed → look at
|
|
40
|
+
# synthesizer.* files first
|
|
41
|
+
# The static lookup below is the reviewer-failure variant; the
|
|
42
|
+
# synth-failure variant is _NEXT_STEPS_40_SYNTH, picked between in
|
|
43
|
+
# `_resolve_next_steps`.
|
|
44
|
+
40: (
|
|
45
|
+
"- A reviewer subprocess failed. Check the `.error.txt` and\n"
|
|
46
|
+
" `.stderr` files for each failed reviewer; the persisted\n"
|
|
47
|
+
" `.error.txt` carries the exception class + message +\n"
|
|
48
|
+
" traceback so you can route by failure shape (auth, network,\n"
|
|
49
|
+
" timeout, binary-not-found). If it's a timeout, re-run with\n"
|
|
50
|
+
" `--timeout <seconds>` or set `[loop] timeout_seconds` in\n"
|
|
51
|
+
" `.syncade/config.toml`. The synthesizer phase was skipped\n"
|
|
52
|
+
" on this path (no silent N-1 degradation), so no\n"
|
|
53
|
+
" `synthesizer.*` artifacts exist for this run."
|
|
54
|
+
),
|
|
55
|
+
50: (
|
|
56
|
+
"- A reviewer's `provider` isn't a known adapter. Fix the\n"
|
|
57
|
+
" `provider` field in `.syncade/config.toml` — the `.error.txt`\n"
|
|
58
|
+
" files name the offending value."
|
|
59
|
+
),
|
|
60
|
+
60: (
|
|
61
|
+
"- Worktree provisioning failed for a reviewer worktree (the\n"
|
|
62
|
+
" pre-dispatch path). Check the `.error.txt` files; a stale\n"
|
|
63
|
+
" `<worktree_base>/<run-id>/` directory from a prior failed run\n"
|
|
64
|
+
" is the usual cause. If the test re-run leg's worktree was\n"
|
|
65
|
+
" the failing one, see the test_worktree_error variant\n"
|
|
66
|
+
" rendered when ``test_skip_reason == 'test_worktree_error'``."
|
|
67
|
+
),
|
|
68
|
+
# Exit 70 has TWO meaningful subcases :
|
|
69
|
+
# - reviewer parse failure → look at per-reviewer .stdout / .error.txt
|
|
70
|
+
# - synth parse failure → look at synthesizer.{stdout,error.txt}
|
|
71
|
+
# The static lookup below is the reviewer-failure variant; the
|
|
72
|
+
# synth-failure variant is _NEXT_STEPS_70_SYNTH, picked between in
|
|
73
|
+
# `_resolve_next_steps` based on whether synth_result is the
|
|
74
|
+
# failing phase. This mirrors the exit-40 split from the fix
|
|
75
|
+
# #10 .
|
|
76
|
+
70: (
|
|
77
|
+
"- A reviewer's output didn't parse as a `ReviewerOutput`.\n"
|
|
78
|
+
" `<reviewer-name>.stdout` has the raw response; look for a\n"
|
|
79
|
+
" `result` field in the JSON envelope or inline JSON in the\n"
|
|
80
|
+
" narrative. The parse exception is in\n"
|
|
81
|
+
" `<reviewer-name>.error.txt`. The synthesizer phase was\n"
|
|
82
|
+
" skipped on this path (no silent N-1 degradation), so no\n"
|
|
83
|
+
" `synthesizer.*` artifacts exist for this run."
|
|
84
|
+
),
|
|
85
|
+
}
|
|
86
|
+
_NEXT_STEPS_FALLBACK = (
|
|
87
|
+
"- See `manifest.json` and the per-reviewer files in this directory\n for details."
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
_NEXT_STEPS_60_DIFF_MALFORMED = (
|
|
91
|
+
"- The diff contained section(s) whose headers could not be identified\n"
|
|
92
|
+
" (unparseable header, malformed C-quoted escape, or invalid UTF-8 in a\n"
|
|
93
|
+
" path). Syncade refused to continue because it could not determine whether\n"
|
|
94
|
+
" these sections are real changes or repo-context files to strip — treating\n"
|
|
95
|
+
" an unreadable change as 'nothing to review' would be a false SHIP.\n"
|
|
96
|
+
" The dropped headers are listed in `diff-refused.txt` in this round's\n"
|
|
97
|
+
" directory and in `manifest.json` under `diff_filter_refusal_headers`.\n"
|
|
98
|
+
" Common causes: binary paths with non-UTF-8 bytes; git C-quoting of\n"
|
|
99
|
+
" unusual characters. If the unreadable sections are repo-context files,\n"
|
|
100
|
+
" add their basenames to `strip_repo_context_files` in\n"
|
|
101
|
+
" `.syncade/config.toml`; if they are real changes you need reviewed,\n"
|
|
102
|
+
" the diff encoding itself may need investigation (e.g. a misconfigured\n"
|
|
103
|
+
" `core.quotepath` or a non-UTF-8 filename)."
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
_NEXT_STEPS_60_DIFF_TOO_LARGE = (
|
|
107
|
+
"- The reviewer-facing diff exceeded `[loop] max_diff_bytes` before any reviewer "
|
|
108
|
+
"was dispatched. The measured size and the ceiling are in `diff-refused.txt`. "
|
|
109
|
+
"Syncade refuses rather than truncating — a verdict on a partial diff is a verdict "
|
|
110
|
+
"on the wrong code. Narrow `--base` to a smaller range, split the PR, or raise "
|
|
111
|
+
"`[loop] max_diff_bytes` in `.syncade/config.toml`."
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
_NEXT_STEPS_60_PROMPT_TOO_LARGE = (
|
|
115
|
+
"- The assembled reviewer prompt exceeded the provider character ceiling before any "
|
|
116
|
+
"reviewer was dispatched. The oversized reviewer and char count are in `diff-refused.txt`. "
|
|
117
|
+
"The assembled prompt includes the diff, the reviewer template, and any prior-round "
|
|
118
|
+
"context. To reduce it: narrow `--base` to a smaller diff range, trim the reviewer "
|
|
119
|
+
"template in `.syncade/templates/`, or lower `[loop] max_diff_bytes` so the diff "
|
|
120
|
+
"contributes fewer characters."
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
_NEXT_STEPS_NO_CHANGES = (
|
|
124
|
+
"- The diff resolved to empty before any reviewer was dispatched: "
|
|
125
|
+
"the base ref resolved but no reviewable changes were found "
|
|
126
|
+
"(either no files changed, or every changed file was a repo-context "
|
|
127
|
+
"file stripped from the reviewer diff). No model work was spent. "
|
|
128
|
+
"If you expected changes to be reviewed, check that the correct "
|
|
129
|
+
"`--base` / `--scope` is specified and that the changed files are "
|
|
130
|
+
"not all listed in `strip_repo_context_files`."
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Variant for exit 40 when the failing subprocess was the synthesizer
|
|
134
|
+
# (every reviewer succeeded; the synth phase ran and failed). Point the operator
|
|
135
|
+
# at synthesizer.error.txt / .stderr first, not at reviewer files which are clean
|
|
136
|
+
# in this case.
|
|
137
|
+
_NEXT_STEPS_40_SYNTH = (
|
|
138
|
+
"- The synthesizer subprocess failed (every reviewer succeeded;\n"
|
|
139
|
+
" the consolidation pass crashed). Open `synthesizer.error.txt`\n"
|
|
140
|
+
" for the exception class + message + traceback, and\n"
|
|
141
|
+
" `synthesizer.stderr` for any captured codex stderr. Common\n"
|
|
142
|
+
" shapes: auth (codex token expired / wrong account; run\n"
|
|
143
|
+
" `codex login`), network (transient — retry), timeout (re-run\n"
|
|
144
|
+
" with `--timeout <seconds>` or set `[loop] timeout_seconds`\n"
|
|
145
|
+
" in `.syncade/config.toml`; the same timeout applies to the\n"
|
|
146
|
+
" synthesizer subprocess in v1). The per-reviewer `.error.txt`\n"
|
|
147
|
+
" files do not exist for this exit code — every reviewer\n"
|
|
148
|
+
" succeeded; their `.parsed.json` files have the structured\n"
|
|
149
|
+
" outputs the synth was asked to consolidate, in case the\n"
|
|
150
|
+
" synth failure is content-driven (very long combined\n"
|
|
151
|
+
" reviewer-output blob, etc.)."
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# variant for exit 30 when the failing leg was the test
|
|
155
|
+
# re-run (not the synthesizer's consolidated_findings). The
|
|
156
|
+
# synthesizer was clean; the operator's tests reported failures.
|
|
157
|
+
# Different fix path: read test-run.stdout for the test runner's
|
|
158
|
+
# own output, not findings.md (which renders the synth's clean
|
|
159
|
+
# verdict with zero blockers).
|
|
160
|
+
_NEXT_STEPS_30_TEST_FAILED = (
|
|
161
|
+
"- The independent test re-run leg reported failures (the\n"
|
|
162
|
+
" cold synthesizer was clean — no consolidated\n"
|
|
163
|
+
" blockers). Open `test-run.stdout` FIRST for the operator-\n"
|
|
164
|
+
" configured test command's output; `test-run.stderr` for\n"
|
|
165
|
+
" any captured stderr; `test-run.exit-code.txt` for the\n"
|
|
166
|
+
" one-line exit code. `findings.md` still renders the\n"
|
|
167
|
+
" synthesizer's consolidated review (zero active blockers)\n"
|
|
168
|
+
" plus a Test Suite section pointing back at this artifact.\n"
|
|
169
|
+
" `summary.md` is the run-level dashboard."
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# variant for exit 40 when the failing subprocess was the
|
|
173
|
+
# test-leg (not a reviewer or the synthesizer). The reviewer and
|
|
174
|
+
# synthesizer subprocesses succeeded; only the test command's
|
|
175
|
+
# subprocess failed (binary missing, timeout, OS launch error).
|
|
176
|
+
# findings.md renders Verdict: ABORT on this path (not SHIP / NO-SHIP).
|
|
177
|
+
_NEXT_STEPS_40_TEST_SUBPROCESS = (
|
|
178
|
+
"- The test re-run leg's subprocess failed (every reviewer\n"
|
|
179
|
+
" succeeded; the synthesizer succeeded; the operator-\n"
|
|
180
|
+
" configured test command couldn't run to completion). Open\n"
|
|
181
|
+
" `test-run.stderr` for any captured stderr from before the\n"
|
|
182
|
+
" kill; `manifest.json`'s `test_run.error_type` names the\n"
|
|
183
|
+
" failure shape (`SubprocessTimeoutError`,\n"
|
|
184
|
+
" `SubprocessNotFoundError`, etc.). Common shapes:\n"
|
|
185
|
+
" - timeout: re-run with `--timeout <seconds>` or set\n"
|
|
186
|
+
" `[loop] test_timeout_seconds` in `.syncade/config.toml`.\n"
|
|
187
|
+
" - binary not found: the operator's `test_command` references\n"
|
|
188
|
+
" a tool that isn't on PATH in the test worktree. Install\n"
|
|
189
|
+
" the tool, or adjust `test_command` to use the installed\n"
|
|
190
|
+
" one.\n"
|
|
191
|
+
" The synthesizer artifacts are present and valid for this\n"
|
|
192
|
+
" exit code; `findings.md` renders Verdict: ABORT (the test\n"
|
|
193
|
+
" signal is indeterminate until the environment problem is\n"
|
|
194
|
+
" fixed)."
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# exit-60 variant when the failing worktree provisioning
|
|
199
|
+
# was the test leg's (not a reviewer's). Reviewer + synth phases
|
|
200
|
+
# already produced valid artifacts; the only thing missing is the
|
|
201
|
+
# test re-run output. Different fix path than the reviewer-
|
|
202
|
+
# worktree-error case.
|
|
203
|
+
_NEXT_STEPS_60_TEST_WORKTREE = (
|
|
204
|
+
"- The test re-run leg's worktree could not be provisioned\n"
|
|
205
|
+
" (every reviewer succeeded; the synthesizer succeeded; only\n"
|
|
206
|
+
" the test-leg worktree-add failed). Reviewer + synthesizer\n"
|
|
207
|
+
" artifacts are valid and on disk — read them as usual:\n"
|
|
208
|
+
" `findings.md` (Verdict: ABORT, indeterminate until the\n"
|
|
209
|
+
" provisioning failure is fixed), the per-reviewer\n"
|
|
210
|
+
" `.parsed.json` files for the structured outputs, and\n"
|
|
211
|
+
" `manifest.json` for the run-level structured view\n"
|
|
212
|
+
" (`test_skip_reason: test_worktree_error`).\n"
|
|
213
|
+
"\n"
|
|
214
|
+
" No `test-run.*` files were written (the leg never ran).\n"
|
|
215
|
+
" The provisioning diagnostic was emitted to the CLI's\n"
|
|
216
|
+
" stderr at the moment of failure (the `[syncade]` worktree-\n"
|
|
217
|
+
" error line) and via the `test re-run skipped (test\n"
|
|
218
|
+
" worktree provisioning failed: ...)` log line in this run's\n"
|
|
219
|
+
" stdout. Re-running after addressing the worktree failure\n"
|
|
220
|
+
" (typical causes: stale `<worktree_base>/<run-id>/` from a\n"
|
|
221
|
+
" prior interrupted run; `.git/worktrees/tests/` metadata\n"
|
|
222
|
+
" drift) will exercise the test leg fresh."
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# Variant for exit 70 when the failing parser was the synthesizer.
|
|
227
|
+
# Reviewer files are clean on this path; don't send the operator
|
|
228
|
+
# there. The common-shapes list names ghost reviewer provenance and
|
|
229
|
+
# out-of-range original indices because those are cross-input
|
|
230
|
+
# provenance-validation failures.
|
|
231
|
+
_NEXT_STEPS_70_SYNTH = (
|
|
232
|
+
"- The synthesizer's output didn't parse as a `SynthesizerOutput`\n"
|
|
233
|
+
" (every reviewer succeeded; only the synth phase failed at\n"
|
|
234
|
+
" parse). `synthesizer.stdout` has the codex JSONL stream; the\n"
|
|
235
|
+
" final `agent_message` event's text is what the parser tried\n"
|
|
236
|
+
" to validate. The parse exception is in `synthesizer.error.txt`.\n"
|
|
237
|
+
" Common shapes: invented findings (empty `provenance`),\n"
|
|
238
|
+
" attempted dismissal of a unanimous blocker (schema-rejected),\n"
|
|
239
|
+
" missing required fields, ghost reviewer name (provenance\n"
|
|
240
|
+
" references a reviewer that doesn't exist in this run), and\n"
|
|
241
|
+
" out-of-range `original_index` (provenance points past the\n"
|
|
242
|
+
" source reviewer's findings list). The per-reviewer `.error.txt`\n"
|
|
243
|
+
" files do not exist for this exit code; their `.parsed.json`\n"
|
|
244
|
+
" files have the structured outputs the synth was asked to\n"
|
|
245
|
+
" consolidate."
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _resolve_next_steps(
|
|
250
|
+
exit_code: int,
|
|
251
|
+
synth_result: SynthesizerResult | None,
|
|
252
|
+
test_result: TestRunResult | None = None,
|
|
253
|
+
test_skip_reason: str | None = None,
|
|
254
|
+
check_results: list[TestRunResult] | None = None,
|
|
255
|
+
*,
|
|
256
|
+
no_changes_to_review: bool = False,
|
|
257
|
+
fail_closed_headers: list[str] | None = None,
|
|
258
|
+
oversize_diff_bytes: int | None = None,
|
|
259
|
+
oversize_prompt_chars: int | None = None,
|
|
260
|
+
) -> str:
|
|
261
|
+
"""Pick the right Next-steps content for the given exit code +
|
|
262
|
+
synthesizer outcome + test outcome + test-skip reason.
|
|
263
|
+
|
|
264
|
+
Exits 30, 40, 60, and 70 each have multiple meaningful
|
|
265
|
+
flavors that demand distinct first instructions. The split
|
|
266
|
+
rules:
|
|
267
|
+
|
|
268
|
+
- Exit 30: ``test_failed`` (clean synth, test reported
|
|
269
|
+
failures) → ``_NEXT_STEPS_30_TEST_FAILED`` (read
|
|
270
|
+
test-run.stdout); else (synth had blocker — the original
|
|
271
|
+
recorded reason) keep the synth-blocker variant from
|
|
272
|
+
``_NEXT_STEPS``.
|
|
273
|
+
- Exit 40: ``test_subprocess_errored`` → test-subprocess
|
|
274
|
+
variant; else ``synth_failed`` → synth variant; else
|
|
275
|
+
reviewer-failed variant from ``_NEXT_STEPS``.
|
|
276
|
+
- Exit 60 : ``test_skip_reason == "test_worktree_error"``
|
|
277
|
+
→ test-leg worktree variant naming the preserved
|
|
278
|
+
reviewer + synth artifacts; else the generic worktree-
|
|
279
|
+
provisioning variant for reviewer-side failures.
|
|
280
|
+
- Exit 70: unchanged — the test leg has no parse path, so
|
|
281
|
+
this code only ever points at reviewer or synth output.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
exit_code: The run's exit code.
|
|
285
|
+
synth_result: The :class:`SynthesizerResult`, or ``None``
|
|
286
|
+
if the synth phase was skipped.
|
|
287
|
+
test_result: The :class:`TestRunResult`, or ``None`` if
|
|
288
|
+
the test phase was skipped.
|
|
289
|
+
test_skip_reason: Why the test leg was skipped. Used to
|
|
290
|
+
distinguish the test-worktree-error variant of
|
|
291
|
+
exit 60 from the reviewer-worktree variant.
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
The Next-steps content string for this exit-code / phase
|
|
295
|
+
combination, or :data:`_NEXT_STEPS_FALLBACK` for any code
|
|
296
|
+
not covered above (10/20 today).
|
|
297
|
+
"""
|
|
298
|
+
# a blocking mechanical check that drove the round's
|
|
299
|
+
# exit code (failed → 30 on a synth-clean round; subprocess_error → 40)
|
|
300
|
+
# takes precedence over the generic per-exit-code guidance, which would
|
|
301
|
+
# otherwise point at the wrong artifact. Returns None for every
|
|
302
|
+
# non-check-driven round (including all zero-check rounds), keeping the
|
|
303
|
+
# rest of this routing byte-identical.
|
|
304
|
+
if fail_closed_headers is not None:
|
|
305
|
+
return _NEXT_STEPS_60_DIFF_MALFORMED
|
|
306
|
+
if oversize_diff_bytes is not None:
|
|
307
|
+
return _NEXT_STEPS_60_DIFF_TOO_LARGE
|
|
308
|
+
if oversize_prompt_chars is not None:
|
|
309
|
+
return _NEXT_STEPS_60_PROMPT_TOO_LARGE
|
|
310
|
+
if no_changes_to_review:
|
|
311
|
+
return _NEXT_STEPS_NO_CHANGES
|
|
312
|
+
|
|
313
|
+
check_override = check_aware_next_steps(exit_code, synth_result, test_result, check_results)
|
|
314
|
+
if check_override is not None:
|
|
315
|
+
return check_override
|
|
316
|
+
|
|
317
|
+
synth_failed = synth_result is not None and synth_result.error is not None
|
|
318
|
+
test_failed = test_result is not None and test_result.outcome == "failed"
|
|
319
|
+
test_subprocess_errored = test_result is not None and test_result.outcome == "subprocess_error"
|
|
320
|
+
|
|
321
|
+
if exit_code == 30 and test_failed:
|
|
322
|
+
return _NEXT_STEPS_30_TEST_FAILED
|
|
323
|
+
if exit_code == 40 and test_subprocess_errored:
|
|
324
|
+
return _NEXT_STEPS_40_TEST_SUBPROCESS
|
|
325
|
+
if exit_code == 40 and synth_failed:
|
|
326
|
+
return _NEXT_STEPS_40_SYNTH
|
|
327
|
+
# exit 60 splits between reviewer-worktree-error
|
|
328
|
+
# (generic provisioning variant) and test-leg-worktree-error
|
|
329
|
+
# (preserved reviewer + synth artifacts variant).
|
|
330
|
+
if exit_code == 60 and test_skip_reason == "test_worktree_error":
|
|
331
|
+
return _NEXT_STEPS_60_TEST_WORKTREE
|
|
332
|
+
if exit_code == 70 and synth_failed:
|
|
333
|
+
return _NEXT_STEPS_70_SYNTH
|
|
334
|
+
return _NEXT_STEPS.get(exit_code, _NEXT_STEPS_FALLBACK)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _resolve_next_steps_with_producer(
|
|
338
|
+
exit_code: int,
|
|
339
|
+
producer_result: ProducerResult,
|
|
340
|
+
escalation_honored: bool = False,
|
|
341
|
+
branch_already_advanced: bool = False,
|
|
342
|
+
) -> str:
|
|
343
|
+
"""Next-steps guidance for a per-round
|
|
344
|
+
summary.md AFTER a producer ran on this round.
|
|
345
|
+
|
|
346
|
+
On a round where the producer already attempted to fix the findings, the
|
|
347
|
+
producer's outcome is the actionable signal for this round.
|
|
348
|
+
|
|
349
|
+
Branches by producer outcome:
|
|
350
|
+
|
|
351
|
+
- ``committed`` — the producer made a commit; the next round
|
|
352
|
+
will dispatch reviewers against the new diff. Operator
|
|
353
|
+
reads ``producer.stdout`` to see what the producer
|
|
354
|
+
attempted.
|
|
355
|
+
- ``stalled`` — the producer subprocess completed cleanly
|
|
356
|
+
but didn't commit. The loop terminated with
|
|
357
|
+
``producer_stalled`` (exit 30 + that termination reason);
|
|
358
|
+
operator should clarify the spec or fix manually.
|
|
359
|
+
- ``subprocess_error`` — the producer subprocess failed.
|
|
360
|
+
Loop terminated with exit 40. Operator reads
|
|
361
|
+
``producer.error.txt`` for the exception trace.
|
|
362
|
+
- ``escalated`` — splits on ``escalation_honored``: when
|
|
363
|
+
True (the escalation covered every active blocker), the loop
|
|
364
|
+
checkpointed at exit 10 and the operator records a decision and
|
|
365
|
+
resumes; when False (the coverage guard rejected it for leaving
|
|
366
|
+
a blocker uncovered), the round is treated as a stall (exit 30,
|
|
367
|
+
no decision-needed.md) and the operator is pointed at the open
|
|
368
|
+
findings, not a checkpoint that doesn't exist.
|
|
369
|
+
"""
|
|
370
|
+
if producer_result.outcome == "committed":
|
|
371
|
+
return (
|
|
372
|
+
f"- The producer subprocess committed `{producer_result.ending_sha[:12]}` "
|
|
373
|
+
f"to attempt a fix for this round's findings. Read "
|
|
374
|
+
f"`{PRODUCER_NAME}.stdout` for the producer's narrative; the "
|
|
375
|
+
f"commit subject + body are in git history at the new SHA. The "
|
|
376
|
+
f"next round of reviewers will see the post-producer diff and "
|
|
377
|
+
f"decide SHIP or NO-SHIP from there. To inspect the consolidated "
|
|
378
|
+
f"review the producer was given, open `findings.md`."
|
|
379
|
+
)
|
|
380
|
+
if producer_result.outcome == "stalled":
|
|
381
|
+
return (
|
|
382
|
+
f"- The producer subprocess completed but did NOT commit "
|
|
383
|
+
f"(HEAD stayed at `{producer_result.starting_sha[:12]}`). The "
|
|
384
|
+
f"loop terminated with `producer_stalled`. Read "
|
|
385
|
+
f"`{PRODUCER_NAME}.stdout` for the producer's narrative; "
|
|
386
|
+
f"common causes are under-specified findings (the spec doesn't "
|
|
387
|
+
f"say what 'right' looks like) or the producer concluding the "
|
|
388
|
+
f"existing code is already correct. The operator's next step "
|
|
389
|
+
f"is to clarify the spec / address the finding manually and "
|
|
390
|
+
f"re-run."
|
|
391
|
+
)
|
|
392
|
+
if producer_result.outcome == "escalated" and escalation_honored:
|
|
393
|
+
_branch_note = (
|
|
394
|
+
"An earlier round already advanced your branch — producer "
|
|
395
|
+
"commits from that round are on it. This round's producer "
|
|
396
|
+
"did not commit."
|
|
397
|
+
if branch_already_advanced
|
|
398
|
+
else "No branch was advanced by this round."
|
|
399
|
+
)
|
|
400
|
+
return (
|
|
401
|
+
"- The producer ESCALATED a finding it determined is an operator "
|
|
402
|
+
"decision (a spec/design conflict), not a code defect. The loop "
|
|
403
|
+
f"checkpointed and terminated (exit 10). {_branch_note} "
|
|
404
|
+
"Read `decision-needed.md` at the run root for the "
|
|
405
|
+
"producer's case + concrete options, record your decision in "
|
|
406
|
+
"`decision.txt`, and run `syncade --resume <run-id>` — the "
|
|
407
|
+
"escalated round re-runs with your decision fed to the producer."
|
|
408
|
+
)
|
|
409
|
+
if producer_result.outcome == "escalated":
|
|
410
|
+
# the escalation did NOT cover every active blocker, so the
|
|
411
|
+
# coverage guard rejected it and the round is treated as a stall
|
|
412
|
+
# (NO-SHIP, exit 30; no decision checkpoint was created). Point the
|
|
413
|
+
# operator at the open findings, not at a `decision-needed.md` that
|
|
414
|
+
# was never written.
|
|
415
|
+
return (
|
|
416
|
+
f"- The producer attempted to ESCALATE a finding as an operator "
|
|
417
|
+
f"decision, but its escalation did not cover every active blocker "
|
|
418
|
+
f"this round, so it was NOT honored — the round is treated as a "
|
|
419
|
+
f"stall (NO-SHIP, exit 30; no branch advanced and no decision "
|
|
420
|
+
f"checkpoint created). Read `findings.md` for the active blockers "
|
|
421
|
+
f"and `{PRODUCER_NAME}.stdout` for the producer's narrative. The "
|
|
422
|
+
f"uncovered blocker(s) carry forward; re-run after the producer can "
|
|
423
|
+
f"fix or fully cover them, or address the finding manually."
|
|
424
|
+
)
|
|
425
|
+
if producer_result.ending_sha != producer_result.starting_sha:
|
|
426
|
+
return (
|
|
427
|
+
f"- The producer subprocess failed after moving HEAD to "
|
|
428
|
+
f"`{producer_result.ending_sha[:12]}`. This is an indeterminate "
|
|
429
|
+
f"producer commit, not a successful committed round: the operator's "
|
|
430
|
+
f"branch was NOT advanced. Read `{PRODUCER_NAME}.stdout`, "
|
|
431
|
+
f"`{PRODUCER_NAME}.stderr`, and `{PRODUCER_NAME}.error.txt`; use "
|
|
432
|
+
f"`git show {producer_result.ending_sha}` to inspect the recorded "
|
|
433
|
+
f"commit before deciding whether to keep or manually replay it."
|
|
434
|
+
)
|
|
435
|
+
return (
|
|
436
|
+
f"- The producer subprocess failed before it could attempt a fix. "
|
|
437
|
+
f"Read `{PRODUCER_NAME}.stderr` and `{PRODUCER_NAME}.error.txt` for "
|
|
438
|
+
f"the failure shape. Common causes are auth (run `claude login` / "
|
|
439
|
+
f"`codex login`), network errors (retry), or a missing CLI binary. "
|
|
440
|
+
f"The operator's branch was NOT advanced for this round; "
|
|
441
|
+
f"`findings.md` still reflects this round's NO-SHIP signal and is "
|
|
442
|
+
f"the operator's manual-fix target."
|
|
443
|
+
)
|