syncade 0.6.3__tar.gz → 0.7.0__tar.gz

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 (481) hide show
  1. {syncade-0.6.3/src/syncade.egg-info → syncade-0.7.0}/PKG-INFO +8 -8
  2. {syncade-0.6.3 → syncade-0.7.0}/README.md +7 -7
  3. {syncade-0.6.3 → syncade-0.7.0}/docs/config-reference.md +6 -1
  4. {syncade-0.6.3 → syncade-0.7.0}/docs/how-to-use.md +4 -3
  5. {syncade-0.6.3 → syncade-0.7.0}/pyproject.toml +2 -1
  6. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/__init__.py +1 -1
  7. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/cli/gc_mode.py +9 -1
  8. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/cli/metrics_mode.py +81 -2
  9. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/cli/parser.py +18 -15
  10. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/config_gc.py +18 -1
  11. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/gc.py +58 -1
  12. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/gc_execute.py +12 -1
  13. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/gc_types.py +13 -2
  14. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/metrics/aggregate.py +42 -21
  15. syncade-0.7.0/src/syncade/metrics/findings.py +358 -0
  16. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/metrics/schema.py +134 -2
  17. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/orchestrator/reviewer_template_failure.py +1 -0
  18. {syncade-0.6.3 → syncade-0.7.0/src/syncade.egg-info}/PKG-INFO +8 -8
  19. {syncade-0.6.3 → syncade-0.7.0}/src/syncade.egg-info/SOURCES.txt +7 -0
  20. {syncade-0.6.3 → syncade-0.7.0}/tests/cli/test_config_menu_rows.py +5 -1
  21. {syncade-0.6.3 → syncade-0.7.0}/tests/cli/test_config_overrides.py +1 -0
  22. {syncade-0.6.3 → syncade-0.7.0}/tests/cli/test_documented_flags_exist.py +34 -0
  23. {syncade-0.6.3 → syncade-0.7.0}/tests/cli/test_gc.py +3 -3
  24. {syncade-0.6.3 → syncade-0.7.0}/tests/cli/test_help_retention_contract.py +16 -4
  25. {syncade-0.6.3 → syncade-0.7.0}/tests/config/test_worktree_base_config.py +1 -0
  26. {syncade-0.6.3 → syncade-0.7.0}/tests/conftest.py +36 -0
  27. {syncade-0.6.3 → syncade-0.7.0}/tests/gc/test_execute.py +17 -17
  28. {syncade-0.6.3 → syncade-0.7.0}/tests/gc/test_plan.py +13 -13
  29. syncade-0.7.0/tests/gc/test_retention_tiers.py +121 -0
  30. {syncade-0.6.3 → syncade-0.7.0}/tests/gc/test_self_orphan_protection.py +10 -6
  31. syncade-0.7.0/tests/gc/test_worktree_age_bound.py +300 -0
  32. {syncade-0.6.3 → syncade-0.7.0}/tests/metrics/test_actor_spend.py +1 -0
  33. {syncade-0.6.3 → syncade-0.7.0}/tests/metrics/test_aggregate.py +19 -4
  34. syncade-0.7.0/tests/metrics/test_consensus_report.py +428 -0
  35. syncade-0.7.0/tests/metrics/test_findings_capture.py +437 -0
  36. {syncade-0.6.3 → syncade-0.7.0}/tests/metrics/test_schema.py +70 -0
  37. syncade-0.7.0/tests/metrics/test_unknown_panel.py +286 -0
  38. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/conftest.py +8 -35
  39. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/test_template_robustness.py +55 -0
  40. syncade-0.7.0/tests/test_worktree_base_isolation.py +58 -0
  41. {syncade-0.6.3 → syncade-0.7.0}/LICENSE +0 -0
  42. {syncade-0.6.3 → syncade-0.7.0}/MANIFEST.in +0 -0
  43. {syncade-0.6.3 → syncade-0.7.0}/setup.cfg +0 -0
  44. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/__main__.py +0 -0
  45. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/adapters/__init__.py +0 -0
  46. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/adapters/anthropic.py +0 -0
  47. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/adapters/base.py +0 -0
  48. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/adapters/fake.py +0 -0
  49. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/adapters/fake_common.py +0 -0
  50. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/adapters/fake_producer_audit_draft.py +0 -0
  51. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/adapters/fake_reviewer_synth.py +0 -0
  52. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/adapters/openai.py +0 -0
  53. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/adapters/openai_parsing.py +0 -0
  54. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/adapters/producer.py +0 -0
  55. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/adapters/producer_anthropic.py +0 -0
  56. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/adapters/producer_openai.py +0 -0
  57. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/adapters/registry.py +0 -0
  58. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/auth_check.py +0 -0
  59. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/auth_preflight.py +0 -0
  60. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/base_resolution.py +0 -0
  61. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/billing.py +0 -0
  62. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/checks_config.py +0 -0
  63. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/cli/__init__.py +0 -0
  64. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/cli/auth_gate.py +0 -0
  65. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/cli/config_keys.py +0 -0
  66. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/cli/config_list.py +0 -0
  67. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/cli/config_menu_rows.py +0 -0
  68. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/cli/config_mode.py +0 -0
  69. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/cli/config_overrides.py +0 -0
  70. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/cli/config_tui.py +0 -0
  71. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/cli/doctor_mode.py +0 -0
  72. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/cli/install_skill.py +0 -0
  73. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/cli/modes.py +0 -0
  74. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/cli/parser_types.py +0 -0
  75. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/cli/paths.py +0 -0
  76. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/cli/preflight_paths.py +0 -0
  77. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/cli/resolve.py +0 -0
  78. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/cli/resume_mode.py +0 -0
  79. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/cli/skill_status.py +0 -0
  80. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/cli/toml_writer.py +0 -0
  81. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/cli/update_mode.py +0 -0
  82. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/cli/update_notice.py +0 -0
  83. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/cli/validate.py +0 -0
  84. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/config.py +0 -0
  85. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/config_auth.py +0 -0
  86. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/config_cold.py +0 -0
  87. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/config_loader.py +0 -0
  88. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/config_loop.py +0 -0
  89. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/config_producer.py +0 -0
  90. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/config_retry.py +0 -0
  91. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/config_types.py +0 -0
  92. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/config_update.py +0 -0
  93. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/diff_filter.py +0 -0
  94. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/dispatcher.py +0 -0
  95. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/doctor.py +0 -0
  96. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/doctor_env.py +0 -0
  97. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/doctor_preview.py +0 -0
  98. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/doctor_types.py +0 -0
  99. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/exit_codes.py +0 -0
  100. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/findings.py +0 -0
  101. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/findings_json.py +0 -0
  102. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/gc_protection.py +0 -0
  103. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/gc_worktrees.py +0 -0
  104. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/git_object_id.py +0 -0
  105. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/git_preconditions.py +0 -0
  106. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/logging.py +0 -0
  107. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/metrics/__init__.py +0 -0
  108. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/orchestrator/__init__.py +0 -0
  109. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/orchestrator/_runs_dir.py +0 -0
  110. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/orchestrator/branch_advance.py +0 -0
  111. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/orchestrator/branch_guard.py +0 -0
  112. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/orchestrator/budget.py +0 -0
  113. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/orchestrator/escalation_coverage.py +0 -0
  114. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/orchestrator/loop.py +0 -0
  115. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/orchestrator/loop_dispatch_check.py +0 -0
  116. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/orchestrator/loop_finalize.py +0 -0
  117. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/orchestrator/loop_preflight.py +0 -0
  118. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/orchestrator/loop_resume.py +0 -0
  119. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/orchestrator/loop_rmtree.py +0 -0
  120. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/orchestrator/loop_round_step.py +0 -0
  121. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/orchestrator/prior_round.py +0 -0
  122. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/orchestrator/producer_phase.py +0 -0
  123. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/orchestrator/results.py +0 -0
  124. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/orchestrator/resume.py +0 -0
  125. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/orchestrator/resume_load.py +0 -0
  126. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/orchestrator/resume_plan.py +0 -0
  127. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/orchestrator/resume_target.py +0 -0
  128. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/orchestrator/resume_types.py +0 -0
  129. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/orchestrator/round.py +0 -0
  130. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/orchestrator/round_checks.py +0 -0
  131. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/orchestrator/round_no_changes.py +0 -0
  132. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/orchestrator/round_predispatch.py +0 -0
  133. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/orchestrator/verdict.py +0 -0
  134. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/persistence/__init__.py +0 -0
  135. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/persistence/_atomic.py +0 -0
  136. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/persistence/_clusters.py +0 -0
  137. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/persistence/_findings_verdict.py +0 -0
  138. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/persistence/_markdown.py +0 -0
  139. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/persistence/_validation.py +0 -0
  140. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/persistence/checks.py +0 -0
  141. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/persistence/decision_needed.py +0 -0
  142. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/persistence/findings_md.py +0 -0
  143. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/persistence/handoff.py +0 -0
  144. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/persistence/handoff_classify.py +0 -0
  145. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/persistence/last_reviewed.py +0 -0
  146. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/persistence/loop_manifest.py +0 -0
  147. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/persistence/loop_summary.py +0 -0
  148. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/persistence/loop_summary_text.py +0 -0
  149. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/persistence/producer.py +0 -0
  150. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/persistence/reviewer.py +0 -0
  151. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/persistence/round_manifest.py +0 -0
  152. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/persistence/run_init.py +0 -0
  153. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/persistence/run_summary.py +0 -0
  154. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/persistence/run_summary_next_steps.py +0 -0
  155. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/persistence/synth.py +0 -0
  156. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/persistence/test_run.py +0 -0
  157. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/presets.py +0 -0
  158. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/pricing_config.py +0 -0
  159. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/process.py +0 -0
  160. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/producer.py +0 -0
  161. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/producer_attempt.py +0 -0
  162. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/producer_escalation.py +0 -0
  163. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/producer_git.py +0 -0
  164. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/producer_result.py +0 -0
  165. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/prompts.py +0 -0
  166. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/prompts_loader.py +0 -0
  167. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/retry.py +0 -0
  168. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/run_inputs.py +0 -0
  169. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/run_status.py +0 -0
  170. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/selfcheck.py +0 -0
  171. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/skills/claude/README.md +0 -0
  172. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/skills/claude/SKILL.md +0 -0
  173. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/skills/codex/README.md +0 -0
  174. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/skills/codex/SKILL.md +0 -0
  175. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/snapshot.py +0 -0
  176. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/spec_audit.py +0 -0
  177. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/spec_audit_schema.py +0 -0
  178. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/spec_draft.py +0 -0
  179. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/spec_source.py +0 -0
  180. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/synthesis.py +0 -0
  181. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/synthesis_clusters.py +0 -0
  182. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/synthesis_repair.py +0 -0
  183. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/synthesis_schema.py +0 -0
  184. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/synthesizer/__init__.py +0 -0
  185. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/synthesizer/constants.py +0 -0
  186. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/synthesizer/driver.py +0 -0
  187. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/synthesizer/rendering.py +0 -0
  188. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/synthesizer/result.py +0 -0
  189. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/synthesizer/validation.py +0 -0
  190. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/synthesizer/workspace.py +0 -0
  191. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/templates/presets/balanced.toml +0 -0
  192. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/templates/presets/cheap.toml +0 -0
  193. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/templates/presets/thorough.toml +0 -0
  194. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/templates/producer.md +0 -0
  195. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/templates/reviewer.md +0 -0
  196. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/templates/reviewer_adversarial.md +0 -0
  197. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/templates/reviewer_codex.md +0 -0
  198. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/templates/spec_audit.md +0 -0
  199. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/templates/spec_draft.md +0 -0
  200. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/templates/synthesizer.md +0 -0
  201. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/test_runner.py +0 -0
  202. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/test_runner_classify.py +0 -0
  203. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/transcript.py +0 -0
  204. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/update_check.py +0 -0
  205. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/usage.py +0 -0
  206. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/worktree.py +0 -0
  207. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/worktree_env.py +0 -0
  208. {syncade-0.6.3 → syncade-0.7.0}/src/syncade/worktree_paths.py +0 -0
  209. {syncade-0.6.3 → syncade-0.7.0}/src/syncade.egg-info/dependency_links.txt +0 -0
  210. {syncade-0.6.3 → syncade-0.7.0}/src/syncade.egg-info/entry_points.txt +0 -0
  211. {syncade-0.6.3 → syncade-0.7.0}/src/syncade.egg-info/requires.txt +0 -0
  212. {syncade-0.6.3 → syncade-0.7.0}/src/syncade.egg-info/top_level.txt +0 -0
  213. {syncade-0.6.3 → syncade-0.7.0}/tests/__init__.py +0 -0
  214. {syncade-0.6.3 → syncade-0.7.0}/tests/adapters/__init__.py +0 -0
  215. {syncade-0.6.3 → syncade-0.7.0}/tests/adapters/_anthropic_helpers.py +0 -0
  216. {syncade-0.6.3 → syncade-0.7.0}/tests/adapters/_openai_helpers.py +0 -0
  217. {syncade-0.6.3 → syncade-0.7.0}/tests/adapters/test_anthropic.py +0 -0
  218. {syncade-0.6.3 → syncade-0.7.0}/tests/adapters/test_anthropic_more.py +0 -0
  219. {syncade-0.6.3 → syncade-0.7.0}/tests/adapters/test_openai.py +0 -0
  220. {syncade-0.6.3 → syncade-0.7.0}/tests/adapters/test_openai_more.py +0 -0
  221. {syncade-0.6.3 → syncade-0.7.0}/tests/adapters/test_producer.py +0 -0
  222. {syncade-0.6.3 → syncade-0.7.0}/tests/adapters/test_producer_more.py +0 -0
  223. {syncade-0.6.3 → syncade-0.7.0}/tests/adapters/test_prompt_transport.py +0 -0
  224. {syncade-0.6.3 → syncade-0.7.0}/tests/auth_check/__init__.py +0 -0
  225. {syncade-0.6.3 → syncade-0.7.0}/tests/auth_check/test_probes.py +0 -0
  226. {syncade-0.6.3 → syncade-0.7.0}/tests/auth_check/test_run_auth_check.py +0 -0
  227. {syncade-0.6.3 → syncade-0.7.0}/tests/cli/__init__.py +0 -0
  228. {syncade-0.6.3 → syncade-0.7.0}/tests/cli/_helpers.py +0 -0
  229. {syncade-0.6.3 → syncade-0.7.0}/tests/cli/_undo_helpers.py +0 -0
  230. {syncade-0.6.3 → syncade-0.7.0}/tests/cli/test_basic_and_mutex.py +0 -0
  231. {syncade-0.6.3 → syncade-0.7.0}/tests/cli/test_cli_library_divergence.py +0 -0
  232. {syncade-0.6.3 → syncade-0.7.0}/tests/cli/test_config_keys.py +0 -0
  233. {syncade-0.6.3 → syncade-0.7.0}/tests/cli/test_config_list.py +0 -0
  234. {syncade-0.6.3 → syncade-0.7.0}/tests/cli/test_config_mode.py +0 -0
  235. {syncade-0.6.3 → syncade-0.7.0}/tests/cli/test_config_tui.py +0 -0
  236. {syncade-0.6.3 → syncade-0.7.0}/tests/cli/test_config_tui_pty.py +0 -0
  237. {syncade-0.6.3 → syncade-0.7.0}/tests/cli/test_documented_exit_codes.py +0 -0
  238. {syncade-0.6.3 → syncade-0.7.0}/tests/cli/test_flags.py +0 -0
  239. {syncade-0.6.3 → syncade-0.7.0}/tests/cli/test_mode_exclusivity_matrix.py +0 -0
  240. {syncade-0.6.3 → syncade-0.7.0}/tests/cli/test_mode_flag_guards.py +0 -0
  241. {syncade-0.6.3 → syncade-0.7.0}/tests/cli/test_resume_reports_hard_kill.py +0 -0
  242. {syncade-0.6.3 → syncade-0.7.0}/tests/cli/test_run_and_config.py +0 -0
  243. {syncade-0.6.3 → syncade-0.7.0}/tests/cli/test_run_status_cli.py +0 -0
  244. {syncade-0.6.3 → syncade-0.7.0}/tests/cli/test_scope_openspec_draft.py +0 -0
  245. {syncade-0.6.3 → syncade-0.7.0}/tests/cli/test_toml_writer.py +0 -0
  246. {syncade-0.6.3 → syncade-0.7.0}/tests/cli/test_undo_auto_init.py +0 -0
  247. {syncade-0.6.3 → syncade-0.7.0}/tests/cli/test_undo_scope.py +0 -0
  248. {syncade-0.6.3 → syncade-0.7.0}/tests/cli/test_update_mode.py +0 -0
  249. {syncade-0.6.3 → syncade-0.7.0}/tests/cli/test_update_notice.py +0 -0
  250. {syncade-0.6.3 → syncade-0.7.0}/tests/cli/test_validate_before_mutate.py +0 -0
  251. {syncade-0.6.3 → syncade-0.7.0}/tests/config/__init__.py +0 -0
  252. {syncade-0.6.3 → syncade-0.7.0}/tests/config/test_auth_enforcement.py +0 -0
  253. {syncade-0.6.3 → syncade-0.7.0}/tests/config/test_auth_modes.py +0 -0
  254. {syncade-0.6.3 → syncade-0.7.0}/tests/config/test_auth_preflight.py +0 -0
  255. {syncade-0.6.3 → syncade-0.7.0}/tests/config/test_auth_report_scope.py +0 -0
  256. {syncade-0.6.3 → syncade-0.7.0}/tests/config/test_budget_config.py +0 -0
  257. {syncade-0.6.3 → syncade-0.7.0}/tests/config/test_cold_actors.py +0 -0
  258. {syncade-0.6.3 → syncade-0.7.0}/tests/config/test_config_checks.py +0 -0
  259. {syncade-0.6.3 → syncade-0.7.0}/tests/config/test_config_reference_drift.py +0 -0
  260. {syncade-0.6.3 → syncade-0.7.0}/tests/config/test_config_schema.py +0 -0
  261. {syncade-0.6.3 → syncade-0.7.0}/tests/config/test_gc_config.py +0 -0
  262. {syncade-0.6.3 → syncade-0.7.0}/tests/config/test_global_config_layer.py +0 -0
  263. {syncade-0.6.3 → syncade-0.7.0}/tests/config/test_presets.py +0 -0
  264. {syncade-0.6.3 → syncade-0.7.0}/tests/config/test_retry_config.py +0 -0
  265. {syncade-0.6.3 → syncade-0.7.0}/tests/config/test_reviewer_config.py +0 -0
  266. {syncade-0.6.3 → syncade-0.7.0}/tests/config/test_reviewer_permissions.py +0 -0
  267. {syncade-0.6.3 → syncade-0.7.0}/tests/dispatcher/__init__.py +0 -0
  268. {syncade-0.6.3 → syncade-0.7.0}/tests/dispatcher/_helpers.py +0 -0
  269. {syncade-0.6.3 → syncade-0.7.0}/tests/dispatcher/test_dispatch_core.py +0 -0
  270. {syncade-0.6.3 → syncade-0.7.0}/tests/dispatcher/test_dispatch_misc.py +0 -0
  271. {syncade-0.6.3 → syncade-0.7.0}/tests/dispatcher/test_dispatch_usage.py +0 -0
  272. {syncade-0.6.3 → syncade-0.7.0}/tests/doctor/__init__.py +0 -0
  273. {syncade-0.6.3 → syncade-0.7.0}/tests/doctor/_helpers.py +0 -0
  274. {syncade-0.6.3 → syncade-0.7.0}/tests/doctor/conftest.py +0 -0
  275. {syncade-0.6.3 → syncade-0.7.0}/tests/doctor/test_doctor.py +0 -0
  276. {syncade-0.6.3 → syncade-0.7.0}/tests/doctor/test_doctor_diff_size.py +0 -0
  277. {syncade-0.6.3 → syncade-0.7.0}/tests/doctor/test_doctor_malformed_diff.py +0 -0
  278. {syncade-0.6.3 → syncade-0.7.0}/tests/doctor/test_doctor_preview.py +0 -0
  279. {syncade-0.6.3 → syncade-0.7.0}/tests/findings/__init__.py +0 -0
  280. {syncade-0.6.3 → syncade-0.7.0}/tests/findings/_helpers.py +0 -0
  281. {syncade-0.6.3 → syncade-0.7.0}/tests/findings/test_parser_and_fields.py +0 -0
  282. {syncade-0.6.3 → syncade-0.7.0}/tests/findings/test_parser_resilience.py +0 -0
  283. {syncade-0.6.3 → syncade-0.7.0}/tests/findings/test_verdict_block_selection.py +0 -0
  284. {syncade-0.6.3 → syncade-0.7.0}/tests/gc/__init__.py +0 -0
  285. {syncade-0.6.3 → syncade-0.7.0}/tests/gc/_helpers.py +0 -0
  286. {syncade-0.6.3 → syncade-0.7.0}/tests/gc/test_autoprune.py +0 -0
  287. {syncade-0.6.3 → syncade-0.7.0}/tests/gc/test_plan_execution_races.py +0 -0
  288. {syncade-0.6.3 → syncade-0.7.0}/tests/gc/test_reaping_scope.py +0 -0
  289. {syncade-0.6.3 → syncade-0.7.0}/tests/gc/test_robustness.py +0 -0
  290. {syncade-0.6.3 → syncade-0.7.0}/tests/gc/test_smoke.py +0 -0
  291. {syncade-0.6.3 → syncade-0.7.0}/tests/gc/test_two_tier_retention.py +0 -0
  292. {syncade-0.6.3 → syncade-0.7.0}/tests/logging/__init__.py +0 -0
  293. {syncade-0.6.3 → syncade-0.7.0}/tests/logging/_helpers.py +0 -0
  294. {syncade-0.6.3 → syncade-0.7.0}/tests/logging/test_logging.py +0 -0
  295. {syncade-0.6.3 → syncade-0.7.0}/tests/logging/test_logging_summary.py +0 -0
  296. {syncade-0.6.3 → syncade-0.7.0}/tests/metrics/conftest.py +0 -0
  297. {syncade-0.6.3 → syncade-0.7.0}/tests/metrics/test_billing_reconciliation.py +0 -0
  298. {syncade-0.6.3 → syncade-0.7.0}/tests/metrics/test_billing_surfaces_agree.py +0 -0
  299. {syncade-0.6.3 → syncade-0.7.0}/tests/metrics/test_budget_metrics.py +0 -0
  300. {syncade-0.6.3 → syncade-0.7.0}/tests/metrics/test_cost_honesty.py +0 -0
  301. {syncade-0.6.3 → syncade-0.7.0}/tests/metrics/test_metrics_cli.py +0 -0
  302. {syncade-0.6.3 → syncade-0.7.0}/tests/metrics/test_metrics_mode.py +0 -0
  303. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/__init__.py +0 -0
  304. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/_helpers.py +0 -0
  305. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/_resume_fixtures.py +0 -0
  306. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/test_budget.py +0 -0
  307. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/test_budget_resume_e2e.py +0 -0
  308. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/test_budget_stop_message.py +0 -0
  309. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/test_budget_warning.py +0 -0
  310. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/test_checks_and_recording.py +0 -0
  311. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/test_contracts.py +0 -0
  312. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/test_cross_round.py +0 -0
  313. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/test_cross_round_more.py +0 -0
  314. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/test_default_branch_guard.py +0 -0
  315. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/test_diff_size_cap.py +0 -0
  316. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/test_dispatch_record.py +0 -0
  317. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/test_empty_diff.py +0 -0
  318. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/test_empty_diff_no_side_effects.py +0 -0
  319. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/test_escalation.py +0 -0
  320. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/test_last_reviewed_truthful.py +0 -0
  321. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/test_loop_audit_remediation.py +0 -0
  322. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/test_matrix_and_e2e.py +0 -0
  323. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/test_multiround.py +0 -0
  324. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/test_multiround_dirty.py +0 -0
  325. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/test_multiround_more.py +0 -0
  326. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/test_paraphrase_escalation.py +0 -0
  327. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/test_pipeline.py +0 -0
  328. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/test_provider_usage_limit.py +0 -0
  329. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/test_rename_boundary.py +0 -0
  330. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/test_resume.py +0 -0
  331. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/test_resume_base_oid.py +0 -0
  332. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/test_reviewer_blindness_is_structural.py +0 -0
  333. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/test_round_input_boundaries.py +0 -0
  334. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/test_run_layout.py +0 -0
  335. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/test_run_status_wiring.py +0 -0
  336. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/test_safety_warning_survives_quiet.py +0 -0
  337. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/test_snapshot_and_base.py +0 -0
  338. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/test_synth_phase.py +0 -0
  339. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/test_synth_phase_more.py +0 -0
  340. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/test_synth_provenance.py +0 -0
  341. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/test_synth_validation.py +0 -0
  342. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/test_termination_reason_drift.py +0 -0
  343. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/test_testleg.py +0 -0
  344. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/test_timeout_is_per_subprocess.py +0 -0
  345. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/test_usage_limit_message.py +0 -0
  346. {syncade-0.6.3 → syncade-0.7.0}/tests/orchestrator/test_worktree_cleanup.py +0 -0
  347. {syncade-0.6.3 → syncade-0.7.0}/tests/persistence/__init__.py +0 -0
  348. {syncade-0.6.3 → syncade-0.7.0}/tests/persistence/_helpers.py +0 -0
  349. {syncade-0.6.3 → syncade-0.7.0}/tests/persistence/test_atomic_writes.py +0 -0
  350. {syncade-0.6.3 → syncade-0.7.0}/tests/persistence/test_checks_surfacing.py +0 -0
  351. {syncade-0.6.3 → syncade-0.7.0}/tests/persistence/test_diff_size_recording.py +0 -0
  352. {syncade-0.6.3 → syncade-0.7.0}/tests/persistence/test_findings_md.py +0 -0
  353. {syncade-0.6.3 → syncade-0.7.0}/tests/persistence/test_findings_md_more.py +0 -0
  354. {syncade-0.6.3 → syncade-0.7.0}/tests/persistence/test_findings_md_verdict.py +0 -0
  355. {syncade-0.6.3 → syncade-0.7.0}/tests/persistence/test_handoff.py +0 -0
  356. {syncade-0.6.3 → syncade-0.7.0}/tests/persistence/test_loop_manifest_checks.py +0 -0
  357. {syncade-0.6.3 → syncade-0.7.0}/tests/persistence/test_loop_summary.py +0 -0
  358. {syncade-0.6.3 → syncade-0.7.0}/tests/persistence/test_loop_summary_more.py +0 -0
  359. {syncade-0.6.3 → syncade-0.7.0}/tests/persistence/test_md_safety.py +0 -0
  360. {syncade-0.6.3 → syncade-0.7.0}/tests/persistence/test_next_steps.py +0 -0
  361. {syncade-0.6.3 → syncade-0.7.0}/tests/persistence/test_producer_persist.py +0 -0
  362. {syncade-0.6.3 → syncade-0.7.0}/tests/persistence/test_reviewer.py +0 -0
  363. {syncade-0.6.3 → syncade-0.7.0}/tests/persistence/test_round_manifest.py +0 -0
  364. {syncade-0.6.3 → syncade-0.7.0}/tests/persistence/test_run_init.py +0 -0
  365. {syncade-0.6.3 → syncade-0.7.0}/tests/persistence/test_run_summary.py +0 -0
  366. {syncade-0.6.3 → syncade-0.7.0}/tests/persistence/test_run_summary_more.py +0 -0
  367. {syncade-0.6.3 → syncade-0.7.0}/tests/persistence/test_run_summary_producer.py +0 -0
  368. {syncade-0.6.3 → syncade-0.7.0}/tests/persistence/test_synth_persist.py +0 -0
  369. {syncade-0.6.3 → syncade-0.7.0}/tests/persistence/test_test_run.py +0 -0
  370. {syncade-0.6.3 → syncade-0.7.0}/tests/producer/__init__.py +0 -0
  371. {syncade-0.6.3 → syncade-0.7.0}/tests/producer/_helpers.py +0 -0
  372. {syncade-0.6.3 → syncade-0.7.0}/tests/producer/test_producer.py +0 -0
  373. {syncade-0.6.3 → syncade-0.7.0}/tests/producer/test_producer_confinement.py +0 -0
  374. {syncade-0.6.3 → syncade-0.7.0}/tests/producer/test_producer_retry.py +0 -0
  375. {syncade-0.6.3 → syncade-0.7.0}/tests/producer/test_producer_run.py +0 -0
  376. {syncade-0.6.3 → syncade-0.7.0}/tests/producer/test_producer_timeout.py +0 -0
  377. {syncade-0.6.3 → syncade-0.7.0}/tests/producer/test_producer_usage.py +0 -0
  378. {syncade-0.6.3 → syncade-0.7.0}/tests/prompts/__init__.py +0 -0
  379. {syncade-0.6.3 → syncade-0.7.0}/tests/prompts/test_load_template.py +0 -0
  380. {syncade-0.6.3 → syncade-0.7.0}/tests/prompts/test_misc.py +0 -0
  381. {syncade-0.6.3 → syncade-0.7.0}/tests/prompts/test_producer.py +0 -0
  382. {syncade-0.6.3 → syncade-0.7.0}/tests/prompts/test_render_synthesizer.py +0 -0
  383. {syncade-0.6.3 → syncade-0.7.0}/tests/prompts/test_synthesizer.py +0 -0
  384. {syncade-0.6.3 → syncade-0.7.0}/tests/resume/__init__.py +0 -0
  385. {syncade-0.6.3 → syncade-0.7.0}/tests/resume/_helpers.py +0 -0
  386. {syncade-0.6.3 → syncade-0.7.0}/tests/resume/test_budget_resume.py +0 -0
  387. {syncade-0.6.3 → syncade-0.7.0}/tests/resume/test_checks_phase_failure.py +0 -0
  388. {syncade-0.6.3 → syncade-0.7.0}/tests/resume/test_cli_malformed_rehydration.py +0 -0
  389. {syncade-0.6.3 → syncade-0.7.0}/tests/resume/test_find_resolve_plan.py +0 -0
  390. {syncade-0.6.3 → syncade-0.7.0}/tests/resume/test_plan_resume.py +0 -0
  391. {syncade-0.6.3 → syncade-0.7.0}/tests/resume/test_round_trip.py +0 -0
  392. {syncade-0.6.3 → syncade-0.7.0}/tests/resume/test_tree_drift_and_escalation.py +0 -0
  393. {syncade-0.6.3 → syncade-0.7.0}/tests/skills/test_config_section.py +0 -0
  394. {syncade-0.6.3 → syncade-0.7.0}/tests/skills/test_install_skill.py +0 -0
  395. {syncade-0.6.3 → syncade-0.7.0}/tests/skills/test_install_skill_multi_harness.py +0 -0
  396. {syncade-0.6.3 → syncade-0.7.0}/tests/skills/test_install_skill_ownership.py +0 -0
  397. {syncade-0.6.3 → syncade-0.7.0}/tests/skills/test_install_skill_refuses.py +0 -0
  398. {syncade-0.6.3 → syncade-0.7.0}/tests/skills/test_skill_drift.py +0 -0
  399. {syncade-0.6.3 → syncade-0.7.0}/tests/skills/test_skill_status.py +0 -0
  400. {syncade-0.6.3 → syncade-0.7.0}/tests/smoke/__init__.py +0 -0
  401. {syncade-0.6.3 → syncade-0.7.0}/tests/smoke/test_anthropic_smoke.py +0 -0
  402. {syncade-0.6.3 → syncade-0.7.0}/tests/smoke/test_auth_check_smoke.py +0 -0
  403. {syncade-0.6.3 → syncade-0.7.0}/tests/smoke/test_codex_smoke.py +0 -0
  404. {syncade-0.6.3 → syncade-0.7.0}/tests/smoke/test_loop_smoke.py +0 -0
  405. {syncade-0.6.3 → syncade-0.7.0}/tests/smoke/test_orchestrator_smoke.py +0 -0
  406. {syncade-0.6.3 → syncade-0.7.0}/tests/smoke/test_orchestrator_smoke_more.py +0 -0
  407. {syncade-0.6.3 → syncade-0.7.0}/tests/smoke/test_producer_smoke.py +0 -0
  408. {syncade-0.6.3 → syncade-0.7.0}/tests/smoke/test_resume_reap_smoke.py +0 -0
  409. {syncade-0.6.3 → syncade-0.7.0}/tests/smoke/test_selfcheck_smoke.py +0 -0
  410. {syncade-0.6.3 → syncade-0.7.0}/tests/smoke/test_synthesizer_smoke.py +0 -0
  411. {syncade-0.6.3 → syncade-0.7.0}/tests/smoke/test_test_runner_smoke.py +0 -0
  412. {syncade-0.6.3 → syncade-0.7.0}/tests/snapshot/__init__.py +0 -0
  413. {syncade-0.6.3 → syncade-0.7.0}/tests/snapshot/_helpers.py +0 -0
  414. {syncade-0.6.3 → syncade-0.7.0}/tests/snapshot/conftest.py +0 -0
  415. {syncade-0.6.3 → syncade-0.7.0}/tests/snapshot/test_snapshot.py +0 -0
  416. {syncade-0.6.3 → syncade-0.7.0}/tests/snapshot/test_snapshot_diff_identity.py +0 -0
  417. {syncade-0.6.3 → syncade-0.7.0}/tests/snapshot/test_snapshot_dirty.py +0 -0
  418. {syncade-0.6.3 → syncade-0.7.0}/tests/snapshot/test_snapshot_warning.py +0 -0
  419. {syncade-0.6.3 → syncade-0.7.0}/tests/snapshot/test_three_dot_diff.py +0 -0
  420. {syncade-0.6.3 → syncade-0.7.0}/tests/synthesis/__init__.py +0 -0
  421. {syncade-0.6.3 → syncade-0.7.0}/tests/synthesis/_helpers.py +0 -0
  422. {syncade-0.6.3 → syncade-0.7.0}/tests/synthesis/test_cluster_repair.py +0 -0
  423. {syncade-0.6.3 → syncade-0.7.0}/tests/synthesis/test_clusters.py +0 -0
  424. {syncade-0.6.3 → syncade-0.7.0}/tests/synthesis/test_models.py +0 -0
  425. {syncade-0.6.3 → syncade-0.7.0}/tests/synthesis/test_models_distinct_keying.py +0 -0
  426. {syncade-0.6.3 → syncade-0.7.0}/tests/synthesis/test_parsing.py +0 -0
  427. {syncade-0.6.3 → syncade-0.7.0}/tests/synthesis/test_unanimous_blocker_downgrade.py +0 -0
  428. {syncade-0.6.3 → syncade-0.7.0}/tests/synthesizer/test_provenance_fabrication.py +0 -0
  429. {syncade-0.6.3 → syncade-0.7.0}/tests/synthesizer/test_provenance_repair.py +0 -0
  430. {syncade-0.6.3 → syncade-0.7.0}/tests/test_adapter_registry.py +0 -0
  431. {syncade-0.6.3 → syncade-0.7.0}/tests/test_adapters_fake.py +0 -0
  432. {syncade-0.6.3 → syncade-0.7.0}/tests/test_auto_init_policy.py +0 -0
  433. {syncade-0.6.3 → syncade-0.7.0}/tests/test_base_resolution.py +0 -0
  434. {syncade-0.6.3 → syncade-0.7.0}/tests/test_check_loc_script.py +0 -0
  435. {syncade-0.6.3 → syncade-0.7.0}/tests/test_checks.py +0 -0
  436. {syncade-0.6.3 → syncade-0.7.0}/tests/test_config_loader.py +0 -0
  437. {syncade-0.6.3 → syncade-0.7.0}/tests/test_cross_model_claim.py +0 -0
  438. {syncade-0.6.3 → syncade-0.7.0}/tests/test_diff_filter.py +0 -0
  439. {syncade-0.6.3 → syncade-0.7.0}/tests/test_diff_filter_binary.py +0 -0
  440. {syncade-0.6.3 → syncade-0.7.0}/tests/test_diff_filter_quoted_paths.py +0 -0
  441. {syncade-0.6.3 → syncade-0.7.0}/tests/test_escalation_coverage.py +0 -0
  442. {syncade-0.6.3 → syncade-0.7.0}/tests/test_exit_codes.py +0 -0
  443. {syncade-0.6.3 → syncade-0.7.0}/tests/test_git_choke_point.py +0 -0
  444. {syncade-0.6.3 → syncade-0.7.0}/tests/test_git_config_isolation.py +0 -0
  445. {syncade-0.6.3 → syncade-0.7.0}/tests/test_git_preconditions.py +0 -0
  446. {syncade-0.6.3 → syncade-0.7.0}/tests/test_last_reviewed.py +0 -0
  447. {syncade-0.6.3 → syncade-0.7.0}/tests/test_parse_repair_extra_keys.py +0 -0
  448. {syncade-0.6.3 → syncade-0.7.0}/tests/test_pricing.py +0 -0
  449. {syncade-0.6.3 → syncade-0.7.0}/tests/test_private_identifier_gate.py +0 -0
  450. {syncade-0.6.3 → syncade-0.7.0}/tests/test_process.py +0 -0
  451. {syncade-0.6.3 → syncade-0.7.0}/tests/test_process_teardown.py +0 -0
  452. {syncade-0.6.3 → syncade-0.7.0}/tests/test_producer_escalation.py +0 -0
  453. {syncade-0.6.3 → syncade-0.7.0}/tests/test_regression_acme.py +0 -0
  454. {syncade-0.6.3 → syncade-0.7.0}/tests/test_retry.py +0 -0
  455. {syncade-0.6.3 → syncade-0.7.0}/tests/test_run_status.py +0 -0
  456. {syncade-0.6.3 → syncade-0.7.0}/tests/test_run_status_signals.py +0 -0
  457. {syncade-0.6.3 → syncade-0.7.0}/tests/test_runner/__init__.py +0 -0
  458. {syncade-0.6.3 → syncade-0.7.0}/tests/test_runner/test_test_runner.py +0 -0
  459. {syncade-0.6.3 → syncade-0.7.0}/tests/test_runner/test_test_runner_more.py +0 -0
  460. {syncade-0.6.3 → syncade-0.7.0}/tests/test_safety_disclosures_survive_quiet.py +0 -0
  461. {syncade-0.6.3 → syncade-0.7.0}/tests/test_selfcheck.py +0 -0
  462. {syncade-0.6.3 → syncade-0.7.0}/tests/test_spec_audit.py +0 -0
  463. {syncade-0.6.3 → syncade-0.7.0}/tests/test_spec_draft.py +0 -0
  464. {syncade-0.6.3 → syncade-0.7.0}/tests/test_spec_source.py +0 -0
  465. {syncade-0.6.3 → syncade-0.7.0}/tests/test_streamed_capture.py +0 -0
  466. {syncade-0.6.3 → syncade-0.7.0}/tests/test_strip_list_semantics.py +0 -0
  467. {syncade-0.6.3 → syncade-0.7.0}/tests/test_synthesizer.py +0 -0
  468. {syncade-0.6.3 → syncade-0.7.0}/tests/test_synthesizer_passthrough.py +0 -0
  469. {syncade-0.6.3 → syncade-0.7.0}/tests/test_synthesizer_provenance.py +0 -0
  470. {syncade-0.6.3 → syncade-0.7.0}/tests/test_transcript.py +0 -0
  471. {syncade-0.6.3 → syncade-0.7.0}/tests/test_update_check.py +0 -0
  472. {syncade-0.6.3 → syncade-0.7.0}/tests/test_usage.py +0 -0
  473. {syncade-0.6.3 → syncade-0.7.0}/tests/test_usage_persistence.py +0 -0
  474. {syncade-0.6.3 → syncade-0.7.0}/tests/test_workflow_action_pins.py +0 -0
  475. {syncade-0.6.3 → syncade-0.7.0}/tests/test_worktree_env.py +0 -0
  476. {syncade-0.6.3 → syncade-0.7.0}/tests/test_worktree_strip_guard.py +0 -0
  477. {syncade-0.6.3 → syncade-0.7.0}/tests/worktree/__init__.py +0 -0
  478. {syncade-0.6.3 → syncade-0.7.0}/tests/worktree/_helpers.py +0 -0
  479. {syncade-0.6.3 → syncade-0.7.0}/tests/worktree/conftest.py +0 -0
  480. {syncade-0.6.3 → syncade-0.7.0}/tests/worktree/test_worktree.py +0 -0
  481. {syncade-0.6.3 → syncade-0.7.0}/tests/worktree/test_worktree_cleanup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: syncade
