syncade 0.7.0__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 (483) hide show
  1. {syncade-0.7.0/src/syncade.egg-info → syncade-0.7.3}/PKG-INFO +28 -4
  2. {syncade-0.7.0 → syncade-0.7.3}/README.md +27 -3
  3. {syncade-0.7.0 → syncade-0.7.3}/docs/config-reference.md +13 -6
  4. {syncade-0.7.0 → syncade-0.7.3}/pyproject.toml +1 -1
  5. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/__init__.py +1 -1
  6. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/parser.py +2 -2
  7. syncade-0.7.3/src/syncade/cli/update_mode.py +473 -0
  8. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/config.py +3 -2
  9. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/config_update.py +6 -5
  10. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/doctor.py +11 -3
  11. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/doctor_env.py +47 -0
  12. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/update_check.py +38 -6
  13. {syncade-0.7.0 → syncade-0.7.3/src/syncade.egg-info}/PKG-INFO +28 -4
  14. {syncade-0.7.0 → syncade-0.7.3}/src/syncade.egg-info/SOURCES.txt +1 -0
  15. {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_documented_flags_exist.py +8 -0
  16. {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_update_mode.py +161 -3
  17. syncade-0.7.3/tests/cli/test_update_post_condition.py +656 -0
  18. {syncade-0.7.0 → syncade-0.7.3}/tests/conftest.py +16 -0
  19. {syncade-0.7.0 → syncade-0.7.3}/tests/doctor/conftest.py +10 -0
  20. {syncade-0.7.0 → syncade-0.7.3}/tests/test_update_check.py +10 -0
  21. syncade-0.7.0/src/syncade/cli/update_mode.py +0 -226
  22. {syncade-0.7.0 → syncade-0.7.3}/LICENSE +0 -0
  23. {syncade-0.7.0 → syncade-0.7.3}/MANIFEST.in +0 -0
  24. {syncade-0.7.0 → syncade-0.7.3}/docs/how-to-use.md +0 -0
  25. {syncade-0.7.0 → syncade-0.7.3}/setup.cfg +0 -0
  26. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/__main__.py +0 -0
  27. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/adapters/__init__.py +0 -0
  28. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/adapters/anthropic.py +0 -0
  29. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/adapters/base.py +0 -0
  30. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/adapters/fake.py +0 -0
  31. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/adapters/fake_common.py +0 -0
  32. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/adapters/fake_producer_audit_draft.py +0 -0
  33. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/adapters/fake_reviewer_synth.py +0 -0
  34. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/adapters/openai.py +0 -0
  35. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/adapters/openai_parsing.py +0 -0
  36. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/adapters/producer.py +0 -0
  37. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/adapters/producer_anthropic.py +0 -0
  38. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/adapters/producer_openai.py +0 -0
  39. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/adapters/registry.py +0 -0
  40. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/auth_check.py +0 -0
  41. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/auth_preflight.py +0 -0
  42. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/base_resolution.py +0 -0
  43. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/billing.py +0 -0
  44. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/checks_config.py +0 -0
  45. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/__init__.py +0 -0
  46. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/auth_gate.py +0 -0
  47. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/config_keys.py +0 -0
  48. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/config_list.py +0 -0
  49. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/config_menu_rows.py +0 -0
  50. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/config_mode.py +0 -0
  51. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/config_overrides.py +0 -0
  52. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/config_tui.py +0 -0
  53. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/doctor_mode.py +0 -0
  54. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/gc_mode.py +0 -0
  55. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/install_skill.py +0 -0
  56. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/metrics_mode.py +0 -0
  57. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/modes.py +0 -0
  58. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/parser_types.py +0 -0
  59. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/paths.py +0 -0
  60. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/preflight_paths.py +0 -0
  61. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/resolve.py +0 -0
  62. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/resume_mode.py +0 -0
  63. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/skill_status.py +0 -0
  64. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/toml_writer.py +0 -0
  65. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/update_notice.py +0 -0
  66. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/validate.py +0 -0
  67. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/config_auth.py +0 -0
  68. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/config_cold.py +0 -0
  69. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/config_gc.py +0 -0
  70. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/config_loader.py +0 -0
  71. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/config_loop.py +0 -0
  72. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/config_producer.py +0 -0
  73. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/config_retry.py +0 -0
  74. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/config_types.py +0 -0
  75. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/diff_filter.py +0 -0
  76. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/dispatcher.py +0 -0
  77. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/doctor_preview.py +0 -0
  78. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/doctor_types.py +0 -0
  79. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/exit_codes.py +0 -0
  80. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/findings.py +0 -0
  81. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/findings_json.py +0 -0
  82. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/gc.py +0 -0
  83. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/gc_execute.py +0 -0
  84. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/gc_protection.py +0 -0
  85. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/gc_types.py +0 -0
  86. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/gc_worktrees.py +0 -0
  87. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/git_object_id.py +0 -0
  88. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/git_preconditions.py +0 -0
  89. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/logging.py +0 -0
  90. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/metrics/__init__.py +0 -0
  91. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/metrics/aggregate.py +0 -0
  92. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/metrics/findings.py +0 -0
  93. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/metrics/schema.py +0 -0
  94. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/__init__.py +0 -0
  95. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/_runs_dir.py +0 -0
  96. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/branch_advance.py +0 -0
  97. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/branch_guard.py +0 -0
  98. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/budget.py +0 -0
  99. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/escalation_coverage.py +0 -0
  100. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/loop.py +0 -0
  101. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/loop_dispatch_check.py +0 -0
  102. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/loop_finalize.py +0 -0
  103. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/loop_preflight.py +0 -0
  104. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/loop_resume.py +0 -0
  105. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/loop_rmtree.py +0 -0
  106. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/loop_round_step.py +0 -0
  107. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/prior_round.py +0 -0
  108. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/producer_phase.py +0 -0
  109. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/results.py +0 -0
  110. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/resume.py +0 -0
  111. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/resume_load.py +0 -0
  112. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/resume_plan.py +0 -0
  113. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/resume_target.py +0 -0
  114. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/resume_types.py +0 -0
  115. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/reviewer_template_failure.py +0 -0
  116. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/round.py +0 -0
  117. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/round_checks.py +0 -0
  118. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/round_no_changes.py +0 -0
  119. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/round_predispatch.py +0 -0
  120. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/verdict.py +0 -0
  121. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/__init__.py +0 -0
  122. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/_atomic.py +0 -0
  123. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/_clusters.py +0 -0
  124. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/_findings_verdict.py +0 -0
  125. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/_markdown.py +0 -0
  126. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/_validation.py +0 -0
  127. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/checks.py +0 -0
  128. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/decision_needed.py +0 -0
  129. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/findings_md.py +0 -0
  130. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/handoff.py +0 -0
  131. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/handoff_classify.py +0 -0
  132. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/last_reviewed.py +0 -0
  133. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/loop_manifest.py +0 -0
  134. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/loop_summary.py +0 -0
  135. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/loop_summary_text.py +0 -0
  136. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/producer.py +0 -0
  137. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/reviewer.py +0 -0
  138. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/round_manifest.py +0 -0
  139. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/run_init.py +0 -0
  140. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/run_summary.py +0 -0
  141. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/run_summary_next_steps.py +0 -0
  142. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/synth.py +0 -0
  143. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/test_run.py +0 -0
  144. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/presets.py +0 -0
  145. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/pricing_config.py +0 -0
  146. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/process.py +0 -0
  147. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/producer.py +0 -0
  148. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/producer_attempt.py +0 -0
  149. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/producer_escalation.py +0 -0
  150. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/producer_git.py +0 -0
  151. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/producer_result.py +0 -0
  152. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/prompts.py +0 -0
  153. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/prompts_loader.py +0 -0
  154. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/retry.py +0 -0
  155. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/run_inputs.py +0 -0
  156. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/run_status.py +0 -0
  157. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/selfcheck.py +0 -0
  158. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/skills/claude/README.md +0 -0
  159. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/skills/claude/SKILL.md +0 -0
  160. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/skills/codex/README.md +0 -0
  161. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/skills/codex/SKILL.md +0 -0
  162. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/snapshot.py +0 -0
  163. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/spec_audit.py +0 -0
  164. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/spec_audit_schema.py +0 -0
  165. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/spec_draft.py +0 -0
  166. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/spec_source.py +0 -0
  167. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/synthesis.py +0 -0
  168. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/synthesis_clusters.py +0 -0
  169. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/synthesis_repair.py +0 -0
  170. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/synthesis_schema.py +0 -0
  171. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/synthesizer/__init__.py +0 -0
  172. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/synthesizer/constants.py +0 -0
  173. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/synthesizer/driver.py +0 -0
  174. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/synthesizer/rendering.py +0 -0
  175. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/synthesizer/result.py +0 -0
  176. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/synthesizer/validation.py +0 -0
  177. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/synthesizer/workspace.py +0 -0
  178. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/templates/presets/balanced.toml +0 -0
  179. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/templates/presets/cheap.toml +0 -0
  180. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/templates/presets/thorough.toml +0 -0
  181. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/templates/producer.md +0 -0
  182. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/templates/reviewer.md +0 -0
  183. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/templates/reviewer_adversarial.md +0 -0
  184. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/templates/reviewer_codex.md +0 -0
  185. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/templates/spec_audit.md +0 -0
  186. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/templates/spec_draft.md +0 -0
  187. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/templates/synthesizer.md +0 -0
  188. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/test_runner.py +0 -0
  189. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/test_runner_classify.py +0 -0
  190. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/transcript.py +0 -0
  191. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/usage.py +0 -0
  192. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/worktree.py +0 -0
  193. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/worktree_env.py +0 -0
  194. {syncade-0.7.0 → syncade-0.7.3}/src/syncade/worktree_paths.py +0 -0
  195. {syncade-0.7.0 → syncade-0.7.3}/src/syncade.egg-info/dependency_links.txt +0 -0
  196. {syncade-0.7.0 → syncade-0.7.3}/src/syncade.egg-info/entry_points.txt +0 -0
  197. {syncade-0.7.0 → syncade-0.7.3}/src/syncade.egg-info/requires.txt +0 -0
  198. {syncade-0.7.0 → syncade-0.7.3}/src/syncade.egg-info/top_level.txt +0 -0
  199. {syncade-0.7.0 → syncade-0.7.3}/tests/__init__.py +0 -0
  200. {syncade-0.7.0 → syncade-0.7.3}/tests/adapters/__init__.py +0 -0
  201. {syncade-0.7.0 → syncade-0.7.3}/tests/adapters/_anthropic_helpers.py +0 -0
  202. {syncade-0.7.0 → syncade-0.7.3}/tests/adapters/_openai_helpers.py +0 -0
  203. {syncade-0.7.0 → syncade-0.7.3}/tests/adapters/test_anthropic.py +0 -0
  204. {syncade-0.7.0 → syncade-0.7.3}/tests/adapters/test_anthropic_more.py +0 -0
  205. {syncade-0.7.0 → syncade-0.7.3}/tests/adapters/test_openai.py +0 -0
  206. {syncade-0.7.0 → syncade-0.7.3}/tests/adapters/test_openai_more.py +0 -0
  207. {syncade-0.7.0 → syncade-0.7.3}/tests/adapters/test_producer.py +0 -0
  208. {syncade-0.7.0 → syncade-0.7.3}/tests/adapters/test_producer_more.py +0 -0
  209. {syncade-0.7.0 → syncade-0.7.3}/tests/adapters/test_prompt_transport.py +0 -0
  210. {syncade-0.7.0 → syncade-0.7.3}/tests/auth_check/__init__.py +0 -0
  211. {syncade-0.7.0 → syncade-0.7.3}/tests/auth_check/test_probes.py +0 -0
  212. {syncade-0.7.0 → syncade-0.7.3}/tests/auth_check/test_run_auth_check.py +0 -0
  213. {syncade-0.7.0 → syncade-0.7.3}/tests/cli/__init__.py +0 -0
  214. {syncade-0.7.0 → syncade-0.7.3}/tests/cli/_helpers.py +0 -0
  215. {syncade-0.7.0 → syncade-0.7.3}/tests/cli/_undo_helpers.py +0 -0
  216. {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_basic_and_mutex.py +0 -0
  217. {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_cli_library_divergence.py +0 -0
  218. {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_config_keys.py +0 -0
  219. {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_config_list.py +0 -0
  220. {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_config_menu_rows.py +0 -0
  221. {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_config_mode.py +0 -0
  222. {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_config_overrides.py +0 -0
  223. {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_config_tui.py +0 -0
  224. {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_config_tui_pty.py +0 -0
  225. {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_documented_exit_codes.py +0 -0
  226. {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_flags.py +0 -0
  227. {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_gc.py +0 -0
  228. {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_help_retention_contract.py +0 -0
  229. {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_mode_exclusivity_matrix.py +0 -0
  230. {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_mode_flag_guards.py +0 -0
  231. {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_resume_reports_hard_kill.py +0 -0
  232. {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_run_and_config.py +0 -0
  233. {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_run_status_cli.py +0 -0
  234. {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_scope_openspec_draft.py +0 -0
  235. {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_toml_writer.py +0 -0
  236. {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_undo_auto_init.py +0 -0
  237. {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_undo_scope.py +0 -0
  238. {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_update_notice.py +0 -0
  239. {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_validate_before_mutate.py +0 -0
  240. {syncade-0.7.0 → syncade-0.7.3}/tests/config/__init__.py +0 -0
  241. {syncade-0.7.0 → syncade-0.7.3}/tests/config/test_auth_enforcement.py +0 -0
  242. {syncade-0.7.0 → syncade-0.7.3}/tests/config/test_auth_modes.py +0 -0
  243. {syncade-0.7.0 → syncade-0.7.3}/tests/config/test_auth_preflight.py +0 -0
  244. {syncade-0.7.0 → syncade-0.7.3}/tests/config/test_auth_report_scope.py +0 -0
  245. {syncade-0.7.0 → syncade-0.7.3}/tests/config/test_budget_config.py +0 -0
  246. {syncade-0.7.0 → syncade-0.7.3}/tests/config/test_cold_actors.py +0 -0
  247. {syncade-0.7.0 → syncade-0.7.3}/tests/config/test_config_checks.py +0 -0
  248. {syncade-0.7.0 → syncade-0.7.3}/tests/config/test_config_reference_drift.py +0 -0
  249. {syncade-0.7.0 → syncade-0.7.3}/tests/config/test_config_schema.py +0 -0
  250. {syncade-0.7.0 → syncade-0.7.3}/tests/config/test_gc_config.py +0 -0
  251. {syncade-0.7.0 → syncade-0.7.3}/tests/config/test_global_config_layer.py +0 -0
  252. {syncade-0.7.0 → syncade-0.7.3}/tests/config/test_presets.py +0 -0
  253. {syncade-0.7.0 → syncade-0.7.3}/tests/config/test_retry_config.py +0 -0
  254. {syncade-0.7.0 → syncade-0.7.3}/tests/config/test_reviewer_config.py +0 -0
  255. {syncade-0.7.0 → syncade-0.7.3}/tests/config/test_reviewer_permissions.py +0 -0
  256. {syncade-0.7.0 → syncade-0.7.3}/tests/config/test_worktree_base_config.py +0 -0
  257. {syncade-0.7.0 → syncade-0.7.3}/tests/dispatcher/__init__.py +0 -0
  258. {syncade-0.7.0 → syncade-0.7.3}/tests/dispatcher/_helpers.py +0 -0
  259. {syncade-0.7.0 → syncade-0.7.3}/tests/dispatcher/test_dispatch_core.py +0 -0
  260. {syncade-0.7.0 → syncade-0.7.3}/tests/dispatcher/test_dispatch_misc.py +0 -0
  261. {syncade-0.7.0 → syncade-0.7.3}/tests/dispatcher/test_dispatch_usage.py +0 -0
  262. {syncade-0.7.0 → syncade-0.7.3}/tests/doctor/__init__.py +0 -0
  263. {syncade-0.7.0 → syncade-0.7.3}/tests/doctor/_helpers.py +0 -0
  264. {syncade-0.7.0 → syncade-0.7.3}/tests/doctor/test_doctor.py +0 -0
  265. {syncade-0.7.0 → syncade-0.7.3}/tests/doctor/test_doctor_diff_size.py +0 -0
  266. {syncade-0.7.0 → syncade-0.7.3}/tests/doctor/test_doctor_malformed_diff.py +0 -0
  267. {syncade-0.7.0 → syncade-0.7.3}/tests/doctor/test_doctor_preview.py +0 -0
  268. {syncade-0.7.0 → syncade-0.7.3}/tests/findings/__init__.py +0 -0
  269. {syncade-0.7.0 → syncade-0.7.3}/tests/findings/_helpers.py +0 -0
  270. {syncade-0.7.0 → syncade-0.7.3}/tests/findings/test_parser_and_fields.py +0 -0
  271. {syncade-0.7.0 → syncade-0.7.3}/tests/findings/test_parser_resilience.py +0 -0
  272. {syncade-0.7.0 → syncade-0.7.3}/tests/findings/test_verdict_block_selection.py +0 -0
  273. {syncade-0.7.0 → syncade-0.7.3}/tests/gc/__init__.py +0 -0
  274. {syncade-0.7.0 → syncade-0.7.3}/tests/gc/_helpers.py +0 -0
  275. {syncade-0.7.0 → syncade-0.7.3}/tests/gc/test_autoprune.py +0 -0
  276. {syncade-0.7.0 → syncade-0.7.3}/tests/gc/test_execute.py +0 -0
  277. {syncade-0.7.0 → syncade-0.7.3}/tests/gc/test_plan.py +0 -0
  278. {syncade-0.7.0 → syncade-0.7.3}/tests/gc/test_plan_execution_races.py +0 -0
  279. {syncade-0.7.0 → syncade-0.7.3}/tests/gc/test_reaping_scope.py +0 -0
  280. {syncade-0.7.0 → syncade-0.7.3}/tests/gc/test_retention_tiers.py +0 -0
  281. {syncade-0.7.0 → syncade-0.7.3}/tests/gc/test_robustness.py +0 -0
  282. {syncade-0.7.0 → syncade-0.7.3}/tests/gc/test_self_orphan_protection.py +0 -0
  283. {syncade-0.7.0 → syncade-0.7.3}/tests/gc/test_smoke.py +0 -0
  284. {syncade-0.7.0 → syncade-0.7.3}/tests/gc/test_two_tier_retention.py +0 -0
  285. {syncade-0.7.0 → syncade-0.7.3}/tests/gc/test_worktree_age_bound.py +0 -0
  286. {syncade-0.7.0 → syncade-0.7.3}/tests/logging/__init__.py +0 -0
  287. {syncade-0.7.0 → syncade-0.7.3}/tests/logging/_helpers.py +0 -0
  288. {syncade-0.7.0 → syncade-0.7.3}/tests/logging/test_logging.py +0 -0
  289. {syncade-0.7.0 → syncade-0.7.3}/tests/logging/test_logging_summary.py +0 -0
  290. {syncade-0.7.0 → syncade-0.7.3}/tests/metrics/conftest.py +0 -0
  291. {syncade-0.7.0 → syncade-0.7.3}/tests/metrics/test_actor_spend.py +0 -0
  292. {syncade-0.7.0 → syncade-0.7.3}/tests/metrics/test_aggregate.py +0 -0
  293. {syncade-0.7.0 → syncade-0.7.3}/tests/metrics/test_billing_reconciliation.py +0 -0
  294. {syncade-0.7.0 → syncade-0.7.3}/tests/metrics/test_billing_surfaces_agree.py +0 -0
  295. {syncade-0.7.0 → syncade-0.7.3}/tests/metrics/test_budget_metrics.py +0 -0
  296. {syncade-0.7.0 → syncade-0.7.3}/tests/metrics/test_consensus_report.py +0 -0
  297. {syncade-0.7.0 → syncade-0.7.3}/tests/metrics/test_cost_honesty.py +0 -0
  298. {syncade-0.7.0 → syncade-0.7.3}/tests/metrics/test_findings_capture.py +0 -0
  299. {syncade-0.7.0 → syncade-0.7.3}/tests/metrics/test_metrics_cli.py +0 -0
  300. {syncade-0.7.0 → syncade-0.7.3}/tests/metrics/test_metrics_mode.py +0 -0
  301. {syncade-0.7.0 → syncade-0.7.3}/tests/metrics/test_schema.py +0 -0
  302. {syncade-0.7.0 → syncade-0.7.3}/tests/metrics/test_unknown_panel.py +0 -0
  303. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/__init__.py +0 -0
  304. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/_helpers.py +0 -0
  305. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/_resume_fixtures.py +0 -0
  306. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/conftest.py +0 -0
  307. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_budget.py +0 -0
  308. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_budget_resume_e2e.py +0 -0
  309. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_budget_stop_message.py +0 -0
  310. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_budget_warning.py +0 -0
  311. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_checks_and_recording.py +0 -0
  312. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_contracts.py +0 -0
  313. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_cross_round.py +0 -0
  314. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_cross_round_more.py +0 -0
  315. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_default_branch_guard.py +0 -0
  316. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_diff_size_cap.py +0 -0
  317. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_dispatch_record.py +0 -0
  318. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_empty_diff.py +0 -0
  319. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_empty_diff_no_side_effects.py +0 -0
  320. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_escalation.py +0 -0
  321. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_last_reviewed_truthful.py +0 -0
  322. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_loop_audit_remediation.py +0 -0
  323. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_matrix_and_e2e.py +0 -0
  324. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_multiround.py +0 -0
  325. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_multiround_dirty.py +0 -0
  326. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_multiround_more.py +0 -0
  327. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_paraphrase_escalation.py +0 -0
  328. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_pipeline.py +0 -0
  329. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_provider_usage_limit.py +0 -0
  330. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_rename_boundary.py +0 -0
  331. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_resume.py +0 -0
  332. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_resume_base_oid.py +0 -0
  333. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_reviewer_blindness_is_structural.py +0 -0
  334. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_round_input_boundaries.py +0 -0
  335. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_run_layout.py +0 -0
  336. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_run_status_wiring.py +0 -0
  337. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_safety_warning_survives_quiet.py +0 -0
  338. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_snapshot_and_base.py +0 -0
  339. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_synth_phase.py +0 -0
  340. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_synth_phase_more.py +0 -0
  341. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_synth_provenance.py +0 -0
  342. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_synth_validation.py +0 -0
  343. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_template_robustness.py +0 -0
  344. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_termination_reason_drift.py +0 -0
  345. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_testleg.py +0 -0
  346. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_timeout_is_per_subprocess.py +0 -0
  347. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_usage_limit_message.py +0 -0
  348. {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_worktree_cleanup.py +0 -0
  349. {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/__init__.py +0 -0
  350. {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/_helpers.py +0 -0
  351. {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/test_atomic_writes.py +0 -0
  352. {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/test_checks_surfacing.py +0 -0
  353. {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/test_diff_size_recording.py +0 -0
  354. {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/test_findings_md.py +0 -0
  355. {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/test_findings_md_more.py +0 -0
  356. {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/test_findings_md_verdict.py +0 -0
  357. {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/test_handoff.py +0 -0
  358. {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/test_loop_manifest_checks.py +0 -0
  359. {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/test_loop_summary.py +0 -0
  360. {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/test_loop_summary_more.py +0 -0
  361. {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/test_md_safety.py +0 -0
  362. {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/test_next_steps.py +0 -0
  363. {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/test_producer_persist.py +0 -0
  364. {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/test_reviewer.py +0 -0
  365. {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/test_round_manifest.py +0 -0
  366. {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/test_run_init.py +0 -0
  367. {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/test_run_summary.py +0 -0
  368. {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/test_run_summary_more.py +0 -0
  369. {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/test_run_summary_producer.py +0 -0
  370. {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/test_synth_persist.py +0 -0
  371. {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/test_test_run.py +0 -0
  372. {syncade-0.7.0 → syncade-0.7.3}/tests/producer/__init__.py +0 -0
  373. {syncade-0.7.0 → syncade-0.7.3}/tests/producer/_helpers.py +0 -0
  374. {syncade-0.7.0 → syncade-0.7.3}/tests/producer/test_producer.py +0 -0
  375. {syncade-0.7.0 → syncade-0.7.3}/tests/producer/test_producer_confinement.py +0 -0
  376. {syncade-0.7.0 → syncade-0.7.3}/tests/producer/test_producer_retry.py +0 -0
  377. {syncade-0.7.0 → syncade-0.7.3}/tests/producer/test_producer_run.py +0 -0
  378. {syncade-0.7.0 → syncade-0.7.3}/tests/producer/test_producer_timeout.py +0 -0
  379. {syncade-0.7.0 → syncade-0.7.3}/tests/producer/test_producer_usage.py +0 -0
  380. {syncade-0.7.0 → syncade-0.7.3}/tests/prompts/__init__.py +0 -0
  381. {syncade-0.7.0 → syncade-0.7.3}/tests/prompts/test_load_template.py +0 -0
  382. {syncade-0.7.0 → syncade-0.7.3}/tests/prompts/test_misc.py +0 -0
  383. {syncade-0.7.0 → syncade-0.7.3}/tests/prompts/test_producer.py +0 -0
  384. {syncade-0.7.0 → syncade-0.7.3}/tests/prompts/test_render_synthesizer.py +0 -0
  385. {syncade-0.7.0 → syncade-0.7.3}/tests/prompts/test_synthesizer.py +0 -0
  386. {syncade-0.7.0 → syncade-0.7.3}/tests/resume/__init__.py +0 -0
  387. {syncade-0.7.0 → syncade-0.7.3}/tests/resume/_helpers.py +0 -0
  388. {syncade-0.7.0 → syncade-0.7.3}/tests/resume/test_budget_resume.py +0 -0
  389. {syncade-0.7.0 → syncade-0.7.3}/tests/resume/test_checks_phase_failure.py +0 -0
  390. {syncade-0.7.0 → syncade-0.7.3}/tests/resume/test_cli_malformed_rehydration.py +0 -0
  391. {syncade-0.7.0 → syncade-0.7.3}/tests/resume/test_find_resolve_plan.py +0 -0
  392. {syncade-0.7.0 → syncade-0.7.3}/tests/resume/test_plan_resume.py +0 -0
  393. {syncade-0.7.0 → syncade-0.7.3}/tests/resume/test_round_trip.py +0 -0
  394. {syncade-0.7.0 → syncade-0.7.3}/tests/resume/test_tree_drift_and_escalation.py +0 -0
  395. {syncade-0.7.0 → syncade-0.7.3}/tests/skills/test_config_section.py +0 -0
  396. {syncade-0.7.0 → syncade-0.7.3}/tests/skills/test_install_skill.py +0 -0
  397. {syncade-0.7.0 → syncade-0.7.3}/tests/skills/test_install_skill_multi_harness.py +0 -0
  398. {syncade-0.7.0 → syncade-0.7.3}/tests/skills/test_install_skill_ownership.py +0 -0
  399. {syncade-0.7.0 → syncade-0.7.3}/tests/skills/test_install_skill_refuses.py +0 -0
  400. {syncade-0.7.0 → syncade-0.7.3}/tests/skills/test_skill_drift.py +0 -0
  401. {syncade-0.7.0 → syncade-0.7.3}/tests/skills/test_skill_status.py +0 -0
  402. {syncade-0.7.0 → syncade-0.7.3}/tests/smoke/__init__.py +0 -0
  403. {syncade-0.7.0 → syncade-0.7.3}/tests/smoke/test_anthropic_smoke.py +0 -0
  404. {syncade-0.7.0 → syncade-0.7.3}/tests/smoke/test_auth_check_smoke.py +0 -0
  405. {syncade-0.7.0 → syncade-0.7.3}/tests/smoke/test_codex_smoke.py +0 -0
  406. {syncade-0.7.0 → syncade-0.7.3}/tests/smoke/test_loop_smoke.py +0 -0
  407. {syncade-0.7.0 → syncade-0.7.3}/tests/smoke/test_orchestrator_smoke.py +0 -0
  408. {syncade-0.7.0 → syncade-0.7.3}/tests/smoke/test_orchestrator_smoke_more.py +0 -0
  409. {syncade-0.7.0 → syncade-0.7.3}/tests/smoke/test_producer_smoke.py +0 -0
  410. {syncade-0.7.0 → syncade-0.7.3}/tests/smoke/test_resume_reap_smoke.py +0 -0
  411. {syncade-0.7.0 → syncade-0.7.3}/tests/smoke/test_selfcheck_smoke.py +0 -0
  412. {syncade-0.7.0 → syncade-0.7.3}/tests/smoke/test_synthesizer_smoke.py +0 -0
  413. {syncade-0.7.0 → syncade-0.7.3}/tests/smoke/test_test_runner_smoke.py +0 -0
  414. {syncade-0.7.0 → syncade-0.7.3}/tests/snapshot/__init__.py +0 -0
  415. {syncade-0.7.0 → syncade-0.7.3}/tests/snapshot/_helpers.py +0 -0
  416. {syncade-0.7.0 → syncade-0.7.3}/tests/snapshot/conftest.py +0 -0
  417. {syncade-0.7.0 → syncade-0.7.3}/tests/snapshot/test_snapshot.py +0 -0
  418. {syncade-0.7.0 → syncade-0.7.3}/tests/snapshot/test_snapshot_diff_identity.py +0 -0
  419. {syncade-0.7.0 → syncade-0.7.3}/tests/snapshot/test_snapshot_dirty.py +0 -0
  420. {syncade-0.7.0 → syncade-0.7.3}/tests/snapshot/test_snapshot_warning.py +0 -0
  421. {syncade-0.7.0 → syncade-0.7.3}/tests/snapshot/test_three_dot_diff.py +0 -0
  422. {syncade-0.7.0 → syncade-0.7.3}/tests/synthesis/__init__.py +0 -0
  423. {syncade-0.7.0 → syncade-0.7.3}/tests/synthesis/_helpers.py +0 -0
  424. {syncade-0.7.0 → syncade-0.7.3}/tests/synthesis/test_cluster_repair.py +0 -0
  425. {syncade-0.7.0 → syncade-0.7.3}/tests/synthesis/test_clusters.py +0 -0
  426. {syncade-0.7.0 → syncade-0.7.3}/tests/synthesis/test_models.py +0 -0
  427. {syncade-0.7.0 → syncade-0.7.3}/tests/synthesis/test_models_distinct_keying.py +0 -0
  428. {syncade-0.7.0 → syncade-0.7.3}/tests/synthesis/test_parsing.py +0 -0
  429. {syncade-0.7.0 → syncade-0.7.3}/tests/synthesis/test_unanimous_blocker_downgrade.py +0 -0
  430. {syncade-0.7.0 → syncade-0.7.3}/tests/synthesizer/test_provenance_fabrication.py +0 -0
  431. {syncade-0.7.0 → syncade-0.7.3}/tests/synthesizer/test_provenance_repair.py +0 -0
  432. {syncade-0.7.0 → syncade-0.7.3}/tests/test_adapter_registry.py +0 -0
  433. {syncade-0.7.0 → syncade-0.7.3}/tests/test_adapters_fake.py +0 -0
  434. {syncade-0.7.0 → syncade-0.7.3}/tests/test_auto_init_policy.py +0 -0
  435. {syncade-0.7.0 → syncade-0.7.3}/tests/test_base_resolution.py +0 -0
  436. {syncade-0.7.0 → syncade-0.7.3}/tests/test_check_loc_script.py +0 -0
  437. {syncade-0.7.0 → syncade-0.7.3}/tests/test_checks.py +0 -0
  438. {syncade-0.7.0 → syncade-0.7.3}/tests/test_config_loader.py +0 -0
  439. {syncade-0.7.0 → syncade-0.7.3}/tests/test_cross_model_claim.py +0 -0
  440. {syncade-0.7.0 → syncade-0.7.3}/tests/test_diff_filter.py +0 -0
  441. {syncade-0.7.0 → syncade-0.7.3}/tests/test_diff_filter_binary.py +0 -0
  442. {syncade-0.7.0 → syncade-0.7.3}/tests/test_diff_filter_quoted_paths.py +0 -0
  443. {syncade-0.7.0 → syncade-0.7.3}/tests/test_escalation_coverage.py +0 -0
  444. {syncade-0.7.0 → syncade-0.7.3}/tests/test_exit_codes.py +0 -0
  445. {syncade-0.7.0 → syncade-0.7.3}/tests/test_git_choke_point.py +0 -0
  446. {syncade-0.7.0 → syncade-0.7.3}/tests/test_git_config_isolation.py +0 -0
  447. {syncade-0.7.0 → syncade-0.7.3}/tests/test_git_preconditions.py +0 -0
  448. {syncade-0.7.0 → syncade-0.7.3}/tests/test_last_reviewed.py +0 -0
  449. {syncade-0.7.0 → syncade-0.7.3}/tests/test_parse_repair_extra_keys.py +0 -0
  450. {syncade-0.7.0 → syncade-0.7.3}/tests/test_pricing.py +0 -0
  451. {syncade-0.7.0 → syncade-0.7.3}/tests/test_private_identifier_gate.py +0 -0
  452. {syncade-0.7.0 → syncade-0.7.3}/tests/test_process.py +0 -0
  453. {syncade-0.7.0 → syncade-0.7.3}/tests/test_process_teardown.py +0 -0
  454. {syncade-0.7.0 → syncade-0.7.3}/tests/test_producer_escalation.py +0 -0
  455. {syncade-0.7.0 → syncade-0.7.3}/tests/test_regression_acme.py +0 -0
  456. {syncade-0.7.0 → syncade-0.7.3}/tests/test_retry.py +0 -0
  457. {syncade-0.7.0 → syncade-0.7.3}/tests/test_run_status.py +0 -0
  458. {syncade-0.7.0 → syncade-0.7.3}/tests/test_run_status_signals.py +0 -0
  459. {syncade-0.7.0 → syncade-0.7.3}/tests/test_runner/__init__.py +0 -0
  460. {syncade-0.7.0 → syncade-0.7.3}/tests/test_runner/test_test_runner.py +0 -0
  461. {syncade-0.7.0 → syncade-0.7.3}/tests/test_runner/test_test_runner_more.py +0 -0
  462. {syncade-0.7.0 → syncade-0.7.3}/tests/test_safety_disclosures_survive_quiet.py +0 -0
  463. {syncade-0.7.0 → syncade-0.7.3}/tests/test_selfcheck.py +0 -0
  464. {syncade-0.7.0 → syncade-0.7.3}/tests/test_spec_audit.py +0 -0
  465. {syncade-0.7.0 → syncade-0.7.3}/tests/test_spec_draft.py +0 -0
  466. {syncade-0.7.0 → syncade-0.7.3}/tests/test_spec_source.py +0 -0
  467. {syncade-0.7.0 → syncade-0.7.3}/tests/test_streamed_capture.py +0 -0
  468. {syncade-0.7.0 → syncade-0.7.3}/tests/test_strip_list_semantics.py +0 -0
  469. {syncade-0.7.0 → syncade-0.7.3}/tests/test_synthesizer.py +0 -0
  470. {syncade-0.7.0 → syncade-0.7.3}/tests/test_synthesizer_passthrough.py +0 -0
  471. {syncade-0.7.0 → syncade-0.7.3}/tests/test_synthesizer_provenance.py +0 -0
  472. {syncade-0.7.0 → syncade-0.7.3}/tests/test_transcript.py +0 -0
  473. {syncade-0.7.0 → syncade-0.7.3}/tests/test_usage.py +0 -0
  474. {syncade-0.7.0 → syncade-0.7.3}/tests/test_usage_persistence.py +0 -0
  475. {syncade-0.7.0 → syncade-0.7.3}/tests/test_workflow_action_pins.py +0 -0
  476. {syncade-0.7.0 → syncade-0.7.3}/tests/test_worktree_base_isolation.py +0 -0
  477. {syncade-0.7.0 → syncade-0.7.3}/tests/test_worktree_env.py +0 -0
  478. {syncade-0.7.0 → syncade-0.7.3}/tests/test_worktree_strip_guard.py +0 -0
  479. {syncade-0.7.0 → syncade-0.7.3}/tests/worktree/__init__.py +0 -0
  480. {syncade-0.7.0 → syncade-0.7.3}/tests/worktree/_helpers.py +0 -0
  481. {syncade-0.7.0 → syncade-0.7.3}/tests/worktree/conftest.py +0 -0
  482. {syncade-0.7.0 → syncade-0.7.3}/tests/worktree/test_worktree.py +0 -0
  483. {syncade-0.7.0 → 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.0
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.0"
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.0"
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.",
@@ -0,0 +1,473 @@
1
+ """``syncade --update`` — PR-h-field-07 item 5.
2
+
3
+ **A process cannot upgrade itself.** Its modules are already imported; if the files change
4
+ underneath it, anything imported LATER comes back new while everything already loaded stays old.
5
+ A half-and-half process is worse than either version, so this upgrades and then EXITS, asking to
6
+ be re-run. `os.execv` was considered and rejected: replacing an executable mid-replacement, plus
7
+ argv preservation and loop-guarding, for a cosmetic gain. In a terminal the re-run is one
8
+ keystroke; in a harness the skill does it, which is what makes it seamless from the chat side.
9
+
10
+ **Install-method detection PROVES or REFUSES; it never guesses.** The method is read from where
11
+ ``syncade`` actually lives — a marker file inside its own install tree — not from a table of
12
+ default paths, which vary with ``UV_TOOL_DIR``, ``PIPX_HOME``, XDG settings and platform. When no
13
+ marker is found, this prints the manual command and says plainly that it could not tell. That is
14
+ the PR-h-04.5 shape one layer up: refuse rather than act on an unproven belief.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import os
20
+ import site
21
+ import subprocess
22
+ import sys
23
+ from dataclasses import dataclass
24
+ from pathlib import Path
25
+
26
+ import syncade
27
+ from syncade.exit_codes import SUCCESS, WORKTREE_ERROR
28
+ from syncade.process import SubprocessError, run_subprocess
29
+ from syncade.update_check import _parse, _text, manifest_once
30
+
31
+ _UPGRADE_TIMEOUT = 300.0
32
+ #: Reading one version string out of a fresh interpreter. Generous, because a cold venv
33
+ #: import is slower than it looks; short, because we already hold the operator's terminal.
34
+ _VERSION_TIMEOUT = 60.0
35
+ #: How far up from ``syncade/__init__.py`` an install marker can sit. A venv layout is
36
+ #: ``<root>/lib/python3.x/site-packages/syncade/__init__.py`` — five parents to the root; the
37
+ #: extra headroom costs nothing and covers layouts that nest one deeper.
38
+ _MARKER_DEPTH = 7
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class InstallMethod:
43
+ kind: str # "uv" | "pipx" | "pip" | "source" | "unknown"
44
+ command: list[str] | None # the upgrade to run, or None when we must not run anything
45
+ where: Path | None
46
+
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
+
128
+ def detect_install() -> InstallMethod:
129
+ """Prove how syncade got here by finding a marker inside its own install tree."""
130
+ start = Path(syncade.__file__).resolve().parent
131
+ for parent in [start, *start.parents][:_MARKER_DEPTH]:
132
+ if (parent / "uv-receipt.toml").is_file():
133
+ return InstallMethod("uv", ["uv", "tool", "upgrade", "syncade"], parent)
134
+ if (parent / "pipx_metadata.json").is_file():
135
+ return InstallMethod("pipx", ["pipx", "upgrade", "syncade"], parent)
136
+ # An ENVIRONMENT ends the walk before any enclosing checkout can claim it. A plain
137
+ # venv install commonly lives at `<checkout>/.venv/lib/pythonX/site-packages/syncade`,
138
+ # and without this the ancestor `pyproject.toml` made it read as a source checkout — so
139
+ # `--update` refused with "use git", which would not update the installed package at
140
+ # all. Checked AFTER the uv/pipx markers, which sit at this same root and are more
141
+ # specific; reaching here means the environment exists but its manager is unproven,
142
+ # which is exactly what "unknown" is for.
143
+ if (parent / "pyvenv.cfg").is_file():
144
+ return _pip_or_unknown(parent)
145
+ # A checkout is the one case where upgrading would be actively wrong: the operator's
146
+ # own working tree is the install, and `uv tool upgrade` would neither touch it nor
147
+ # tell them why nothing changed. Detected by the project file naming THIS project, so a
148
+ # coincidental pyproject.toml higher up cannot claim it.
149
+ pyproject = parent / "pyproject.toml"
150
+ if pyproject.is_file() and 'name = "syncade"' in _read(pyproject):
151
+ return InstallMethod("source", None, parent)
152
+ return _pip_or_unknown(None)
153
+
154
+
155
+ def _read(path: Path) -> str:
156
+ try:
157
+ return path.read_text(encoding="utf-8", errors="replace")
158
+ except OSError:
159
+ return ""
160
+
161
+
162
+ def live_run(cwd: Path) -> Path | None:
163
+ """A run in ``cwd`` that is still going, or ``None``.
164
+
165
+ Upgrading underneath a running review is the same half-and-half hazard from outside the
166
+ process: the reviewers and producer it has yet to spawn would load the new code while the
167
+ orchestrator that planned them is the old one. Scoped to this repo's ``.syncade/runs/``,
168
+ which is where the breadcrumb lives; a run in some other checkout is not visible here and
169
+ the message says so rather than implying a global guarantee.
170
+ """
171
+ import json
172
+
173
+ from syncade.run_status import STATUS_FILENAME, is_stale_running
174
+
175
+ runs = cwd / ".syncade" / "runs"
176
+ if not runs.is_dir():
177
+ return None
178
+ for status_path in sorted(runs.glob(f"*/{STATUS_FILENAME}")):
179
+ try:
180
+ status = json.loads(status_path.read_text(encoding="utf-8"))
181
+ except (OSError, ValueError):
182
+ continue
183
+ # `is_stale_running` is true for a HARD-KILLED run, i.e. running-but-dead. A live run is
184
+ # running and NOT stale — reusing the existing predicate rather than re-deriving
185
+ # liveness, so the two cannot drift apart on what "still going" means.
186
+ if isinstance(status, dict) and status.get("state") == "running":
187
+ if not is_stale_running(status):
188
+ return status_path.parent
189
+ return None
190
+
191
+
192
+ def _syncade_argv() -> list[str]:
193
+ # Use sys.executable rather than shutil.which("syncade"): the latter resolves against
194
+ # ambient PATH and would invoke a shadow when the operator ran syncade by absolute path
195
+ # or from a venv while a different syncade is earlier on PATH. sys.executable is the
196
+ # interpreter of the upgraded install, so the subprocess always runs the right package.
197
+ return [sys.executable, "-m", "syncade"]
198
+
199
+
200
+ def _resync_skills(out) -> bool:
201
+ """Re-install every skill the operator ALREADY has, using the UPGRADED code.
202
+
203
+ A subprocess, not an in-process call: this process is still running the OLD package, so
204
+ installing from here would faithfully write the OLD skill files — the exact drift the
205
+ upgrade is meant to close. Harnesses with no skill installed are skipped rather than gaining
206
+ one they never asked for.
207
+
208
+ Returns True when every installed skill was successfully re-synced, False when at least one
209
+ was refused (so the caller can propagate the partial failure rather than claiming success).
210
+ """
211
+ from syncade.cli.skill_status import HARNESSES, skill_status
212
+
213
+ all_ok = True
214
+ for harness in HARNESSES:
215
+ if skill_status(harness).status == "absent":
216
+ continue
217
+ result = subprocess.run( # noqa: S603
218
+ [*_syncade_argv(), "--install-skill", harness],
219
+ capture_output=True,
220
+ text=True,
221
+ )
222
+ if result.returncode == SUCCESS:
223
+ print(f"[syncade] re-installed the {harness} skill", file=out)
224
+ else:
225
+ all_ok = False
226
+ # `--install-skill` refuses rather than destroying — a non-zero here usually means
227
+ # the operator edited their copy or the skill has no install record (unknown state).
228
+ # Tell them the exact command to force a resync with their explicit consent.
229
+ print(
230
+ f"[syncade] the {harness} skill was NOT re-installed — syncade refused rather "
231
+ f"than overwrite it. Run `syncade --install-skill {harness} --force-install` "
232
+ "to sync it regardless.",
233
+ file=out,
234
+ )
235
+ return all_ok
236
+
237
+
238
+ def _installed_version() -> str | None:
239
+ """The version on disk NOW, or ``None`` when it cannot be established.
240
+
241
+ It CANNOT be ``syncade.__version__``: this process imported that before the upgrade ran, so
242
+ it is by definition the old number. A fresh interpreter re-reads the metadata the upgrade
243
+ just rewrote, and ``sys.executable`` is the tool venv's own python under both uv and pipx —
244
+ precisely the environment that changed.
245
+
246
+ ``None`` means "could not verify", which is deliberately NOT the same as "did not change":
247
+ the caller must not turn an unreadable version into a failure report.
248
+ """
249
+ try:
250
+ result = run_subprocess(
251
+ # `-I` (isolated) is load-bearing, not tidiness. Without it the probe inherits
252
+ # PYTHONPATH and the user site dir, so a `sitecustomize` on that path runs inside
253
+ # the answer and can emit stdout before OR after the version print.
254
+ # `-I` keeps the venv's own site-packages, so `importlib.metadata` still finds
255
+ # the install (verified) — but it does NOT suppress `sitecustomize.py` that lives
256
+ # in those site-packages. An atexit hook registered there (syncade ships such a
257
+ # shim for worktree imports) can append a version-shaped line AFTER the real one.
258
+ # `os._exit(0)` bypasses all atexit handlers: the process terminates immediately
259
+ # after `os.write`, so nothing can append to our stdout (reproduced: a hostile
260
+ # sitecustomize turned "0.7.0" into "0.7.0\n99.0.0" before this fix).
261
+ # `os.write` is used instead of `print` because `os._exit` skips stdio flushing;
262
+ # an unbuffered syscall guarantees the version byte reaches the pipe.
263
+ [
264
+ sys.executable,
265
+ "-I",
266
+ "-c",
267
+ "import importlib.metadata as m, os; "
268
+ "os.write(1, (m.version('syncade') + '\\n').encode()); "
269
+ "os._exit(0)",
270
+ ],
271
+ timeout=_VERSION_TIMEOUT,
272
+ )
273
+ except SubprocessError:
274
+ return None
275
+ if result.returncode != 0:
276
+ return None
277
+ raw = result.stdout.strip()
278
+ if not raw:
279
+ return None
280
+ # sitecustomize from PYTHONPATH (suppressed by -I) could emit noise before -c runs;
281
+ # the venv's own sitecustomize can still do so. Take the LAST line — atexit is now
282
+ # impossible (os._exit), so the last line is what os.write put there — and validate it
283
+ # parses as a version. Noise that accidentally ends in a dotted string still fails _parse.
284
+ candidate = raw.splitlines()[-1].strip()
285
+ return candidate if _parse(candidate) is not None else None
286
+
287
+
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.
296
+
297
+ Only ever consulted on the unhappy path — the version failed to move — so the happy path
298
+ stays offline. Anything that leaves the question unanswered (unreachable manifest, no usable
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.
303
+
304
+ **This compares versions itself and must never delegate to ``evaluate()``.** That function
305
+ answers a DIFFERENT question — "should the operator see a notice?" — and the two answers
306
+ only usually coincide. They part company in the documented critical-no-fix state: when
307
+ ``critical_below`` is above the installed version and no newer release exists, ``evaluate()``
308
+ returns a notice while the operator is genuinely current, so reading its ``None``-ness told a
309
+ fully up-to-date install that its upgrade had failed and it was probably pinned. Two dogfood
310
+ rounds landed on this function for the same underlying reason — a near-miss reuse of a
311
+ neighbouring answer — and the fix both times was to ask the actual question.
312
+
313
+ There is deliberately no special case for an unreachable manifest: ``{}`` has no usable
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.
324
+ """
325
+ manifest = manifest_once(enabled=enabled) or {}
326
+ latest = _parse(_text(manifest.get("latest")))
327
+ current = _parse(installed)
328
+ if latest is None or current is None:
329
+ return None
330
+ return current >= latest
331
+
332
+
333
+ def run_update(*, cwd: Path | None = None, out=None) -> int:
334
+ """Upgrade syncade in place, then exit asking to be re-run."""
335
+ out = out or sys.stderr
336
+ cwd = cwd or Path.cwd()
337
+
338
+ # Resolve to the actual git repo root so live_run() does not miss a run when invoked
339
+ # from a subdirectory — `.syncade/runs/` is at the root, not at the invocation directory.
340
+ # Fails open: if discovery raises (not in a git repo, git not on PATH, etc.) we fall back
341
+ # to cwd and check there, which is correct behaviour for a non-repo invocation.
342
+ try:
343
+ from syncade.snapshot import discover_repo_root
344
+
345
+ cwd = discover_repo_root(cwd)
346
+ except Exception: # noqa: BLE001
347
+ pass
348
+
349
+ running = live_run(cwd)
350
+ if running is not None:
351
+ print(
352
+ f"[syncade] refusing to update: a run is still going ({running.name}).\n"
353
+ " Upgrading underneath it would leave the run half-old and half-new.\n"
354
+ " Wait for it to finish, then run `syncade --update` again.",
355
+ file=out,
356
+ )
357
+ return WORKTREE_ERROR
358
+
359
+ method = detect_install()
360
+ if method.kind == "source":
361
+ print(
362
+ f"[syncade] this syncade runs from a source checkout at {method.where}.\n"
363
+ " Update it with git, not a package manager.",
364
+ file=out,
365
+ )
366
+ return WORKTREE_ERROR
367
+ if method.command is None:
368
+ print(
369
+ "[syncade] could not determine how syncade was installed, so nothing was run.\n"
370
+ f" If you used pip: {sys.executable} -m pip install -U syncade",
371
+ file=out,
372
+ )
373
+ return WORKTREE_ERROR
374
+
375
+ print(f"[syncade] {' '.join(method.command)}", file=out)
376
+ # `SubprocessResult.returncode` — NOT `exit_code`, which does not exist. The first version
377
+ # read `exit_code` and every test passed, because the test double was hand-written with that
378
+ # attribute: the fake encoded the belief instead of the type, so the suite and the mutation
379
+ # run both validated the same wrong contract. A real upgrade would have raised AttributeError
380
+ # AFTER upgrading, losing the skill re-sync and the re-run instruction. The tests now build
381
+ # the real SubprocessResult, so a rename breaks them instead of hiding in them.
382
+ try:
383
+ result = run_subprocess(method.command, timeout=_UPGRADE_TIMEOUT)
384
+ except SubprocessError as exc:
385
+ # `uv`/`pipx` absent from PATH, or unlaunchable. Reported, not escaped: an operator who
386
+ # ran --update deserves to be told the package manager is missing.
387
+ print(f"[syncade] update failed: {exc}", file=out)
388
+ return WORKTREE_ERROR
389
+ if result.returncode != 0:
390
+ print(
391
+ f"[syncade] update failed (exit {result.returncode}). Nothing was changed.\n"
392
+ + (result.stderr.strip() or result.stdout.strip())[:800],
393
+ file=out,
394
+ )
395
+ return WORKTREE_ERROR
396
+
397
+ # THE POST-CONDITION. Exit 0 from the package manager means "the command ran", NOT "the
398
+ # version moved", and the two come apart routinely: `uv tool install syncade==0.6.3` records
399
+ # `specifier = "==0.6.3"` in its receipt, so `uv tool upgrade` honours the pin, prints
400
+ # "Nothing to upgrade" and exits 0. Held packages, resolver conflicts and a manifest that is
401
+ # ahead of PyPI all land in the same place. Without this check syncade says "updated. Re-run
402
+ # your command", the operator re-runs, gets the same version, and is told again next session
403
+ # — forever. Measured live on the 0.7.0 release, which is why item 5 of that release existed.
404
+ now = _installed_version()
405
+ if now is not None and now == syncade.__version__:
406
+ # The version did not move. There are TWO reasons for that and they need opposite
407
+ # answers, so ask the manifest which one this is rather than guessing. Guessing was the
408
+ # first version of this guard and it told an operator who was simply already current
409
+ # that their install was pinned and broken — a fresh false claim inside the fix for a
410
+ # false claim, caught by testing the path instead of reasoning about it.
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:
420
+ skills_ok = _resync_skills(out)
421
+ print(f"[syncade] already up to date ({now}).", file=out)
422
+ return SUCCESS if skills_ok else WORKTREE_ERROR
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
+ )
460
+ return WORKTREE_ERROR
461
+
462
+ skills_ok = _resync_skills(out)
463
+ moved = f" to {now}" if now else ""
464
+ print(
465
+ f"[syncade] updated{moved}. Re-run your command to use the new version — this process is\n"
466
+ " still running the old one and cannot switch to it.",
467
+ file=out,
468
+ )
469
+ if not skills_ok:
470
+ # The package upgrade succeeded but at least one skill resync was refused. The operator
471
+ # now has a fresh CLI and a stale or recordless skill — not a clean update end to end.
472
+ return WORKTREE_ERROR
473
+ return SUCCESS