syncade 0.7.3__tar.gz → 0.7.5__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.3/src/syncade.egg-info → syncade-0.7.5}/PKG-INFO +1 -1
  2. {syncade-0.7.3 → syncade-0.7.5}/pyproject.toml +1 -1
  3. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/__init__.py +1 -1
  4. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/cli/update_mode.py +51 -56
  5. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/config.py +6 -1
  6. {syncade-0.7.3 → syncade-0.7.5/src/syncade.egg-info}/PKG-INFO +1 -1
  7. {syncade-0.7.3 → syncade-0.7.5}/src/syncade.egg-info/SOURCES.txt +1 -0
  8. {syncade-0.7.3 → syncade-0.7.5}/tests/cli/test_update_post_condition.py +3 -123
  9. syncade-0.7.5/tests/cli/test_version_probe.py +158 -0
  10. {syncade-0.7.3 → syncade-0.7.5}/tests/conftest.py +11 -12
  11. {syncade-0.7.3 → syncade-0.7.5}/tests/test_worktree_base_isolation.py +2 -2
  12. {syncade-0.7.3 → syncade-0.7.5}/LICENSE +0 -0
  13. {syncade-0.7.3 → syncade-0.7.5}/MANIFEST.in +0 -0
  14. {syncade-0.7.3 → syncade-0.7.5}/README.md +0 -0
  15. {syncade-0.7.3 → syncade-0.7.5}/docs/config-reference.md +0 -0
  16. {syncade-0.7.3 → syncade-0.7.5}/docs/how-to-use.md +0 -0
  17. {syncade-0.7.3 → syncade-0.7.5}/setup.cfg +0 -0
  18. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/__main__.py +0 -0
  19. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/adapters/__init__.py +0 -0
  20. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/adapters/anthropic.py +0 -0
  21. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/adapters/base.py +0 -0
  22. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/adapters/fake.py +0 -0
  23. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/adapters/fake_common.py +0 -0
  24. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/adapters/fake_producer_audit_draft.py +0 -0
  25. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/adapters/fake_reviewer_synth.py +0 -0
  26. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/adapters/openai.py +0 -0
  27. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/adapters/openai_parsing.py +0 -0
  28. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/adapters/producer.py +0 -0
  29. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/adapters/producer_anthropic.py +0 -0
  30. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/adapters/producer_openai.py +0 -0
  31. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/adapters/registry.py +0 -0
  32. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/auth_check.py +0 -0
  33. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/auth_preflight.py +0 -0
  34. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/base_resolution.py +0 -0
  35. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/billing.py +0 -0
  36. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/checks_config.py +0 -0
  37. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/cli/__init__.py +0 -0
  38. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/cli/auth_gate.py +0 -0
  39. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/cli/config_keys.py +0 -0
  40. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/cli/config_list.py +0 -0
  41. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/cli/config_menu_rows.py +0 -0
  42. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/cli/config_mode.py +0 -0
  43. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/cli/config_overrides.py +0 -0
  44. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/cli/config_tui.py +0 -0
  45. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/cli/doctor_mode.py +0 -0
  46. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/cli/gc_mode.py +0 -0
  47. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/cli/install_skill.py +0 -0
  48. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/cli/metrics_mode.py +0 -0
  49. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/cli/modes.py +0 -0
  50. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/cli/parser.py +0 -0
  51. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/cli/parser_types.py +0 -0
  52. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/cli/paths.py +0 -0
  53. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/cli/preflight_paths.py +0 -0
  54. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/cli/resolve.py +0 -0
  55. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/cli/resume_mode.py +0 -0
  56. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/cli/skill_status.py +0 -0
  57. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/cli/toml_writer.py +0 -0
  58. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/cli/update_notice.py +0 -0
  59. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/cli/validate.py +0 -0
  60. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/config_auth.py +0 -0
  61. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/config_cold.py +0 -0
  62. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/config_gc.py +0 -0
  63. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/config_loader.py +0 -0
  64. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/config_loop.py +0 -0
  65. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/config_producer.py +0 -0
  66. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/config_retry.py +0 -0
  67. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/config_types.py +0 -0
  68. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/config_update.py +0 -0
  69. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/diff_filter.py +0 -0
  70. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/dispatcher.py +0 -0
  71. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/doctor.py +0 -0
  72. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/doctor_env.py +0 -0
  73. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/doctor_preview.py +0 -0
  74. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/doctor_types.py +0 -0
  75. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/exit_codes.py +0 -0
  76. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/findings.py +0 -0
  77. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/findings_json.py +0 -0
  78. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/gc.py +0 -0
  79. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/gc_execute.py +0 -0
  80. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/gc_protection.py +0 -0
  81. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/gc_types.py +0 -0
  82. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/gc_worktrees.py +0 -0
  83. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/git_object_id.py +0 -0
  84. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/git_preconditions.py +0 -0
  85. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/logging.py +0 -0
  86. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/metrics/__init__.py +0 -0
  87. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/metrics/aggregate.py +0 -0
  88. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/metrics/findings.py +0 -0
  89. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/metrics/schema.py +0 -0
  90. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/orchestrator/__init__.py +0 -0
  91. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/orchestrator/_runs_dir.py +0 -0
  92. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/orchestrator/branch_advance.py +0 -0
  93. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/orchestrator/branch_guard.py +0 -0
  94. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/orchestrator/budget.py +0 -0
  95. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/orchestrator/escalation_coverage.py +0 -0
  96. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/orchestrator/loop.py +0 -0
  97. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/orchestrator/loop_dispatch_check.py +0 -0
  98. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/orchestrator/loop_finalize.py +0 -0
  99. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/orchestrator/loop_preflight.py +0 -0
  100. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/orchestrator/loop_resume.py +0 -0
  101. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/orchestrator/loop_rmtree.py +0 -0
  102. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/orchestrator/loop_round_step.py +0 -0
  103. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/orchestrator/prior_round.py +0 -0
  104. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/orchestrator/producer_phase.py +0 -0
  105. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/orchestrator/results.py +0 -0
  106. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/orchestrator/resume.py +0 -0
  107. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/orchestrator/resume_load.py +0 -0
  108. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/orchestrator/resume_plan.py +0 -0
  109. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/orchestrator/resume_target.py +0 -0
  110. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/orchestrator/resume_types.py +0 -0
  111. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/orchestrator/reviewer_template_failure.py +0 -0
  112. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/orchestrator/round.py +0 -0
  113. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/orchestrator/round_checks.py +0 -0
  114. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/orchestrator/round_no_changes.py +0 -0
  115. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/orchestrator/round_predispatch.py +0 -0
  116. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/orchestrator/verdict.py +0 -0
  117. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/persistence/__init__.py +0 -0
  118. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/persistence/_atomic.py +0 -0
  119. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/persistence/_clusters.py +0 -0
  120. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/persistence/_findings_verdict.py +0 -0
  121. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/persistence/_markdown.py +0 -0
  122. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/persistence/_validation.py +0 -0
  123. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/persistence/checks.py +0 -0
  124. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/persistence/decision_needed.py +0 -0
  125. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/persistence/findings_md.py +0 -0
  126. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/persistence/handoff.py +0 -0
  127. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/persistence/handoff_classify.py +0 -0
  128. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/persistence/last_reviewed.py +0 -0
  129. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/persistence/loop_manifest.py +0 -0
  130. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/persistence/loop_summary.py +0 -0
  131. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/persistence/loop_summary_text.py +0 -0
  132. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/persistence/producer.py +0 -0
  133. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/persistence/reviewer.py +0 -0
  134. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/persistence/round_manifest.py +0 -0
  135. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/persistence/run_init.py +0 -0
  136. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/persistence/run_summary.py +0 -0
  137. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/persistence/run_summary_next_steps.py +0 -0
  138. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/persistence/synth.py +0 -0
  139. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/persistence/test_run.py +0 -0
  140. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/presets.py +0 -0
  141. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/pricing_config.py +0 -0
  142. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/process.py +0 -0
  143. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/producer.py +0 -0
  144. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/producer_attempt.py +0 -0
  145. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/producer_escalation.py +0 -0
  146. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/producer_git.py +0 -0
  147. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/producer_result.py +0 -0
  148. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/prompts.py +0 -0
  149. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/prompts_loader.py +0 -0
  150. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/retry.py +0 -0
  151. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/run_inputs.py +0 -0
  152. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/run_status.py +0 -0
  153. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/selfcheck.py +0 -0
  154. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/skills/claude/README.md +0 -0
  155. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/skills/claude/SKILL.md +0 -0
  156. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/skills/codex/README.md +0 -0
  157. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/skills/codex/SKILL.md +0 -0
  158. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/snapshot.py +0 -0
  159. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/spec_audit.py +0 -0
  160. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/spec_audit_schema.py +0 -0
  161. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/spec_draft.py +0 -0
  162. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/spec_source.py +0 -0
  163. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/synthesis.py +0 -0
  164. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/synthesis_clusters.py +0 -0
  165. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/synthesis_repair.py +0 -0
  166. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/synthesis_schema.py +0 -0
  167. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/synthesizer/__init__.py +0 -0
  168. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/synthesizer/constants.py +0 -0
  169. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/synthesizer/driver.py +0 -0
  170. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/synthesizer/rendering.py +0 -0
  171. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/synthesizer/result.py +0 -0
  172. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/synthesizer/validation.py +0 -0
  173. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/synthesizer/workspace.py +0 -0
  174. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/templates/presets/balanced.toml +0 -0
  175. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/templates/presets/cheap.toml +0 -0
  176. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/templates/presets/thorough.toml +0 -0
  177. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/templates/producer.md +0 -0
  178. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/templates/reviewer.md +0 -0
  179. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/templates/reviewer_adversarial.md +0 -0
  180. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/templates/reviewer_codex.md +0 -0
  181. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/templates/spec_audit.md +0 -0
  182. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/templates/spec_draft.md +0 -0
  183. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/templates/synthesizer.md +0 -0
  184. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/test_runner.py +0 -0
  185. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/test_runner_classify.py +0 -0
  186. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/transcript.py +0 -0
  187. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/update_check.py +0 -0
  188. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/usage.py +0 -0
  189. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/worktree.py +0 -0
  190. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/worktree_env.py +0 -0
  191. {syncade-0.7.3 → syncade-0.7.5}/src/syncade/worktree_paths.py +0 -0
  192. {syncade-0.7.3 → syncade-0.7.5}/src/syncade.egg-info/dependency_links.txt +0 -0
  193. {syncade-0.7.3 → syncade-0.7.5}/src/syncade.egg-info/entry_points.txt +0 -0
  194. {syncade-0.7.3 → syncade-0.7.5}/src/syncade.egg-info/requires.txt +0 -0
  195. {syncade-0.7.3 → syncade-0.7.5}/src/syncade.egg-info/top_level.txt +0 -0
  196. {syncade-0.7.3 → syncade-0.7.5}/tests/__init__.py +0 -0
  197. {syncade-0.7.3 → syncade-0.7.5}/tests/adapters/__init__.py +0 -0
  198. {syncade-0.7.3 → syncade-0.7.5}/tests/adapters/_anthropic_helpers.py +0 -0
  199. {syncade-0.7.3 → syncade-0.7.5}/tests/adapters/_openai_helpers.py +0 -0
  200. {syncade-0.7.3 → syncade-0.7.5}/tests/adapters/test_anthropic.py +0 -0
  201. {syncade-0.7.3 → syncade-0.7.5}/tests/adapters/test_anthropic_more.py +0 -0
  202. {syncade-0.7.3 → syncade-0.7.5}/tests/adapters/test_openai.py +0 -0
  203. {syncade-0.7.3 → syncade-0.7.5}/tests/adapters/test_openai_more.py +0 -0
  204. {syncade-0.7.3 → syncade-0.7.5}/tests/adapters/test_producer.py +0 -0
  205. {syncade-0.7.3 → syncade-0.7.5}/tests/adapters/test_producer_more.py +0 -0
  206. {syncade-0.7.3 → syncade-0.7.5}/tests/adapters/test_prompt_transport.py +0 -0
  207. {syncade-0.7.3 → syncade-0.7.5}/tests/auth_check/__init__.py +0 -0
  208. {syncade-0.7.3 → syncade-0.7.5}/tests/auth_check/test_probes.py +0 -0
  209. {syncade-0.7.3 → syncade-0.7.5}/tests/auth_check/test_run_auth_check.py +0 -0
  210. {syncade-0.7.3 → syncade-0.7.5}/tests/cli/__init__.py +0 -0
  211. {syncade-0.7.3 → syncade-0.7.5}/tests/cli/_helpers.py +0 -0
  212. {syncade-0.7.3 → syncade-0.7.5}/tests/cli/_undo_helpers.py +0 -0
  213. {syncade-0.7.3 → syncade-0.7.5}/tests/cli/test_basic_and_mutex.py +0 -0
  214. {syncade-0.7.3 → syncade-0.7.5}/tests/cli/test_cli_library_divergence.py +0 -0
  215. {syncade-0.7.3 → syncade-0.7.5}/tests/cli/test_config_keys.py +0 -0
  216. {syncade-0.7.3 → syncade-0.7.5}/tests/cli/test_config_list.py +0 -0
  217. {syncade-0.7.3 → syncade-0.7.5}/tests/cli/test_config_menu_rows.py +0 -0
  218. {syncade-0.7.3 → syncade-0.7.5}/tests/cli/test_config_mode.py +0 -0
  219. {syncade-0.7.3 → syncade-0.7.5}/tests/cli/test_config_overrides.py +0 -0
  220. {syncade-0.7.3 → syncade-0.7.5}/tests/cli/test_config_tui.py +0 -0
  221. {syncade-0.7.3 → syncade-0.7.5}/tests/cli/test_config_tui_pty.py +0 -0
  222. {syncade-0.7.3 → syncade-0.7.5}/tests/cli/test_documented_exit_codes.py +0 -0
  223. {syncade-0.7.3 → syncade-0.7.5}/tests/cli/test_documented_flags_exist.py +0 -0
  224. {syncade-0.7.3 → syncade-0.7.5}/tests/cli/test_flags.py +0 -0
  225. {syncade-0.7.3 → syncade-0.7.5}/tests/cli/test_gc.py +0 -0
  226. {syncade-0.7.3 → syncade-0.7.5}/tests/cli/test_help_retention_contract.py +0 -0
  227. {syncade-0.7.3 → syncade-0.7.5}/tests/cli/test_mode_exclusivity_matrix.py +0 -0
  228. {syncade-0.7.3 → syncade-0.7.5}/tests/cli/test_mode_flag_guards.py +0 -0
  229. {syncade-0.7.3 → syncade-0.7.5}/tests/cli/test_resume_reports_hard_kill.py +0 -0
  230. {syncade-0.7.3 → syncade-0.7.5}/tests/cli/test_run_and_config.py +0 -0
  231. {syncade-0.7.3 → syncade-0.7.5}/tests/cli/test_run_status_cli.py +0 -0
  232. {syncade-0.7.3 → syncade-0.7.5}/tests/cli/test_scope_openspec_draft.py +0 -0
  233. {syncade-0.7.3 → syncade-0.7.5}/tests/cli/test_toml_writer.py +0 -0
  234. {syncade-0.7.3 → syncade-0.7.5}/tests/cli/test_undo_auto_init.py +0 -0
  235. {syncade-0.7.3 → syncade-0.7.5}/tests/cli/test_undo_scope.py +0 -0
  236. {syncade-0.7.3 → syncade-0.7.5}/tests/cli/test_update_mode.py +0 -0
  237. {syncade-0.7.3 → syncade-0.7.5}/tests/cli/test_update_notice.py +0 -0
  238. {syncade-0.7.3 → syncade-0.7.5}/tests/cli/test_validate_before_mutate.py +0 -0
  239. {syncade-0.7.3 → syncade-0.7.5}/tests/config/__init__.py +0 -0
  240. {syncade-0.7.3 → syncade-0.7.5}/tests/config/test_auth_enforcement.py +0 -0
  241. {syncade-0.7.3 → syncade-0.7.5}/tests/config/test_auth_modes.py +0 -0
  242. {syncade-0.7.3 → syncade-0.7.5}/tests/config/test_auth_preflight.py +0 -0
  243. {syncade-0.7.3 → syncade-0.7.5}/tests/config/test_auth_report_scope.py +0 -0
  244. {syncade-0.7.3 → syncade-0.7.5}/tests/config/test_budget_config.py +0 -0
  245. {syncade-0.7.3 → syncade-0.7.5}/tests/config/test_cold_actors.py +0 -0
  246. {syncade-0.7.3 → syncade-0.7.5}/tests/config/test_config_checks.py +0 -0
  247. {syncade-0.7.3 → syncade-0.7.5}/tests/config/test_config_reference_drift.py +0 -0
  248. {syncade-0.7.3 → syncade-0.7.5}/tests/config/test_config_schema.py +0 -0
  249. {syncade-0.7.3 → syncade-0.7.5}/tests/config/test_gc_config.py +0 -0
  250. {syncade-0.7.3 → syncade-0.7.5}/tests/config/test_global_config_layer.py +0 -0
  251. {syncade-0.7.3 → syncade-0.7.5}/tests/config/test_presets.py +0 -0
  252. {syncade-0.7.3 → syncade-0.7.5}/tests/config/test_retry_config.py +0 -0
  253. {syncade-0.7.3 → syncade-0.7.5}/tests/config/test_reviewer_config.py +0 -0
  254. {syncade-0.7.3 → syncade-0.7.5}/tests/config/test_reviewer_permissions.py +0 -0
  255. {syncade-0.7.3 → syncade-0.7.5}/tests/config/test_worktree_base_config.py +0 -0
  256. {syncade-0.7.3 → syncade-0.7.5}/tests/dispatcher/__init__.py +0 -0
  257. {syncade-0.7.3 → syncade-0.7.5}/tests/dispatcher/_helpers.py +0 -0
  258. {syncade-0.7.3 → syncade-0.7.5}/tests/dispatcher/test_dispatch_core.py +0 -0
  259. {syncade-0.7.3 → syncade-0.7.5}/tests/dispatcher/test_dispatch_misc.py +0 -0
  260. {syncade-0.7.3 → syncade-0.7.5}/tests/dispatcher/test_dispatch_usage.py +0 -0
  261. {syncade-0.7.3 → syncade-0.7.5}/tests/doctor/__init__.py +0 -0
  262. {syncade-0.7.3 → syncade-0.7.5}/tests/doctor/_helpers.py +0 -0
  263. {syncade-0.7.3 → syncade-0.7.5}/tests/doctor/conftest.py +0 -0
  264. {syncade-0.7.3 → syncade-0.7.5}/tests/doctor/test_doctor.py +0 -0
  265. {syncade-0.7.3 → syncade-0.7.5}/tests/doctor/test_doctor_diff_size.py +0 -0
  266. {syncade-0.7.3 → syncade-0.7.5}/tests/doctor/test_doctor_malformed_diff.py +0 -0
  267. {syncade-0.7.3 → syncade-0.7.5}/tests/doctor/test_doctor_preview.py +0 -0
  268. {syncade-0.7.3 → syncade-0.7.5}/tests/findings/__init__.py +0 -0
  269. {syncade-0.7.3 → syncade-0.7.5}/tests/findings/_helpers.py +0 -0
  270. {syncade-0.7.3 → syncade-0.7.5}/tests/findings/test_parser_and_fields.py +0 -0
  271. {syncade-0.7.3 → syncade-0.7.5}/tests/findings/test_parser_resilience.py +0 -0
  272. {syncade-0.7.3 → syncade-0.7.5}/tests/findings/test_verdict_block_selection.py +0 -0
  273. {syncade-0.7.3 → syncade-0.7.5}/tests/gc/__init__.py +0 -0
  274. {syncade-0.7.3 → syncade-0.7.5}/tests/gc/_helpers.py +0 -0
  275. {syncade-0.7.3 → syncade-0.7.5}/tests/gc/test_autoprune.py +0 -0
  276. {syncade-0.7.3 → syncade-0.7.5}/tests/gc/test_execute.py +0 -0
  277. {syncade-0.7.3 → syncade-0.7.5}/tests/gc/test_plan.py +0 -0
  278. {syncade-0.7.3 → syncade-0.7.5}/tests/gc/test_plan_execution_races.py +0 -0
  279. {syncade-0.7.3 → syncade-0.7.5}/tests/gc/test_reaping_scope.py +0 -0
  280. {syncade-0.7.3 → syncade-0.7.5}/tests/gc/test_retention_tiers.py +0 -0
  281. {syncade-0.7.3 → syncade-0.7.5}/tests/gc/test_robustness.py +0 -0
  282. {syncade-0.7.3 → syncade-0.7.5}/tests/gc/test_self_orphan_protection.py +0 -0
  283. {syncade-0.7.3 → syncade-0.7.5}/tests/gc/test_smoke.py +0 -0
  284. {syncade-0.7.3 → syncade-0.7.5}/tests/gc/test_two_tier_retention.py +0 -0
  285. {syncade-0.7.3 → syncade-0.7.5}/tests/gc/test_worktree_age_bound.py +0 -0
  286. {syncade-0.7.3 → syncade-0.7.5}/tests/logging/__init__.py +0 -0
  287. {syncade-0.7.3 → syncade-0.7.5}/tests/logging/_helpers.py +0 -0
  288. {syncade-0.7.3 → syncade-0.7.5}/tests/logging/test_logging.py +0 -0
  289. {syncade-0.7.3 → syncade-0.7.5}/tests/logging/test_logging_summary.py +0 -0
  290. {syncade-0.7.3 → syncade-0.7.5}/tests/metrics/conftest.py +0 -0
  291. {syncade-0.7.3 → syncade-0.7.5}/tests/metrics/test_actor_spend.py +0 -0
  292. {syncade-0.7.3 → syncade-0.7.5}/tests/metrics/test_aggregate.py +0 -0
  293. {syncade-0.7.3 → syncade-0.7.5}/tests/metrics/test_billing_reconciliation.py +0 -0
  294. {syncade-0.7.3 → syncade-0.7.5}/tests/metrics/test_billing_surfaces_agree.py +0 -0
  295. {syncade-0.7.3 → syncade-0.7.5}/tests/metrics/test_budget_metrics.py +0 -0
  296. {syncade-0.7.3 → syncade-0.7.5}/tests/metrics/test_consensus_report.py +0 -0
  297. {syncade-0.7.3 → syncade-0.7.5}/tests/metrics/test_cost_honesty.py +0 -0
  298. {syncade-0.7.3 → syncade-0.7.5}/tests/metrics/test_findings_capture.py +0 -0
  299. {syncade-0.7.3 → syncade-0.7.5}/tests/metrics/test_metrics_cli.py +0 -0
  300. {syncade-0.7.3 → syncade-0.7.5}/tests/metrics/test_metrics_mode.py +0 -0
  301. {syncade-0.7.3 → syncade-0.7.5}/tests/metrics/test_schema.py +0 -0
  302. {syncade-0.7.3 → syncade-0.7.5}/tests/metrics/test_unknown_panel.py +0 -0
  303. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/__init__.py +0 -0
  304. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/_helpers.py +0 -0
  305. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/_resume_fixtures.py +0 -0
  306. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/conftest.py +0 -0
  307. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/test_budget.py +0 -0
  308. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/test_budget_resume_e2e.py +0 -0
  309. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/test_budget_stop_message.py +0 -0
  310. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/test_budget_warning.py +0 -0
  311. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/test_checks_and_recording.py +0 -0
  312. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/test_contracts.py +0 -0
  313. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/test_cross_round.py +0 -0
  314. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/test_cross_round_more.py +0 -0
  315. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/test_default_branch_guard.py +0 -0
  316. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/test_diff_size_cap.py +0 -0
  317. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/test_dispatch_record.py +0 -0
  318. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/test_empty_diff.py +0 -0
  319. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/test_empty_diff_no_side_effects.py +0 -0
  320. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/test_escalation.py +0 -0
  321. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/test_last_reviewed_truthful.py +0 -0
  322. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/test_loop_audit_remediation.py +0 -0
  323. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/test_matrix_and_e2e.py +0 -0
  324. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/test_multiround.py +0 -0
  325. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/test_multiround_dirty.py +0 -0
  326. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/test_multiround_more.py +0 -0
  327. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/test_paraphrase_escalation.py +0 -0
  328. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/test_pipeline.py +0 -0
  329. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/test_provider_usage_limit.py +0 -0
  330. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/test_rename_boundary.py +0 -0
  331. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/test_resume.py +0 -0
  332. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/test_resume_base_oid.py +0 -0
  333. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/test_reviewer_blindness_is_structural.py +0 -0
  334. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/test_round_input_boundaries.py +0 -0
  335. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/test_run_layout.py +0 -0
  336. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/test_run_status_wiring.py +0 -0
  337. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/test_safety_warning_survives_quiet.py +0 -0
  338. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/test_snapshot_and_base.py +0 -0
  339. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/test_synth_phase.py +0 -0
  340. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/test_synth_phase_more.py +0 -0
  341. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/test_synth_provenance.py +0 -0
  342. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/test_synth_validation.py +0 -0
  343. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/test_template_robustness.py +0 -0
  344. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/test_termination_reason_drift.py +0 -0
  345. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/test_testleg.py +0 -0
  346. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/test_timeout_is_per_subprocess.py +0 -0
  347. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/test_usage_limit_message.py +0 -0
  348. {syncade-0.7.3 → syncade-0.7.5}/tests/orchestrator/test_worktree_cleanup.py +0 -0
  349. {syncade-0.7.3 → syncade-0.7.5}/tests/persistence/__init__.py +0 -0
  350. {syncade-0.7.3 → syncade-0.7.5}/tests/persistence/_helpers.py +0 -0
  351. {syncade-0.7.3 → syncade-0.7.5}/tests/persistence/test_atomic_writes.py +0 -0
  352. {syncade-0.7.3 → syncade-0.7.5}/tests/persistence/test_checks_surfacing.py +0 -0
  353. {syncade-0.7.3 → syncade-0.7.5}/tests/persistence/test_diff_size_recording.py +0 -0
  354. {syncade-0.7.3 → syncade-0.7.5}/tests/persistence/test_findings_md.py +0 -0
  355. {syncade-0.7.3 → syncade-0.7.5}/tests/persistence/test_findings_md_more.py +0 -0
  356. {syncade-0.7.3 → syncade-0.7.5}/tests/persistence/test_findings_md_verdict.py +0 -0
  357. {syncade-0.7.3 → syncade-0.7.5}/tests/persistence/test_handoff.py +0 -0
  358. {syncade-0.7.3 → syncade-0.7.5}/tests/persistence/test_loop_manifest_checks.py +0 -0
  359. {syncade-0.7.3 → syncade-0.7.5}/tests/persistence/test_loop_summary.py +0 -0
  360. {syncade-0.7.3 → syncade-0.7.5}/tests/persistence/test_loop_summary_more.py +0 -0
  361. {syncade-0.7.3 → syncade-0.7.5}/tests/persistence/test_md_safety.py +0 -0
  362. {syncade-0.7.3 → syncade-0.7.5}/tests/persistence/test_next_steps.py +0 -0
  363. {syncade-0.7.3 → syncade-0.7.5}/tests/persistence/test_producer_persist.py +0 -0
  364. {syncade-0.7.3 → syncade-0.7.5}/tests/persistence/test_reviewer.py +0 -0
  365. {syncade-0.7.3 → syncade-0.7.5}/tests/persistence/test_round_manifest.py +0 -0
  366. {syncade-0.7.3 → syncade-0.7.5}/tests/persistence/test_run_init.py +0 -0
  367. {syncade-0.7.3 → syncade-0.7.5}/tests/persistence/test_run_summary.py +0 -0
  368. {syncade-0.7.3 → syncade-0.7.5}/tests/persistence/test_run_summary_more.py +0 -0
  369. {syncade-0.7.3 → syncade-0.7.5}/tests/persistence/test_run_summary_producer.py +0 -0
  370. {syncade-0.7.3 → syncade-0.7.5}/tests/persistence/test_synth_persist.py +0 -0
  371. {syncade-0.7.3 → syncade-0.7.5}/tests/persistence/test_test_run.py +0 -0
  372. {syncade-0.7.3 → syncade-0.7.5}/tests/producer/__init__.py +0 -0
  373. {syncade-0.7.3 → syncade-0.7.5}/tests/producer/_helpers.py +0 -0
  374. {syncade-0.7.3 → syncade-0.7.5}/tests/producer/test_producer.py +0 -0
  375. {syncade-0.7.3 → syncade-0.7.5}/tests/producer/test_producer_confinement.py +0 -0
  376. {syncade-0.7.3 → syncade-0.7.5}/tests/producer/test_producer_retry.py +0 -0
  377. {syncade-0.7.3 → syncade-0.7.5}/tests/producer/test_producer_run.py +0 -0
  378. {syncade-0.7.3 → syncade-0.7.5}/tests/producer/test_producer_timeout.py +0 -0
  379. {syncade-0.7.3 → syncade-0.7.5}/tests/producer/test_producer_usage.py +0 -0
  380. {syncade-0.7.3 → syncade-0.7.5}/tests/prompts/__init__.py +0 -0
  381. {syncade-0.7.3 → syncade-0.7.5}/tests/prompts/test_load_template.py +0 -0
  382. {syncade-0.7.3 → syncade-0.7.5}/tests/prompts/test_misc.py +0 -0
  383. {syncade-0.7.3 → syncade-0.7.5}/tests/prompts/test_producer.py +0 -0
  384. {syncade-0.7.3 → syncade-0.7.5}/tests/prompts/test_render_synthesizer.py +0 -0
  385. {syncade-0.7.3 → syncade-0.7.5}/tests/prompts/test_synthesizer.py +0 -0
  386. {syncade-0.7.3 → syncade-0.7.5}/tests/resume/__init__.py +0 -0
  387. {syncade-0.7.3 → syncade-0.7.5}/tests/resume/_helpers.py +0 -0
  388. {syncade-0.7.3 → syncade-0.7.5}/tests/resume/test_budget_resume.py +0 -0
  389. {syncade-0.7.3 → syncade-0.7.5}/tests/resume/test_checks_phase_failure.py +0 -0
  390. {syncade-0.7.3 → syncade-0.7.5}/tests/resume/test_cli_malformed_rehydration.py +0 -0
  391. {syncade-0.7.3 → syncade-0.7.5}/tests/resume/test_find_resolve_plan.py +0 -0
  392. {syncade-0.7.3 → syncade-0.7.5}/tests/resume/test_plan_resume.py +0 -0
  393. {syncade-0.7.3 → syncade-0.7.5}/tests/resume/test_round_trip.py +0 -0
  394. {syncade-0.7.3 → syncade-0.7.5}/tests/resume/test_tree_drift_and_escalation.py +0 -0
  395. {syncade-0.7.3 → syncade-0.7.5}/tests/skills/test_config_section.py +0 -0
  396. {syncade-0.7.3 → syncade-0.7.5}/tests/skills/test_install_skill.py +0 -0
  397. {syncade-0.7.3 → syncade-0.7.5}/tests/skills/test_install_skill_multi_harness.py +0 -0
  398. {syncade-0.7.3 → syncade-0.7.5}/tests/skills/test_install_skill_ownership.py +0 -0
  399. {syncade-0.7.3 → syncade-0.7.5}/tests/skills/test_install_skill_refuses.py +0 -0
  400. {syncade-0.7.3 → syncade-0.7.5}/tests/skills/test_skill_drift.py +0 -0
  401. {syncade-0.7.3 → syncade-0.7.5}/tests/skills/test_skill_status.py +0 -0
  402. {syncade-0.7.3 → syncade-0.7.5}/tests/smoke/__init__.py +0 -0
  403. {syncade-0.7.3 → syncade-0.7.5}/tests/smoke/test_anthropic_smoke.py +0 -0
  404. {syncade-0.7.3 → syncade-0.7.5}/tests/smoke/test_auth_check_smoke.py +0 -0
  405. {syncade-0.7.3 → syncade-0.7.5}/tests/smoke/test_codex_smoke.py +0 -0
  406. {syncade-0.7.3 → syncade-0.7.5}/tests/smoke/test_loop_smoke.py +0 -0
  407. {syncade-0.7.3 → syncade-0.7.5}/tests/smoke/test_orchestrator_smoke.py +0 -0
  408. {syncade-0.7.3 → syncade-0.7.5}/tests/smoke/test_orchestrator_smoke_more.py +0 -0
  409. {syncade-0.7.3 → syncade-0.7.5}/tests/smoke/test_producer_smoke.py +0 -0
  410. {syncade-0.7.3 → syncade-0.7.5}/tests/smoke/test_resume_reap_smoke.py +0 -0
  411. {syncade-0.7.3 → syncade-0.7.5}/tests/smoke/test_selfcheck_smoke.py +0 -0
  412. {syncade-0.7.3 → syncade-0.7.5}/tests/smoke/test_synthesizer_smoke.py +0 -0
  413. {syncade-0.7.3 → syncade-0.7.5}/tests/smoke/test_test_runner_smoke.py +0 -0
  414. {syncade-0.7.3 → syncade-0.7.5}/tests/snapshot/__init__.py +0 -0
  415. {syncade-0.7.3 → syncade-0.7.5}/tests/snapshot/_helpers.py +0 -0
  416. {syncade-0.7.3 → syncade-0.7.5}/tests/snapshot/conftest.py +0 -0
  417. {syncade-0.7.3 → syncade-0.7.5}/tests/snapshot/test_snapshot.py +0 -0
  418. {syncade-0.7.3 → syncade-0.7.5}/tests/snapshot/test_snapshot_diff_identity.py +0 -0
  419. {syncade-0.7.3 → syncade-0.7.5}/tests/snapshot/test_snapshot_dirty.py +0 -0
  420. {syncade-0.7.3 → syncade-0.7.5}/tests/snapshot/test_snapshot_warning.py +0 -0
  421. {syncade-0.7.3 → syncade-0.7.5}/tests/snapshot/test_three_dot_diff.py +0 -0
  422. {syncade-0.7.3 → syncade-0.7.5}/tests/synthesis/__init__.py +0 -0
  423. {syncade-0.7.3 → syncade-0.7.5}/tests/synthesis/_helpers.py +0 -0
  424. {syncade-0.7.3 → syncade-0.7.5}/tests/synthesis/test_cluster_repair.py +0 -0
  425. {syncade-0.7.3 → syncade-0.7.5}/tests/synthesis/test_clusters.py +0 -0
  426. {syncade-0.7.3 → syncade-0.7.5}/tests/synthesis/test_models.py +0 -0
  427. {syncade-0.7.3 → syncade-0.7.5}/tests/synthesis/test_models_distinct_keying.py +0 -0
  428. {syncade-0.7.3 → syncade-0.7.5}/tests/synthesis/test_parsing.py +0 -0
  429. {syncade-0.7.3 → syncade-0.7.5}/tests/synthesis/test_unanimous_blocker_downgrade.py +0 -0
  430. {syncade-0.7.3 → syncade-0.7.5}/tests/synthesizer/test_provenance_fabrication.py +0 -0
  431. {syncade-0.7.3 → syncade-0.7.5}/tests/synthesizer/test_provenance_repair.py +0 -0
  432. {syncade-0.7.3 → syncade-0.7.5}/tests/test_adapter_registry.py +0 -0
  433. {syncade-0.7.3 → syncade-0.7.5}/tests/test_adapters_fake.py +0 -0
  434. {syncade-0.7.3 → syncade-0.7.5}/tests/test_auto_init_policy.py +0 -0
  435. {syncade-0.7.3 → syncade-0.7.5}/tests/test_base_resolution.py +0 -0
  436. {syncade-0.7.3 → syncade-0.7.5}/tests/test_check_loc_script.py +0 -0
  437. {syncade-0.7.3 → syncade-0.7.5}/tests/test_checks.py +0 -0
  438. {syncade-0.7.3 → syncade-0.7.5}/tests/test_config_loader.py +0 -0
  439. {syncade-0.7.3 → syncade-0.7.5}/tests/test_cross_model_claim.py +0 -0
  440. {syncade-0.7.3 → syncade-0.7.5}/tests/test_diff_filter.py +0 -0
  441. {syncade-0.7.3 → syncade-0.7.5}/tests/test_diff_filter_binary.py +0 -0
  442. {syncade-0.7.3 → syncade-0.7.5}/tests/test_diff_filter_quoted_paths.py +0 -0
  443. {syncade-0.7.3 → syncade-0.7.5}/tests/test_escalation_coverage.py +0 -0
  444. {syncade-0.7.3 → syncade-0.7.5}/tests/test_exit_codes.py +0 -0
  445. {syncade-0.7.3 → syncade-0.7.5}/tests/test_git_choke_point.py +0 -0
  446. {syncade-0.7.3 → syncade-0.7.5}/tests/test_git_config_isolation.py +0 -0
  447. {syncade-0.7.3 → syncade-0.7.5}/tests/test_git_preconditions.py +0 -0
  448. {syncade-0.7.3 → syncade-0.7.5}/tests/test_last_reviewed.py +0 -0
  449. {syncade-0.7.3 → syncade-0.7.5}/tests/test_parse_repair_extra_keys.py +0 -0
  450. {syncade-0.7.3 → syncade-0.7.5}/tests/test_pricing.py +0 -0
  451. {syncade-0.7.3 → syncade-0.7.5}/tests/test_private_identifier_gate.py +0 -0
  452. {syncade-0.7.3 → syncade-0.7.5}/tests/test_process.py +0 -0
  453. {syncade-0.7.3 → syncade-0.7.5}/tests/test_process_teardown.py +0 -0
  454. {syncade-0.7.3 → syncade-0.7.5}/tests/test_producer_escalation.py +0 -0
  455. {syncade-0.7.3 → syncade-0.7.5}/tests/test_regression_acme.py +0 -0
  456. {syncade-0.7.3 → syncade-0.7.5}/tests/test_retry.py +0 -0
  457. {syncade-0.7.3 → syncade-0.7.5}/tests/test_run_status.py +0 -0
  458. {syncade-0.7.3 → syncade-0.7.5}/tests/test_run_status_signals.py +0 -0
  459. {syncade-0.7.3 → syncade-0.7.5}/tests/test_runner/__init__.py +0 -0
  460. {syncade-0.7.3 → syncade-0.7.5}/tests/test_runner/test_test_runner.py +0 -0
  461. {syncade-0.7.3 → syncade-0.7.5}/tests/test_runner/test_test_runner_more.py +0 -0
  462. {syncade-0.7.3 → syncade-0.7.5}/tests/test_safety_disclosures_survive_quiet.py +0 -0
  463. {syncade-0.7.3 → syncade-0.7.5}/tests/test_selfcheck.py +0 -0
  464. {syncade-0.7.3 → syncade-0.7.5}/tests/test_spec_audit.py +0 -0
  465. {syncade-0.7.3 → syncade-0.7.5}/tests/test_spec_draft.py +0 -0
  466. {syncade-0.7.3 → syncade-0.7.5}/tests/test_spec_source.py +0 -0
  467. {syncade-0.7.3 → syncade-0.7.5}/tests/test_streamed_capture.py +0 -0
  468. {syncade-0.7.3 → syncade-0.7.5}/tests/test_strip_list_semantics.py +0 -0
  469. {syncade-0.7.3 → syncade-0.7.5}/tests/test_synthesizer.py +0 -0
  470. {syncade-0.7.3 → syncade-0.7.5}/tests/test_synthesizer_passthrough.py +0 -0
  471. {syncade-0.7.3 → syncade-0.7.5}/tests/test_synthesizer_provenance.py +0 -0
  472. {syncade-0.7.3 → syncade-0.7.5}/tests/test_transcript.py +0 -0
  473. {syncade-0.7.3 → syncade-0.7.5}/tests/test_update_check.py +0 -0
  474. {syncade-0.7.3 → syncade-0.7.5}/tests/test_usage.py +0 -0
  475. {syncade-0.7.3 → syncade-0.7.5}/tests/test_usage_persistence.py +0 -0
  476. {syncade-0.7.3 → syncade-0.7.5}/tests/test_workflow_action_pins.py +0 -0
  477. {syncade-0.7.3 → syncade-0.7.5}/tests/test_worktree_env.py +0 -0
  478. {syncade-0.7.3 → syncade-0.7.5}/tests/test_worktree_strip_guard.py +0 -0
  479. {syncade-0.7.3 → syncade-0.7.5}/tests/worktree/__init__.py +0 -0
  480. {syncade-0.7.3 → syncade-0.7.5}/tests/worktree/_helpers.py +0 -0
  481. {syncade-0.7.3 → syncade-0.7.5}/tests/worktree/conftest.py +0 -0
  482. {syncade-0.7.3 → syncade-0.7.5}/tests/worktree/test_worktree.py +0 -0
  483. {syncade-0.7.3 → syncade-0.7.5}/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.3