3
- Version: 0.6.3
3
+ Version: 0.7.0
4
4
  Summary: External blind multi-judge review orchestrator for AI-assisted coding
5
5
  Author-email: Syncade <build@syncade.ai>
6
6
  License-Expression: Apache-2.0
@@ -287,7 +287,7 @@ syncade runs AI coding-agent CLIs with **elevated tool access on your repo**:
287
287
  - **Reviewers** run headless inside throwaway worktree copies under `/tmp/syncade/` (Codex reviewers are additionally sandboxed to theirs).
288
288
  - **The producer** runs unsandboxed and **commits to your current branch** (syncade refuses the default branch by default).
289
289
  - Full LLM transcripts — including source a reviewer read — are written to `.syncade/runs/` (gitignored, auto-pruned). **A secret in a file a reviewer opens lands there in plaintext.**
290
- - syncade makes **no network calls of its own**; its only egress is the provider CLIs you already authenticated and the test/check commands you configure.
290
+ - syncade makes **one network call of its own**: a once-per-session GET to its public update manifest to check for a newer version (suppressible via `[update] check = false`). `syncade --update` additionally invokes your package manager (`uv tool upgrade`, `pipx upgrade`, or `pip install -U`) — operator-requested and only when you run that flag. All other egress is the provider CLIs you already authenticated and the test/check commands you configure.
291
291
 
