syncade 0.7.0__tar.gz → 0.7.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (482) hide show
  1. {syncade-0.7.0/src/syncade.egg-info → syncade-0.7.1}/PKG-INFO +1 -1
  2. {syncade-0.7.0 → syncade-0.7.1}/pyproject.toml +1 -1
  3. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/__init__.py +1 -1
  4. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/update_mode.py +114 -1
  5. {syncade-0.7.0 → syncade-0.7.1/src/syncade.egg-info}/PKG-INFO +1 -1
  6. {syncade-0.7.0 → syncade-0.7.1}/src/syncade.egg-info/SOURCES.txt +1 -0
  7. {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_update_mode.py +1 -1
  8. syncade-0.7.1/tests/cli/test_update_post_condition.py +398 -0
  9. {syncade-0.7.0 → syncade-0.7.1}/LICENSE +0 -0
  10. {syncade-0.7.0 → syncade-0.7.1}/MANIFEST.in +0 -0
  11. {syncade-0.7.0 → syncade-0.7.1}/README.md +0 -0
  12. {syncade-0.7.0 → syncade-0.7.1}/docs/config-reference.md +0 -0
  13. {syncade-0.7.0 → syncade-0.7.1}/docs/how-to-use.md +0 -0
  14. {syncade-0.7.0 → syncade-0.7.1}/setup.cfg +0 -0
  15. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/__main__.py +0 -0
  16. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/adapters/__init__.py +0 -0
  17. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/adapters/anthropic.py +0 -0
  18. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/adapters/base.py +0 -0
  19. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/adapters/fake.py +0 -0
  20. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/adapters/fake_common.py +0 -0
  21. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/adapters/fake_producer_audit_draft.py +0 -0
  22. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/adapters/fake_reviewer_synth.py +0 -0
  23. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/adapters/openai.py +0 -0
  24. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/adapters/openai_parsing.py +0 -0
  25. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/adapters/producer.py +0 -0
  26. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/adapters/producer_anthropic.py +0 -0
  27. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/adapters/producer_openai.py +0 -0
  28. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/adapters/registry.py +0 -0
  29. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/auth_check.py +0 -0
  30. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/auth_preflight.py +0 -0
  31. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/base_resolution.py +0 -0
  32. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/billing.py +0 -0
  33. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/checks_config.py +0 -0
  34. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/__init__.py +0 -0
  35. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/auth_gate.py +0 -0
  36. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/config_keys.py +0 -0
  37. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/config_list.py +0 -0
  38. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/config_menu_rows.py +0 -0
  39. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/config_mode.py +0 -0
  40. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/config_overrides.py +0 -0
  41. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/config_tui.py +0 -0
  42. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/doctor_mode.py +0 -0
  43. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/gc_mode.py +0 -0
  44. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/install_skill.py +0 -0
  45. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/metrics_mode.py +0 -0
  46. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/modes.py +0 -0
  47. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/parser.py +0 -0
  48. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/parser_types.py +0 -0
  49. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/paths.py +0 -0
  50. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/preflight_paths.py +0 -0
  51. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/resolve.py +0 -0
  52. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/resume_mode.py +0 -0
  53. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/skill_status.py +0 -0
  54. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/toml_writer.py +0 -0
  55. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/update_notice.py +0 -0
  56. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/cli/validate.py +0 -0
  57. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/config.py +0 -0
  58. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/config_auth.py +0 -0
  59. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/config_cold.py +0 -0
  60. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/config_gc.py +0 -0
  61. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/config_loader.py +0 -0
  62. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/config_loop.py +0 -0
  63. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/config_producer.py +0 -0
  64. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/config_retry.py +0 -0
  65. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/config_types.py +0 -0
  66. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/config_update.py +0 -0
  67. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/diff_filter.py +0 -0
  68. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/dispatcher.py +0 -0
  69. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/doctor.py +0 -0
  70. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/doctor_env.py +0 -0
  71. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/doctor_preview.py +0 -0
  72. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/doctor_types.py +0 -0
  73. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/exit_codes.py +0 -0
  74. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/findings.py +0 -0
  75. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/findings_json.py +0 -0
  76. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/gc.py +0 -0
  77. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/gc_execute.py +0 -0
  78. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/gc_protection.py +0 -0
  79. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/gc_types.py +0 -0
  80. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/gc_worktrees.py +0 -0
  81. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/git_object_id.py +0 -0
  82. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/git_preconditions.py +0 -0
  83. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/logging.py +0 -0
  84. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/metrics/__init__.py +0 -0
  85. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/metrics/aggregate.py +0 -0
  86. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/metrics/findings.py +0 -0
  87. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/metrics/schema.py +0 -0
  88. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/__init__.py +0 -0
  89. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/_runs_dir.py +0 -0
  90. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/branch_advance.py +0 -0
  91. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/branch_guard.py +0 -0
  92. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/budget.py +0 -0
  93. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/escalation_coverage.py +0 -0
  94. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/loop.py +0 -0
  95. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/loop_dispatch_check.py +0 -0
  96. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/loop_finalize.py +0 -0
  97. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/loop_preflight.py +0 -0
  98. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/loop_resume.py +0 -0
  99. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/loop_rmtree.py +0 -0
  100. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/loop_round_step.py +0 -0
  101. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/prior_round.py +0 -0
  102. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/producer_phase.py +0 -0
  103. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/results.py +0 -0
  104. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/resume.py +0 -0
  105. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/resume_load.py +0 -0
  106. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/resume_plan.py +0 -0
  107. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/resume_target.py +0 -0
  108. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/resume_types.py +0 -0
  109. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/reviewer_template_failure.py +0 -0
  110. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/round.py +0 -0
  111. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/round_checks.py +0 -0
  112. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/round_no_changes.py +0 -0
  113. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/round_predispatch.py +0 -0
  114. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/orchestrator/verdict.py +0 -0
  115. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/__init__.py +0 -0
  116. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/_atomic.py +0 -0
  117. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/_clusters.py +0 -0
  118. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/_findings_verdict.py +0 -0
  119. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/_markdown.py +0 -0
  120. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/_validation.py +0 -0
  121. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/checks.py +0 -0
  122. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/decision_needed.py +0 -0
  123. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/findings_md.py +0 -0
  124. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/handoff.py +0 -0
  125. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/handoff_classify.py +0 -0
  126. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/last_reviewed.py +0 -0
  127. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/loop_manifest.py +0 -0
  128. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/loop_summary.py +0 -0
  129. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/loop_summary_text.py +0 -0
  130. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/producer.py +0 -0
  131. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/reviewer.py +0 -0
  132. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/round_manifest.py +0 -0
  133. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/run_init.py +0 -0
  134. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/run_summary.py +0 -0
  135. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/run_summary_next_steps.py +0 -0
  136. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/synth.py +0 -0
  137. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/persistence/test_run.py +0 -0
  138. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/presets.py +0 -0
  139. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/pricing_config.py +0 -0
  140. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/process.py +0 -0
  141. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/producer.py +0 -0
  142. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/producer_attempt.py +0 -0
  143. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/producer_escalation.py +0 -0
  144. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/producer_git.py +0 -0
  145. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/producer_result.py +0 -0
  146. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/prompts.py +0 -0
  147. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/prompts_loader.py +0 -0
  148. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/retry.py +0 -0
  149. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/run_inputs.py +0 -0
  150. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/run_status.py +0 -0
  151. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/selfcheck.py +0 -0
  152. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/skills/claude/README.md +0 -0
  153. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/skills/claude/SKILL.md +0 -0
  154. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/skills/codex/README.md +0 -0
  155. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/skills/codex/SKILL.md +0 -0
  156. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/snapshot.py +0 -0
  157. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/spec_audit.py +0 -0
  158. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/spec_audit_schema.py +0 -0
  159. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/spec_draft.py +0 -0
  160. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/spec_source.py +0 -0
  161. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/synthesis.py +0 -0
  162. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/synthesis_clusters.py +0 -0
  163. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/synthesis_repair.py +0 -0
  164. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/synthesis_schema.py +0 -0
  165. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/synthesizer/__init__.py +0 -0
  166. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/synthesizer/constants.py +0 -0
  167. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/synthesizer/driver.py +0 -0
  168. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/synthesizer/rendering.py +0 -0
  169. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/synthesizer/result.py +0 -0
  170. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/synthesizer/validation.py +0 -0
  171. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/synthesizer/workspace.py +0 -0
  172. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/templates/presets/balanced.toml +0 -0
  173. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/templates/presets/cheap.toml +0 -0
  174. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/templates/presets/thorough.toml +0 -0
  175. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/templates/producer.md +0 -0
  176. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/templates/reviewer.md +0 -0
  177. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/templates/reviewer_adversarial.md +0 -0
  178. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/templates/reviewer_codex.md +0 -0
  179. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/templates/spec_audit.md +0 -0
  180. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/templates/spec_draft.md +0 -0
  181. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/templates/synthesizer.md +0 -0
  182. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/test_runner.py +0 -0
  183. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/test_runner_classify.py +0 -0
  184. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/transcript.py +0 -0
  185. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/update_check.py +0 -0
  186. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/usage.py +0 -0
  187. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/worktree.py +0 -0
  188. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/worktree_env.py +0 -0
  189. {syncade-0.7.0 → syncade-0.7.1}/src/syncade/worktree_paths.py +0 -0
  190. {syncade-0.7.0 → syncade-0.7.1}/src/syncade.egg-info/dependency_links.txt +0 -0
  191. {syncade-0.7.0 → syncade-0.7.1}/src/syncade.egg-info/entry_points.txt +0 -0
  192. {syncade-0.7.0 → syncade-0.7.1}/src/syncade.egg-info/requires.txt +0 -0
  193. {syncade-0.7.0 → syncade-0.7.1}/src/syncade.egg-info/top_level.txt +0 -0
  194. {syncade-0.7.0 → syncade-0.7.1}/tests/__init__.py +0 -0
  195. {syncade-0.7.0 → syncade-0.7.1}/tests/adapters/__init__.py +0 -0
  196. {syncade-0.7.0 → syncade-0.7.1}/tests/adapters/_anthropic_helpers.py +0 -0
  197. {syncade-0.7.0 → syncade-0.7.1}/tests/adapters/_openai_helpers.py +0 -0
  198. {syncade-0.7.0 → syncade-0.7.1}/tests/adapters/test_anthropic.py +0 -0
  199. {syncade-0.7.0 → syncade-0.7.1}/tests/adapters/test_anthropic_more.py +0 -0
  200. {syncade-0.7.0 → syncade-0.7.1}/tests/adapters/test_openai.py +0 -0
  201. {syncade-0.7.0 → syncade-0.7.1}/tests/adapters/test_openai_more.py +0 -0
  202. {syncade-0.7.0 → syncade-0.7.1}/tests/adapters/test_producer.py +0 -0
  203. {syncade-0.7.0 → syncade-0.7.1}/tests/adapters/test_producer_more.py +0 -0
  204. {syncade-0.7.0 → syncade-0.7.1}/tests/adapters/test_prompt_transport.py +0 -0
  205. {syncade-0.7.0 → syncade-0.7.1}/tests/auth_check/__init__.py +0 -0
  206. {syncade-0.7.0 → syncade-0.7.1}/tests/auth_check/test_probes.py +0 -0
  207. {syncade-0.7.0 → syncade-0.7.1}/tests/auth_check/test_run_auth_check.py +0 -0
  208. {syncade-0.7.0 → syncade-0.7.1}/tests/cli/__init__.py +0 -0
  209. {syncade-0.7.0 → syncade-0.7.1}/tests/cli/_helpers.py +0 -0
  210. {syncade-0.7.0 → syncade-0.7.1}/tests/cli/_undo_helpers.py +0 -0
  211. {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_basic_and_mutex.py +0 -0
  212. {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_cli_library_divergence.py +0 -0
  213. {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_config_keys.py +0 -0
  214. {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_config_list.py +0 -0
  215. {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_config_menu_rows.py +0 -0
  216. {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_config_mode.py +0 -0
  217. {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_config_overrides.py +0 -0
  218. {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_config_tui.py +0 -0
  219. {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_config_tui_pty.py +0 -0
  220. {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_documented_exit_codes.py +0 -0
  221. {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_documented_flags_exist.py +0 -0
  222. {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_flags.py +0 -0
  223. {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_gc.py +0 -0
  224. {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_help_retention_contract.py +0 -0
  225. {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_mode_exclusivity_matrix.py +0 -0
  226. {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_mode_flag_guards.py +0 -0
  227. {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_resume_reports_hard_kill.py +0 -0
  228. {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_run_and_config.py +0 -0
  229. {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_run_status_cli.py +0 -0
  230. {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_scope_openspec_draft.py +0 -0
  231. {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_toml_writer.py +0 -0
  232. {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_undo_auto_init.py +0 -0
  233. {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_undo_scope.py +0 -0
  234. {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_update_notice.py +0 -0
  235. {syncade-0.7.0 → syncade-0.7.1}/tests/cli/test_validate_before_mutate.py +0 -0
  236. {syncade-0.7.0 → syncade-0.7.1}/tests/config/__init__.py +0 -0
  237. {syncade-0.7.0 → syncade-0.7.1}/tests/config/test_auth_enforcement.py +0 -0
  238. {syncade-0.7.0 → syncade-0.7.1}/tests/config/test_auth_modes.py +0 -0
  239. {syncade-0.7.0 → syncade-0.7.1}/tests/config/test_auth_preflight.py +0 -0
  240. {syncade-0.7.0 → syncade-0.7.1}/tests/config/test_auth_report_scope.py +0 -0
  241. {syncade-0.7.0 → syncade-0.7.1}/tests/config/test_budget_config.py +0 -0
  242. {syncade-0.7.0 → syncade-0.7.1}/tests/config/test_cold_actors.py +0 -0
  243. {syncade-0.7.0 → syncade-0.7.1}/tests/config/test_config_checks.py +0 -0
  244. {syncade-0.7.0 → syncade-0.7.1}/tests/config/test_config_reference_drift.py +0 -0
  245. {syncade-0.7.0 → syncade-0.7.1}/tests/config/test_config_schema.py +0 -0
  246. {syncade-0.7.0 → syncade-0.7.1}/tests/config/test_gc_config.py +0 -0
  247. {syncade-0.7.0 → syncade-0.7.1}/tests/config/test_global_config_layer.py +0 -0
  248. {syncade-0.7.0 → syncade-0.7.1}/tests/config/test_presets.py +0 -0
  249. {syncade-0.7.0 → syncade-0.7.1}/tests/config/test_retry_config.py +0 -0
  250. {syncade-0.7.0 → syncade-0.7.1}/tests/config/test_reviewer_config.py +0 -0
  251. {syncade-0.7.0 → syncade-0.7.1}/tests/config/test_reviewer_permissions.py +0 -0
  252. {syncade-0.7.0 → syncade-0.7.1}/tests/config/test_worktree_base_config.py +0 -0
  253. {syncade-0.7.0 → syncade-0.7.1}/tests/conftest.py +0 -0
  254. {syncade-0.7.0 → syncade-0.7.1}/tests/dispatcher/__init__.py +0 -0
  255. {syncade-0.7.0 → syncade-0.7.1}/tests/dispatcher/_helpers.py +0 -0
  256. {syncade-0.7.0 → syncade-0.7.1}/tests/dispatcher/test_dispatch_core.py +0 -0
  257. {syncade-0.7.0 → syncade-0.7.1}/tests/dispatcher/test_dispatch_misc.py +0 -0
  258. {syncade-0.7.0 → syncade-0.7.1}/tests/dispatcher/test_dispatch_usage.py +0 -0
  259. {syncade-0.7.0 → syncade-0.7.1}/tests/doctor/__init__.py +0 -0
  260. {syncade-0.7.0 → syncade-0.7.1}/tests/doctor/_helpers.py +0 -0
  261. {syncade-0.7.0 → syncade-0.7.1}/tests/doctor/conftest.py +0 -0
  262. {syncade-0.7.0 → syncade-0.7.1}/tests/doctor/test_doctor.py +0 -0
  263. {syncade-0.7.0 → syncade-0.7.1}/tests/doctor/test_doctor_diff_size.py +0 -0
  264. {syncade-0.7.0 → syncade-0.7.1}/tests/doctor/test_doctor_malformed_diff.py +0 -0
  265. {syncade-0.7.0 → syncade-0.7.1}/tests/doctor/test_doctor_preview.py +0 -0
  266. {syncade-0.7.0 → syncade-0.7.1}/tests/findings/__init__.py +0 -0
  267. {syncade-0.7.0 → syncade-0.7.1}/tests/findings/_helpers.py +0 -0
  268. {syncade-0.7.0 → syncade-0.7.1}/tests/findings/test_parser_and_fields.py +0 -0
  269. {syncade-0.7.0 → syncade-0.7.1}/tests/findings/test_parser_resilience.py +0 -0
  270. {syncade-0.7.0 → syncade-0.7.1}/tests/findings/test_verdict_block_selection.py +0 -0
  271. {syncade-0.7.0 → syncade-0.7.1}/tests/gc/__init__.py +0 -0
  272. {syncade-0.7.0 → syncade-0.7.1}/tests/gc/_helpers.py +0 -0
  273. {syncade-0.7.0 → syncade-0.7.1}/tests/gc/test_autoprune.py +0 -0
  274. {syncade-0.7.0 → syncade-0.7.1}/tests/gc/test_execute.py +0 -0
  275. {syncade-0.7.0 → syncade-0.7.1}/tests/gc/test_plan.py +0 -0
  276. {syncade-0.7.0 → syncade-0.7.1}/tests/gc/test_plan_execution_races.py +0 -0
  277. {syncade-0.7.0 → syncade-0.7.1}/tests/gc/test_reaping_scope.py +0 -0
  278. {syncade-0.7.0 → syncade-0.7.1}/tests/gc/test_retention_tiers.py +0 -0
  279. {syncade-0.7.0 → syncade-0.7.1}/tests/gc/test_robustness.py +0 -0
  280. {syncade-0.7.0 → syncade-0.7.1}/tests/gc/test_self_orphan_protection.py +0 -0
  281. {syncade-0.7.0 → syncade-0.7.1}/tests/gc/test_smoke.py +0 -0
  282. {syncade-0.7.0 → syncade-0.7.1}/tests/gc/test_two_tier_retention.py +0 -0
  283. {syncade-0.7.0 → syncade-0.7.1}/tests/gc/test_worktree_age_bound.py +0 -0
  284. {syncade-0.7.0 → syncade-0.7.1}/tests/logging/__init__.py +0 -0
  285. {syncade-0.7.0 → syncade-0.7.1}/tests/logging/_helpers.py +0 -0
  286. {syncade-0.7.0 → syncade-0.7.1}/tests/logging/test_logging.py +0 -0
  287. {syncade-0.7.0 → syncade-0.7.1}/tests/logging/test_logging_summary.py +0 -0
  288. {syncade-0.7.0 → syncade-0.7.1}/tests/metrics/conftest.py +0 -0
  289. {syncade-0.7.0 → syncade-0.7.1}/tests/metrics/test_actor_spend.py +0 -0
  290. {syncade-0.7.0 → syncade-0.7.1}/tests/metrics/test_aggregate.py +0 -0
  291. {syncade-0.7.0 → syncade-0.7.1}/tests/metrics/test_billing_reconciliation.py +0 -0
  292. {syncade-0.7.0 → syncade-0.7.1}/tests/metrics/test_billing_surfaces_agree.py +0 -0
  293. {syncade-0.7.0 → syncade-0.7.1}/tests/metrics/test_budget_metrics.py +0 -0
  294. {syncade-0.7.0 → syncade-0.7.1}/tests/metrics/test_consensus_report.py +0 -0
  295. {syncade-0.7.0 → syncade-0.7.1}/tests/metrics/test_cost_honesty.py +0 -0
  296. {syncade-0.7.0 → syncade-0.7.1}/tests/metrics/test_findings_capture.py +0 -0
  297. {syncade-0.7.0 → syncade-0.7.1}/tests/metrics/test_metrics_cli.py +0 -0
  298. {syncade-0.7.0 → syncade-0.7.1}/tests/metrics/test_metrics_mode.py +0 -0
  299. {syncade-0.7.0 → syncade-0.7.1}/tests/metrics/test_schema.py +0 -0
  300. {syncade-0.7.0 → syncade-0.7.1}/tests/metrics/test_unknown_panel.py +0 -0
  301. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/__init__.py +0 -0
  302. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/_helpers.py +0 -0
  303. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/_resume_fixtures.py +0 -0
  304. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/conftest.py +0 -0
  305. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_budget.py +0 -0
  306. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_budget_resume_e2e.py +0 -0
  307. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_budget_stop_message.py +0 -0
  308. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_budget_warning.py +0 -0
  309. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_checks_and_recording.py +0 -0
  310. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_contracts.py +0 -0
  311. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_cross_round.py +0 -0
  312. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_cross_round_more.py +0 -0
  313. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_default_branch_guard.py +0 -0
  314. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_diff_size_cap.py +0 -0
  315. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_dispatch_record.py +0 -0
  316. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_empty_diff.py +0 -0
  317. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_empty_diff_no_side_effects.py +0 -0
  318. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_escalation.py +0 -0
  319. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_last_reviewed_truthful.py +0 -0
  320. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_loop_audit_remediation.py +0 -0
  321. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_matrix_and_e2e.py +0 -0
  322. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_multiround.py +0 -0
  323. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_multiround_dirty.py +0 -0
  324. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_multiround_more.py +0 -0
  325. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_paraphrase_escalation.py +0 -0
  326. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_pipeline.py +0 -0
  327. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_provider_usage_limit.py +0 -0
  328. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_rename_boundary.py +0 -0
  329. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_resume.py +0 -0
  330. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_resume_base_oid.py +0 -0
  331. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_reviewer_blindness_is_structural.py +0 -0
  332. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_round_input_boundaries.py +0 -0
  333. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_run_layout.py +0 -0
  334. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_run_status_wiring.py +0 -0
  335. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_safety_warning_survives_quiet.py +0 -0
  336. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_snapshot_and_base.py +0 -0
  337. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_synth_phase.py +0 -0
  338. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_synth_phase_more.py +0 -0
  339. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_synth_provenance.py +0 -0
  340. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_synth_validation.py +0 -0
  341. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_template_robustness.py +0 -0
  342. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_termination_reason_drift.py +0 -0
  343. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_testleg.py +0 -0
  344. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_timeout_is_per_subprocess.py +0 -0
  345. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_usage_limit_message.py +0 -0
  346. {syncade-0.7.0 → syncade-0.7.1}/tests/orchestrator/test_worktree_cleanup.py +0 -0
  347. {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/__init__.py +0 -0
  348. {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/_helpers.py +0 -0
  349. {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/test_atomic_writes.py +0 -0
  350. {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/test_checks_surfacing.py +0 -0
  351. {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/test_diff_size_recording.py +0 -0
  352. {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/test_findings_md.py +0 -0
  353. {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/test_findings_md_more.py +0 -0
  354. {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/test_findings_md_verdict.py +0 -0
  355. {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/test_handoff.py +0 -0
  356. {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/test_loop_manifest_checks.py +0 -0
  357. {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/test_loop_summary.py +0 -0
  358. {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/test_loop_summary_more.py +0 -0
  359. {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/test_md_safety.py +0 -0
  360. {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/test_next_steps.py +0 -0
  361. {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/test_producer_persist.py +0 -0
  362. {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/test_reviewer.py +0 -0
  363. {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/test_round_manifest.py +0 -0
  364. {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/test_run_init.py +0 -0
  365. {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/test_run_summary.py +0 -0
  366. {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/test_run_summary_more.py +0 -0
  367. {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/test_run_summary_producer.py +0 -0
  368. {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/test_synth_persist.py +0 -0
  369. {syncade-0.7.0 → syncade-0.7.1}/tests/persistence/test_test_run.py +0 -0
  370. {syncade-0.7.0 → syncade-0.7.1}/tests/producer/__init__.py +0 -0
  371. {syncade-0.7.0 → syncade-0.7.1}/tests/producer/_helpers.py +0 -0
  372. {syncade-0.7.0 → syncade-0.7.1}/tests/producer/test_producer.py +0 -0
  373. {syncade-0.7.0 → syncade-0.7.1}/tests/producer/test_producer_confinement.py +0 -0
  374. {syncade-0.7.0 → syncade-0.7.1}/tests/producer/test_producer_retry.py +0 -0
  375. {syncade-0.7.0 → syncade-0.7.1}/tests/producer/test_producer_run.py +0 -0
  376. {syncade-0.7.0 → syncade-0.7.1}/tests/producer/test_producer_timeout.py +0 -0
  377. {syncade-0.7.0 → syncade-0.7.1}/tests/producer/test_producer_usage.py +0 -0
  378. {syncade-0.7.0 → syncade-0.7.1}/tests/prompts/__init__.py +0 -0
  379. {syncade-0.7.0 → syncade-0.7.1}/tests/prompts/test_load_template.py +0 -0
  380. {syncade-0.7.0 → syncade-0.7.1}/tests/prompts/test_misc.py +0 -0
  381. {syncade-0.7.0 → syncade-0.7.1}/tests/prompts/test_producer.py +0 -0
  382. {syncade-0.7.0 → syncade-0.7.1}/tests/prompts/test_render_synthesizer.py +0 -0
  383. {syncade-0.7.0 → syncade-0.7.1}/tests/prompts/test_synthesizer.py +0 -0
  384. {syncade-0.7.0 → syncade-0.7.1}/tests/resume/__init__.py +0 -0
  385. {syncade-0.7.0 → syncade-0.7.1}/tests/resume/_helpers.py +0 -0
  386. {syncade-0.7.0 → syncade-0.7.1}/tests/resume/test_budget_resume.py +0 -0
  387. {syncade-0.7.0 → syncade-0.7.1}/tests/resume/test_checks_phase_failure.py +0 -0
  388. {syncade-0.7.0 → syncade-0.7.1}/tests/resume/test_cli_malformed_rehydration.py +0 -0
  389. {syncade-0.7.0 → syncade-0.7.1}/tests/resume/test_find_resolve_plan.py +0 -0
  390. {syncade-0.7.0 → syncade-0.7.1}/tests/resume/test_plan_resume.py +0 -0
  391. {syncade-0.7.0 → syncade-0.7.1}/tests/resume/test_round_trip.py +0 -0
  392. {syncade-0.7.0 → syncade-0.7.1}/tests/resume/test_tree_drift_and_escalation.py +0 -0
  393. {syncade-0.7.0 → syncade-0.7.1}/tests/skills/test_config_section.py +0 -0
  394. {syncade-0.7.0 → syncade-0.7.1}/tests/skills/test_install_skill.py +0 -0
  395. {syncade-0.7.0 → syncade-0.7.1}/tests/skills/test_install_skill_multi_harness.py +0 -0
  396. {syncade-0.7.0 → syncade-0.7.1}/tests/skills/test_install_skill_ownership.py +0 -0
  397. {syncade-0.7.0 → syncade-0.7.1}/tests/skills/test_install_skill_refuses.py +0 -0
  398. {syncade-0.7.0 → syncade-0.7.1}/tests/skills/test_skill_drift.py +0 -0
  399. {syncade-0.7.0 → syncade-0.7.1}/tests/skills/test_skill_status.py +0 -0
  400. {syncade-0.7.0 → syncade-0.7.1}/tests/smoke/__init__.py +0 -0
  401. {syncade-0.7.0 → syncade-0.7.1}/tests/smoke/test_anthropic_smoke.py +0 -0
  402. {syncade-0.7.0 → syncade-0.7.1}/tests/smoke/test_auth_check_smoke.py +0 -0
  403. {syncade-0.7.0 → syncade-0.7.1}/tests/smoke/test_codex_smoke.py +0 -0
  404. {syncade-0.7.0 → syncade-0.7.1}/tests/smoke/test_loop_smoke.py +0 -0
  405. {syncade-0.7.0 → syncade-0.7.1}/tests/smoke/test_orchestrator_smoke.py +0 -0
  406. {syncade-0.7.0 → syncade-0.7.1}/tests/smoke/test_orchestrator_smoke_more.py +0 -0
  407. {syncade-0.7.0 → syncade-0.7.1}/tests/smoke/test_producer_smoke.py +0 -0
  408. {syncade-0.7.0 → syncade-0.7.1}/tests/smoke/test_resume_reap_smoke.py +0 -0
  409. {syncade-0.7.0 → syncade-0.7.1}/tests/smoke/test_selfcheck_smoke.py +0 -0
  410. {syncade-0.7.0 → syncade-0.7.1}/tests/smoke/test_synthesizer_smoke.py +0 -0
  411. {syncade-0.7.0 → syncade-0.7.1}/tests/smoke/test_test_runner_smoke.py +0 -0
  412. {syncade-0.7.0 → syncade-0.7.1}/tests/snapshot/__init__.py +0 -0
  413. {syncade-0.7.0 → syncade-0.7.1}/tests/snapshot/_helpers.py +0 -0
  414. {syncade-0.7.0 → syncade-0.7.1}/tests/snapshot/conftest.py +0 -0
  415. {syncade-0.7.0 → syncade-0.7.1}/tests/snapshot/test_snapshot.py +0 -0
  416. {syncade-0.7.0 → syncade-0.7.1}/tests/snapshot/test_snapshot_diff_identity.py +0 -0
  417. {syncade-0.7.0 → syncade-0.7.1}/tests/snapshot/test_snapshot_dirty.py +0 -0
  418. {syncade-0.7.0 → syncade-0.7.1}/tests/snapshot/test_snapshot_warning.py +0 -0
  419. {syncade-0.7.0 → syncade-0.7.1}/tests/snapshot/test_three_dot_diff.py +0 -0
  420. {syncade-0.7.0 → syncade-0.7.1}/tests/synthesis/__init__.py +0 -0
  421. {syncade-0.7.0 → syncade-0.7.1}/tests/synthesis/_helpers.py +0 -0
  422. {syncade-0.7.0 → syncade-0.7.1}/tests/synthesis/test_cluster_repair.py +0 -0
  423. {syncade-0.7.0 → syncade-0.7.1}/tests/synthesis/test_clusters.py +0 -0
  424. {syncade-0.7.0 → syncade-0.7.1}/tests/synthesis/test_models.py +0 -0
  425. {syncade-0.7.0 → syncade-0.7.1}/tests/synthesis/test_models_distinct_keying.py +0 -0
  426. {syncade-0.7.0 → syncade-0.7.1}/tests/synthesis/test_parsing.py +0 -0
  427. {syncade-0.7.0 → syncade-0.7.1}/tests/synthesis/test_unanimous_blocker_downgrade.py +0 -0
  428. {syncade-0.7.0 → syncade-0.7.1}/tests/synthesizer/test_provenance_fabrication.py +0 -0
  429. {syncade-0.7.0 → syncade-0.7.1}/tests/synthesizer/test_provenance_repair.py +0 -0
  430. {syncade-0.7.0 → syncade-0.7.1}/tests/test_adapter_registry.py +0 -0
  431. {syncade-0.7.0 → syncade-0.7.1}/tests/test_adapters_fake.py +0 -0
  432. {syncade-0.7.0 → syncade-0.7.1}/tests/test_auto_init_policy.py +0 -0
  433. {syncade-0.7.0 → syncade-0.7.1}/tests/test_base_resolution.py +0 -0
  434. {syncade-0.7.0 → syncade-0.7.1}/tests/test_check_loc_script.py +0 -0
  435. {syncade-0.7.0 → syncade-0.7.1}/tests/test_checks.py +0 -0
  436. {syncade-0.7.0 → syncade-0.7.1}/tests/test_config_loader.py +0 -0
  437. {syncade-0.7.0 → syncade-0.7.1}/tests/test_cross_model_claim.py +0 -0
  438. {syncade-0.7.0 → syncade-0.7.1}/tests/test_diff_filter.py +0 -0
  439. {syncade-0.7.0 → syncade-0.7.1}/tests/test_diff_filter_binary.py +0 -0
  440. {syncade-0.7.0 → syncade-0.7.1}/tests/test_diff_filter_quoted_paths.py +0 -0
  441. {syncade-0.7.0 → syncade-0.7.1}/tests/test_escalation_coverage.py +0 -0
  442. {syncade-0.7.0 → syncade-0.7.1}/tests/test_exit_codes.py +0 -0
  443. {syncade-0.7.0 → syncade-0.7.1}/tests/test_git_choke_point.py +0 -0
  444. {syncade-0.7.0 → syncade-0.7.1}/tests/test_git_config_isolation.py +0 -0
  445. {syncade-0.7.0 → syncade-0.7.1}/tests/test_git_preconditions.py +0 -0
  446. {syncade-0.7.0 → syncade-0.7.1}/tests/test_last_reviewed.py +0 -0
  447. {syncade-0.7.0 → syncade-0.7.1}/tests/test_parse_repair_extra_keys.py +0 -0
  448. {syncade-0.7.0 → syncade-0.7.1}/tests/test_pricing.py +0 -0
  449. {syncade-0.7.0 → syncade-0.7.1}/tests/test_private_identifier_gate.py +0 -0
  450. {syncade-0.7.0 → syncade-0.7.1}/tests/test_process.py +0 -0
  451. {syncade-0.7.0 → syncade-0.7.1}/tests/test_process_teardown.py +0 -0
  452. {syncade-0.7.0 → syncade-0.7.1}/tests/test_producer_escalation.py +0 -0
  453. {syncade-0.7.0 → syncade-0.7.1}/tests/test_regression_acme.py +0 -0
  454. {syncade-0.7.0 → syncade-0.7.1}/tests/test_retry.py +0 -0
  455. {syncade-0.7.0 → syncade-0.7.1}/tests/test_run_status.py +0 -0
  456. {syncade-0.7.0 → syncade-0.7.1}/tests/test_run_status_signals.py +0 -0
  457. {syncade-0.7.0 → syncade-0.7.1}/tests/test_runner/__init__.py +0 -0
  458. {syncade-0.7.0 → syncade-0.7.1}/tests/test_runner/test_test_runner.py +0 -0
  459. {syncade-0.7.0 → syncade-0.7.1}/tests/test_runner/test_test_runner_more.py +0 -0
  460. {syncade-0.7.0 → syncade-0.7.1}/tests/test_safety_disclosures_survive_quiet.py +0 -0
  461. {syncade-0.7.0 → syncade-0.7.1}/tests/test_selfcheck.py +0 -0
  462. {syncade-0.7.0 → syncade-0.7.1}/tests/test_spec_audit.py +0 -0
  463. {syncade-0.7.0 → syncade-0.7.1}/tests/test_spec_draft.py +0 -0
  464. {syncade-0.7.0 → syncade-0.7.1}/tests/test_spec_source.py +0 -0
  465. {syncade-0.7.0 → syncade-0.7.1}/tests/test_streamed_capture.py +0 -0
  466. {syncade-0.7.0 → syncade-0.7.1}/tests/test_strip_list_semantics.py +0 -0
  467. {syncade-0.7.0 → syncade-0.7.1}/tests/test_synthesizer.py +0 -0
  468. {syncade-0.7.0 → syncade-0.7.1}/tests/test_synthesizer_passthrough.py +0 -0
  469. {syncade-0.7.0 → syncade-0.7.1}/tests/test_synthesizer_provenance.py +0 -0
  470. {syncade-0.7.0 → syncade-0.7.1}/tests/test_transcript.py +0 -0
  471. {syncade-0.7.0 → syncade-0.7.1}/tests/test_update_check.py +0 -0
  472. {syncade-0.7.0 → syncade-0.7.1}/tests/test_usage.py +0 -0
  473. {syncade-0.7.0 → syncade-0.7.1}/tests/test_usage_persistence.py +0 -0
  474. {syncade-0.7.0 → syncade-0.7.1}/tests/test_workflow_action_pins.py +0 -0
  475. {syncade-0.7.0 → syncade-0.7.1}/tests/test_worktree_base_isolation.py +0 -0
  476. {syncade-0.7.0 → syncade-0.7.1}/tests/test_worktree_env.py +0 -0
  477. {syncade-0.7.0 → syncade-0.7.1}/tests/test_worktree_strip_guard.py +0 -0
  478. {syncade-0.7.0 → syncade-0.7.1}/tests/worktree/__init__.py +0 -0
  479. {syncade-0.7.0 → syncade-0.7.1}/tests/worktree/_helpers.py +0 -0
  480. {syncade-0.7.0 → syncade-0.7.1}/tests/worktree/conftest.py +0 -0
  481. {syncade-0.7.0 → syncade-0.7.1}/tests/worktree/test_worktree.py +0 -0
  482. {syncade-0.7.0 → syncade-0.7.1}/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.1
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.0"
12
+ version = "0.7.1"
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.1"
@@ -24,8 +24,12 @@ from pathlib import Path
24
24
  import syncade
25
25
  from syncade.exit_codes import SUCCESS, WORKTREE_ERROR
26
26
  from syncade.process import SubprocessError, run_subprocess
27
+ from syncade.update_check import MANIFEST_URL, _fetch, _parse, _text
27
28
 
28
29
  _UPGRADE_TIMEOUT = 300.0
30
+ #: Reading one version string out of a fresh interpreter. Generous, because a cold venv
31
+ #: import is slower than it looks; short, because we already hold the operator's terminal.
32
+ _VERSION_TIMEOUT = 60.0
29
33
  #: How far up from ``syncade/__init__.py`` an install marker can sit. A venv layout is
30
34
  #: ``<root>/lib/python3.x/site-packages/syncade/__init__.py`` — five parents to the root; the
31
35
  #: extra headroom costs nothing and covers layouts that nest one deeper.
@@ -149,6 +153,86 @@ def _resync_skills(out) -> bool:
149
153
  return all_ok
150
154
 
151
155
 
156
+ def _installed_version() -> str | None:
157
+ """The version on disk NOW, or ``None`` when it cannot be established.
158
+
159
+ It CANNOT be ``syncade.__version__``: this process imported that before the upgrade ran, so
160
+ it is by definition the old number. A fresh interpreter re-reads the metadata the upgrade
161
+ just rewrote, and ``sys.executable`` is the tool venv's own python under both uv and pipx —
162
+ precisely the environment that changed.
163
+
164
+ ``None`` means "could not verify", which is deliberately NOT the same as "did not change":
165
+ the caller must not turn an unreadable version into a failure report.
166
+ """
167
+ try:
168
+ result = run_subprocess(
169
+ # `-I` (isolated) is load-bearing, not tidiness. Without it the probe inherits
170
+ # PYTHONPATH and the user site dir, so a `sitecustomize` on that path runs inside
171
+ # the answer and can emit stdout before OR after the version print.
172
+ # `-I` keeps the venv's own site-packages, so `importlib.metadata` still finds
173
+ # the install (verified) — but it does NOT suppress `sitecustomize.py` that lives
174
+ # in those site-packages. An atexit hook registered there (syncade ships such a
175
+ # shim for worktree imports) can append a version-shaped line AFTER the real one.
176
+ # `os._exit(0)` bypasses all atexit handlers: the process terminates immediately
177
+ # after `os.write`, so nothing can append to our stdout (reproduced: a hostile
178
+ # sitecustomize turned "0.7.0" into "0.7.0\n99.0.0" before this fix).
179
+ # `os.write` is used instead of `print` because `os._exit` skips stdio flushing;
180
+ # an unbuffered syscall guarantees the version byte reaches the pipe.
181
+ [
182
+ sys.executable,
183
+ "-I",
184
+ "-c",
185
+ "import importlib.metadata as m, os; "
186
+ "os.write(1, (m.version('syncade') + '\\n').encode()); "
187
+ "os._exit(0)",
188
+ ],
189
+ timeout=_VERSION_TIMEOUT,
190
+ )
191
+ except SubprocessError:
192
+ return None
193
+ if result.returncode != 0:
194
+ return None
195
+ raw = result.stdout.strip()
196
+ if not raw:
197
+ return None
198
+ # sitecustomize from PYTHONPATH (suppressed by -I) could emit noise before -c runs;
199
+ # the venv's own sitecustomize can still do so. Take the LAST line — atexit is now
200
+ # impossible (os._exit), so the last line is what os.write put there — and validate it
201
+ # parses as a version. Noise that accidentally ends in a dotted string still fails _parse.
202
+ candidate = raw.splitlines()[-1].strip()
203
+ return candidate if _parse(candidate) is not None else None
204
+
205
+
206
+ def _is_newest(installed: str) -> bool:
207
+ """Whether ``installed`` is at or beyond the newest version the manifest advertises.
208
+
209
+ Only ever consulted on the unhappy path — the version failed to move — so the happy path
210
+ stays offline. Anything that leaves the question unanswered (unreachable manifest, no usable
211
+ ``latest``, an installed version we cannot parse) returns ``False``, i.e. "cannot prove you
212
+ are current", which routes to the louder message. That is the safe direction: telling someone
213
+ to check for a pin when they were merely current is a wasted minute; telling someone they are
214
+ up to date when the upgrade silently failed strands them on an old version believing it is
215
+ the new one.
216
+
217
+ **This compares versions itself and must never delegate to ``evaluate()``.** That function
218
+ answers a DIFFERENT question — "should the operator see a notice?" — and the two answers
219
+ only usually coincide. They part company in the documented critical-no-fix state: when
220
+ ``critical_below`` is above the installed version and no newer release exists, ``evaluate()``
221
+ returns a notice while the operator is genuinely current, so reading its ``None``-ness told a
222
+ fully up-to-date install that its upgrade had failed and it was probably pinned. Two dogfood
223
+ rounds landed on this function for the same underlying reason — a near-miss reuse of a
224
+ neighbouring answer — and the fix both times was to ask the actual question.
225
+
226
+ There is deliberately no special case for an unreachable manifest: ``{}`` has no usable
227
+ ``latest``, so the one parse rule already answers it. An explicit branch was there and a
228
+ mutation run proved it dead — every input reached the same verdict without it.
229
+ """
230
+ manifest = _fetch(MANIFEST_URL) or {}
231
+ latest = _parse(_text(manifest.get("latest")))
232
+ current = _parse(installed)
233
+ return latest is not None and current is not None and current >= latest
234
+
235
+
152
236
  def run_update(*, cwd: Path | None = None, out=None) -> int:
153
237
  """Upgrade syncade in place, then exit asking to be re-run."""
154
238
  out = out or sys.stderr
@@ -213,9 +297,38 @@ def run_update(*, cwd: Path | None = None, out=None) -> int:
213
297
  )
214
298
  return WORKTREE_ERROR
215
299
 
300
+ # THE POST-CONDITION. Exit 0 from the package manager means "the command ran", NOT "the
301
+ # version moved", and the two come apart routinely: `uv tool install syncade==0.6.3` records
302
+ # `specifier = "==0.6.3"` in its receipt, so `uv tool upgrade` honours the pin, prints
303
+ # "Nothing to upgrade" and exits 0. Held packages, resolver conflicts and a manifest that is
304
+ # ahead of PyPI all land in the same place. Without this check syncade says "updated. Re-run
305
+ # your command", the operator re-runs, gets the same version, and is told again next session
306
+ # — forever. Measured live on the 0.7.0 release, which is why item 5 of that release existed.
307
+ now = _installed_version()
308
+ if now is not None and now == syncade.__version__:
309
+ # The version did not move. There are TWO reasons for that and they need opposite
310
+ # answers, so ask the manifest which one this is rather than guessing. Guessing was the
311
+ # first version of this guard and it told an operator who was simply already current
312
+ # that their install was pinned and broken — a fresh false claim inside the fix for a
313
+ # false claim, caught by testing the path instead of reasoning about it.
314
+ if _is_newest(now):
315
+ skills_ok = _resync_skills(out)
316
+ print(f"[syncade] already up to date ({now}).", file=out)
317
+ return SUCCESS if skills_ok else WORKTREE_ERROR
318
+ print(
319
+ f"[syncade] the upgrade command succeeded but syncade is still "
320
+ f"{syncade.__version__} — nothing was installed.\n"
321
+ f" Most often the install pins a version: check `uv tool list` / "
322
+ f"`pipx list`, then\n"
323
+ f" reinstall unpinned (e.g. `uv tool install --force syncade`).",
324
+ file=out,
325
+ )
326
+ return WORKTREE_ERROR
327
+
216
328
  skills_ok = _resync_skills(out)
329
+ moved = f" to {now}" if now else ""
217
330
  print(
218
- "[syncade] updated. Re-run your command to use the new version — this process is\n"
331
+ f"[syncade] updated{moved}. Re-run your command to use the new version — this process is\n"
219
332
  " still running the old one and cannot switch to it.",
220
333
  file=out,
221
334
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: syncade
3
- Version: 0.7.0
3
+ Version: 0.7.1
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
@@ -275,6 +275,7 @@ tests/cli/test_undo_auto_init.py
275
275
  tests/cli/test_undo_scope.py
276
276
  tests/cli/test_update_mode.py
277
277
  tests/cli/test_update_notice.py
278
+ tests/cli/test_update_post_condition.py
278
279
  tests/cli/test_validate_before_mutate.py
279
280
  tests/config/__init__.py
280
281
  tests/config/test_auth_enforcement.py
@@ -12,6 +12,7 @@ from __future__ import annotations
12
12
 
13
13
  import json
14
14
  import pathlib
15
+ import sys
15
16
  from pathlib import Path
16
17
 
17
18
  import pytest
@@ -359,7 +360,6 @@ def test_syncade_argv_uses_current_interpreter() -> None:
359
360
 
360
361
  Verified: this test would fail against the pre-fix code that called shutil.which.
361
362
  """
362
- import sys
363
363
 
364
364
  assert update_mode._syncade_argv() == [sys.executable, "-m", "syncade"]
365
365
 
@@ -0,0 +1,398 @@
1
+ """PR-h-field-08 — `--update` must not report success having installed nothing.
2
+
3
+ Split out of ``test_update_mode.py`` when these tests took it to 523 of a 500 code-LOC cap.
4
+ The same thing happened to ``test_config_schema.py`` and `CLAUDE.md` records it; the fix is the
5
+ same, and it is a real separation rather than a dumping ground: everything here is about ONE
6
+ question — *did the version actually move, and can we prove it?* — while the original file is
7
+ about install-method detection and the upgrade command.
8
+
9
+ **Four revisions of this code, four dogfood blockers, all the same shape:** something was trusted
10
+ to mean what it nearly meant. The package manager's exit code (ran != upgraded), then
11
+ ``evaluate()`` returning ``None`` (no notice != no newer release), then ``evaluate()`` again for
12
+ the comparison (critical-no-fix), then the probe's raw stdout (any text != a version). The rule
13
+ that finally held is to validate a value as the thing it claims to be, at the point it enters.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import sys
19
+ from pathlib import Path
20
+
21
+ import pytest
22
+
23
+ import syncade
24
+ from syncade.cli import update_mode
25
+ from syncade.exit_codes import SUCCESS, WORKTREE_ERROR
26
+ from syncade.process import SubprocessError, SubprocessResult
27
+ from tests.cli.test_update_mode import _install_tree, _point_syncade_at, _Result
28
+
29
+ # --------------------------------------------------------------------- the post-condition
30
+ #
31
+ # Exit 0 from the package manager means "the command ran", NOT "the version moved". Found live
32
+ # on the 0.7.0 release: `uv tool install syncade==0.6.3` records `specifier = "==0.6.3"` in its
33
+ # receipt, so `uv tool upgrade` honours the pin, prints "Nothing to upgrade" and exits 0 — and
34
+ # syncade said "updated. Re-run your command" having installed nothing. The operator re-runs,
35
+ # sees the same version, and is told again every session, forever.
36
+
37
+
38
+ def _upgrade_reporting_success(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
39
+ """A proven uv install whose upgrade command exits 0."""
40
+ pkg = _install_tree(tmp_path, "uv-receipt.toml")
41
+ _point_syncade_at(monkeypatch, pkg)
42
+ monkeypatch.setattr(update_mode, "run_subprocess", lambda *a, **k: _Result(0))
43
+ monkeypatch.setattr(update_mode, "_resync_skills", lambda out: True)
44
+
45
+
46
+ def test_an_upgrade_that_changed_nothing_is_reported_as_failure(
47
+ tmp_path, monkeypatch, capsys
48
+ ) -> None:
49
+ """The defect itself: same version after a 'successful' upgrade must not read as success."""
50
+ _upgrade_reporting_success(monkeypatch, tmp_path)
51
+ monkeypatch.setattr(update_mode, "_installed_version", lambda: syncade.__version__)
52
+ monkeypatch.setattr(update_mode, "_fetch", lambda url: {"latest": "99.0.0"})
53
+
54
+ assert update_mode.run_update() == WORKTREE_ERROR
55
+ out = capsys.readouterr().err
56
+ assert "nothing was installed" in out
57
+ assert "updated." not in out, "it must not also claim success"
58
+ assert "pins a version" in out, "name the most likely cause, not just the symptom"
59
+
60
+
61
+ def test_a_real_upgrade_still_succeeds_and_names_the_new_version(
62
+ tmp_path, monkeypatch, capsys
63
+ ) -> None:
64
+ """The guard must not fire on the path it exists to protect."""
65
+ _upgrade_reporting_success(monkeypatch, tmp_path)
66
+ monkeypatch.setattr(update_mode, "_installed_version", lambda: "99.0.0")
67
+
68
+ assert update_mode.run_update() == SUCCESS
69
+ assert "updated to 99.0.0" in capsys.readouterr().err
70
+
71
+
72
+ def test_an_unverifiable_version_is_not_reported_as_failure(tmp_path, monkeypatch, capsys) -> None:
73
+ """`None` means "could not verify", which is NOT "did not change".
74
+
75
+ Turning an unreadable version into a failure would break every install whose metadata we
76
+ cannot probe, having actually upgraded them — a worse error than the one being fixed.
77
+ """
78
+ _upgrade_reporting_success(monkeypatch, tmp_path)
79
+ monkeypatch.setattr(update_mode, "_installed_version", lambda: None)
80
+
81
+ assert update_mode.run_update() == SUCCESS
82
+ out = capsys.readouterr().err
83
+ assert "updated." in out
84
+ assert "nothing was installed" not in out
85
+ assert "updated to" not in out, "no version may be named when none was established"
86
+
87
+
88
+ def test_the_version_probe_reads_a_fresh_interpreter_not_this_one(monkeypatch) -> None:
89
+ """It must not answer from `syncade.__version__`, which this process imported BEFORE the
90
+ upgrade and is therefore the old number by construction."""
91
+ seen: list[list[str]] = []
92
+
93
+ def spy(cmd, **kw):
94
+ seen.append(cmd)
95
+ return SubprocessResult(returncode=0, stdout="1.2.3\n", stderr="", duration_seconds=0.0)
96
+
97
+ monkeypatch.setattr(update_mode, "run_subprocess", spy)
98
+ assert update_mode._installed_version() == "1.2.3"
99
+ assert seen and seen[0][0] == sys.executable, "must re-invoke the interpreter, not import"
100
+ assert "importlib.metadata" in seen[0][-1]
101
+
102
+
103
+ def test_an_unreadable_probe_degrades_to_none(monkeypatch) -> None:
104
+ for outcome in (
105
+ lambda *a, **k: (_ for _ in ()).throw(SubprocessError("no python")),
106
+ lambda *a, **k: SubprocessResult(returncode=1, stdout="", stderr="x", duration_seconds=0.0),
107
+ lambda *a, **k: SubprocessResult(
108
+ returncode=0, stdout=" \n", stderr="", duration_seconds=0.0
109
+ ),
110
+ ):
111
+ monkeypatch.setattr(update_mode, "run_subprocess", outcome)
112
+ assert update_mode._installed_version() is None
113
+
114
+
115
+ def test_startup_noise_before_version_line_is_stripped(monkeypatch) -> None:
116
+ """sitecustomize imported from PYTHONPATH can emit stdout before the version print.
117
+
118
+ The old code returned the full stdout verbatim, so "noise\\n0.7.0" != "0.7.0" and
119
+ run_update treated the install as having moved — a false success on an unchanged version.
120
+ """
121
+ monkeypatch.setattr(
122
+ update_mode,
123
+ "run_subprocess",
124
+ lambda *a, **k: SubprocessResult(
125
+ returncode=0, stdout="startup noise\n1.2.3\n", stderr="", duration_seconds=0.0
126
+ ),
127
+ )
128
+ assert update_mode._installed_version() == "1.2.3"
129
+
130
+
131
+ def test_non_version_stdout_degrades_to_none(monkeypatch) -> None:
132
+ """stdout that doesn't end with a parseable version string means 'could not verify'."""
133
+ for stdout in ("not a version\n", "noise only\n", "1.2\n", "1.2.3.4\n"):
134
+ monkeypatch.setattr(
135
+ update_mode,
136
+ "run_subprocess",
137
+ lambda *a, stdout=stdout, **k: SubprocessResult(
138
+ returncode=0, stdout=stdout, stderr="", duration_seconds=0.0
139
+ ),
140
+ )
141
+ assert update_mode._installed_version() is None, f"should be None for {stdout!r}"
142
+
143
+
144
+ def test_an_already_current_install_is_not_reported_as_a_failed_upgrade(
145
+ tmp_path, monkeypatch, capsys
146
+ ) -> None:
147
+ """The post-condition's own false positive, found by exercising it rather than reasoning.
148
+
149
+ The version not moving has TWO causes needing opposite answers: a pinned/held install (a
150
+ real failure) and simply being current already (not a failure at all). The first version of
151
+ the guard could not tell them apart and told an operator who ran `--update` while current
152
+ that their install was pinned and broken — a fresh false claim inside the fix for one.
153
+ """
154
+ _upgrade_reporting_success(monkeypatch, tmp_path)
155
+ monkeypatch.setattr(update_mode, "_installed_version", lambda: syncade.__version__)
156
+ monkeypatch.setattr(update_mode, "_fetch", lambda url: {"latest": syncade.__version__})
157
+
158
+ assert update_mode.run_update() == SUCCESS
159
+ out = capsys.readouterr().err
160
+ assert "already up to date" in out
161
+ assert "pins a version" not in out
162
+
163
+
164
+ def test_an_unreachable_manifest_cannot_certify_you_as_current(
165
+ tmp_path, monkeypatch, capsys
166
+ ) -> None:
167
+ """`_fetch` returning None must NOT read as "you are current".
168
+
169
+ `evaluate(None, v)` also returns None, so collapsing the two would tell an operator whose
170
+ upgrade silently failed while offline that they were up to date — stranding them on an old
171
+ version believing it is the new one. The louder message is the safe direction here.
172
+ """
173
+ _upgrade_reporting_success(monkeypatch, tmp_path)
174
+ monkeypatch.setattr(update_mode, "_installed_version", lambda: syncade.__version__)
175
+ monkeypatch.setattr(update_mode, "_fetch", lambda url: None)
176
+
177
+ assert update_mode.run_update() == WORKTREE_ERROR
178
+ assert "already up to date" not in capsys.readouterr().err
179
+
180
+
181
+ def test_the_happy_path_never_touches_the_network(tmp_path, monkeypatch) -> None:
182
+ """The manifest is consulted ONLY when the version failed to move, so a normal successful
183
+ upgrade adds no request and no latency."""
184
+ _upgrade_reporting_success(monkeypatch, tmp_path)
185
+ monkeypatch.setattr(update_mode, "_installed_version", lambda: "99.0.0")
186
+ monkeypatch.setattr(
187
+ update_mode, "_fetch", lambda url: pytest.fail("the happy path must not fetch")
188
+ )
189
+
190
+ assert update_mode.run_update() == SUCCESS
191
+
192
+
193
+ # ---------------------------------------------------------------- malformed-manifest regression
194
+ #
195
+ # A reachable manifest that has no parseable latest field must NOT certify the operator as
196
+ # current. evaluate() returns None for BOTH "already current" and "no usable latest version",
197
+ # so trusting None alone on a dict manifest restores the false-success class for any manifest
198
+ # that is syntactically valid JSON but semantically unusable.
199
+
200
+
201
+ @pytest.mark.parametrize(
202
+ "manifest",
203
+ [
204
+ {},
205
+ {"latest": None},
206
+ {"latest": "not-a-version"},
207
+ {"latest": "0.7"}, # two-part, not three-part
208
+ {"latest": ""},
209
+ ],
210
+ ids=["empty", "null-latest", "unparseable-latest", "two-part-latest", "empty-latest"],
211
+ )
212
+ def test_is_newest_treats_malformed_manifest_as_unverifiable(manifest, monkeypatch) -> None:
213
+ """A reachable manifest with no parseable latest must not certify any version as current.
214
+
215
+ Verified: this test fails against the pre-fix _is_newest that only checked
216
+ `evaluate(manifest, installed) is None` — all five manifests above make evaluate() return
217
+ None while providing no actual version information, so the pre-fix code returned True and
218
+ certified the operator as already-current on an unusable manifest.
219
+ """
220
+ monkeypatch.setattr(update_mode, "_fetch", lambda url: manifest)
221
+ assert update_mode._is_newest("0.7.0") is False, (
222
+ f"manifest {manifest!r} cannot prove current status; _is_newest must return False"
223
+ )
224
+
225
+
226
+ def test_a_malformed_manifest_does_not_produce_already_up_to_date(
227
+ tmp_path, monkeypatch, capsys
228
+ ) -> None:
229
+ """End-to-end: an empty manifest must not report success to an operator whose version
230
+ did not move. Verified: fails against the pre-fix code."""
231
+ _upgrade_reporting_success(monkeypatch, tmp_path)
232
+ monkeypatch.setattr(update_mode, "_installed_version", lambda: syncade.__version__)
233
+ monkeypatch.setattr(update_mode, "_fetch", lambda url: {}) # reachable but malformed
234
+
235
+ assert update_mode.run_update() == WORKTREE_ERROR
236
+ out = capsys.readouterr().err
237
+ assert "already up to date" not in out
238
+ assert "nothing was installed" in out
239
+
240
+
241
+ # ------------------------------------------------------------ already-current resync regression
242
+
243
+
244
+ def test_skills_are_resynced_on_the_already_current_path(tmp_path, monkeypatch) -> None:
245
+ """--update must re-install skills even when the package is already at the newest version.
246
+
247
+ The already-current early return was added by this PR. Without explicit resync on that
248
+ path, a stale or missing skill is silently left behind on every invocation where the
249
+ package is current. Verified: fails against the pre-fix code where the already-current
250
+ branch returned before _resync_skills() was called.
251
+ """
252
+ _point_syncade_at(monkeypatch, _install_tree(tmp_path, "uv-receipt.toml"))
253
+ monkeypatch.setattr(update_mode, "run_subprocess", lambda *a, **k: _Result(0))
254
+ monkeypatch.setattr(update_mode, "_installed_version", lambda: syncade.__version__)
255
+ monkeypatch.setattr(update_mode, "_fetch", lambda url: {"latest": syncade.__version__})
256
+
257
+ calls: list = []
258
+ monkeypatch.setattr(update_mode, "_resync_skills", lambda out: calls.append(1) or True)
259
+
260
+ assert update_mode.run_update(cwd=tmp_path) == SUCCESS
261
+ assert calls == [1], "skills must be resynced even when the package is already current"
262
+
263
+
264
+ def test_a_failed_skill_resync_on_already_current_path_returns_non_zero(
265
+ tmp_path, monkeypatch, capsys
266
+ ) -> None:
267
+ """A failed resync on the already-current path must propagate the failure code."""
268
+ _point_syncade_at(monkeypatch, _install_tree(tmp_path, "uv-receipt.toml"))
269
+ monkeypatch.setattr(update_mode, "run_subprocess", lambda *a, **k: _Result(0))
270
+ monkeypatch.setattr(update_mode, "_installed_version", lambda: syncade.__version__)
271
+ monkeypatch.setattr(update_mode, "_fetch", lambda url: {"latest": syncade.__version__})
272
+ monkeypatch.setattr(update_mode, "_resync_skills", lambda out: False)
273
+
274
+ assert update_mode.run_update(cwd=tmp_path) == WORKTREE_ERROR
275
+ assert "already up to date" in capsys.readouterr().err
276
+
277
+
278
+ def test_a_critical_advisory_with_no_newer_release_is_still_current(
279
+ tmp_path, monkeypatch, capsys
280
+ ) -> None:
281
+ """`_is_newest` must compare VERSIONS, never delegate to `evaluate()`.
282
+
283
+ `evaluate()` answers "should the operator see a notice?", which is a different question and
284
+ only usually gives the same answer. In the documented critical-no-fix state — `critical_below`
285
+ above the installed version, no newer release published — it returns a notice while the
286
+ operator is genuinely current. Reading its None-ness therefore told a fully up-to-date install
287
+ that its upgrade had failed and it was probably pinned. Unanimous dogfood blocker; it survived
288
+ a producer round because the previous fix repaired the malformed-manifest instance while
289
+ keeping the same near-miss delegation.
290
+ """
291
+ _upgrade_reporting_success(monkeypatch, tmp_path)
292
+ monkeypatch.setattr(update_mode, "_installed_version", lambda: syncade.__version__)
293
+ monkeypatch.setattr(
294
+ update_mode,
295
+ "_fetch",
296
+ lambda url: {
297
+ "latest": syncade.__version__,
298
+ "critical_below": "99.0.0",
299
+ "critical_reason": "a critical fix with no newer release",
300
+ },
301
+ )
302
+
303
+ assert update_mode.run_update() == SUCCESS
304
+ out = capsys.readouterr().err
305
+ assert "already up to date" in out
306
+ assert "pins a version" not in out
307
+
308
+
309
+ def test_is_newest_answers_the_version_question_across_every_manifest_shape(monkeypatch) -> None:
310
+ """One table, so a future edit cannot fix one shape and reopen another — which is exactly how
311
+ this function reached its third revision."""
312
+ installed = "0.7.0"
313
+ cases = [
314
+ ({"latest": "0.7.0"}, True, "manifest agrees"),
315
+ ({"latest": "0.6.3"}, True, "manifest is BEHIND the install"),
316
+ ({"latest": "0.7.0", "critical_below": "99.0.0"}, True, "critical, no newer release"),
317
+ ({"latest": "99.0.0"}, False, "a newer release exists"),
318
+ (None, False, "unreachable"),
319
+ ({}, False, "no latest key"),
320
+ ({"latest": None}, False, "latest is null"),
321
+ ({"latest": ""}, False, "latest is empty"),
322
+ ({"latest": "garbage"}, False, "latest unparseable"),
323
+ ({"latest": 5}, False, "latest not a string"),
324
+ ({"latest": "1.2"}, False, "latest not three components"),
325
+ ]
326
+ for manifest, expected, why in cases:
327
+ monkeypatch.setattr(update_mode, "_fetch", lambda url, m=manifest: m)
328
+ assert update_mode._is_newest(installed) is expected, why
329
+
330
+
331
+ def test_is_newest_cannot_certify_an_unparseable_installed_version(monkeypatch) -> None:
332
+ monkeypatch.setattr(update_mode, "_fetch", lambda url: {"latest": "0.7.0"})
333
+ assert update_mode._is_newest("not-a-version") is False
334
+
335
+
336
+ def test_the_probe_is_isolated_from_ambient_interpreter_state(monkeypatch) -> None:
337
+ """`-I` is load-bearing: without it the probe inherits PYTHONPATH and the user site dir.
338
+
339
+ A `sitecustomize` on that path runs INSIDE the answer — an atexit hook printing a version
340
+ number becomes the last stdout line, so a no-op upgrade reads as a move. Reproduced live: a
341
+ hostile PYTHONPATH turned the probe's `"0.7.0"` into `"0.7.0\\n99.0.0"`. Syncade ships such a
342
+ shim itself for worktree imports, so this is not purely adversarial.
343
+
344
+ `-I` suppresses PYTHONPATH but NOT the venv's own site-packages, where a `sitecustomize.py`
345
+ can still register atexit hooks. The probe must call `os._exit(0)` to bypass atexit entirely —
346
+ the only guarantee against a venv-local hook appending to stdout after the version line.
347
+ `os.write` is asserted alongside it: `os._exit` skips stdio flushing, so `print()` would lose
348
+ the version; the unbuffered syscall guarantees delivery before the process exits.
349
+
350
+ Pinned as flag assertions because the behavioural twin below cannot run the failure case
351
+ without installing a real adversarial sitecustomize.
352
+ """
353
+ seen: list[list[str]] = []
354
+ monkeypatch.setattr(
355
+ update_mode,
356
+ "run_subprocess",
357
+ lambda cmd, **kw: (
358
+ seen.append(cmd)
359
+ or SubprocessResult(returncode=0, stdout="1.2.3\n", stderr="", duration_seconds=0.0)
360
+ ),
361
+ )
362
+ update_mode._installed_version()
363
+ assert seen and "-I" in seen[0], (
364
+ "the version probe must run isolated; without -I a sitecustomize on PYTHONPATH can "
365
+ "append a version number to its stdout and fake a successful upgrade"
366
+ )
367
+ assert seen[0].index("-I") < seen[0].index("-c"), "-I must precede -c to take effect"
368
+ c_arg = seen[0][seen[0].index("-c") + 1]
369
+ assert "os._exit(0)" in c_arg, (
370
+ "the probe must call os._exit(0) to bypass atexit handlers; a venv-local "
371
+ "sitecustomize can register atexit hooks that append a fake version line after the real one"
372
+ )
373
+ assert "os.write" in c_arg, (
374
+ "the probe must use os.write (unbuffered) not print; os._exit skips stdio flushing "
375
+ "so print output would be lost before reaching the pipe"
376
+ )
377
+
378
+
379
+ def test_the_isolated_probe_still_resolves_the_real_install() -> None:
380
+ """The behavioural twin: `-I` must not break what the probe is FOR.
381
+
382
+ It drops PYTHONPATH and the user site dir but keeps the environment's own site-packages, so
383
+ `importlib.metadata` still finds the package. Runs the real subprocess — a flag assertion
384
+ alone would happily pin an isolation level that returns None for everyone.
385
+
386
+ We assert the result is a parseable, non-None version, NOT that it equals
387
+ `syncade.__version__`. In a dev worktree the source version and the installed distribution
388
+ version legitimately differ (e.g. 0.7.0 in source, 0.1.0 in site-packages); asserting
389
+ equality would make this test prove the ambient installation, not the probe's behaviour.
390
+ """
391
+ from syncade.update_check import _parse
392
+
393
+ result = update_mode._installed_version()
394
+ assert result is not None, (
395
+ "the probe must find the installed package under -I "
396
+ "(drops PYTHONPATH but keeps the environment's own site-packages)"
397
+ )
398
+ assert _parse(result) is not None, f"result {result!r} must be a parseable version string"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes