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,91 @@
|
|
|
1
|
+
"""Resume-state rehydration for the multi-round loop.
|
|
2
|
+
|
|
3
|
+
Split out of ``loop.py`` to keep that module under the LOC gate. The
|
|
4
|
+
rebound ``run_review`` calls :func:`_rehydrate_resume_state` exactly once,
|
|
5
|
+
before the round loop, when resuming an aborted/interrupted run.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from syncade.config import SyncadeConfig
|
|
13
|
+
from syncade.worktree import WorktreeError
|
|
14
|
+
|
|
15
|
+
from .results import RoundArtifacts, RoundResult
|
|
16
|
+
from .resume import ResumePlan, load_completed_round
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _rehydrate_resume_state(
|
|
20
|
+
resume_plan: ResumePlan,
|
|
21
|
+
run_dir: Path,
|
|
22
|
+
run_id: str,
|
|
23
|
+
config: SyncadeConfig,
|
|
24
|
+
) -> tuple[list[RoundResult], list[RoundArtifacts], int, bool, SyncadeConfig]:
|
|
25
|
+
"""Rehydrate a resumed run's completed rounds and derive its loop bounds.
|
|
26
|
+
|
|
27
|
+
Returns ``(round_results, round_artifacts_list, resumed_round_start,
|
|
28
|
+
branch_advanced_during_run, config)``.
|
|
29
|
+
|
|
30
|
+
When resuming, pre-populate ``round_results`` with each completed
|
|
31
|
+
round's RoundResult so downstream consumers (loop-summary rendering,
|
|
32
|
+
handoff rendering, cross-round context disk reads) see a
|
|
33
|
+
consistent picture. ``branch_advanced_during_run`` is set to True if
|
|
34
|
+
ANY rehydrated round had a committed producer — that's a load-bearing
|
|
35
|
+
signal for the post-loop "your branch has been advanced" warning.
|
|
36
|
+
"""
|
|
37
|
+
round_results: list[RoundResult] = []
|
|
38
|
+
round_artifacts_list: list[RoundArtifacts] = []
|
|
39
|
+
branch_advanced_during_run = False
|
|
40
|
+
for r_idx in resume_plan.completed_rounds:
|
|
41
|
+
rehydrated = load_completed_round(run_dir / f"round-{r_idx}")
|
|
42
|
+
if rehydrated is None:
|
|
43
|
+
# plan_resume admitted this round; its manifest
|
|
44
|
+
# disappearing between plan_resume and here is a
|
|
45
|
+
# caller-induced race. Surface as WorktreeError so
|
|
46
|
+
# the CLI exits 60 with a useful message.
|
|
47
|
+
raise WorktreeError(
|
|
48
|
+
f"resumed run {run_id}: completed round {r_idx} "
|
|
49
|
+
f"lost its manifest.json between plan_resume and "
|
|
50
|
+
f"orchestrator entry — race condition or operator "
|
|
51
|
+
f"interference. Cannot proceed."
|
|
52
|
+
)
|
|
53
|
+
round_results.append(rehydrated)
|
|
54
|
+
round_artifacts_list.append(rehydrated.artifacts)
|
|
55
|
+
if (
|
|
56
|
+
rehydrated.producer_result is not None
|
|
57
|
+
and rehydrated.producer_result.outcome == "committed"
|
|
58
|
+
):
|
|
59
|
+
branch_advanced_during_run = True
|
|
60
|
+
resumed_round_start = resume_plan.resumed_round
|
|
61
|
+
|
|
62
|
+
# --- Loop-bound consistency with the resume planner ---------
|
|
63
|
+
# INVARIANT: a resume must run >= 1 round (never zero rounds +
|
|
64
|
+
# a silent exit-0 SHIP). ``plan_resume`` derives ``resumed_round``
|
|
65
|
+
# against ``effective_max_rounds = max(original_max_rounds,
|
|
66
|
+
# --max-rounds override)``, so ``resumed_round`` is always < that
|
|
67
|
+
# effective cap. But the round loop AND the terminator's "final
|
|
68
|
+
# round" check (``round_idx == config.loop.max_rounds - 1`` in
|
|
69
|
+
# ``_run_round_step``) BOTH read ``config.loop.max_rounds`` — the
|
|
70
|
+
# RAW override. When the operator resumes with ``--max-rounds N`` <=
|
|
71
|
+
# ``resumed_round`` (e.g. a 3-round run aborted at round 2 resumed
|
|
72
|
+
# with ``--max-rounds 1``), ``range(resumed_round, N)`` is empty: no
|
|
73
|
+
# round runs, ``final_exit_code`` stays SUCCESS, ``termination_reason``
|
|
74
|
+
# stays None, and ``_finalize_run`` returns a false exit-0 SHIP
|
|
75
|
+
# pointing at a rehydrated NO-SHIP round. Bump ``max_rounds`` to the
|
|
76
|
+
# SAME effective cap the planner used so both the loop bound and the
|
|
77
|
+
# terminator cover ``resumed_round`` (model_copy leaves the caller's
|
|
78
|
+
# config object untouched).
|
|
79
|
+
effective_max_rounds = max(config.loop.max_rounds, resume_plan.max_rounds)
|
|
80
|
+
if effective_max_rounds != config.loop.max_rounds:
|
|
81
|
+
config = config.model_copy(
|
|
82
|
+
update={"loop": config.loop.model_copy(update={"max_rounds": effective_max_rounds})}
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
round_results,
|
|
87
|
+
round_artifacts_list,
|
|
88
|
+
resumed_round_start,
|
|
89
|
+
branch_advanced_during_run,
|
|
90
|
+
config,
|
|
91
|
+
)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Guarded removal of resumed-run leftovers.
|
|
2
|
+
|
|
3
|
+
Split out of :mod:`syncade.orchestrator.loop` to keep that module under the
|
|
4
|
+
file-length cap. ``loop`` re-imports :func:`_safe_resume_rmtree` so
|
|
5
|
+
``run_review``'s call site — and the tests that monkeypatch it on the ``loop``
|
|
6
|
+
module — are unchanged. Its GC imports stay function-local for the same
|
|
7
|
+
import-cycle reason documented on ``loop._autoprune_old_transcripts``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import shutil
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _safe_resume_rmtree(target: Path, base: Path, repo_root: Path, *, reap: bool = False) -> None:
|
|
17
|
+
"""Remove a resumed-run leftover dir, but only after proving it is a
|
|
18
|
+
real (non-symlink) directory strictly under ``base`` and not an
|
|
19
|
+
ancestor of ``repo_root``.
|
|
20
|
+
|
|
21
|
+
Mirrors the containment + ``(st_dev, st_ino)`` identity guards GC
|
|
22
|
+
applies in :mod:`syncade.gc_execute`, reusing
|
|
23
|
+
:func:`~syncade.gc_worktrees.tree_identity` and
|
|
24
|
+
:func:`~syncade.gc_worktrees.tree_contains_repo_root`, so a swapped
|
|
25
|
+
symlink or an out-of-base path can never redirect the delete. Any
|
|
26
|
+
failed guard — including a missing target — is a safe no-op.
|
|
27
|
+
|
|
28
|
+
When ``reap`` is true the proven-safe tree also has any in-cwd process
|
|
29
|
+
reaped (:func:`~syncade.gc_execute.reap_processes_in_tree`) before the
|
|
30
|
+
``rmtree``, so the dir is never removed out from under a live subprocess
|
|
31
|
+
— mirroring GC's worktree-tree removal. This is for EXTERNAL worktrees
|
|
32
|
+
only (throwaway trees under the worktree base, where an orphaned
|
|
33
|
+
subprocess is syncade's own). Persisted run-artifact dirs under
|
|
34
|
+
``.syncade/runs/`` are removed with ``reap=False``: GC never SIGKILLs
|
|
35
|
+
inside ``.syncade/runs/``, and an operator may be inspecting them, so
|
|
36
|
+
SIGKILL would over-reach (M2).
|
|
37
|
+
"""
|
|
38
|
+
from syncade.gc_worktrees import tree_contains_repo_root, tree_identity
|
|
39
|
+
|
|
40
|
+
identity = tree_identity(target)
|
|
41
|
+
if identity is None:
|
|
42
|
+
# Missing, a symlink, or unstatable → refuse.
|
|
43
|
+
return
|
|
44
|
+
try:
|
|
45
|
+
resolved = target.resolve()
|
|
46
|
+
base_resolved = base.resolve()
|
|
47
|
+
except OSError:
|
|
48
|
+
return
|
|
49
|
+
if resolved == base_resolved:
|
|
50
|
+
return
|
|
51
|
+
try:
|
|
52
|
+
resolved.relative_to(base_resolved)
|
|
53
|
+
except ValueError:
|
|
54
|
+
# Escapes the base (e.g. via a parent symlink) → refuse.
|
|
55
|
+
return
|
|
56
|
+
if tree_contains_repo_root(resolved, repo_root):
|
|
57
|
+
return
|
|
58
|
+
if tree_identity(resolved) != identity:
|
|
59
|
+
# Identity changed between stat and removal → refuse (TOCTOU guard).
|
|
60
|
+
return
|
|
61
|
+
# Tree is proven safe. For external worktrees (reap=True) kill any in-cwd
|
|
62
|
+
# process first, so we never delete out from under a live subprocess —
|
|
63
|
+
# GC-equivalent. Run-artifact dirs pass reap=False: a plain guarded
|
|
64
|
+
# rmtree, never SIGKILL (M2). Reap is reached
|
|
65
|
+
# ONLY past every guard above, so a refused/out-of-base tree is untouched.
|
|
66
|
+
if reap:
|
|
67
|
+
from syncade.gc_execute import reap_processes_in_tree
|
|
68
|
+
|
|
69
|
+
reap_processes_in_tree(resolved)
|
|
70
|
+
shutil.rmtree(resolved, ignore_errors=True)
|