292
292
  Running syncade is comparable to running the target repo's `Makefile` — it executes code
293
293
  with your permissions. See **[SECURITY.md](SECURITY.md)** for the full threat model and how
@@ -302,12 +302,12 @@ Early access. These are measured, not suspected.
302
302
  Both findings were real. If a SHIP arrives over code you did not change since a NO-SHIP, treat
303
303
  it as the weaker signal — compare the verdict to what the *code* changed, not to the previous
304
304
  verdict.
305
- - **NO-SHIP runs keep their worktrees.** They are preserved so you can inspect what a reviewer
306
- saw, and `syncade --gc` deliberately will not reclaim a run that is still resumable — so
307
- `<worktree_base>` grows, and each worktree is a full checkout. It reached 4.4 GB on this
308
- machine before a cleanup. Point `worktree_base` somewhere you do not mind, delete old run
309
- directories once you are done with them, and run `git worktree prune` afterwards: removing the
310
- directory does not remove git's registration of it.
305
+ - **Worktrees accumulate under `<worktree_base>`.** NO-SHIP runs keep their worktrees for
306
+ inspection. `syncade --gc` removes worktrees once a run can no longer plausibly be resumed —
307
+ controlled by `gc.worktree_max_age_days` (default 14 days), which applies even to runs that are
308
+ technically still resume-eligible. It reached 4.4 GB on this machine before a cleanup. Point
309
+ `worktree_base` somewhere you do not mind, and run `git worktree prune` after manual removals:
310
+ removing the directory does not remove git's registration of it.
311
311
  - **A hard-killed run keeps almost all of what its reviewers had written.** Reviewer stdout and
312
312
  stderr are copied to `<round>/<name>.stdout` / `<round>/<name>.stderr` as the child produces
313
313
  them, rather than held in memory until it exits, so a run ended by `SIGKILL` (or a machine
@@ -257,7 +257,7 @@ syncade runs AI coding-agent CLIs with **elevated tool access on your repo**:
257
257
  - **Reviewers** run headless inside throwaway worktree copies under `/tmp/syncade/` (Codex reviewers are additionally sandboxed to theirs).
258
258
  - **The producer** runs unsandboxed and **commits to your current branch** (syncade refuses the default branch by default).
259
259
  - Full LLM transcripts — including source a reviewer read — are written to `.syncade/runs/` (gitignored, auto-pruned). **A secret in a file a reviewer opens lands there in plaintext.**
260
- - syncade makes **no network calls of its own**; its only egress is the provider CLIs you already authenticated and the test/check commands you configure.
260
+ - syncade makes **one network call of its own**: a once-per-session GET to its public update manifest to check for a newer version (suppressible via `[update] check = false`). `syncade --update` additionally invokes your package manager (`uv tool upgrade`, `pipx upgrade`, or `pip install -U`) — operator-requested and only when you run that flag. All other egress is the provider CLIs you already authenticated and the test/check commands you configure.
261
261
 
262
262
  Running syncade is comparable to running the target repo's `Makefile` — it executes code
263
263
  with your permissions. See **[SECURITY.md](SECURITY.md)** for the full threat model and how
@@ -272,12 +272,12 @@ Early access. These are measured, not suspected.
272
272
  Both findings were real. If a SHIP arrives over code you did not change since a NO-SHIP, treat
273
273
  it as the weaker signal — compare the verdict to what the *code* changed, not to the previous
274
274
  verdict.
275
- - **NO-SHIP runs keep their worktrees.** They are preserved so you can inspect what a reviewer
276
- saw, and `syncade --gc` deliberately will not reclaim a run that is still resumable — so
277
- `<worktree_base>` grows, and each worktree is a full checkout. It reached 4.4 GB on this
278
- machine before a cleanup. Point `worktree_base` somewhere you do not mind, delete old run
279
- directories once you are done with them, and run `git worktree prune` afterwards: removing the
280
- directory does not remove git's registration of it.
275
+ - **Worktrees accumulate under `<worktree_base>`.** NO-SHIP runs keep their worktrees for
276
+ inspection. `syncade --gc` removes worktrees once a run can no longer plausibly be resumed —
277
+ controlled by `gc.worktree_max_age_days` (default 14 days), which applies even to runs that are
278
+ technically still resume-eligible. It reached 4.4 GB on this machine before a cleanup. Point
279
+ `worktree_base` somewhere you do not mind, and run `git worktree prune` after manual removals:
280
+ removing the directory does not remove git's registration of it.
281
281
  - **A hard-killed run keeps almost all of what its reviewers had written.** Reviewer stdout and
282
282
  stderr are copied to `<round>/<name>.stdout` / `<round>/<name>.stderr` as the child produces
283
283
  them, rather than held in memory until it exits, so a run ended by `SIGKILL` (or a machine
@@ -222,13 +222,18 @@ any spend.
222
222
  ## `[gc]` — `GcConfig`
223
223
 
224
224
  Governs both the per-loop auto-prune and an explicit `syncade --gc`. Run directories are never
225
- deleted — only bulky subprocess transcripts. CLI `--gc-keep` / `--gc-max-age-days` override these.
225
+ deleted. `keep` / `max_age_days` prune bulky subprocess transcripts; `worktree_max_age_days`
226
+ removes stale worktrees under `worktree_base`, which is a separate retention tier because a
227
+ worktree is reconstructible from a recorded SHA. CLI `--gc-keep` / `--gc-max-age-days` override
228
+ the first two; the worktree bound is config-only (set it with
229
+ `syncade --config set gc.worktree_max_age_days N`).
226
230
 
227
231
  <!-- config-fields: GcConfig -->
228
232
  | Field | Type | Default | What it does |
229
233
  |---|---|---|---|
230
234
  | `keep` | int ≥ 0 | `20` | Newest N runs whose transcripts are always kept. |
231
235
  | `max_age_days` | int ≥ 0 | `0` | Additional age floor: a beyond-`keep` run is pruned only if also older than this. `0` disables the age floor. |
236
+ | `worktree_max_age_days` | int ≥ 0 | `14` | Days a run's **worktree** is kept, after which it is removed — including while the run is still resume-eligible. This is the ONLY rule that selects a worktree: one is never removed because its transcripts became prunable, so a young worktree you are still inspecting survives a busy week of runs. Separate from `max_age_days`, which gates transcripts only — a worktree is reconstructible from the SHA the run records, so removing one costs a `git worktree add` and never history. `0` disables the bound and restores the previous never-ending protection. |
232
237
 
233
238
  ## `[update]` — `UpdateConfig`
234
239
 
@@ -450,9 +450,10 @@ below** before you commit anything yourself.
450
450
  - **Loop mode refuses a tracked-modified dirty tree** unless you pass `--force-dirty`. Single-pass
451
451
  review does not refuse: reviewers see committed HEAD, so uncommitted work is invisible to them.
452
452
  - **`<worktree_base>` grows.** NO-SHIP runs keep their worktrees so you can inspect what a reviewer
453
- saw, and `syncade --gc` deliberately will not reclaim a run that is still resumable. It reached
454
- 4.4 GB on this machine before a cleanup. Point `worktree_base` somewhere you don't mind, delete
455
- old run directories when you're done, and run `git worktree prune` afterward — removing the
453
+ saw. `syncade --gc` removes worktrees once a run can no longer plausibly be resumed, controlled by
454
+ `gc.worktree_max_age_days` (default 14 days) — this applies even to runs that remain
455
+ resume-eligible. It reached 4.4 GB on this machine before a cleanup. Point `worktree_base`
456
+ somewhere you don't mind and run `git worktree prune` after manual removals — removing the
456
457
  directory does not remove git's registration of it.
457
458
  - **Run artifacts are never deleted**, only transcripts are pruned. `syncade --metrics` is derived
458
459
  from that tree and would lose your history otherwise.
@@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta"
9
9
 
10
10
  [project]
11
11
  name = "syncade"
12
- version = "0.6.3"
12
+ version = "0.7.0"
13
13
  description = "External blind multi-judge review orchestrator for AI-assisted coding"
14
14
  readme = "README.md"
15
15
  requires-python = ">=3.11"
@@ -73,6 +73,7 @@ syncade = ["templates/*.md", "templates/presets/*.toml", "skills/*/*.md"]
73
73
  testpaths = ["tests"]
74
74
  markers = [
75
75
  "smoke: end-to-end integration tests that shell out to real provider CLIs (opt-in via `pytest -m smoke`)",
76
+ "real_worktree_base: opt out of the autouse worktree-base isolation, for the drift tests that must assert the REAL default (PR-h-12 item 1b)",
76
77
  ]
77
78
  addopts = "-m 'not smoke'"
78
79
 
@@ -1,3 +1,3 @@
1
1
  """Syncade — external blind multi-judge review orchestrator."""
2
2
 
3
- __version__ = "0.6.3"
3
+ __version__ = "0.7.0"
@@ -70,7 +70,15 @@ def _run_gc(args) -> int:
70
70
  # worktree_base: clean the base the runs actually use (config.worktree_base / --worktree-base),
71
71
  # not the hardcoded default — else a relocated base leaves worktree leftovers uncollected.
72
72
  plan = plan_gc(
73
- repo_root, keep=keep, max_age_days=max_age_days, worktree_base=config.worktree_base
73
+ repo_root,
74
+ keep=keep,
75
+ max_age_days=max_age_days,
76
+ # Tier 3 has no CLI flag on purpose: it is a machine-level retention preference set once,
77
+ # not a per-invocation choice, and `--config set gc.worktree_max_age_days N` already
78
+ # reaches it. Another flag would need parser, validation, docs and two drift-test entries
79
+ # for a knob nobody tunes twice.
80
+ worktree_max_age_days=config.gc.worktree_max_age_days,
81
+ worktree_base=config.worktree_base,
74
82
  )
75
83
  report = execute_gc(plan, dry_run=args.gc_dry_run, repo_root=repo_root)
76
84
 
@@ -17,6 +17,7 @@ from syncade import billing
17
17
  from syncade.billing import Billing
18
18
  from syncade.exit_codes import SUCCESS, WORKTREE_ERROR
19
19
  from syncade.metrics.aggregate import backfill
20
+ from syncade.metrics.findings import blocker_curve, consensus_stats
20
21
  from syncade.metrics.schema import open_db
21
22
  from syncade.snapshot import SnapshotError, discover_repo_root
22
23
 
@@ -238,7 +239,10 @@ def render_report(conn: sqlite3.Connection, last_n: int | None = None) -> str:
238
239
  "SELECT COALESCE(SUM(blockers),0), COALESCE(SUM(minors),0), COALESCE(SUM(nits),0), "
239
240
  "COALESCE(SUM(dismissed),0) FROM runs"
240
241
  ).fetchone()
241
- rounds = conn.execute("SELECT COALESCE(SUM(rounds_executed),0) FROM runs").fetchone()[0]
242
+ # From `rounds`, not `runs.rounds_executed`: the latter comes from loop-manifest.json, so an
243
+ # interrupted run with perfectly good round artifacts reported "rounds: 0 total" while the
244
+ # blocker curve simultaneously knew round 0 had happened.
245
+ rounds = conn.execute("SELECT COUNT(*) FROM rounds").fetchone()[0]
242
246
  handoffs, decisions = conn.execute(
243
247
  "SELECT COALESCE(SUM(handoff),0), COALESCE(SUM(decision_needed),0) FROM runs"
244
248
  ).fetchone()
@@ -294,11 +298,86 @@ def render_report(conn: sqlite3.Connection, last_n: int | None = None) -> str:
294
298
  if reviewer_rows:
295
299
  lines.append(" reviewers by model (findings/wall-clock):")
296
300
  for provider, model, findings, run_count, wall, tok in reviewer_rows:
297
- label = f"{provider}/{model}" if model else (provider or "?")
301
+ # A row with no model is reported as UNKNOWN, never as a bare provider. A bare
302
+ # provider reads like a panel that had no model, which is a claim we cannot make;
303
+ # "unknown" says the thing that is actually true. (No such row exists in this
304
+ # corpus today — every recorded panel carries both — but the renderer must not be
305
+ # the reason a future one silently looks answered.)
306
+ label = f"{provider}/{model}" if model else f"{provider or '?'}/unknown"
298
307
  lines.append(
299
308
  f" {label:24} findings={findings} runs={run_count} "
300
309
  f"wall={int(wall)}s tok={int(tok)}"
301
310
  )
311
+ # Rounds whose panel was never recorded are INVISIBLE in the table above, which is worse
312
+ # than being wrong: anyone dividing findings by runs gets a denominator quietly missing
313
+ # them. They come from runs interrupted before the loop terminator wrote a manifest, so
314
+ # nothing ever summarised their reviewers. Never guessed — the configured roster is not
315
+ # evidence of what a killed run actually dispatched.
316
+ #
317
+ # DO NOT write a measured count here. This comment said "all 49" until the release audit
318
+ # found the line below printing 22: excluding known-empty panels changed the number and
319
+ # the prose kept the old one. A count in a comment is a measurement with no test reading
320
+ # it, which is the same defect this release had to fix in its own CHANGELOG.
321
+
322
+ # OUTSIDE the `if reviewer_rows` block on purpose. Nested inside it, a corpus where EVERY
323
+ # panel is unknown printed no warning at all — the one corpus that most needs it, silent.
324
+ # panel_source='unknown' means no manifest had a `reviewers` key for that round. This is
325
+ # independent of blockers_source: a manifest can record a blocker count without a reviewer
326
+ # list, and a no-dispatch round (no_changes_to_review) has panel_source='recorded' and
327
+ # panel_size=0 — explicitly empty, not unknown.
328
+ unknown_rounds, total_rounds = conn.execute(
329
+ "SELECT COALESCE(SUM(CASE WHEN panel_source = 'unknown' THEN 1 ELSE 0 END), 0), "
330
+ "COUNT(*) FROM rounds"
331
+ ).fetchone()
332
+ if unknown_rounds:
333
+ lines.append(
334
+ f" {unknown_rounds} of {total_rounds} rounds record no reviewer panel "
335
+ f"(no manifest evidence; excluded from consensus)"
336
+ )
337
+ manifest_only, unknown_only = conn.execute(
338
+ "SELECT "
339
+ "COALESCE(SUM(CASE WHEN blockers_source='manifest' THEN 1 ELSE 0 END), 0), "
340
+ "COALESCE(SUM(CASE WHEN blockers_source='unknown' THEN 1 ELSE 0 END), 0) "
341
+ "FROM rounds "
342
+ "WHERE NOT (panel_source='recorded' AND panel_size=0 AND blockers=0)"
343
+ ).fetchone()
344
+ if manifest_only:
345
+ lines.append(
346
+ f" {manifest_only} of {total_rounds} rounds have no synthesizer artifact; their "
347
+ f"blocker counts come from round manifests and carry no provenance"
348
+ )
349
+ if unknown_only:
350
+ lines.append(
351
+ f" {unknown_only} of {total_rounds} rounds have no blocker evidence "
352
+ f"(killed/interrupted before any artifact or manifest); "
353
+ f"excluded from the blocker curve"
354
+ )
355
+
356
+ consensus = consensus_stats(conn)
357
+ if consensus is not None:
358
+ total, single = consensus
359
+ lines.append(
360
+ f" consensus: {single} of {total} active blockers ({single / total:.0%}) were "
361
+ f"raised by exactly ONE reviewer"
362
+ )
363
+ lines.append(" (over runs with a multi-reviewer panel; what a second reviewer earns)")
364
+
365
+ curve = blocker_curve(conn)
366
+ if curve:
367
+ # Density, never the raw total alone. The blocker count falls steeply across rounds and
368
+ # reads as convergence; it is SURVIVORSHIP — fewer runs reach each round, so the
369
+ # denominator falls with it. This repo published the raw sequence as convergence once
370
+ # and had to correct it, so the renderer shows both numbers and the per-round rate.
371
+ lines.append(
372
+ " blockers by round (artifact- or manifest-backed only; "
373
+ "killed runs with no evidence excluded):"
374
+ )
375
+ for round_index, blockers, rounds in curve:
376
+ density = blockers / rounds if rounds else 0.0
377
+ lines.append(
378
+ f" round {round_index}: {blockers} blockers over {rounds} rounds "
379
+ f"({density:.2f}/round)"
380
+ )
302
381
 
303
382
  if last_n:
304
383
  ids = [
@@ -381,22 +381,25 @@ def build_parser() -> argparse.ArgumentParser:
381
381
  parser.add_argument(
382
382
  "--gc",
383
383
  action="store_true",
384
- help="Run a one-shot maintenance pass. Retention is TWO-TIER: run "
384
+ help="Run a one-shot maintenance pass. Retention is FOUR-TIER: run "
385
385
  "history is kept FOREVER (loop-manifest.json, findings.md, run-init.json, "
386
- "round manifests, summaries) and only the bulky subprocess transcripts "
387
- "(round-*/*.stdout, *.stderr - 90%% of the corpus) are pruned, for runs "
388
- "beyond --gc-keep and any --gc-max-age-days floor. NO run directory is "
389
- "ever deleted: .syncade/metrics.db is a derived view over .syncade/runs/, "
390
- "so deleting a run would destroy its history the next time that view "
391
- "rebuilds. Also removes matching <worktree_base>/<run-id>/ worktree leftovers "
392
- "whose directory identity still matches the GC plan, and safely reaps "
393
- "orphaned reviewer/producer subprocesses whose working dir is INSIDE a "
394
- "worktree being removed. Runs also auto-prune their transcripts at the "
395
- "start of every fresh loop, so --gc is for worktree/process cleanup and "
396
- "one-off maintenance rather than routine disk hygiene. Resume-eligible "
397
- "runs are ALWAYS protected (they keep even their transcripts); non-run "
398
- "state (config.toml, last-reviewed.json, draft-spec-*.md) is never "
399
- "touched. "
386
+ "round manifests, summaries); transcripts (round-*/*.stdout, *.stderr - "
387
+ "90%% of the corpus) are pruned beyond --gc-keep and any --gc-max-age-days "
388
+ "floor; worktrees are removed once a run can no longer plausibly be resumed "
389
+ "(worktrees are reconstructible from the recorded SHA, so removing one "
390
+ "costs a git worktree add, never history) — resume-eligible runs keep their "
391
+ "worktree while younger than gc.worktree_max_age_days (default 14 days, "
392
+ "set via syncade --config set gc.worktree_max_age_days N); "
393
+ "and metrics.db is a droppable derived view. NO run directory is ever "
394
+ "deleted: .syncade/metrics.db is a derived view over .syncade/runs/, so "
395
+ "deleting a run would destroy its history the next time that view rebuilds. "
396
+ "Also removes matching <worktree_base>/<run-id>/ worktree leftovers whose "
397
+ "directory identity still matches the GC plan, and safely reaps orphaned "
398
+ "reviewer/producer subprocesses whose working dir is INSIDE a worktree "
399
+ "being removed. Runs also auto-prune their transcripts at the start of "
400
+ "every fresh loop, so --gc is for worktree/process cleanup and one-off "
401
+ "maintenance rather than routine disk hygiene. Non-run state "
402
+ "(config.toml, last-reviewed.json, draft-spec-*.md) is never touched. "
400
403
  "Mutually exclusive with PR_DOC, --selfcheck, --auth-check, "
401
404
  "--spec-audit, --draft-spec, --resume, --openspec.",
402
405
  )
@@ -18,6 +18,9 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator
18
18
  # Drift-guarded literal mirrors of syncade.gc.DEFAULT_KEEP / DEFAULT_MAX_AGE_DAYS (see docstring).
19
19
  _DEFAULT_KEEP = 20
20
20
  _DEFAULT_MAX_AGE_DAYS = 0
21
+ #: Calibrated on 421 runs / 65 resume-protected: 14d releases 86% while keeping 9.
22
+ #: 7d buys 6% more for half the margin; 30d leaves 19 runs at ~130 MB/round accruing.
23
+ _DEFAULT_WORKTREE_MAX_AGE_DAYS = 14 # mirrors gc.DEFAULT_WORKTREE_MAX_AGE_DAYS
21
24
 
22
25
 
23
26
  class GcConfig(BaseModel):
@@ -41,7 +44,21 @@ class GcConfig(BaseModel):
41
44
  ),
42
45
  )
43
46
 
44
- @field_validator("keep", "max_age_days", mode="before")
47
+ worktree_max_age_days: int = Field(
48
+ default=_DEFAULT_WORKTREE_MAX_AGE_DAYS,
49
+ ge=0,
50
+ description=(
51
+ "Days after which a run's WORKTREE is removed even though the run is still "
52
+ f"resume-eligible (default {_DEFAULT_WORKTREE_MAX_AGE_DAYS}; 0 disables the bound "
53
+ "and restores the previous never-ending protection). Tier 3 of the retention "
54
+ "policy: a worktree is reconstructible from the SHA the run already records, so "
55
+ "removing one costs a `git worktree add` and never history. Distinct from "
56
+ "`max_age_days`, which gates TRANSCRIPTS only — one key cannot be non-zero for "
57
+ "worktrees and zero for transcripts at once."
58
+ ),
59
+ )
60
+
61
+ @field_validator("keep", "max_age_days", "worktree_max_age_days", mode="before")
45
62
  @classmethod
46
63
  def _strict_int(cls, value: object) -> object:
47
64
  # Pydantic's lax mode coerces a quoted number ("0"→0), an exact float (1.0→1), and a boolean
@@ -79,6 +79,12 @@ DEFAULT_KEEP: int = 20
79
79
  """Default number of most-recent non-protected runs to keep."""
