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.
Files changed (177) hide show
  1. syncade/__init__.py +3 -0
  2. syncade/__main__.py +6 -0
  3. syncade/adapters/__init__.py +0 -0
  4. syncade/adapters/anthropic.py +457 -0
  5. syncade/adapters/base.py +221 -0
  6. syncade/adapters/fake.py +73 -0
  7. syncade/adapters/fake_common.py +29 -0
  8. syncade/adapters/fake_producer_audit_draft.py +460 -0
  9. syncade/adapters/fake_reviewer_synth.py +310 -0
  10. syncade/adapters/openai.py +484 -0
  11. syncade/adapters/openai_parsing.py +119 -0
  12. syncade/adapters/producer.py +221 -0
  13. syncade/adapters/producer_anthropic.py +300 -0
  14. syncade/adapters/producer_openai.py +226 -0
  15. syncade/adapters/registry.py +81 -0
  16. syncade/auth_check.py +554 -0
  17. syncade/auth_preflight.py +342 -0
  18. syncade/base_resolution.py +214 -0
  19. syncade/billing.py +141 -0
  20. syncade/checks_config.py +113 -0
  21. syncade/cli/__init__.py +546 -0
  22. syncade/cli/auth_gate.py +59 -0
  23. syncade/cli/config_keys.py +135 -0
  24. syncade/cli/config_list.py +82 -0
  25. syncade/cli/config_menu_rows.py +166 -0
  26. syncade/cli/config_mode.py +609 -0
  27. syncade/cli/config_overrides.py +122 -0
  28. syncade/cli/config_tui.py +476 -0
  29. syncade/cli/doctor_mode.py +72 -0
  30. syncade/cli/gc_mode.py +109 -0
  31. syncade/cli/install_skill.py +514 -0
  32. syncade/cli/metrics_mode.py +363 -0
  33. syncade/cli/modes.py +573 -0
  34. syncade/cli/parser.py +450 -0
  35. syncade/cli/parser_types.py +137 -0
  36. syncade/cli/paths.py +38 -0
  37. syncade/cli/preflight_paths.py +90 -0
  38. syncade/cli/resolve.py +116 -0
  39. syncade/cli/resume_mode.py +324 -0
  40. syncade/cli/toml_writer.py +410 -0
  41. syncade/cli/validate.py +421 -0
  42. syncade/config.py +478 -0
  43. syncade/config_auth.py +310 -0
  44. syncade/config_cold.py +209 -0
  45. syncade/config_gc.py +55 -0
  46. syncade/config_loader.py +182 -0
  47. syncade/config_loop.py +282 -0
  48. syncade/config_producer.py +222 -0
  49. syncade/config_retry.py +49 -0
  50. syncade/config_types.py +59 -0
  51. syncade/diff_filter.py +437 -0
  52. syncade/dispatcher.py +571 -0
  53. syncade/doctor.py +425 -0
  54. syncade/doctor_env.py +218 -0
  55. syncade/doctor_preview.py +524 -0
  56. syncade/doctor_types.py +28 -0
  57. syncade/exit_codes.py +82 -0
  58. syncade/findings.py +242 -0
  59. syncade/findings_json.py +456 -0
  60. syncade/gc.py +211 -0
  61. syncade/gc_execute.py +372 -0
  62. syncade/gc_protection.py +129 -0
  63. syncade/gc_types.py +50 -0
  64. syncade/gc_worktrees.py +200 -0
  65. syncade/git_object_id.py +12 -0
  66. syncade/git_preconditions.py +389 -0
  67. syncade/logging.py +289 -0
  68. syncade/metrics/__init__.py +32 -0
  69. syncade/metrics/aggregate.py +550 -0
  70. syncade/metrics/schema.py +221 -0
  71. syncade/orchestrator/__init__.py +61 -0
  72. syncade/orchestrator/_runs_dir.py +24 -0
  73. syncade/orchestrator/branch_advance.py +165 -0
  74. syncade/orchestrator/branch_guard.py +98 -0
  75. syncade/orchestrator/budget.py +107 -0
  76. syncade/orchestrator/escalation_coverage.py +81 -0
  77. syncade/orchestrator/loop.py +611 -0
  78. syncade/orchestrator/loop_dispatch_check.py +112 -0
  79. syncade/orchestrator/loop_finalize.py +404 -0
  80. syncade/orchestrator/loop_preflight.py +131 -0
  81. syncade/orchestrator/loop_resume.py +91 -0
  82. syncade/orchestrator/loop_rmtree.py +70 -0
  83. syncade/orchestrator/loop_round_step.py +599 -0
  84. syncade/orchestrator/prior_round.py +336 -0
  85. syncade/orchestrator/producer_phase.py +169 -0
  86. syncade/orchestrator/results.py +306 -0
  87. syncade/orchestrator/resume.py +96 -0
  88. syncade/orchestrator/resume_load.py +483 -0
  89. syncade/orchestrator/resume_plan.py +554 -0
  90. syncade/orchestrator/resume_target.py +215 -0
  91. syncade/orchestrator/resume_types.py +182 -0
  92. syncade/orchestrator/reviewer_template_failure.py +99 -0
  93. syncade/orchestrator/round.py +573 -0
  94. syncade/orchestrator/round_checks.py +91 -0
  95. syncade/orchestrator/round_no_changes.py +369 -0
  96. syncade/orchestrator/round_predispatch.py +212 -0
  97. syncade/orchestrator/verdict.py +279 -0
  98. syncade/persistence/__init__.py +189 -0
  99. syncade/persistence/_atomic.py +33 -0
  100. syncade/persistence/_clusters.py +70 -0
  101. syncade/persistence/_findings_verdict.py +201 -0
  102. syncade/persistence/_markdown.py +286 -0
  103. syncade/persistence/_validation.py +37 -0
  104. syncade/persistence/checks.py +249 -0
  105. syncade/persistence/decision_needed.py +289 -0
  106. syncade/persistence/findings_md.py +389 -0
  107. syncade/persistence/handoff.py +389 -0
  108. syncade/persistence/handoff_classify.py +196 -0
  109. syncade/persistence/last_reviewed.py +67 -0
  110. syncade/persistence/loop_manifest.py +165 -0
  111. syncade/persistence/loop_summary.py +352 -0
  112. syncade/persistence/loop_summary_text.py +428 -0
  113. syncade/persistence/producer.py +250 -0
  114. syncade/persistence/reviewer.py +198 -0
  115. syncade/persistence/round_manifest.py +238 -0
  116. syncade/persistence/run_init.py +153 -0
  117. syncade/persistence/run_summary.py +585 -0
  118. syncade/persistence/run_summary_next_steps.py +443 -0
  119. syncade/persistence/synth.py +242 -0
  120. syncade/persistence/test_run.py +152 -0
  121. syncade/presets.py +36 -0
  122. syncade/pricing_config.py +72 -0
  123. syncade/process.py +600 -0
  124. syncade/producer.py +189 -0
  125. syncade/producer_attempt.py +463 -0
  126. syncade/producer_escalation.py +146 -0
  127. syncade/producer_git.py +199 -0
  128. syncade/producer_result.py +205 -0
  129. syncade/prompts.py +448 -0
  130. syncade/prompts_loader.py +238 -0
  131. syncade/retry.py +159 -0
  132. syncade/run_inputs.py +40 -0
  133. syncade/run_status.py +198 -0
  134. syncade/selfcheck.py +471 -0
  135. syncade/skills/claude/README.md +221 -0
  136. syncade/skills/claude/SKILL.md +625 -0
  137. syncade/skills/codex/README.md +116 -0
  138. syncade/skills/codex/SKILL.md +574 -0
  139. syncade/snapshot.py +598 -0
  140. syncade/spec_audit.py +437 -0
  141. syncade/spec_audit_schema.py +190 -0
  142. syncade/spec_draft.py +423 -0
  143. syncade/spec_source.py +135 -0
  144. syncade/synthesis.py +428 -0
  145. syncade/synthesis_clusters.py +203 -0
  146. syncade/synthesis_repair.py +230 -0
  147. syncade/synthesis_schema.py +65 -0
  148. syncade/synthesizer/__init__.py +38 -0
  149. syncade/synthesizer/constants.py +33 -0
  150. syncade/synthesizer/driver.py +531 -0
  151. syncade/synthesizer/rendering.py +63 -0
  152. syncade/synthesizer/result.py +73 -0
  153. syncade/synthesizer/validation.py +421 -0
  154. syncade/synthesizer/workspace.py +208 -0
  155. syncade/templates/presets/balanced.toml +13 -0
  156. syncade/templates/presets/cheap.toml +12 -0
  157. syncade/templates/presets/thorough.toml +9 -0
  158. syncade/templates/producer.md +231 -0
  159. syncade/templates/reviewer.md +279 -0
  160. syncade/templates/reviewer_adversarial.md +164 -0
  161. syncade/templates/reviewer_codex.md +165 -0
  162. syncade/templates/spec_audit.md +168 -0
  163. syncade/templates/spec_draft.md +62 -0
  164. syncade/templates/synthesizer.md +204 -0
  165. syncade/test_runner.py +476 -0
  166. syncade/test_runner_classify.py +98 -0
  167. syncade/transcript.py +150 -0
  168. syncade/usage.py +407 -0
  169. syncade/worktree.py +497 -0
  170. syncade/worktree_env.py +133 -0
  171. syncade/worktree_paths.py +139 -0
  172. syncade-0.6.2.dist-info/METADATA +314 -0
  173. syncade-0.6.2.dist-info/RECORD +177 -0
  174. syncade-0.6.2.dist-info/WHEEL +5 -0
  175. syncade-0.6.2.dist-info/entry_points.txt +2 -0
  176. syncade-0.6.2.dist-info/licenses/LICENSE +202 -0
  177. 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)