3
+ Version: 0.7.5
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
@@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta"
9
9
 
10
10
  [project]
11
11
  name = "syncade"
12
- version = "0.7.3"
12
+ version = "0.7.5"
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.3"
3
+ __version__ = "0.7.5"
@@ -21,6 +21,7 @@ import site
21
21
  import subprocess
22
22
  import sys
23
23
  from dataclasses import dataclass
24
+ from importlib.metadata import Distribution
24
25
  from pathlib import Path
25
26
 
26
27
  import syncade
@@ -29,9 +30,6 @@ from syncade.process import SubprocessError, run_subprocess
29
30
  from syncade.update_check import _parse, _text, manifest_once
30
31
 
31
32
  _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
33
  #: How far up from ``syncade/__init__.py`` an install marker can sit. A venv layout is
36
34
  #: ``<root>/lib/python3.x/site-packages/syncade/__init__.py`` — five parents to the root; the
37
35
  #: extra headroom costs nothing and covers layouts that nest one deeper.
@@ -59,6 +57,21 @@ def _in_user_site() -> bool:
59
57
  return user_site in Path(syncade.__file__).resolve().parents
60
58
 
61
59
 
60
+ def _scoped_dist_infos() -> list[Path]:
61
+ """The dist-info directories beside the package that supplied this process's code."""
62
+ try:
63
+ site_packages = Path(syncade.__file__).resolve().parent.parent
64
+ return sorted(
65
+ entry
66
+ for entry in site_packages.iterdir()
67
+ if entry.name.casefold().startswith("syncade-")
68
+ and entry.name.casefold().endswith(".dist-info")
69
+ and entry.is_dir()
70
+ )
71
+ except (OSError, RuntimeError, TypeError):
72
+ return []
73
+
74
+
62
75
  def _read_scoped_installer() -> str:
