syncade 0.7.1__tar.gz → 0.7.3__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 (482) hide show
  1. {syncade-0.7.1/src/syncade.egg-info → syncade-0.7.3}/PKG-INFO +28 -4
  2. {syncade-0.7.1 → syncade-0.7.3}/README.md +27 -3
  3. {syncade-0.7.1 → syncade-0.7.3}/docs/config-reference.md +13 -6
  4. {syncade-0.7.1 → syncade-0.7.3}/pyproject.toml +1 -1
  5. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/__init__.py +1 -1
  6. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/parser.py +2 -2
  7. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/update_mode.py +159 -25
  8. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/config.py +3 -2
  9. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/config_update.py +6 -5
  10. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/doctor.py +11 -3
  11. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/doctor_env.py +47 -0
  12. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/update_check.py +38 -6
  13. {syncade-0.7.1 → syncade-0.7.3/src/syncade.egg-info}/PKG-INFO +28 -4
  14. {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_documented_flags_exist.py +8 -0
  15. {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_update_mode.py +160 -2
  16. {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_update_post_condition.py +282 -24
  17. {syncade-0.7.1 → syncade-0.7.3}/tests/conftest.py +16 -0
  18. {syncade-0.7.1 → syncade-0.7.3}/tests/doctor/conftest.py +10 -0
  19. {syncade-0.7.1 → syncade-0.7.3}/tests/test_update_check.py +10 -0
  20. {syncade-0.7.1 → syncade-0.7.3}/LICENSE +0 -0
  21. {syncade-0.7.1 → syncade-0.7.3}/MANIFEST.in +0 -0
  22. {syncade-0.7.1 → syncade-0.7.3}/docs/how-to-use.md +0 -0
  23. {syncade-0.7.1 → syncade-0.7.3}/setup.cfg +0 -0
  24. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/__main__.py +0 -0
  25. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/adapters/__init__.py +0 -0
  26. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/adapters/anthropic.py +0 -0
  27. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/adapters/base.py +0 -0
  28. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/adapters/fake.py +0 -0
  29. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/adapters/fake_common.py +0 -0
  30. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/adapters/fake_producer_audit_draft.py +0 -0
  31. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/adapters/fake_reviewer_synth.py +0 -0
  32. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/adapters/openai.py +0 -0
  33. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/adapters/openai_parsing.py +0 -0
  34. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/adapters/producer.py +0 -0
  35. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/adapters/producer_anthropic.py +0 -0
  36. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/adapters/producer_openai.py +0 -0
  37. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/adapters/registry.py +0 -0
  38. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/auth_check.py +0 -0
  39. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/auth_preflight.py +0 -0
  40. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/base_resolution.py +0 -0
  41. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/billing.py +0 -0
  42. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/checks_config.py +0 -0
  43. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/__init__.py +0 -0
  44. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/auth_gate.py +0 -0
  45. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/config_keys.py +0 -0
  46. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/config_list.py +0 -0
  47. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/config_menu_rows.py +0 -0
  48. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/config_mode.py +0 -0
  49. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/config_overrides.py +0 -0
  50. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/config_tui.py +0 -0
  51. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/doctor_mode.py +0 -0
  52. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/gc_mode.py +0 -0
  53. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/install_skill.py +0 -0
  54. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/metrics_mode.py +0 -0
  55. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/modes.py +0 -0
  56. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/parser_types.py +0 -0
  57. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/paths.py +0 -0
  58. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/preflight_paths.py +0 -0
  59. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/resolve.py +0 -0
  60. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/resume_mode.py +0 -0
  61. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/skill_status.py +0 -0
  62. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/toml_writer.py +0 -0
  63. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/update_notice.py +0 -0
  64. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/validate.py +0 -0
  65. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/config_auth.py +0 -0
  66. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/config_cold.py +0 -0
  67. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/config_gc.py +0 -0
  68. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/config_loader.py +0 -0
  69. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/config_loop.py +0 -0
  70. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/config_producer.py +0 -0
  71. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/config_retry.py +0 -0
  72. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/config_types.py +0 -0
  73. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/diff_filter.py +0 -0
  74. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/dispatcher.py +0 -0
  75. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/doctor_preview.py +0 -0
  76. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/doctor_types.py +0 -0
  77. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/exit_codes.py +0 -0
  78. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/findings.py +0 -0
  79. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/findings_json.py +0 -0
  80. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/gc.py +0 -0
  81. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/gc_execute.py +0 -0
  82. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/gc_protection.py +0 -0
  83. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/gc_types.py +0 -0
  84. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/gc_worktrees.py +0 -0
  85. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/git_object_id.py +0 -0
  86. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/git_preconditions.py +0 -0
  87. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/logging.py +0 -0
  88. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/metrics/__init__.py +0 -0
  89. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/metrics/aggregate.py +0 -0
  90. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/metrics/findings.py +0 -0
  91. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/metrics/schema.py +0 -0
  92. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/__init__.py +0 -0
  93. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/_runs_dir.py +0 -0
  94. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/branch_advance.py +0 -0
  95. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/branch_guard.py +0 -0
  96. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/budget.py +0 -0
  97. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/escalation_coverage.py +0 -0
  98. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/loop.py +0 -0
  99. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/loop_dispatch_check.py +0 -0
  100. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/loop_finalize.py +0 -0
  101. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/loop_preflight.py +0 -0
  102. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/loop_resume.py +0 -0
  103. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/loop_rmtree.py +0 -0
  104. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/loop_round_step.py +0 -0
  105. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/prior_round.py +0 -0
  106. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/producer_phase.py +0 -0
  107. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/results.py +0 -0
  108. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/resume.py +0 -0
  109. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/resume_load.py +0 -0
  110. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/resume_plan.py +0 -0
  111. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/resume_target.py +0 -0
  112. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/resume_types.py +0 -0
  113. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/reviewer_template_failure.py +0 -0
  114. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/round.py +0 -0
  115. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/round_checks.py +0 -0
  116. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/round_no_changes.py +0 -0
  117. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/round_predispatch.py +0 -0
  118. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/verdict.py +0 -0
  119. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/__init__.py +0 -0
  120. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/_atomic.py +0 -0
  121. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/_clusters.py +0 -0
  122. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/_findings_verdict.py +0 -0
  123. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/_markdown.py +0 -0
  124. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/_validation.py +0 -0
  125. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/checks.py +0 -0
  126. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/decision_needed.py +0 -0
  127. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/findings_md.py +0 -0
  128. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/handoff.py +0 -0
  129. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/handoff_classify.py +0 -0
  130. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/last_reviewed.py +0 -0
  131. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/loop_manifest.py +0 -0
  132. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/loop_summary.py +0 -0
  133. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/loop_summary_text.py +0 -0
  134. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/producer.py +0 -0
  135. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/reviewer.py +0 -0
  136. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/round_manifest.py +0 -0
  137. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/run_init.py +0 -0
  138. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/run_summary.py +0 -0
  139. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/run_summary_next_steps.py +0 -0
  140. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/synth.py +0 -0
  141. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/test_run.py +0 -0
  142. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/presets.py +0 -0
  143. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/pricing_config.py +0 -0
  144. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/process.py +0 -0
  145. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/producer.py +0 -0
  146. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/producer_attempt.py +0 -0
  147. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/producer_escalation.py +0 -0
  148. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/producer_git.py +0 -0
  149. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/producer_result.py +0 -0
  150. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/prompts.py +0 -0
  151. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/prompts_loader.py +0 -0
  152. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/retry.py +0 -0
  153. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/run_inputs.py +0 -0
  154. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/run_status.py +0 -0
  155. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/selfcheck.py +0 -0
  156. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/skills/claude/README.md +0 -0
  157. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/skills/claude/SKILL.md +0 -0
  158. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/skills/codex/README.md +0 -0
  159. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/skills/codex/SKILL.md +0 -0
  160. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/snapshot.py +0 -0
  161. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/spec_audit.py +0 -0
  162. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/spec_audit_schema.py +0 -0
  163. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/spec_draft.py +0 -0
  164. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/spec_source.py +0 -0
  165. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/synthesis.py +0 -0
  166. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/synthesis_clusters.py +0 -0
  167. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/synthesis_repair.py +0 -0
  168. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/synthesis_schema.py +0 -0
  169. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/synthesizer/__init__.py +0 -0
  170. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/synthesizer/constants.py +0 -0
  171. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/synthesizer/driver.py +0 -0
  172. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/synthesizer/rendering.py +0 -0
  173. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/synthesizer/result.py +0 -0
  174. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/synthesizer/validation.py +0 -0
  175. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/synthesizer/workspace.py +0 -0
  176. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/templates/presets/balanced.toml +0 -0
  177. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/templates/presets/cheap.toml +0 -0
  178. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/templates/presets/thorough.toml +0 -0
  179. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/templates/producer.md +0 -0
  180. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/templates/reviewer.md +0 -0
  181. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/templates/reviewer_adversarial.md +0 -0
  182. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/templates/reviewer_codex.md +0 -0
  183. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/templates/spec_audit.md +0 -0
  184. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/templates/spec_draft.md +0 -0
  185. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/templates/synthesizer.md +0 -0
  186. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/test_runner.py +0 -0
  187. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/test_runner_classify.py +0 -0
  188. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/transcript.py +0 -0
  189. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/usage.py +0 -0
  190. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/worktree.py +0 -0
  191. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/worktree_env.py +0 -0
  192. {syncade-0.7.1 → syncade-0.7.3}/src/syncade/worktree_paths.py +0 -0
  193. {syncade-0.7.1 → syncade-0.7.3}/src/syncade.egg-info/SOURCES.txt +0 -0
  194. {syncade-0.7.1 → syncade-0.7.3}/src/syncade.egg-info/dependency_links.txt +0 -0
  195. {syncade-0.7.1 → syncade-0.7.3}/src/syncade.egg-info/entry_points.txt +0 -0
  196. {syncade-0.7.1 → syncade-0.7.3}/src/syncade.egg-info/requires.txt +0 -0
  197. {syncade-0.7.1 → syncade-0.7.3}/src/syncade.egg-info/top_level.txt +0 -0
  198. {syncade-0.7.1 → syncade-0.7.3}/tests/__init__.py +0 -0
  199. {syncade-0.7.1 → syncade-0.7.3}/tests/adapters/__init__.py +0 -0
  200. {syncade-0.7.1 → syncade-0.7.3}/tests/adapters/_anthropic_helpers.py +0 -0
  201. {syncade-0.7.1 → syncade-0.7.3}/tests/adapters/_openai_helpers.py +0 -0
  202. {syncade-0.7.1 → syncade-0.7.3}/tests/adapters/test_anthropic.py +0 -0
  203. {syncade-0.7.1 → syncade-0.7.3}/tests/adapters/test_anthropic_more.py +0 -0
  204. {syncade-0.7.1 → syncade-0.7.3}/tests/adapters/test_openai.py +0 -0
  205. {syncade-0.7.1 → syncade-0.7.3}/tests/adapters/test_openai_more.py +0 -0
  206. {syncade-0.7.1 → syncade-0.7.3}/tests/adapters/test_producer.py +0 -0
  207. {syncade-0.7.1 → syncade-0.7.3}/tests/adapters/test_producer_more.py +0 -0
  208. {syncade-0.7.1 → syncade-0.7.3}/tests/adapters/test_prompt_transport.py +0 -0
  209. {syncade-0.7.1 → syncade-0.7.3}/tests/auth_check/__init__.py +0 -0
  210. {syncade-0.7.1 → syncade-0.7.3}/tests/auth_check/test_probes.py +0 -0
  211. {syncade-0.7.1 → syncade-0.7.3}/tests/auth_check/test_run_auth_check.py +0 -0
  212. {syncade-0.7.1 → syncade-0.7.3}/tests/cli/__init__.py +0 -0
  213. {syncade-0.7.1 → syncade-0.7.3}/tests/cli/_helpers.py +0 -0
  214. {syncade-0.7.1 → syncade-0.7.3}/tests/cli/_undo_helpers.py +0 -0
  215. {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_basic_and_mutex.py +0 -0
  216. {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_cli_library_divergence.py +0 -0
  217. {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_config_keys.py +0 -0
  218. {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_config_list.py +0 -0
  219. {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_config_menu_rows.py +0 -0
  220. {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_config_mode.py +0 -0
  221. {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_config_overrides.py +0 -0
  222. {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_config_tui.py +0 -0
  223. {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_config_tui_pty.py +0 -0
  224. {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_documented_exit_codes.py +0 -0
  225. {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_flags.py +0 -0
  226. {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_gc.py +0 -0
  227. {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_help_retention_contract.py +0 -0
  228. {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_mode_exclusivity_matrix.py +0 -0
  229. {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_mode_flag_guards.py +0 -0
  230. {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_resume_reports_hard_kill.py +0 -0
  231. {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_run_and_config.py +0 -0
  232. {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_run_status_cli.py +0 -0
  233. {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_scope_openspec_draft.py +0 -0
  234. {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_toml_writer.py +0 -0
  235. {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_undo_auto_init.py +0 -0
  236. {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_undo_scope.py +0 -0
  237. {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_update_notice.py +0 -0
  238. {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_validate_before_mutate.py +0 -0
  239. {syncade-0.7.1 → syncade-0.7.3}/tests/config/__init__.py +0 -0
  240. {syncade-0.7.1 → syncade-0.7.3}/tests/config/test_auth_enforcement.py +0 -0
  241. {syncade-0.7.1 → syncade-0.7.3}/tests/config/test_auth_modes.py +0 -0
  242. {syncade-0.7.1 → syncade-0.7.3}/tests/config/test_auth_preflight.py +0 -0
  243. {syncade-0.7.1 → syncade-0.7.3}/tests/config/test_auth_report_scope.py +0 -0
  244. {syncade-0.7.1 → syncade-0.7.3}/tests/config/test_budget_config.py +0 -0
  245. {syncade-0.7.1 → syncade-0.7.3}/tests/config/test_cold_actors.py +0 -0
  246. {syncade-0.7.1 → syncade-0.7.3}/tests/config/test_config_checks.py +0 -0
  247. {syncade-0.7.1 → syncade-0.7.3}/tests/config/test_config_reference_drift.py +0 -0
  248. {syncade-0.7.1 → syncade-0.7.3}/tests/config/test_config_schema.py +0 -0
  249. {syncade-0.7.1 → syncade-0.7.3}/tests/config/test_gc_config.py +0 -0
  250. {syncade-0.7.1 → syncade-0.7.3}/tests/config/test_global_config_layer.py +0 -0
  251. {syncade-0.7.1 → syncade-0.7.3}/tests/config/test_presets.py +0 -0
  252. {syncade-0.7.1 → syncade-0.7.3}/tests/config/test_retry_config.py +0 -0
  253. {syncade-0.7.1 → syncade-0.7.3}/tests/config/test_reviewer_config.py +0 -0
  254. {syncade-0.7.1 → syncade-0.7.3}/tests/config/test_reviewer_permissions.py +0 -0
  255. {syncade-0.7.1 → syncade-0.7.3}/tests/config/test_worktree_base_config.py +0 -0
  256. {syncade-0.7.1 → syncade-0.7.3}/tests/dispatcher/__init__.py +0 -0
  257. {syncade-0.7.1 → syncade-0.7.3}/tests/dispatcher/_helpers.py +0 -0
  258. {syncade-0.7.1 → syncade-0.7.3}/tests/dispatcher/test_dispatch_core.py +0 -0
  259. {syncade-0.7.1 → syncade-0.7.3}/tests/dispatcher/test_dispatch_misc.py +0 -0
  260. {syncade-0.7.1 → syncade-0.7.3}/tests/dispatcher/test_dispatch_usage.py +0 -0
  261. {syncade-0.7.1 → syncade-0.7.3}/tests/doctor/__init__.py +0 -0
  262. {syncade-0.7.1 → syncade-0.7.3}/tests/doctor/_helpers.py +0 -0
  263. {syncade-0.7.1 → syncade-0.7.3}/tests/doctor/test_doctor.py +0 -0
  264. {syncade-0.7.1 → syncade-0.7.3}/tests/doctor/test_doctor_diff_size.py +0 -0
  265. {syncade-0.7.1 → syncade-0.7.3}/tests/doctor/test_doctor_malformed_diff.py +0 -0
  266. {syncade-0.7.1 → syncade-0.7.3}/tests/doctor/test_doctor_preview.py +0 -0
  267. {syncade-0.7.1 → syncade-0.7.3}/tests/findings/__init__.py +0 -0
  268. {syncade-0.7.1 → syncade-0.7.3}/tests/findings/_helpers.py +0 -0
  269. {syncade-0.7.1 → syncade-0.7.3}/tests/findings/test_parser_and_fields.py +0 -0
  270. {syncade-0.7.1 → syncade-0.7.3}/tests/findings/test_parser_resilience.py +0 -0
  271. {syncade-0.7.1 → syncade-0.7.3}/tests/findings/test_verdict_block_selection.py +0 -0
  272. {syncade-0.7.1 → syncade-0.7.3}/tests/gc/__init__.py +0 -0
  273. {syncade-0.7.1 → syncade-0.7.3}/tests/gc/_helpers.py +0 -0
  274. {syncade-0.7.1 → syncade-0.7.3}/tests/gc/test_autoprune.py +0 -0
  275. {syncade-0.7.1 → syncade-0.7.3}/tests/gc/test_execute.py +0 -0
  276. {syncade-0.7.1 → syncade-0.7.3}/tests/gc/test_plan.py +0 -0
  277. {syncade-0.7.1 → syncade-0.7.3}/tests/gc/test_plan_execution_races.py +0 -0
  278. {syncade-0.7.1 → syncade-0.7.3}/tests/gc/test_reaping_scope.py +0 -0
  279. {syncade-0.7.1 → syncade-0.7.3}/tests/gc/test_retention_tiers.py +0 -0
  280. {syncade-0.7.1 → syncade-0.7.3}/tests/gc/test_robustness.py +0 -0
  281. {syncade-0.7.1 → syncade-0.7.3}/tests/gc/test_self_orphan_protection.py +0 -0
  282. {syncade-0.7.1 → syncade-0.7.3}/tests/gc/test_smoke.py +0 -0
  283. {syncade-0.7.1 → syncade-0.7.3}/tests/gc/test_two_tier_retention.py +0 -0
  284. {syncade-0.7.1 → syncade-0.7.3}/tests/gc/test_worktree_age_bound.py +0 -0
  285. {syncade-0.7.1 → syncade-0.7.3}/tests/logging/__init__.py +0 -0
  286. {syncade-0.7.1 → syncade-0.7.3}/tests/logging/_helpers.py +0 -0
  287. {syncade-0.7.1 → syncade-0.7.3}/tests/logging/test_logging.py +0 -0
  288. {syncade-0.7.1 → syncade-0.7.3}/tests/logging/test_logging_summary.py +0 -0
  289. {syncade-0.7.1 → syncade-0.7.3}/tests/metrics/conftest.py +0 -0
  290. {syncade-0.7.1 → syncade-0.7.3}/tests/metrics/test_actor_spend.py +0 -0
  291. {syncade-0.7.1 → syncade-0.7.3}/tests/metrics/test_aggregate.py +0 -0
  292. {syncade-0.7.1 → syncade-0.7.3}/tests/metrics/test_billing_reconciliation.py +0 -0
  293. {syncade-0.7.1 → syncade-0.7.3}/tests/metrics/test_billing_surfaces_agree.py +0 -0
  294. {syncade-0.7.1 → syncade-0.7.3}/tests/metrics/test_budget_metrics.py +0 -0
  295. {syncade-0.7.1 → syncade-0.7.3}/tests/metrics/test_consensus_report.py +0 -0
  296. {syncade-0.7.1 → syncade-0.7.3}/tests/metrics/test_cost_honesty.py +0 -0
  297. {syncade-0.7.1 → syncade-0.7.3}/tests/metrics/test_findings_capture.py +0 -0
  298. {syncade-0.7.1 → syncade-0.7.3}/tests/metrics/test_metrics_cli.py +0 -0
  299. {syncade-0.7.1 → syncade-0.7.3}/tests/metrics/test_metrics_mode.py +0 -0
  300. {syncade-0.7.1 → syncade-0.7.3}/tests/metrics/test_schema.py +0 -0
  301. {syncade-0.7.1 → syncade-0.7.3}/tests/metrics/test_unknown_panel.py +0 -0
  302. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/__init__.py +0 -0
  303. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/_helpers.py +0 -0
  304. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/_resume_fixtures.py +0 -0
  305. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/conftest.py +0 -0
  306. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_budget.py +0 -0
  307. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_budget_resume_e2e.py +0 -0
  308. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_budget_stop_message.py +0 -0
  309. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_budget_warning.py +0 -0
  310. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_checks_and_recording.py +0 -0
  311. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_contracts.py +0 -0
  312. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_cross_round.py +0 -0
  313. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_cross_round_more.py +0 -0
  314. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_default_branch_guard.py +0 -0
  315. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_diff_size_cap.py +0 -0
  316. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_dispatch_record.py +0 -0
  317. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_empty_diff.py +0 -0
  318. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_empty_diff_no_side_effects.py +0 -0
  319. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_escalation.py +0 -0
  320. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_last_reviewed_truthful.py +0 -0
  321. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_loop_audit_remediation.py +0 -0
  322. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_matrix_and_e2e.py +0 -0
  323. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_multiround.py +0 -0
  324. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_multiround_dirty.py +0 -0
  325. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_multiround_more.py +0 -0
  326. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_paraphrase_escalation.py +0 -0
  327. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_pipeline.py +0 -0
  328. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_provider_usage_limit.py +0 -0
  329. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_rename_boundary.py +0 -0
  330. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_resume.py +0 -0
  331. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_resume_base_oid.py +0 -0
  332. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_reviewer_blindness_is_structural.py +0 -0
  333. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_round_input_boundaries.py +0 -0
  334. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_run_layout.py +0 -0
  335. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_run_status_wiring.py +0 -0
  336. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_safety_warning_survives_quiet.py +0 -0
  337. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_snapshot_and_base.py +0 -0
  338. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_synth_phase.py +0 -0
  339. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_synth_phase_more.py +0 -0
  340. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_synth_provenance.py +0 -0
  341. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_synth_validation.py +0 -0
  342. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_template_robustness.py +0 -0
  343. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_termination_reason_drift.py +0 -0
  344. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_testleg.py +0 -0
  345. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_timeout_is_per_subprocess.py +0 -0
  346. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_usage_limit_message.py +0 -0
  347. {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_worktree_cleanup.py +0 -0
  348. {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/__init__.py +0 -0
  349. {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/_helpers.py +0 -0
  350. {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/test_atomic_writes.py +0 -0
  351. {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/test_checks_surfacing.py +0 -0
  352. {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/test_diff_size_recording.py +0 -0
  353. {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/test_findings_md.py +0 -0
  354. {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/test_findings_md_more.py +0 -0
  355. {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/test_findings_md_verdict.py +0 -0
  356. {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/test_handoff.py +0 -0
  357. {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/test_loop_manifest_checks.py +0 -0
  358. {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/test_loop_summary.py +0 -0
  359. {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/test_loop_summary_more.py +0 -0
  360. {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/test_md_safety.py +0 -0
  361. {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/test_next_steps.py +0 -0
  362. {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/test_producer_persist.py +0 -0
  363. {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/test_reviewer.py +0 -0
  364. {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/test_round_manifest.py +0 -0
  365. {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/test_run_init.py +0 -0
  366. {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/test_run_summary.py +0 -0
  367. {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/test_run_summary_more.py +0 -0
  368. {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/test_run_summary_producer.py +0 -0
  369. {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/test_synth_persist.py +0 -0
  370. {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/test_test_run.py +0 -0
  371. {syncade-0.7.1 → syncade-0.7.3}/tests/producer/__init__.py +0 -0
  372. {syncade-0.7.1 → syncade-0.7.3}/tests/producer/_helpers.py +0 -0
  373. {syncade-0.7.1 → syncade-0.7.3}/tests/producer/test_producer.py +0 -0
  374. {syncade-0.7.1 → syncade-0.7.3}/tests/producer/test_producer_confinement.py +0 -0
  375. {syncade-0.7.1 → syncade-0.7.3}/tests/producer/test_producer_retry.py +0 -0
  376. {syncade-0.7.1 → syncade-0.7.3}/tests/producer/test_producer_run.py +0 -0
  377. {syncade-0.7.1 → syncade-0.7.3}/tests/producer/test_producer_timeout.py +0 -0
  378. {syncade-0.7.1 → syncade-0.7.3}/tests/producer/test_producer_usage.py +0 -0
  379. {syncade-0.7.1 → syncade-0.7.3}/tests/prompts/__init__.py +0 -0
  380. {syncade-0.7.1 → syncade-0.7.3}/tests/prompts/test_load_template.py +0 -0
  381. {syncade-0.7.1 → syncade-0.7.3}/tests/prompts/test_misc.py +0 -0
  382. {syncade-0.7.1 → syncade-0.7.3}/tests/prompts/test_producer.py +0 -0
  383. {syncade-0.7.1 → syncade-0.7.3}/tests/prompts/test_render_synthesizer.py +0 -0
  384. {syncade-0.7.1 → syncade-0.7.3}/tests/prompts/test_synthesizer.py +0 -0
  385. {syncade-0.7.1 → syncade-0.7.3}/tests/resume/__init__.py +0 -0
  386. {syncade-0.7.1 → syncade-0.7.3}/tests/resume/_helpers.py +0 -0
  387. {syncade-0.7.1 → syncade-0.7.3}/tests/resume/test_budget_resume.py +0 -0
  388. {syncade-0.7.1 → syncade-0.7.3}/tests/resume/test_checks_phase_failure.py +0 -0
  389. {syncade-0.7.1 → syncade-0.7.3}/tests/resume/test_cli_malformed_rehydration.py +0 -0
  390. {syncade-0.7.1 → syncade-0.7.3}/tests/resume/test_find_resolve_plan.py +0 -0
  391. {syncade-0.7.1 → syncade-0.7.3}/tests/resume/test_plan_resume.py +0 -0
  392. {syncade-0.7.1 → syncade-0.7.3}/tests/resume/test_round_trip.py +0 -0
  393. {syncade-0.7.1 → syncade-0.7.3}/tests/resume/test_tree_drift_and_escalation.py +0 -0
  394. {syncade-0.7.1 → syncade-0.7.3}/tests/skills/test_config_section.py +0 -0
  395. {syncade-0.7.1 → syncade-0.7.3}/tests/skills/test_install_skill.py +0 -0
  396. {syncade-0.7.1 → syncade-0.7.3}/tests/skills/test_install_skill_multi_harness.py +0 -0
  397. {syncade-0.7.1 → syncade-0.7.3}/tests/skills/test_install_skill_ownership.py +0 -0
  398. {syncade-0.7.1 → syncade-0.7.3}/tests/skills/test_install_skill_refuses.py +0 -0
  399. {syncade-0.7.1 → syncade-0.7.3}/tests/skills/test_skill_drift.py +0 -0
  400. {syncade-0.7.1 → syncade-0.7.3}/tests/skills/test_skill_status.py +0 -0
  401. {syncade-0.7.1 → syncade-0.7.3}/tests/smoke/__init__.py +0 -0
  402. {syncade-0.7.1 → syncade-0.7.3}/tests/smoke/test_anthropic_smoke.py +0 -0
  403. {syncade-0.7.1 → syncade-0.7.3}/tests/smoke/test_auth_check_smoke.py +0 -0
  404. {syncade-0.7.1 → syncade-0.7.3}/tests/smoke/test_codex_smoke.py +0 -0
  405. {syncade-0.7.1 → syncade-0.7.3}/tests/smoke/test_loop_smoke.py +0 -0
  406. {syncade-0.7.1 → syncade-0.7.3}/tests/smoke/test_orchestrator_smoke.py +0 -0
  407. {syncade-0.7.1 → syncade-0.7.3}/tests/smoke/test_orchestrator_smoke_more.py +0 -0
  408. {syncade-0.7.1 → syncade-0.7.3}/tests/smoke/test_producer_smoke.py +0 -0
  409. {syncade-0.7.1 → syncade-0.7.3}/tests/smoke/test_resume_reap_smoke.py +0 -0
  410. {syncade-0.7.1 → syncade-0.7.3}/tests/smoke/test_selfcheck_smoke.py +0 -0
  411. {syncade-0.7.1 → syncade-0.7.3}/tests/smoke/test_synthesizer_smoke.py +0 -0
  412. {syncade-0.7.1 → syncade-0.7.3}/tests/smoke/test_test_runner_smoke.py +0 -0
  413. {syncade-0.7.1 → syncade-0.7.3}/tests/snapshot/__init__.py +0 -0
  414. {syncade-0.7.1 → syncade-0.7.3}/tests/snapshot/_helpers.py +0 -0
  415. {syncade-0.7.1 → syncade-0.7.3}/tests/snapshot/conftest.py +0 -0
  416. {syncade-0.7.1 → syncade-0.7.3}/tests/snapshot/test_snapshot.py +0 -0
  417. {syncade-0.7.1 → syncade-0.7.3}/tests/snapshot/test_snapshot_diff_identity.py +0 -0
  418. {syncade-0.7.1 → syncade-0.7.3}/tests/snapshot/test_snapshot_dirty.py +0 -0
  419. {syncade-0.7.1 → syncade-0.7.3}/tests/snapshot/test_snapshot_warning.py +0 -0
  420. {syncade-0.7.1 → syncade-0.7.3}/tests/snapshot/test_three_dot_diff.py +0 -0
  421. {syncade-0.7.1 → syncade-0.7.3}/tests/synthesis/__init__.py +0 -0
  422. {syncade-0.7.1 → syncade-0.7.3}/tests/synthesis/_helpers.py +0 -0
  423. {syncade-0.7.1 → syncade-0.7.3}/tests/synthesis/test_cluster_repair.py +0 -0
  424. {syncade-0.7.1 → syncade-0.7.3}/tests/synthesis/test_clusters.py +0 -0
  425. {syncade-0.7.1 → syncade-0.7.3}/tests/synthesis/test_models.py +0 -0
  426. {syncade-0.7.1 → syncade-0.7.3}/tests/synthesis/test_models_distinct_keying.py +0 -0
  427. {syncade-0.7.1 → syncade-0.7.3}/tests/synthesis/test_parsing.py +0 -0
  428. {syncade-0.7.1 → syncade-0.7.3}/tests/synthesis/test_unanimous_blocker_downgrade.py +0 -0
  429. {syncade-0.7.1 → syncade-0.7.3}/tests/synthesizer/test_provenance_fabrication.py +0 -0
  430. {syncade-0.7.1 → syncade-0.7.3}/tests/synthesizer/test_provenance_repair.py +0 -0
  431. {syncade-0.7.1 → syncade-0.7.3}/tests/test_adapter_registry.py +0 -0
  432. {syncade-0.7.1 → syncade-0.7.3}/tests/test_adapters_fake.py +0 -0
  433. {syncade-0.7.1 → syncade-0.7.3}/tests/test_auto_init_policy.py +0 -0
  434. {syncade-0.7.1 → syncade-0.7.3}/tests/test_base_resolution.py +0 -0
  435. {syncade-0.7.1 → syncade-0.7.3}/tests/test_check_loc_script.py +0 -0
  436. {syncade-0.7.1 → syncade-0.7.3}/tests/test_checks.py +0 -0
  437. {syncade-0.7.1 → syncade-0.7.3}/tests/test_config_loader.py +0 -0
  438. {syncade-0.7.1 → syncade-0.7.3}/tests/test_cross_model_claim.py +0 -0
  439. {syncade-0.7.1 → syncade-0.7.3}/tests/test_diff_filter.py +0 -0
  440. {syncade-0.7.1 → syncade-0.7.3}/tests/test_diff_filter_binary.py +0 -0
  441. {syncade-0.7.1 → syncade-0.7.3}/tests/test_diff_filter_quoted_paths.py +0 -0
  442. {syncade-0.7.1 → syncade-0.7.3}/tests/test_escalation_coverage.py +0 -0
  443. {syncade-0.7.1 → syncade-0.7.3}/tests/test_exit_codes.py +0 -0
  444. {syncade-0.7.1 → syncade-0.7.3}/tests/test_git_choke_point.py +0 -0
  445. {syncade-0.7.1 → syncade-0.7.3}/tests/test_git_config_isolation.py +0 -0
  446. {syncade-0.7.1 → syncade-0.7.3}/tests/test_git_preconditions.py +0 -0
  447. {syncade-0.7.1 → syncade-0.7.3}/tests/test_last_reviewed.py +0 -0
  448. {syncade-0.7.1 → syncade-0.7.3}/tests/test_parse_repair_extra_keys.py +0 -0
  449. {syncade-0.7.1 → syncade-0.7.3}/tests/test_pricing.py +0 -0
  450. {syncade-0.7.1 → syncade-0.7.3}/tests/test_private_identifier_gate.py +0 -0
  451. {syncade-0.7.1 → syncade-0.7.3}/tests/test_process.py +0 -0
  452. {syncade-0.7.1 → syncade-0.7.3}/tests/test_process_teardown.py +0 -0
  453. {syncade-0.7.1 → syncade-0.7.3}/tests/test_producer_escalation.py +0 -0
  454. {syncade-0.7.1 → syncade-0.7.3}/tests/test_regression_acme.py +0 -0
  455. {syncade-0.7.1 → syncade-0.7.3}/tests/test_retry.py +0 -0
  456. {syncade-0.7.1 → syncade-0.7.3}/tests/test_run_status.py +0 -0
  457. {syncade-0.7.1 → syncade-0.7.3}/tests/test_run_status_signals.py +0 -0
  458. {syncade-0.7.1 → syncade-0.7.3}/tests/test_runner/__init__.py +0 -0
  459. {syncade-0.7.1 → syncade-0.7.3}/tests/test_runner/test_test_runner.py +0 -0
  460. {syncade-0.7.1 → syncade-0.7.3}/tests/test_runner/test_test_runner_more.py +0 -0
  461. {syncade-0.7.1 → syncade-0.7.3}/tests/test_safety_disclosures_survive_quiet.py +0 -0
  462. {syncade-0.7.1 → syncade-0.7.3}/tests/test_selfcheck.py +0 -0
  463. {syncade-0.7.1 → syncade-0.7.3}/tests/test_spec_audit.py +0 -0
  464. {syncade-0.7.1 → syncade-0.7.3}/tests/test_spec_draft.py +0 -0
  465. {syncade-0.7.1 → syncade-0.7.3}/tests/test_spec_source.py +0 -0
  466. {syncade-0.7.1 → syncade-0.7.3}/tests/test_streamed_capture.py +0 -0
  467. {syncade-0.7.1 → syncade-0.7.3}/tests/test_strip_list_semantics.py +0 -0
  468. {syncade-0.7.1 → syncade-0.7.3}/tests/test_synthesizer.py +0 -0
  469. {syncade-0.7.1 → syncade-0.7.3}/tests/test_synthesizer_passthrough.py +0 -0
  470. {syncade-0.7.1 → syncade-0.7.3}/tests/test_synthesizer_provenance.py +0 -0
  471. {syncade-0.7.1 → syncade-0.7.3}/tests/test_transcript.py +0 -0
  472. {syncade-0.7.1 → syncade-0.7.3}/tests/test_usage.py +0 -0
  473. {syncade-0.7.1 → syncade-0.7.3}/tests/test_usage_persistence.py +0 -0
  474. {syncade-0.7.1 → syncade-0.7.3}/tests/test_workflow_action_pins.py +0 -0
  475. {syncade-0.7.1 → syncade-0.7.3}/tests/test_worktree_base_isolation.py +0 -0
  476. {syncade-0.7.1 → syncade-0.7.3}/tests/test_worktree_env.py +0 -0
  477. {syncade-0.7.1 → syncade-0.7.3}/tests/test_worktree_strip_guard.py +0 -0
  478. {syncade-0.7.1 → syncade-0.7.3}/tests/worktree/__init__.py +0 -0
  479. {syncade-0.7.1 → syncade-0.7.3}/tests/worktree/_helpers.py +0 -0
  480. {syncade-0.7.1 → syncade-0.7.3}/tests/worktree/conftest.py +0 -0
  481. {syncade-0.7.1 → syncade-0.7.3}/tests/worktree/test_worktree.py +0 -0
  482. {syncade-0.7.1 → syncade-0.7.3}/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.7.1
3
+ Version: 0.7.3
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
@@ -123,11 +123,35 @@ syncade --update # upgrade, then re-run your command
123
123
  ```
124
124
 
125
125
  `--update` works out how syncade was installed by looking for a marker inside its own install
126
- tree, and **refuses rather than guessing** if it cannot tell — printing the manual `pip` command
127
- instead. It also refuses while a review is still running, and declines to touch a source
126
+ tree — `uv`, `pipx`, or the `INSTALLER` record pip leaves — and **refuses rather than guessing**
127
+ if no marker proves one, printing the manual command instead. It also refuses while a review is still running, and declines to touch a source
128
128
  checkout. A running process cannot switch to the version it just installed, so it exits and asks
129
129
  you to re-run.
130
130
 
131
+ > **Installed before 0.6.3? You will never be told about updates.** The check itself arrived in
132
+ > **0.6.3**; every release before it — `0.1.0` through `0.6.2` — has no such code, so it cannot
133
+ > announce anything and `--update` is not a flag it knows. There is no way for us to reach those
134
+ > installs, which is why this paragraph exists. Check with `syncade --version`, and if it is below
135
+ > `0.6.3`, upgrade once by hand:
136
+ >
137
+ > ```bash
138
+ > uv tool install --force syncade # if you installed with uv
139
+ > pipx upgrade syncade # if you installed with pipx
140
+ > python3 -m pip install --user -U syncade # if you installed with pip --user
141
+ > python3 -m pip install -U syncade # if you installed with pip
142
+ > ```
143
+ >
144
+ > **Check `syncade --version` afterwards either way** — do not trust the success message. Each
145
+ > of these can report success while changing nothing: `pipx` refuses a package you have
146
+ > `pipx pin`ned, `pip` may target a different interpreter than the one `syncade` resolves to,
147
+ > and on `0.6.3`/`0.7.0` `--update` itself could claim success having installed nothing (fixed
148
+ > in `0.7.1`). The `uv` line is the exception: it upgrades *and* clears any pin.
149
+ >
150
+ > From `0.6.3` on syncade tells you about updates itself — **when it can reach the manifest**.
151
+ > That check is silent on failure by design, so a machine that cannot reach it looks exactly
152
+ > like a machine that is up to date. If you want certainty, compare `syncade --version` against
153
+ > the [releases page](https://github.com/syncade-ai/syncade-ai/releases).
154
+
131
155
  **Requirements**
132
156
  - **Python 3.11+**, on **macOS or Linux** (on Windows, use WSL).
133
157
  - **`codex`** (OpenAI), on your `PATH` and authenticated — required for the default
@@ -287,7 +311,7 @@ syncade runs AI coding-agent CLIs with **elevated tool access on your repo**:
287
311
  - **Reviewers** run headless inside throwaway worktree copies under `/tmp/syncade/` (Codex reviewers are additionally sandboxed to theirs).
288
312
  - **The producer** runs unsandboxed and **commits to your current branch** (syncade refuses the default branch by default).
289
313
  - 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 **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.
314
+ - syncade makes a small number of network calls of its own: a **once-per-session** GET to its public update manifest at the first invocation in each terminal or harness window (suppressible via `[update] check = false`, or set `CI`). `syncade --update` and `syncade --doctor` check every time you run them — operator-requested, so not session-gated. **At most one manifest GET per invocation** either way: a single `syncade` process fetches once and shares it, so `--doctor` does not add a request on top of the session check. `syncade --update` also invokes your package manager (`uv tool upgrade`, `pipx upgrade`, or your own interpreter's `-m pip install -U`) — only when you run that flag. All other egress is the provider CLIs you already authenticated and the test/check commands you configure.
291
315
 
292
316
  Running syncade is comparable to running the target repo's `Makefile` — it executes code
293
317
  with your permissions. See **[SECURITY.md](SECURITY.md)** for the full threat model and how
@@ -93,11 +93,35 @@ syncade --update # upgrade, then re-run your command
93
93
  ```
94
94
 
95
95
  `--update` works out how syncade was installed by looking for a marker inside its own install
96
- tree, and **refuses rather than guessing** if it cannot tell — printing the manual `pip` command
97
- instead. It also refuses while a review is still running, and declines to touch a source
96
+ tree — `uv`, `pipx`, or the `INSTALLER` record pip leaves — and **refuses rather than guessing**
97
+ if no marker proves one, printing the manual command instead. It also refuses while a review is still running, and declines to touch a source
98
98
  checkout. A running process cannot switch to the version it just installed, so it exits and asks
99
99
  you to re-run.
100
100
 
101
+ > **Installed before 0.6.3? You will never be told about updates.** The check itself arrived in
102
+ > **0.6.3**; every release before it — `0.1.0` through `0.6.2` — has no such code, so it cannot
103
+ > announce anything and `--update` is not a flag it knows. There is no way for us to reach those
104
+ > installs, which is why this paragraph exists. Check with `syncade --version`, and if it is below
105
+ > `0.6.3`, upgrade once by hand:
106
+ >
107
+ > ```bash
108
+ > uv tool install --force syncade # if you installed with uv
109
+ > pipx upgrade syncade # if you installed with pipx
110
+ > python3 -m pip install --user -U syncade # if you installed with pip --user
111
+ > python3 -m pip install -U syncade # if you installed with pip
112
+ > ```
113
+ >
114
+ > **Check `syncade --version` afterwards either way** — do not trust the success message. Each
115
+ > of these can report success while changing nothing: `pipx` refuses a package you have
116
+ > `pipx pin`ned, `pip` may target a different interpreter than the one `syncade` resolves to,
117
+ > and on `0.6.3`/`0.7.0` `--update` itself could claim success having installed nothing (fixed
118
+ > in `0.7.1`). The `uv` line is the exception: it upgrades *and* clears any pin.
119
+ >
120
+ > From `0.6.3` on syncade tells you about updates itself — **when it can reach the manifest**.
121
+ > That check is silent on failure by design, so a machine that cannot reach it looks exactly
122
+ > like a machine that is up to date. If you want certainty, compare `syncade --version` against
123
+ > the [releases page](https://github.com/syncade-ai/syncade-ai/releases).
124
+
101
125
  **Requirements**
102
126
  - **Python 3.11+**, on **macOS or Linux** (on Windows, use WSL).
103
127
  - **`codex`** (OpenAI), on your `PATH` and authenticated — required for the default
@@ -257,7 +281,7 @@ syncade runs AI coding-agent CLIs with **elevated tool access on your repo**:
257
281
  - **Reviewers** run headless inside throwaway worktree copies under `/tmp/syncade/` (Codex reviewers are additionally sandboxed to theirs).
258
282
  - **The producer** runs unsandboxed and **commits to your current branch** (syncade refuses the default branch by default).
259
283
  - 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 **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.
284
+ - syncade makes a small number of network calls of its own: a **once-per-session** GET to its public update manifest at the first invocation in each terminal or harness window (suppressible via `[update] check = false`, or set `CI`). `syncade --update` and `syncade --doctor` check every time you run them — operator-requested, so not session-gated. **At most one manifest GET per invocation** either way: a single `syncade` process fetches once and shares it, so `--doctor` does not add a request on top of the session check. `syncade --update` also invokes your package manager (`uv tool upgrade`, `pipx upgrade`, or your own interpreter's `-m pip install -U`) — only when you run that flag. All other egress is the provider CLIs you already authenticated and the test/check commands you configure.
261
285
 
262
286
  Running syncade is comparable to running the target repo's `Makefile` — it executes code
263
287
  with your permissions. See **[SECURITY.md](SECURITY.md)** for the full threat model and how
@@ -99,7 +99,7 @@ below; `worktree_base` is the one top-level scalar.
99
99
  | `review` | `[review]` table | — | What reviewers see. See [`[review]`](#review--reviewconfig). |
100
100
  | `retry` | `[retry]` table | — | Transient-error retry bound. See [`[retry]`](#retry--retryconfig). |
101
101
  | `gc` | `[gc]` table | — | Run-artifact retention. See [`[gc]`](#gc--gcconfig). |
102
- | `update` | `[update]` table | — | The once-per-session update check. See [`[update]`](#update--updateconfig). |
102
+ | `update` | `[update]` table | — | Background update check and opt-out. See [`[update]`](#update--updateconfig). |
103
103
  | `worktree_base` | path | `/tmp/syncade` | Base dir for per-run git worktrees. Overridable with `--worktree-base`; point it at a fast local disk if `/tmp` is small or slow. |
104
104
  | `checks` | `[[checks]]` list | none | Mechanical exit-code gates. See [`[[checks]]`](#checks--checkconfig). |
105
105
  | `pricing` | `[pricing]` table | packaged price table | Per-model token pricing for cost estimation. See [`[pricing]`](#pricing--pricingconfig). |
@@ -237,10 +237,17 @@ the first two; the worktree bound is config-only (set it with
237
237
 
238
238
  ## `[update]` — `UpdateConfig`
239
239
 
240
- Whether syncade checks for a newer release. The check only ever **prints**: it cannot block a run,
241
- change an exit code, or fail one — every error (offline, timeout, bad JSON) is silent. It fires at
242
- most once per window, at the first syncade invocation, and is skipped entirely whenever `CI` is
243
- set. Nothing about your repo, diff, run, or identity is sent.
240
+ Whether syncade checks for a newer release. Nothing about your repo, diff, run, or identity is
241
+ sent. The check has two distinct modes with different behaviors:
242
+
243
+ **Background check** (automatic, at first invocation): fires at most once per session window,
244
+ only ever **prints** — it cannot block a run, change an exit code, or fail one. Every error
245
+ (offline, timeout, bad JSON) is silent. Skipped entirely when `CI` is set.
246
+
247
+ **Explicit check** (`--doctor` and `--update`): each makes its own manifest fetch (one per
248
+ process, independent of the background check). `--doctor` displays the result as a row in the
249
+ preflight table and exits 60 if the manifest is unreachable. `--update` surfaces an unreachable
250
+ manifest as an explicit message. Both are also skipped when `check = false` or `CI` is set.
244
251
 
245
252
  The advisory source is deliberately not configurable — a repo-local file that could repoint or
246
253
  silence a security notice would be a hole, not a feature.
@@ -248,7 +255,7 @@ silence a security notice would be a hole, not a feature.
248
255
  <!-- config-fields: UpdateConfig -->
249
256
  | Field | Type | Default | What it does |
250
257
  |---|---|---|---|
251
- | `check` | bool | `true` | Check once per session whether a newer syncade is published. Set `false` to never make the request. |
258
+ | `check` | bool | `true` | Enable update checks. Set `false` to suppress all manifest fetches — background, `--doctor`, and `--update`. Also skipped whenever `CI` is set. At most one fetch per invocation: the background check is once per session, `--doctor` and `--update` check whenever you run them, and a single process never fetches twice. |
252
259
 
253
260
  ## `[[checks]]` — `CheckConfig`
254
261
 
@@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta"
9
9
 
10
10
  [project]
11
11
  name = "syncade"
12
- version = "0.7.1"
12
+ version = "0.7.3"
13
13
  description = "External blind multi-judge review orchestrator for AI-assisted coding"
14
14
  readme = "README.md"
15
15
  requires-python = ">=3.11"
@@ -1,3 +1,3 @@
1
1
  """Syncade — external blind multi-judge review orchestrator."""
2
2
 
3
- __version__ = "0.7.1"
3
+ __version__ = "0.7.3"
@@ -310,8 +310,8 @@ def build_parser() -> argparse.ArgumentParser:
310
310
  "--update",
311
311
  action="store_true",
312
312
  help="Upgrade syncade to the latest published release, then exit. Detects how syncade "
313
- "was installed by finding a marker inside its own install tree (uv tool / pipx) and "
314
- "REFUSES rather than guessing when it cannot tell, printing the manual pip command "
313
+ "was installed by finding a marker inside its own install tree (uv tool / pipx / pip) "
314
+ "and REFUSES rather than guessing when it cannot tell, printing the manual pip command "
315
315
  "instead. Refuses while a run is still going. Re-installs any skill you already have, "
316
316
  "using the upgraded code. A running process cannot switch to the new version, so this "
317
317
  "exits and asks you to re-run your command.",
@@ -16,6 +16,8 @@ the PR-h-04.5 shape one layer up: refuse rather than act on an unproven belief.
16
16
 
17
17
  from __future__ import annotations
18
18
 
19
+ import os
20
+ import site
19
21
  import subprocess
20
22
  import sys
21
23
  from dataclasses import dataclass
@@ -24,7 +26,7 @@ from pathlib import Path
24
26
  import syncade
25
27
  from syncade.exit_codes import SUCCESS, WORKTREE_ERROR
26
28
  from syncade.process import SubprocessError, run_subprocess
27
- from syncade.update_check import MANIFEST_URL, _fetch, _parse, _text
29
+ from syncade.update_check import _parse, _text, manifest_once
28
30
 
29
31
  _UPGRADE_TIMEOUT = 300.0
30
32
  #: Reading one version string out of a fresh interpreter. Generous, because a cold venv
@@ -38,11 +40,91 @@ _MARKER_DEPTH = 7
38
40
 
39
41
  @dataclass(frozen=True)
40
42
  class InstallMethod:
41
- kind: str # "uv" | "pipx" | "source" | "unknown"
43
+ kind: str # "uv" | "pipx" | "pip" | "source" | "unknown"
42
44
  command: list[str] | None # the upgrade to run, or None when we must not run anything
43
45
  where: Path | None
44
46
 
45
47
 
48
+ def _in_user_site() -> bool:
49
+ """Whether THIS syncade lives in the per-user site directory.
50
+
51
+ Decides one thing: whether the pip upgrade needs ``--user``. A user-site install requires it
52
+ and a system-site install rejects it, so guessing either way breaks half the population. The
53
+ answer is derivable from paths we already hold, so it is derived rather than assumed.
54
+ """
55
+ try:
56
+ user_site = Path(site.getusersitepackages()).resolve()
57
+ except (AttributeError, OSError): # no user site on this interpreter
58
+ return False
59
+ return user_site in Path(syncade.__file__).resolve().parents
60
+
61
+
62
+ def _read_scoped_installer() -> str:
63
+ """Read INSTALLER from the dist-info directory that BELONGS to this syncade install.
64
+
65
+ Path-scoped to the site-packages directory containing ``syncade/__init__.py``, so an
66
+ unrelated dist-info for a different interpreter's syncade cannot prove this tree as pip.
67
+ Returns the installer name (e.g. ``"pip"``), or empty string when not found or ambiguous.
68
+
69
+ Contradictory duplicate dist-info directories (a broken packaging state) are refused
70
+ conservatively: if two ``syncade-*.dist-info`` entries disagree on INSTALLER, the result
71
+ is empty string — the same as not found. First-match would misclassify in that state,
72
+ which contradicts the PROVE-or-refuse posture the rest of this module maintains.
73
+ """
74
+ site_packages = Path(syncade.__file__).resolve().parent.parent
75
+ found: list[str] = []
76
+ try:
77
+ for entry in site_packages.iterdir():
78
+ if (
79
+ entry.name.startswith("syncade-")
80
+ and entry.name.endswith(".dist-info")
81
+ and entry.is_dir()
82
+ ):
83
+ try:
84
+ found.append((entry / "INSTALLER").read_text(encoding="utf-8").strip())
85
+ except OSError:
86
+ pass # No INSTALLER file — this dist-info cannot prove anything.
87
+ except OSError:
88
+ pass
89
+ if not found:
90
+ return ""
91
+ # Contradictory entries in a broken packaging state are not proof of anything.
92
+ return found[0] if len(set(found)) == 1 else ""
93
+
94
+
95
+ def _pip_or_unknown(where: Path | None) -> InstallMethod:
96
+ """``pip`` when the distribution records pip as its installer, else ``unknown``.
97
+
98
+ Reached ONLY after the marker walk has failed to prove uv, pipx or a checkout. That ordering
99
+ is load-bearing, not tidiness: uv and pipx write ``dist-info/INSTALLER`` naming THEMSELVES, so
100
+ consulting it earlier would run pip against a uv tool. Their own markers are the specific ones
101
+ and they are checked first.
102
+
103
+ ``INSTALLER`` is a marker file naming its writer, exactly like ``uv-receipt.toml`` — reading it
104
+ keeps this PROVING rather than guessing, which is the distinction the refusal exists to
105
+ protect. Before this, a plain pip install was ``unknown`` and ``--update`` refused: correct
106
+ under the rule, and wrong about the facts, because pip had left proof all along and syncade
107
+ never looked. Found by an operator whose real install was exactly this shape.
108
+
109
+ The lookup is PATH-SCOPED to the site-packages directory containing ``syncade/__init__.py``.
110
+ Reading by project name from ambient ``importlib.metadata`` would match an unrelated
111
+ dist-info (e.g. from a different interpreter's install) and classify a markerless tree as
112
+ pip when it is not — measured in a worktree where ``syncade.__file__`` was the reviewed
113
+ source while metadata pointed at a real installed dist-info.
114
+
115
+ ``sys.executable -m pip``, never a bare ``pip`` from PATH — that resolves to whichever pip
116
+ comes first and would upgrade some other interpreter's syncade as often as this one's.
117
+ """
118
+ installer = _read_scoped_installer()
119
+ if installer != "pip":
120
+ # Some other installer, or a name we do not recognise. Unproven is still unproven.
121
+ return InstallMethod("unknown", None, where)
122
+ user = ["--user"] if _in_user_site() else []
123
+ return InstallMethod(
124
+ "pip", [sys.executable, "-m", "pip", "install", "-U", *user, "syncade"], where
125
+ )
126
+
127
+
46
128
  def detect_install() -> InstallMethod:
47
129
  """Prove how syncade got here by finding a marker inside its own install tree."""
48
130
  start = Path(syncade.__file__).resolve().parent
@@ -59,7 +141,7 @@ def detect_install() -> InstallMethod:
59
141
  # specific; reaching here means the environment exists but its manager is unproven,
60
142
  # which is exactly what "unknown" is for.
61
143
  if (parent / "pyvenv.cfg").is_file():
62
- return InstallMethod("unknown", None, parent)
144
+ return _pip_or_unknown(parent)
63
145
  # A checkout is the one case where upgrading would be actively wrong: the operator's
64
146
  # own working tree is the install, and `uv tool upgrade` would neither touch it nor
65
147
  # tell them why nothing changed. Detected by the project file naming THIS project, so a
@@ -67,7 +149,7 @@ def detect_install() -> InstallMethod:
67
149
  pyproject = parent / "pyproject.toml"
68
150
  if pyproject.is_file() and 'name = "syncade"' in _read(pyproject):
69
151
  return InstallMethod("source", None, parent)
70
- return InstallMethod("unknown", None, None)
152
+ return _pip_or_unknown(None)
71
153
 
72
154
 
73
155
  def _read(path: Path) -> str:
@@ -203,16 +285,21 @@ def _installed_version() -> str | None:
203
285
  return candidate if _parse(candidate) is not None else None
204
286
 
205
287
 
206
- def _is_newest(installed: str) -> bool:
207
- """Whether ``installed`` is at or beyond the newest version the manifest advertises.
288
+ def _is_newest(installed: str, *, enabled: bool = True) -> bool | None:
289
+ """Whether ``installed`` is at or beyond the newest the manifest advertises, or ``None``.
290
+
291
+ THREE answers, because the caller has three outcomes and two states could not express
292
+ them. ``None`` means the manifest could not be read AT ALL, which is NOT "a newer version
293
+ exists" — collapsing those is a shipped defect: on a machine whose TLS verification fails,
294
+ an already-current install was told "nothing was installed, most often the install pins a
295
+ version" and sent hunting a pin that did not exist.
208
296
 
209
297
  Only ever consulted on the unhappy path — the version failed to move — so the happy path
210
298
  stays offline. Anything that leaves the question unanswered (unreachable manifest, no usable
211
- ``latest``, an installed version we cannot parse) returns ``False``, i.e. "cannot prove you
212
- are current", which routes to the louder message. That is the safe direction: telling someone
213
- to check for a pin when they were merely current is a wasted minute; telling someone they are
214
- up to date when the upgrade silently failed strands them on an old version believing it is
215
- the new one.
299
+ ``latest``, an installed version we cannot parse) returns ``None`` — the third state meaning
300
+ "could not determine". That is the safe direction when used with care: the caller must render
301
+ ``None`` as "manifest unreachable" rather than a pin diagnosis, because collapsing the two was
302
+ the shipped defect this three-valued return exists to fix.
216
303
 
217
304
  **This compares versions itself and must never delegate to ``evaluate()``.** That function
218
305
  answers a DIFFERENT question — "should the operator see a notice?" — and the two answers
@@ -224,13 +311,23 @@ def _is_newest(installed: str) -> bool:
224
311
  neighbouring answer — and the fix both times was to ask the actual question.
225
312
 
226
313
  There is deliberately no special case for an unreachable manifest: ``{}`` has no usable
227
- ``latest``, so the one parse rule already answers it. An explicit branch was there and a
228
- mutation run proved it dead — every input reached the same verdict without it.
314
+ ``latest``, so the one parse rule already answers it. An explicit ``if manifest is None``
315
+ branch has been added and removed TWICE, both times proved dead by a mutation run — the
316
+ second time by the same person who wrote this sentence, four lines below it. If you are about
317
+ to add it again, mutate it first: replace ``manifest_once(...) or {}`` and watch nothing
318
+ fail.
319
+
320
+ ``enabled=False`` fetches NOTHING and yields ``None`` — an operator who set
321
+ ``[update] check = false`` gets no egress from the explicit path either, which is what
322
+ makes README's "one network call of its own, suppressible" true. The caller already
323
+ reports ``None`` as "could not check", so honouring the opt-out costs no new branch.
229
324
  """
230
- manifest = _fetch(MANIFEST_URL) or {}
325
+ manifest = manifest_once(enabled=enabled) or {}
231
326
  latest = _parse(_text(manifest.get("latest")))
232
327
  current = _parse(installed)
233
- return latest is not None and current is not None and current >= latest
328
+ if latest is None or current is None:
329
+ return None
330
+ return current >= latest
234
331
 
235
332
 
236
333
  def run_update(*, cwd: Path | None = None, out=None) -> int:
@@ -270,7 +367,7 @@ def run_update(*, cwd: Path | None = None, out=None) -> int:
270
367
  if method.command is None:
271
368
  print(
272
369
  "[syncade] could not determine how syncade was installed, so nothing was run.\n"
273
- " If you used pip: pip install -U syncade",
370
+ f" If you used pip: {sys.executable} -m pip install -U syncade",
274
371
  file=out,
275
372
  )
276
373
  return WORKTREE_ERROR
@@ -311,18 +408,55 @@ def run_update(*, cwd: Path | None = None, out=None) -> int:
311
408
  # first version of this guard and it told an operator who was simply already current
312
409
  # that their install was pinned and broken — a fresh false claim inside the fix for a
313
410
  # false claim, caught by testing the path instead of reasoning about it.
314
- if _is_newest(now):
411
+ # The opt-out reaches HERE too, not just the startup notice: `[update] check = false`
412
+ # means no manifest request from any path, which is the documented contract.
413
+ from syncade.cli.update_notice import _check_enabled
414
+
415
+ # CI is honoured here for the same reason check_for_update honours it: no human is
416
+ # reading the output, so a manifest request adds no value and contradicts the
417
+ # documented "also skipped whenever CI is set" guarantee for [update] check.
418
+ newest = _is_newest(now, enabled=_check_enabled(cwd) and not bool(os.environ.get("CI")))
419
+ if newest:
315
420
  skills_ok = _resync_skills(out)
316
421
  print(f"[syncade] already up to date ({now}).", file=out)
317
422
  return SUCCESS if skills_ok else WORKTREE_ERROR
318
- print(
319
- f"[syncade] the upgrade command succeeded but syncade is still "
320
- f"{syncade.__version__} — nothing was installed.\n"
321
- f" Most often the install pins a version: check `uv tool list` / "
322
- f"`pipx list`, then\n"
323
- f" reinstall unpinned (e.g. `uv tool install --force syncade`).",
324
- file=out,
325
- )
423
+ if newest is None:
424
+ # Cannot read the manifest, so whether an upgrade was even due is unknown. Rendering
425
+ # the pin diagnosis here sends the operator hunting a pin that may not exist —
426
+ # measured on macOS framework Python, where a missing CA bundle fails TLS in 0.05s
427
+ # and leaves every update check silently dead. The manifest may also be reachable
428
+ # but malformed; either way, the CA remedy is a conditional suggestion, not a
429
+ # diagnosis.
430
+ print(
431
+ f"[syncade] syncade is still {now}, and the update manifest could not be\n"
432
+ " read, so whether that is correct is unknown. Compare your version\n"
433
+ " against https://github.com/syncade-ai/syncade-ai/releases\n"
434
+ " On macOS python.org builds a missing CA bundle can cause this —\n"
435
+ " run `Install Certificates.command` from your Python's Applications\n"
436
+ " folder if you see TLS errors.",
437
+ file=out,
438
+ )
439
+ return WORKTREE_ERROR
440
+ if method.kind == "pip":
441
+ user_flag = " --user" if _in_user_site() else ""
442
+ print(
443
+ f"[syncade] the upgrade command succeeded but syncade is still "
444
+ f"{syncade.__version__} — nothing was installed.\n"
445
+ " Most often a resolver constraint or pinned requirement prevented "
446
+ "the upgrade.\n"
447
+ " Reinstall unpinned: "
448
+ f"{sys.executable} -m pip install -U{user_flag} --force-reinstall syncade",
449
+ file=out,
450
+ )
451
+ else:
452
+ print(
453
+ f"[syncade] the upgrade command succeeded but syncade is still "
454
+ f"{syncade.__version__} — nothing was installed.\n"
455
+ f" Most often the install pins a version: check `uv tool list` / "
456
+ f"`pipx list`, then\n"
457
+ f" reinstall unpinned (e.g. `uv tool install --force syncade`).",
458
+ file=out,
459
+ )
326
460
  return WORKTREE_ERROR
327
461
 
328
462
  skills_ok = _resync_skills(out)
@@ -355,8 +355,9 @@ class SyncadeConfig(BaseModel):
355
355
  # Run-artifact retention (PR-v2-9). Defaults reproduce gc.DEFAULT_KEEP / DEFAULT_MAX_AGE_DAYS;
356
356
  # governs BOTH the per-loop auto-prune and an explicit ``syncade --gc``.
357
357
  gc: GcConfig = Field(default_factory=GcConfig)
358
- # One-key off switch for the once-per-session update check (PR-h-field-07). The check
359
- # only ever prints — it cannot block a run or change an exit code.
358
+ # One-key opt-out for update manifest fetches (PR-h-field-07). Suppresses background
359
+ # check, --update, and --doctor's manifest row when false. --doctor's red manifest row
360
+ # exits 60; the background check only prints, silent errors.
360
361
  update: UpdateConfig = Field(default_factory=UpdateConfig)
361
362
  # Where per-run git worktrees are provisioned (PR-v2-9). A single top-level value (not a
362
363
  # ``[worktree]`` block — Q5); default reproduces worktree.DEFAULT_WORKTREE_BASE.
@@ -19,11 +19,12 @@ class UpdateConfig(BaseModel):
19
19
  check: bool = Field(
20
20
  default=True,
21
21
  description=(
22
- "Check once per session whether a newer syncade is published (default true). One "
23
- "HTTPS GET to the public repo's update manifest, 1.5s timeout, at the first syncade "
24
- "invocation in a window and never again in that window. It only ever PRINTS: it "
25
- "cannot block a run, change an exit code, or fail one — every error is silent. "
26
- "Nothing about your repo, diff, run, or identity is sent. Also skipped whenever CI "
22
+ "Enable update checks (default true). An HTTPS GET to the public repo's update "
23
+ "manifest, 1.5s timeout. Nothing about your repo, diff, run, or identity is sent. "
24
+ "Set false to suppress all fetches — background, --update, and --doctor. "
25
+ "Background: fires at most once per session window, only prints, every error is "
26
+ "silent. Explicit (--update, --doctor): each makes its own fetch per process; "
27
+ "--doctor exits 60 when the manifest is unreachable. Also skipped whenever CI "
27
28
  "is set."
28
29
  ),
29
30
  )
@@ -37,15 +37,16 @@ from syncade.config import SyncadeConfig
37
37
  from syncade.config_auth import ALL_BLOCKS
38
38
 
39
39
  # Re-exported for import stability after the PR-h-field-01 split (Decomposition Rule): callers
40
- # keep importing these from `syncade.doctor`. MONKEYPATCH TARGETS ARE `doctor_env`, NOT
41
- # here — these names are bindings, and rebinding one does not change what the function
42
- # bodies in doctor_env read.
40
+ # keep importing these from `syncade.doctor`. MONKEYPATCH TARGETS ARE `syncade.doctor`, NOT
41
+ # `doctor_env` — `collect_checks()` calls these names through the binding HERE, so patching
42
+ # `doctor_env` does not affect what the function body reads.
43
43
  from syncade.doctor_env import ( # noqa: F401
44
44
  _CLI_LAUNCH_TIMEOUT,
45
45
  _MIN_FREE_DISK_BYTES,
46
46
  _PROVIDER_CLI,
47
47
  _check_config,
48
48
  _check_provider_clis,
49
+ _check_update_manifest,
49
50
  _check_worktree_root,
50
51
  _configured_providers,
51
52
  _probe_cli_launch,
@@ -330,6 +331,10 @@ def collect_checks(
330
331
  check_cost(config, repo_root, max_rounds=max_rounds),
331
332
  check_budget(config),
332
333
  ]
334
+ # The update-manifest check is informational: an unreachable manifest does not refuse a real
335
+ # run, so it must NOT be in `cheap` (whose reds gate the live legs). It is run after the
336
+ # gating decision, and its red status does not suppress auth or producer-commit.
337
+ update_check = _check_update_manifest(enabled=config.update.check)
333
338
  checks = list(cheap)
334
339
  if quick:
335
340
  checks.append(DoctorCheck("auth", _SKIP, "skipped (--quick): credential probe not run"))
@@ -355,6 +360,9 @@ def collect_checks(
355
360
  )
356
361
  else:
357
362
  checks.append(_check_producer_commit(config, timeout_seconds=timeout_seconds))
363
+ # Append the update-manifest check after the live-legs decision: it is informational and
364
+ # its red status must not appear to gate the live legs (which were decided above without it).
365
+ checks.append(update_check)
358
366
  return checks
359
367
 
360
368
 
@@ -216,3 +216,50 @@ def _check_worktree_root(worktree_base: Path) -> DoctorCheck:
216
216
  fix="free up disk space; each reviewer/producer/test leg checks out a worktree",
217
217
  )
218
218
  return DoctorCheck("worktree", _OK, f"{probe_dir} writable, {free_gib:.1f} GiB free")
219
+
220
+
221
+ def _check_update_manifest(*, enabled: bool = True) -> DoctorCheck:
222
+ """Can this machine reach the update manifest at all?
223
+
224
+ A red row here does not affect a review — the background check is silent by design and must
225
+ never slow or fail a run. It is worth a row because its failure is INVISIBLE everywhere else:
226
+ an unreachable manifest and an up-to-date syncade look identical, forever, so an operator can
227
+ sit years behind believing they are current. Doctor exists to surface exactly that kind of
228
+ quietly-broken setup for $0.
229
+
230
+ Measured cause on the reporting machine: python.org's macOS framework build ships no CA
231
+ bundle, its `openssl_cafile` points at a `cert.pem` that does not exist, and TLS verification
232
+ fails in 0.05s. Not a timeout — no timeout value would have changed it.
233
+
234
+ Costs one bounded GET (1.5s ceiling, fails open), the same request the first run of any
235
+ session already makes, so this adds no capability doctor did not already exercise.
236
+
237
+ Skipped (with a SKIP row, not red) when ``enabled`` is False or when the ``CI`` environment
238
+ variable is set — the same gate ``check_for_update`` applies to the background notice.
239
+ A disabled check cannot be meaningfully diagnosed, and CI environments rarely have a human
240
+ to act on the result.
241
+ """
242
+ import os
243
+
244
+ from syncade.update_check import _parse, _text, manifest_once
245
+
246
+ if not enabled or os.environ.get("CI"):
247
+ return DoctorCheck(
248
+ "update check",
249
+ _SKIP,
250
+ "skipped: update check is disabled (check=false or CI is set)",
251
+ )
252
+ manifest = manifest_once(enabled=True) # `enabled` already gated above
253
+ if manifest is None:
254
+ return DoctorCheck(
255
+ "update check",
256
+ _RED,
257
+ "cannot reach the update manifest — new releases will never be announced",
258
+ "on macOS python.org builds, run `Install Certificates.command` from your "
259
+ "Python's Applications folder; otherwise check network/proxy access to "
260
+ "raw.githubusercontent.com",
261
+ )
262
+ latest = _text(manifest.get("latest"))
263
+ if _parse(latest) is None:
264
+ return DoctorCheck("update check", _RED, f"manifest reachable but unusable: {latest!r}")
265
+ return DoctorCheck("update check", _OK, f"reachable; latest release is {latest}")
@@ -18,11 +18,13 @@ could stop a run would be a remote kill switch, and a cheap one — one JSON lin
18
18
  write plus a release plus a publish, and needing no action from the operator to take effect.
19
19
  Print-only means the worst a compromised manifest can do is lie, which is visible and survivable.
20
20
 
21
- **It pings ONCE PER SESSION.** Not once per invocation and not once per N hours: the operator's
22
- rule is that a later invocation in the same window must not re-ping. The session is marked
23
- checked BEFORE the fetch, deliberately — marking after a *successful* fetch would make a flaky
24
- network cost a 1.5s stall on every invocation for the whole session, where marking first costs
25
- at most one missed notice.
21
+ **The background check pings ONCE PER SESSION via ``check_for_update``.** Not once per
22
+ invocation and not once per N hours: the operator's rule is that a later invocation in the same
23
+ window must not re-ping. The session is marked checked BEFORE the fetch, deliberately — marking
24
+ after a *successful* fetch would make a flaky network cost a 1.5s stall on every invocation for
25
+ the whole session, where marking first costs at most one missed notice. Explicit operator
26
+ invocations (``--update``, ``--doctor``) bypass the session gate and make their own fetch — they
27
+ are diagnostic tools, not background checks, and the docs say so.
26
28
 
27
29
  Severity cannot come from PyPI's JSON, which has no such field. It comes from a manifest we host,
28
30
  so a version can be marked critical the day a hole is found rather than when the fix ships.
@@ -239,6 +241,36 @@ def _exclusive_check_and_mark(path: Path, key: str) -> bool:
239
241
  return _mark_checked(path, key)
240
242
 
241
243
 
244
+ #: Sentinel distinguishing "not fetched yet" from a fetch that legitimately returned ``None``.
245
+ _UNFETCHED = object()
246
+ _manifest_cache: object = _UNFETCHED
247
+
248
+
249
+ def manifest_once(*, enabled: bool) -> dict | None:
250
+ """The update manifest, fetched AT MOST ONCE per process. ``None`` when disabled or unreadable.
251
+
252
+ THE single egress point for the manifest. Three callers want it — the startup notice,
253
+ ``--update`` and ``--doctor`` — and before this each fetched independently, so a CLI
254
+ ``--doctor`` made two requests and ``--update`` fetched even with ``[update] check = false``.
255
+ That made `README`'s "one network call of its own, suppressible" FALSE, which is a published
256
+ privacy claim rather than a tidiness point. A blind panel raised it in all four rounds of one
257
+ dogfood and three producer attempts each fixed a different facet, because the fix is to remove
258
+ the duplication, not to patch each site.
259
+
260
+ ``enabled`` is a PARAMETER, not read here: this module is a stdlib-only leaf and importing the
261
+ config surface to answer it would end that. Callers hold the config; this holds the socket.
262
+
263
+ A failed fetch is cached too. A syncade process is short-lived, and retrying inside one would
264
+ reintroduce exactly the multiplication this exists to prevent.
265
+ """
266
+ global _manifest_cache
267
+ if not enabled:
268
+ return None
269
+ if _manifest_cache is _UNFETCHED:
270
+ _manifest_cache = _fetch(MANIFEST_URL)
271
+ return _manifest_cache
272
+
273
+
242
274
  def check_for_update(installed: str, *, enabled: bool = True) -> UpdateNotice | None:
243
275
  """One ping per session, or ``None``. Never raises, never blocks, never touches exit codes.
244
276
 
@@ -255,7 +287,7 @@ def check_for_update(installed: str, *, enabled: bool = True) -> UpdateNotice |
255
287
  # first invocations from all fetching simultaneously.
256
288
  if not _exclusive_check_and_mark(path, key):
257
289
  return None
258
- return evaluate(_fetch(MANIFEST_URL), installed)
290
+ return evaluate(manifest_once(enabled=True), installed)
259
291
  except Exception: # noqa: BLE001
260
292
  # The outermost guarantee, not a duplicate of the inner ones: `Path.home()` raises
261
293
  # RuntimeError with no $HOME and no passwd entry, and this runs on the review path where