80
80
 
81
81
  DEFAULT_MAX_AGE_DAYS: int = 0
82
+
83
+ #: Tier 3 (PR-h-12 item 2). Calibrated on this repo's corpus rather than picked round: over 421
84
+ #: runs, 65 of them resume-protected, a 14-day floor releases 56 (86%) and keeps 9. A 7-day floor
85
+ #: buys 6% more for half the safety margin; 30 leaves 19 runs at ~130 MB/round still accruing,
86
+ #: which is the unbounded growth this exists to stop. ``0`` disables the bound entirely.
87
+ DEFAULT_WORKTREE_MAX_AGE_DAYS: int = 14
82
88
  """Default age floor in days. ``0`` disables the age gate."""
83
89
 
84
90
 
@@ -116,6 +122,7 @@ def plan_gc(
116
122
  *,
117
123
  keep: int = DEFAULT_KEEP,
118
124
  max_age_days: int = DEFAULT_MAX_AGE_DAYS,
125
+ worktree_max_age_days: int = DEFAULT_WORKTREE_MAX_AGE_DAYS,
119
126
  worktree_base: Path = DEFAULT_WORKTREE_BASE,
120
127
  skip_worktrees: bool = False,
121
128
  ) -> GcPlan:
@@ -145,11 +152,27 @@ def plan_gc(
145
152
  worktree_trees_to_remove=[],
146
153
  orphan_worktree_trees=[],
147
154
  worktree_tree_identities={},
155
+ worktree_age_released=[],
148
156
  )