63
76
  """Read INSTALLER from the dist-info directory that BELONGS to this syncade install.
64
77
 
@@ -71,27 +84,44 @@ def _read_scoped_installer() -> str:
71
84
  is empty string — the same as not found. First-match would misclassify in that state,
72
85
  which contradicts the PROVE-or-refuse posture the rest of this module maintains.
73
86
  """
74
- site_packages = Path(syncade.__file__).resolve().parent.parent
75
87
  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
88
+ for entry in _scoped_dist_infos():
89
+ try:
90
+ found.append((entry / "INSTALLER").read_text(encoding="utf-8").strip())
91
+ except OSError:
92
+ pass # No INSTALLER file — this dist-info cannot prove anything.
89
93
  if not found:
90
94
  return ""
91
95
  # Contradictory entries in a broken packaging state are not proof of anything.
92
96
  return found[0] if len(set(found)) == 1 else ""
93
97
 
94
98
 
99
+ def _read_scoped_version() -> str | None:
100
+ """Read ``Version`` from this syncade install's one adjacent dist-info.
101
+
102
+ The package directory is already the authoritative identity: it supplied the code running
103
+ this function. Reading its sibling metadata after the package manager returns sees the new
104
+ on-disk version without resolving the name ``syncade`` through ``sys.path`` again. A missing,
105
+ malformed, or duplicate dist-info is ambiguous and therefore unverifiable, never first-match.
106
+ """
107
+ candidates = _scoped_dist_infos()
108
+ if len(candidates) != 1:
109
+ return None
110
+ try:
111
+ if not (candidates[0] / "METADATA").is_file():
112
+ return None
113
+ metadata = Distribution.at(candidates[0]).metadata
114
+ names = metadata.get_all("Name") or []
115
+ versions = metadata.get_all("Version") or []
116
+ except Exception: # noqa: BLE001 — broken install metadata degrades to "unverifiable".
117
+ return None
118
+ if len(names) != 1 or names[0].strip().casefold() != "syncade":
119
+ return None
120
+ if len(versions) != 1:
121
+ return None
122
+ return versions[0].strip()
123
+
124
+
95
125
  def _pip_or_unknown(where: Path | None) -> InstallMethod:
96
126
  """``pip`` when the distribution records pip as its installer, else ``unknown``.
97
127
 
@@ -239,49 +269,14 @@ def _installed_version() -> str | None:
239
269
  """The version on disk NOW, or ``None`` when it cannot be established.
240
270
 
241
271
  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.
272
+ it is by definition the old number. The value is read from the dist-info tree that belongs
273
+ to this install, so the answer follows the package on disk instead of any ambient metadata
274
+ discovery or subprocess import state.
245
275
 
246
276
  ``None`` means "could not verify", which is deliberately NOT the same as "did not change":
247
277
  the caller must not turn an unreadable version into a failure report.
248
278
  """
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()
279
+ candidate = _read_scoped_version()
285
280
  return candidate if _parse(candidate) is not None else None
286
281
 
287
282
 
@@ -327,6 +327,11 @@ def _default_reviewers() -> list[ReviewerConfig]:
327
327
  # model="claude-opus-4-6", adversarial_lens=True)
328
328
 
329
329
 
330
+ def _default_worktree_base() -> Path:
331
+ """Resolve the worktree default when a config instance is created."""
332
+ return DEFAULT_WORKTREE_BASE
333
+
334
+
330
335
  class SyncadeConfig(BaseModel):
331
336
  """Top-level ``.syncade/config.toml`` schema.