149
157
 
158
+ # TIER 3 (PR-h-12 item 2). Worktree removal is independent of tier-2 transcript slimming:
159
+ # `slim_names` is the transcript filter (beyond-keep AND old-enough), but a run INSIDE the
160
+ # keep window can still have an ancient worktree nobody is inspecting. The two tiers must be
161
+ # selected by their own criteria — `worktree_max_age_days` for tier 3, `keep`/`max_age_days`
162
+ # for tier 2 — so they can be non-zero for one and zero for the other simultaneously.
163
+ #
164
+ # `released` is the PROTECTED half: protected runs aged past the floor, with a TOCTOU bypass
165
+ # flag so execute_gc skips the on-disk protection re-check (the disk state hasn't changed —
166
+ # only age releases it). `worktree_aged` is the UNPROTECTED half: candidates aged past the
167
+ # floor, selected independently of slim_names. Both are empty when worktree_max_age_days=0
168
+ # (the opt-out that reproduces the pre-PR-h-12 behaviour).
169
+ # TIER 3, selected by its OWN rule — never derived from `slim_names`. See
170
+ # `_worktree_removable`: deriving tier-3 targets from tier-2's selection is what let a
171
+ # one-day-old worktree be deleted because its transcripts had aged out.
172
+ all_worktree_ids, released = _worktree_removable(run_dirs, protected, worktree_max_age_days)
150
173
  worktree_trees = [
151
174
  tree
152
- for tree in existing_worktree_trees(worktree_base, slim_names)
175
+ for tree in existing_worktree_trees(worktree_base, all_worktree_ids)
153
176
  if not tree_contains_repo_root(tree, repo_root)
154
177
  ]
155
178
  known_run_ids = {d.name for d in run_dirs} | protected
@@ -168,6 +191,7 @@ def plan_gc(
168
191
  worktree_trees_to_remove=worktree_trees,
169
192
  orphan_worktree_trees=orphan_trees,
170
193
  worktree_tree_identities=tree_identities,
194
+ worktree_age_released=released,
171
195
  )
172
196
 
173
197
 
@@ -197,6 +221,39 @@ def _started_at_timestamp(run_dir: Path) -> float | None:
197
221
  return dt.timestamp()
198
222
 
199
223
 
224
+ def _worktree_removable(
225
+ run_dirs: list[Path], protected: set[str], worktree_max_age_days: int
226
+ ) -> tuple[list[str], list[str]]:
227
+ """``(removable_run_ids, of_those_that_were_protected)`` — TIER 3's only rule.
228
+
229
+ One predicate, because the coupling was the defect. Worktree selection used to be a union
230
+ with ``slim_names``, so a run beyond ``gc.keep`` lost its worktree the moment its TRANSCRIPTS
231
+ became eligible — a one-day-old inspection worktree deleted under normal run volume, with the
232
+ age floor bypassed entirely. That contradicted the four-tier policy in ``CLAUDE.md``, which
233
+ says tier 3 is removed on its own age rule; the document was right and the code never matched
234
+ it. Two dogfood rounds fixed half of it each, because each added a source instead of removing
235
+ the coupling.
236
+
237
+ A worktree is removable when it is OLDER than the floor, and — for a run that is still
238
+ resume-eligible — only when a floor is actually configured::
239
+
240
+ age > floor and (not protected or floor > 0)
241
+
242
+ ``worktree_max_age_days = 0`` is the opt-out and keeps its meaning at both ends: an
243
+ unprotected run's worktree is collectable immediately, exactly as before PR-h-12, while a
244
+ resumable run's is never released. The second return value is the protected subset, which
245
+ ``execute_gc`` needs to bypass its protection re-check for those specific runs.
246
+ """
247
+ cutoff = datetime.now(UTC).timestamp() - (worktree_max_age_days * 86400)
248
+ removable = [d for d in run_dirs if _run_sort_key(d) < cutoff]
249
+ if worktree_max_age_days <= 0:
250
+ removable = [d for d in removable if d.name not in protected]
251
+ return (
252
+ sorted(d.name for d in removable),
253
+ sorted(d.name for d in removable if d.name in protected),
254
+ )
255
+
256
+
200
257
  def _select_for_slimming(
201
258
  candidates_newest_first: list[Path],
202
259
  *,
@@ -28,10 +28,21 @@ def execute_gc(plan: GcPlan, *, dry_run: bool, repo_root: Path) -> GcReport:
28
28
  pids_reaped: list[int] = []
29
29
  worktrees_removed: list[Path] = []
30
30
  protected = set(plan.protected_run_ids) | current_protected_run_ids(repo_root)
31
+ # Tier-3 age release (PR-h-12 item 2). Subtracting these from `protected` above does NOT
32
+ # work and the first version tried it: `run_id_protected_now` falls through to a DISK
33
+ # re-check that re-derives protection from the run directory, which has not changed —
34
+ # only the run's AGE releases it. So the bound computed correctly and then removed nothing.
35
+ # Unit tests over `plan_gc` could not see that; the end-to-end CLI run did.
36
+ #
37
+ # Bypassing the re-check here is safe precisely because age is the criterion: the TOCTOU
38
+ # guard exists for a run that became resume-eligible BETWEEN plan and execute, and these
39
+ # were already resume-eligible at plan time and released on an age that cannot change in
40
+ # the seconds since. Transcripts still go through the unmodified guard.
41
+ age_released = set(plan.worktree_age_released)
31
42
 
32
43
  pruned_any = False
33
44
  for tree in plan.worktree_trees_to_remove:
34
- if run_id_protected_now(repo_root, tree.name, protected):
45
+ if tree.name not in age_released and run_id_protected_now(repo_root, tree.name, protected):
35
46
  continue
36
47
  if tree_contains_repo_root(tree, repo_root):
37
48
  errors.append(f"skipping worktree tree {tree}: it contains repo root {repo_root}")
@@ -19,8 +19,13 @@ GC used to ``rmtree`` whole run directories. That is now a data-loss bug, not a
19
19
  cleanup: ``.syncade/metrics.db`` is a *derived, rebuildable view* over
20
20
  ``.syncade/runs/`` — it drop-and-recreates itself on schema drift — so deleting a run
21
21
  directory destroys that run's history permanently the next time the view rebuilds.
22
- Retention is therefore two-tier: **transcripts are disposable, everything else is
23
- kept forever.**"""
22
+ Retention is therefore **four-tier**: run history (``runs/``) kept forever; transcripts
23
+ disposable beyond the configured keep window; worktrees removed once a run can no longer
24
+ plausibly be resumed (age-bounded by ``gc.worktree_max_age_days``, default 14 days);
25
+ and ``metrics.db`` is a droppable derived view. The asymmetry between tiers 1 and 3 is
26
+ intentional: a worktree is reconstructible from the run's recorded SHA (costs a
27
+ ``git worktree add``, never history), so GC can safely remove it; a run directory is
28
+ not reconstructible, so it is never deleted."""
24
29
 
25
30
 
26
31
  @dataclass(frozen=True)
@@ -33,6 +38,12 @@ class GcPlan:
33
38
  every structured artifact in it SURVIVE — see :data:`BULK_ARTIFACT_SUFFIXES`."""
34
39
  worktree_trees_to_remove: list[Path]
35
40
  orphan_worktree_trees: list[Path]
41
+ worktree_age_released: list[str] = field(default_factory=list)
42
+ """Run-ids still resume-eligible whose WORKTREES have aged past
43
+ ``gc.worktree_max_age_days``. Their transcripts and run directories stay protected — this
44
+ releases tier 3 only. Carried on the plan so ``execute_gc`` can keep its TOCTOU re-check
45
+ (a run may have become protected since planning) without that re-check silently undoing
46
+ the age bound."""
36
47
  worktree_tree_identities: dict[Path, tuple[int, int, int]] = field(default_factory=dict)
37
48
 
38
49
 
@@ -15,12 +15,19 @@ import sqlite3
15
15
  import sys
16
16
  from pathlib import Path
17
17
 
18
+ from syncade.metrics.findings import (
19
+ read_findings,
20
+ read_rounds,
21
+ )
18
22
  from syncade.metrics.schema import (
19
23
  ActorStatRow,
20
24
  ReviewerStatRow,
21
25
  RunRow,
22
26
  upsert_actor_stat,
27
+ upsert_finding,
28
+ upsert_finding_provenance,
23
29
  upsert_reviewer_stat,
30
+ upsert_round,
24
31
  upsert_run,
25
32
  )
26
33
  from syncade.synthesizer.constants import (
@@ -105,17 +112,6 @@ def _text_or_none(value: object) -> str | None:
105
112
  return value if type(value) is str else None
106
113
 
107
114
 
108
- def _reviewer_models(init: dict) -> dict[str, str]:
109
- config = init.get("config_snapshot")
110
- roster = config.get("reviewers") if isinstance(config, dict) else None
111
- models: dict[str, str] = {}
112
- if isinstance(roster, list):
113
- for r in roster:
114
- if isinstance(r, dict) and isinstance(r.get("name"), str):
115
- models[r["name"]] = _text_or_none(r.get("model")) or ""
116
- return models
117
-
118
-
119
115
  def _load_init(run_dir: Path) -> dict:
120
116
  """Load ``run-init.json`` as a dict; warn (don't silently blank) when it is
121
117
  present but unreadable — a broken model-join source should be visible, not
@@ -239,7 +235,7 @@ def read_run(run_dir: Path | str) -> tuple[RunRow, list[ReviewerStatRow]] | None
239
235
  tokens=int(_tok) if _tok is not None else None,
240
236
  cost_usd=_cost,
241
237
  )
242
- return row, _reviewer_stats(run_id, rounds, init)
238
+ return row, _reviewer_stats(run_id, rounds)
243
239
 
244
240
 
245
241
  def read_actor_stats(run_dir: Path | str) -> list[ActorStatRow]:
@@ -261,7 +257,7 @@ def read_actor_stats(run_dir: Path | str) -> list[ActorStatRow]:
261
257
  rounds = rounds if isinstance(rounds, list) else []
262
258
  run_id = manifest.get("run_id")
263
259
  run_id = run_id if isinstance(run_id, str) and run_id else run_dir.name
264
- return _actor_stats(run_id, rounds, _load_init(run_dir))
260
+ return _actor_stats(run_id, rounds)
265
261
 
266
262
 
267
263
  def _merge_cost_source(
@@ -293,9 +289,8 @@ def _merge_sources(current: str, incoming: str) -> str:
293
289
  return "estimated"
294
290
 
295
291
 
296
- def _actor_stats(run_id: str, rounds: list, init: dict) -> list[ActorStatRow]:
292
+ def _actor_stats(run_id: str, rounds: list) -> list[ActorStatRow]:
297
293
  """Aggregate usage across reviewers, synthesizer, and producer."""
298
- models = _reviewer_models(init)
299
294
  agg: dict[tuple[str, str, str, str], dict[str, object]] = {}
300
295
 
301
296
  def remember(
@@ -360,7 +355,7 @@ def _actor_stats(run_id: str, rounds: list, init: dict) -> list[ActorStatRow]:
360
355
  role="reviewer",
361
356
  name=name,
362
357
  provider=_text_or_none(rev.get("provider")) or "",
363
- model=_text_or_none(rev.get("model")) or models.get(name, ""),
358
+ model=_text_or_none(rev.get("model")) or "",
364
359
  tokens_value=rev.get("tokens"),
365
360
  cost_value=rev.get("cost_usd"),
366
361
  cost_source_value=rev.get("cost_source"),
@@ -415,13 +410,13 @@ def _actor_stats(run_id: str, rounds: list, init: dict) -> list[ActorStatRow]:
415
410
  ]
416
411
 
417
412
 
418
- def _reviewer_stats(run_id: str, rounds: list, init: dict) -> list[ReviewerStatRow]:
413
+ def _reviewer_stats(run_id: str, rounds: list) -> list[ReviewerStatRow]:
419
414
  """Aggregate per-reviewer finding counts across rounds by artifact model.
420
415
 
421
- New manifests carry ``model`` beside reviewer usage. Legacy manifests fall
422
- back to the run-init config snapshot keyed by reviewer name.
416
+ When a round manifest did not record a model the field stays blank rather
417
+ than being guessed from the run-init config snapshot — guessing would rewrite
418
+ historical panel composition every time the config changes.
423
419
  """
424
- models = _reviewer_models(init)
425
420
  agg: dict[tuple[str, str, str], dict] = {}
426
421
  for rnd in rounds:
427
422
  if not isinstance(rnd, dict):
@@ -436,7 +431,7 @@ def _reviewer_stats(run_id: str, rounds: list, init: dict) -> list[ReviewerStatR
436
431
  if not isinstance(name, str) or not name:
437
432
  continue
438
433
  provider = _text_or_none(rev.get("provider")) or ""
439
- model = _text_or_none(rev.get("model")) or models.get(name, "")
434
+ model = _text_or_none(rev.get("model")) or ""
440
435
  entry = agg.setdefault(
441
436
  (name, provider, model),
442
437
  {
@@ -530,10 +525,33 @@ def backfill(conn: sqlite3.Connection, runs_root: Path | str) -> None:
530
525
  upsert_run(conn, row)
531
526
  conn.execute("DELETE FROM reviewer_stats WHERE run_id = ?", (row.run_id,))
532
527
  conn.execute("DELETE FROM actor_stats WHERE run_id = ?", (row.run_id,))
528
+ conn.execute("DELETE FROM findings WHERE run_id = ?", (row.run_id,))
529
+ conn.execute("DELETE FROM finding_provenance WHERE run_id = ?", (row.run_id,))
530
+ conn.execute("DELETE FROM rounds WHERE run_id = ?", (row.run_id,))
533
531
  for stat in stats:
534
532
  upsert_reviewer_stat(conn, stat)
535
533
  for stat in read_actor_stats(d):
536
534
  upsert_actor_stat(conn, stat)
535
+
536
+ findings, provenance = read_findings(d, row.run_id)
537
+ for finding in findings:
538
+ upsert_finding(conn, finding)
539
+ for entry in provenance:
540
+ upsert_finding_provenance(conn, entry)
541
+
542
+ # ONE derivation, no branch. `rounds` already resolved artifacts-vs-manifest per round
543
+ # and recorded which it used, so the run total is a sum rather than a choice between two
544
+ # counts that could disagree. What stood here was a reached-set supplement plus a
545
+ # conditional derive-or-preserve — three rounds of patching the same question, which is
546
+ # what the single authority replaces.
547
+ round_rows = read_rounds(d, row.run_id)
548
+ for round_row in round_rows:
549
+ upsert_round(conn, round_row)
550
+ conn.execute(
551
+ "UPDATE runs SET blockers = ? WHERE run_id = ?",
552
+ (sum(r.blockers for r in round_rows), row.run_id),
553
+ )
554
+
537
555
  present.append(row.run_id)
538
556
  _prune_absent(conn, present)
539
557
  conn.commit()
@@ -548,3 +566,6 @@ def _prune_absent(conn: sqlite3.Connection, present_ids: list[str]) -> None:
548
566
  conn.execute("DELETE FROM runs WHERE run_id = ?", (run_id,))
549
567
  conn.execute("DELETE FROM reviewer_stats WHERE run_id = ?", (run_id,))
550
568
  conn.execute("DELETE FROM actor_stats WHERE run_id = ?", (run_id,))
569
+ conn.execute("DELETE FROM findings WHERE run_id = ?", (run_id,))
570
+ conn.execute("DELETE FROM finding_provenance WHERE run_id = ?", (run_id,))
571
+ conn.execute("DELETE FROM rounds WHERE run_id = ?", (run_id,))