332
337
 
@@ -363,7 +368,7 @@ class SyncadeConfig(BaseModel):
363
368
  # ``[worktree]`` block — Q5); default reproduces worktree.DEFAULT_WORKTREE_BASE.
364
369
  # ``--worktree-base`` overrides per-invocation; threaded into the review run and doctor preview.
365
370
  worktree_base: Path = Field(
366
- default=DEFAULT_WORKTREE_BASE,
371
+ default_factory=_default_worktree_base,
367
372
  description=(
368
373
  "Base directory under which each run's per-reviewer/producer/test git worktrees are "
369
374
  f"created (default ``{DEFAULT_WORKTREE_BASE}``). Overridable per-run with "
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: syncade
3
- Version: 0.7.3
3
+ Version: 0.7.5
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
@@ -277,6 +277,7 @@ tests/cli/test_update_mode.py
277
277
  tests/cli/test_update_notice.py
278
278
  tests/cli/test_update_post_condition.py
279
279
  tests/cli/test_validate_before_mutate.py
280
+ tests/cli/test_version_probe.py
280
281
  tests/config/__init__.py
281
282
  tests/config/test_auth_enforcement.py
282
283
  tests/config/test_auth_modes.py
@@ -15,7 +15,6 @@ that finally held is to validate a value as the thing it claims to be, at the po
15
15
 
16
16
  from __future__ import annotations
17
17
 
18
- import sys
19
18
  from pathlib import Path
20
19
 
21
20
  import pytest
@@ -23,7 +22,6 @@ import pytest
23
22
  import syncade
24
23
  from syncade.cli import update_mode
25
24
  from syncade.exit_codes import SUCCESS, WORKTREE_ERROR
26
- from syncade.process import SubprocessError, SubprocessResult
27
25
  from tests.cli.test_update_mode import (
28
26
  _install_tree,
29
27
  _point_syncade_at,
@@ -90,62 +88,6 @@ def test_an_unverifiable_version_is_not_reported_as_failure(tmp_path, monkeypatc
90
88
  assert "updated to" not in out, "no version may be named when none was established"
91
89
 
92
90
 
93
- def test_the_version_probe_reads_a_fresh_interpreter_not_this_one(monkeypatch) -> None:
94
- """It must not answer from `syncade.__version__`, which this process imported BEFORE the
95
- upgrade and is therefore the old number by construction."""
96
- seen: list[list[str]] = []
97
-
98
- def spy(cmd, **kw):
99
- seen.append(cmd)
100
- return SubprocessResult(returncode=0, stdout="1.2.3\n", stderr="", duration_seconds=0.0)
101
-
102
- monkeypatch.setattr(update_mode, "run_subprocess", spy)
103
- assert update_mode._installed_version() == "1.2.3"
104
- assert seen and seen[0][0] == sys.executable, "must re-invoke the interpreter, not import"
105
- assert "importlib.metadata" in seen[0][-1]
106
-
107
-
108
- def test_an_unreadable_probe_degrades_to_none(monkeypatch) -> None:
109
- for outcome in (
110
- lambda *a, **k: (_ for _ in ()).throw(SubprocessError("no python")),
111
- lambda *a, **k: SubprocessResult(returncode=1, stdout="", stderr="x", duration_seconds=0.0),
112
- lambda *a, **k: SubprocessResult(
113
- returncode=0, stdout=" \n", stderr="", duration_seconds=0.0
114
- ),
115
- ):
116
- monkeypatch.setattr(update_mode, "run_subprocess", outcome)
117
- assert update_mode._installed_version() is None
118
-
119
-
120
- def test_startup_noise_before_version_line_is_stripped(monkeypatch) -> None:
121
- """sitecustomize imported from PYTHONPATH can emit stdout before the version print.
122
-
123
- The old code returned the full stdout verbatim, so "noise\\n0.7.0" != "0.7.0" and
124
- run_update treated the install as having moved — a false success on an unchanged version.
125
- """
126
- monkeypatch.setattr(
127
- update_mode,
128
- "run_subprocess",
129
- lambda *a, **k: SubprocessResult(
130
- returncode=0, stdout="startup noise\n1.2.3\n", stderr="", duration_seconds=0.0
131
- ),
132
- )
133
- assert update_mode._installed_version() == "1.2.3"
134
-
135
-
136
- def test_non_version_stdout_degrades_to_none(monkeypatch) -> None:
137
- """stdout that doesn't end with a parseable version string means 'could not verify'."""
138
- for stdout in ("not a version\n", "noise only\n", "1.2\n", "1.2.3.4\n"):
139
- monkeypatch.setattr(
140
- update_mode,
141
- "run_subprocess",
142
- lambda *a, stdout=stdout, **k: SubprocessResult(
143
- returncode=0, stdout=stdout, stderr="", duration_seconds=0.0
144
- ),
145
- )
146
- assert update_mode._installed_version() is None, f"should be None for {stdout!r}"
147
-
148
-
149
91
  def test_an_already_current_install_is_not_reported_as_a_failed_upgrade(
150
92
  tmp_path, monkeypatch, capsys
151
93
  ) -> None:
@@ -358,71 +300,6 @@ def test_is_newest_cannot_certify_an_unparseable_installed_version(monkeypatch)
358
300
  assert update_mode._is_newest("not-a-version") is None
359
301
 
360
302
 
361
- def test_the_probe_is_isolated_from_ambient_interpreter_state(monkeypatch) -> None:
362
- """`-I` is load-bearing: without it the probe inherits PYTHONPATH and the user site dir.
363
-
364
- A `sitecustomize` on that path runs INSIDE the answer — an atexit hook printing a version
365
- number becomes the last stdout line, so a no-op upgrade reads as a move. Reproduced live: a
366
- hostile PYTHONPATH turned the probe's `"0.7.0"` into `"0.7.0\\n99.0.0"`. Syncade ships such a
367
- shim itself for worktree imports, so this is not purely adversarial.
368
-
369
- `-I` suppresses PYTHONPATH but NOT the venv's own site-packages, where a `sitecustomize.py`
370
- can still register atexit hooks. The probe must call `os._exit(0)` to bypass atexit entirely —
371
- the only guarantee against a venv-local hook appending to stdout after the version line.
372
- `os.write` is asserted alongside it: `os._exit` skips stdio flushing, so `print()` would lose
373
- the version; the unbuffered syscall guarantees delivery before the process exits.
374
-
375
- Pinned as flag assertions because the behavioural twin below cannot run the failure case
376
- without installing a real adversarial sitecustomize.
377
- """
378
- seen: list[list[str]] = []
379
- monkeypatch.setattr(
380
- update_mode,
381
- "run_subprocess",
382
- lambda cmd, **kw: (
383
- seen.append(cmd)
384
- or SubprocessResult(returncode=0, stdout="1.2.3\n", stderr="", duration_seconds=0.0)
385
- ),
386
- )
387
- update_mode._installed_version()
388
- assert seen and "-I" in seen[0], (
389
- "the version probe must run isolated; without -I a sitecustomize on PYTHONPATH can "
390
- "append a version number to its stdout and fake a successful upgrade"
391
- )
392
- assert seen[0].index("-I") < seen[0].index("-c"), "-I must precede -c to take effect"
393
- c_arg = seen[0][seen[0].index("-c") + 1]
394
- assert "os._exit(0)" in c_arg, (
395
- "the probe must call os._exit(0) to bypass atexit handlers; a venv-local "
396
- "sitecustomize can register atexit hooks that append a fake version line after the real one"
397
- )
398
- assert "os.write" in c_arg, (
399
- "the probe must use os.write (unbuffered) not print; os._exit skips stdio flushing "
400
- "so print output would be lost before reaching the pipe"
401
- )
402
-
403
-
404
- def test_the_isolated_probe_still_resolves_the_real_install() -> None:
405
- """The behavioural twin: `-I` must not break what the probe is FOR.
406
-
407
- It drops PYTHONPATH and the user site dir but keeps the environment's own site-packages, so
408
- `importlib.metadata` still finds the package. Runs the real subprocess — a flag assertion
409
- alone would happily pin an isolation level that returns None for everyone.
410
-
411
- We assert the result is a parseable, non-None version, NOT that it equals
412
- `syncade.__version__`. In a dev worktree the source version and the installed distribution
413
- version legitimately differ (e.g. 0.7.0 in source, 0.1.0 in site-packages); asserting
414
- equality would make this test prove the ambient installation, not the probe's behaviour.
415
- """
416
- from syncade.update_check import _parse
417
-
418
- result = update_mode._installed_version()
419
- assert result is not None, (
420
- "the probe must find the installed package under -I "
421
- "(drops PYTHONPATH but keeps the environment's own site-packages)"
422
- )
423
- assert _parse(result) is not None, f"result {result!r} must be a parseable version string"
424
-
425
-
426
303
  # ------------------------------------------------- unreachable manifest is not a pin (field-09)
427
304
 
428
305
 
@@ -654,3 +531,6 @@ def test_doctor_skips_the_check_under_ci(monkeypatch) -> None:
654
531
  check = _check_update_manifest(enabled=True)
655
532
  assert check.status == "skip"
656
533
  assert "CI" in check.detail
534
+
535
+
536
+ # ------------------------------------- the probe must see a user-site install (pr-h-field-10)
@@ -0,0 +1,158 @@
1
+ """The post-update version comes from the exact install tree running syncade.
2
+
3
+ The old probe spawned another interpreter and asked which distribution the name ``syncade``
4
+ resolved to. Isolation could hide a user-site install; relaxing it let CWD, user-site metadata,
5
+ ``sitecustomize``, and shutdown hooks influence the answer. These tests pin the re-derived
6
+ property instead: read fresh metadata beside ``syncade.__file__`` and never resolve the name.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ import pytest
15
+
16
+ from syncade.cli import update_mode
17
+ from syncade.exit_codes import SUCCESS
18
+ from syncade.process import SubprocessResult
19
+
20
+
21
+ def _metadata(version: str, *, name: str = "syncade") -> str:
22
+ return f"Metadata-Version: 2.1\nName: {name}\nVersion: {version}\n"
23
+
24
+
25
+ def _install_tree(
26
+ tmp_path: Path, version: str = "1.2.3", *, installer: str | None = None
27
+ ) -> tuple[Path, Path]:
28
+ site_packages = tmp_path / "site-packages"
29
+ package_file = site_packages / "syncade" / "__init__.py"
30
+ package_file.parent.mkdir(parents=True)
31
+ package_file.write_text("__version__ = 'loaded-before-the-update'\n", encoding="utf-8")
32
+ dist_info = site_packages / f"syncade-{version}.dist-info"
33
+ dist_info.mkdir()
34
+ (dist_info / "METADATA").write_text(_metadata(version), encoding="utf-8")
35
+ if installer is not None:
36
+ (dist_info / "INSTALLER").write_text(installer, encoding="utf-8")
37
+ return package_file, dist_info
38
+
39
+
40
+ def _point_syncade_at(monkeypatch: pytest.MonkeyPatch, package_file: Path) -> None:
41
+ monkeypatch.setattr(update_mode.syncade, "__file__", str(package_file))
42
+
43
+
44
+ def test_reads_fresh_version_from_the_running_install_tree(tmp_path, monkeypatch) -> None:
45
+ package_file, dist_info = _install_tree(tmp_path, "1.2.3")
46
+ _point_syncade_at(monkeypatch, package_file)
47
+
48
+ assert update_mode._installed_version() == "1.2.3"
49
+
50
+ replacement = dist_info.with_name("syncade-1.2.4.dist-info")
51
+ dist_info.rename(replacement)
52
+ (replacement / "METADATA").write_text(_metadata("1.2.4"), encoding="utf-8")
53
+ assert update_mode._installed_version() == "1.2.4", "the post-condition must not be cached"
54
+
55
+
56
+ def test_ambient_python_state_and_shutdown_hooks_are_not_probe_channels(
57
+ tmp_path, monkeypatch
58
+ ) -> None:
59
+ package_file, _ = _install_tree(tmp_path / "real", "1.2.3")
60
+ _point_syncade_at(monkeypatch, package_file)
61
+
62
+ hostile_path = tmp_path / "hostile-pythonpath"
63
+ hostile_path.mkdir()
64
+ (hostile_path / "sitecustomize.py").write_text(
65
+ "import atexit\nprint('98.0.0')\natexit.register(lambda: print('99.0.0'))\n",
66
+ encoding="utf-8",
67
+ )
68
+ cwd = tmp_path / "cwd"
69
+ cwd.mkdir()
70
+ cwd_dist = cwd / "syncade-97.0.0.dist-info"
71
+ cwd_dist.mkdir()
72
+ (cwd_dist / "METADATA").write_text(_metadata("97.0.0"), encoding="utf-8")
73
+ monkeypatch.chdir(cwd)
74
+ monkeypatch.setenv("PYTHONPATH", str(hostile_path))
75
+ monkeypatch.setenv("PYTHONUSERBASE", str(tmp_path / "unrelated-user-base"))
76
+ monkeypatch.setattr(
77
+ update_mode,
78
+ "run_subprocess",
79
+ lambda *a, **k: pytest.fail("reading an installed version must not spawn a subprocess"),
80
+ )
81
+
82
+ assert update_mode._installed_version() == "1.2.3"
83
+
84
+
85
+ @pytest.mark.parametrize(
86
+ "metadata_bytes",
87
+ [
88
+ None,
89
+ b"\xff",
90
+ b"Metadata-Version: 2.1\nVersion: 1.2.3\n",
91
+ b"Metadata-Version: 2.1\nName: other\nVersion: 1.2.3\n",
92
+ b"Metadata-Version: 2.1\nName: syncade\n",
93
+ b"Metadata-Version: 2.1\nName: syncade\nVersion: 1.2\n",
94
+ b"Metadata-Version: 2.1\nName: syncade\nName: syncade\nVersion: 1.2.3\n",
95
+ b"Metadata-Version: 2.1\nName: syncade\nVersion: 1.2.3\nVersion: 9.9.9\n",
96
+ ],
97
+ )
98
+ def test_missing_or_malformed_metadata_is_unverifiable(
99
+ tmp_path, monkeypatch, metadata_bytes
100
+ ) -> None:
101
+ package_file, dist_info = _install_tree(tmp_path)
102
+ metadata_path = dist_info / "METADATA"
103
+ if metadata_bytes is None:
104
+ metadata_path.unlink()
105
+ else:
106
+ metadata_path.write_bytes(metadata_bytes)
107
+ _point_syncade_at(monkeypatch, package_file)
108
+
109
+ assert update_mode._installed_version() is None
110
+
111
+
112
+ def test_duplicate_adjacent_dist_info_is_unverifiable(tmp_path, monkeypatch) -> None:
113
+ package_file, _ = _install_tree(tmp_path, "1.2.3")
114
+ site_packages = package_file.parent.parent
115
+ stale = site_packages / "syncade-9.9.9.dist-info"
116
+ stale.mkdir()
117
+ (stale / "METADATA").write_text(_metadata("9.9.9"), encoding="utf-8")
118
+ _point_syncade_at(monkeypatch, package_file)
119
+
120
+ assert update_mode._installed_version() is None
121
+
122
+
123
+ def test_non_adjacent_metadata_and_editable_metadata_are_ignored(tmp_path, monkeypatch) -> None:
124
+ package_file = tmp_path / "checkout" / "src" / "syncade" / "__init__.py"
125
+ package_file.parent.mkdir(parents=True)
126
+ package_file.write_text("__version__ = '0.0.0'\n", encoding="utf-8")
127
+ egg_info = package_file.parent.parent / "syncade.egg-info"
128
+ egg_info.mkdir()
129
+ (egg_info / "PKG-INFO").write_text(_metadata("8.8.8"), encoding="utf-8")
130
+ _install_tree(tmp_path / "other-install", "9.9.9")
131
+ _point_syncade_at(monkeypatch, package_file)
132
+
133
+ assert update_mode._installed_version() is None
134
+
135
+
136
+ def test_user_site_noop_update_reports_already_current(tmp_path, monkeypatch, capsys) -> None:
137
+ current = update_mode.syncade.__version__
138
+ package_file, _ = _install_tree(tmp_path, current, installer="pip")
139
+ site_packages = package_file.parent.parent.resolve()
140
+ _point_syncade_at(monkeypatch, package_file)
141
+ monkeypatch.setattr(update_mode.site, "getusersitepackages", lambda: str(site_packages))
142
+ monkeypatch.setattr(update_mode, "live_run", lambda cwd: None)
143
+ monkeypatch.setattr(update_mode, "_resync_skills", lambda out: True)
144
+ monkeypatch.setattr(update_mode, "manifest_once", lambda *, enabled=True: {"latest": current})
145
+ monkeypatch.delenv("CI", raising=False)
146
+ commands: list[list[str]] = []
147
+
148
+ def successful_package_manager(cmd, **kwargs):
149
+ commands.append(cmd)
150
+ return SubprocessResult(returncode=0, stdout="", stderr="", duration_seconds=0.0)
151
+
152
+ monkeypatch.setattr(update_mode, "run_subprocess", successful_package_manager)
153
+
154
+ assert update_mode.run_update(cwd=tmp_path) == SUCCESS
155
+ assert commands == [[sys.executable, "-m", "pip", "install", "-U", "--user", "syncade"]]
156
+ out = capsys.readouterr().err
157
+ assert f"already up to date ({current})" in out
158
+ assert "[syncade] updated." not in out
@@ -79,7 +79,7 @@ def _isolate_global_config(monkeypatch, tmp_path_factory):
79
79
 
80
80
 
81
81
  @pytest.fixture(autouse=True)
82
- def _isolated_worktree_base(request, tmp_path):
82
+ def _isolated_worktree_base(request, tmp_path, monkeypatch):
83
83
  """No test may touch the SHARED worktree base — PR-h-12 item 1b.
84
84
 
85
85
  Moved here from ``tests/orchestrator/conftest.py``, where it was package-scoped. Its own
@@ -89,12 +89,12 @@ def _isolated_worktree_base(request, tmp_path):
89
89
  worktrees leaked — so the brief's original 4.4 GB attribution was wrong, exactly as it later
90
90
  corrected itself to say. The gap was real; the magnitude was not.
91
91
 
92
- ``run_review`` falls back to ``config.worktree_base`` (PR-v2-9), so the PYDANTIC FIELD DEFAULT
93
- is what must move: a bare ``SyncadeConfig()`` has to inherit the per-test path too.
94
- ``model_rebuild(force=True)`` recompiles the core schema after the patch, and both are
95
- restored at teardown so no schema state bleeds between tests.
92
+ ``run_review`` falls back to ``config.worktree_base`` (PR-v2-9), so the config default is what
93
+ must move: a bare ``SyncadeConfig()`` has to inherit the per-test path too. The field uses a
94
+ default factory so patching its module-level source works across the supported Pydantic range
95
+ without mutating Pydantic's compiled schema.
96
96
  """
97
- from syncade.config import SyncadeConfig
97
+ from syncade import config as config_module
98
98
 
99
99
  # A DRIFT TEST must be able to see the real default. Two of them assert exactly that
100
100
  # (`SyncadeConfig().worktree_base == DEFAULT_WORKTREE_BASE`, "byte-identical"), and this
@@ -105,13 +105,12 @@ def _isolated_worktree_base(request, tmp_path):
105
105
  yield
106
106
  return
107
107
 
108
- field = SyncadeConfig.model_fields["worktree_base"]
109
- old_default = field.default
110
- field.default = tmp_path / "syncade-worktrees"
111
- SyncadeConfig.model_rebuild(force=True)
108
+ monkeypatch.setattr(
109
+ config_module,
110
+ "DEFAULT_WORKTREE_BASE",
111
+ tmp_path / "syncade-worktrees",
112
+ )
112
113
  yield
113
- field.default = old_default
114
- SyncadeConfig.model_rebuild(force=True)
115
114
 
116
115
 
117
116
  @pytest.fixture(autouse=True)
@@ -23,8 +23,8 @@ _ROOT = Path(__file__).resolve().parent
23
23
 
24
24
 
25
25
  def test_a_bare_config_never_resolves_to_the_shared_base(tmp_path: Path) -> None:
26
- """The fixture patches the PYDANTIC FIELD DEFAULT, because `run_review` falls back to
27
- `config.worktree_base` (PR-v2-9). Patching only an explicit argument would leave
26
+ """The fixture patches the default factory's source, because `run_review` falls back to
27
+ `config.worktree_base` (PR-v2-9). Patching only an explicit argument would leave a bare
28
28
  `SyncadeConfig()` pointing at the shared base — which is how six packages kept reaching it.
29
29
  """
30
30
  assert SyncadeConfig().worktree_base != DEFAULT_WORKTREE_BASE
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes