syncade 0.6.2__tar.gz → 0.6.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (474) hide show
  1. {syncade-0.6.2/src/syncade.egg-info → syncade-0.6.3}/PKG-INFO +27 -3
  2. {syncade-0.6.2 → syncade-0.6.3}/README.md +26 -2
  3. {syncade-0.6.2 → syncade-0.6.3}/docs/config-reference.md +16 -0
  4. {syncade-0.6.2 → syncade-0.6.3}/pyproject.toml +1 -1
  5. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/__init__.py +1 -1
  6. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/cli/__init__.py +31 -0
  7. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/cli/install_skill.py +17 -4
  8. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/cli/parser.py +10 -0
  9. syncade-0.6.3/src/syncade/cli/skill_status.py +142 -0
  10. syncade-0.6.3/src/syncade/cli/update_mode.py +226 -0
  11. syncade-0.6.3/src/syncade/cli/update_notice.py +191 -0
  12. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/cli/validate.py +33 -0
  13. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/config.py +4 -0
  14. syncade-0.6.3/src/syncade/config_update.py +29 -0
  15. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/skills/claude/README.md +3 -2
  16. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/skills/claude/SKILL.md +19 -0
  17. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/skills/codex/README.md +3 -2
  18. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/skills/codex/SKILL.md +19 -0
  19. syncade-0.6.3/src/syncade/update_check.py +263 -0
  20. {syncade-0.6.2 → syncade-0.6.3/src/syncade.egg-info}/PKG-INFO +27 -3
  21. {syncade-0.6.2 → syncade-0.6.3}/src/syncade.egg-info/SOURCES.txt +9 -0
  22. {syncade-0.6.2 → syncade-0.6.3}/tests/cli/test_mode_exclusivity_matrix.py +1 -0
  23. syncade-0.6.3/tests/cli/test_update_mode.py +407 -0
  24. syncade-0.6.3/tests/cli/test_update_notice.py +505 -0
  25. syncade-0.6.3/tests/skills/test_skill_status.py +265 -0
  26. syncade-0.6.3/tests/test_update_check.py +426 -0
  27. {syncade-0.6.2 → syncade-0.6.3}/LICENSE +0 -0
  28. {syncade-0.6.2 → syncade-0.6.3}/MANIFEST.in +0 -0
  29. {syncade-0.6.2 → syncade-0.6.3}/docs/how-to-use.md +0 -0
  30. {syncade-0.6.2 → syncade-0.6.3}/setup.cfg +0 -0
  31. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/__main__.py +0 -0
  32. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/adapters/__init__.py +0 -0
  33. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/adapters/anthropic.py +0 -0
  34. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/adapters/base.py +0 -0
  35. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/adapters/fake.py +0 -0
  36. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/adapters/fake_common.py +0 -0
  37. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/adapters/fake_producer_audit_draft.py +0 -0
  38. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/adapters/fake_reviewer_synth.py +0 -0
  39. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/adapters/openai.py +0 -0
  40. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/adapters/openai_parsing.py +0 -0
  41. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/adapters/producer.py +0 -0
  42. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/adapters/producer_anthropic.py +0 -0
  43. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/adapters/producer_openai.py +0 -0
  44. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/adapters/registry.py +0 -0
  45. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/auth_check.py +0 -0
  46. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/auth_preflight.py +0 -0
  47. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/base_resolution.py +0 -0
  48. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/billing.py +0 -0
  49. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/checks_config.py +0 -0
  50. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/cli/auth_gate.py +0 -0
  51. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/cli/config_keys.py +0 -0
  52. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/cli/config_list.py +0 -0
  53. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/cli/config_menu_rows.py +0 -0
  54. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/cli/config_mode.py +0 -0
  55. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/cli/config_overrides.py +0 -0
  56. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/cli/config_tui.py +0 -0
  57. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/cli/doctor_mode.py +0 -0
  58. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/cli/gc_mode.py +0 -0
  59. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/cli/metrics_mode.py +0 -0
  60. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/cli/modes.py +0 -0
  61. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/cli/parser_types.py +0 -0
  62. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/cli/paths.py +0 -0
  63. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/cli/preflight_paths.py +0 -0
  64. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/cli/resolve.py +0 -0
  65. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/cli/resume_mode.py +0 -0
  66. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/cli/toml_writer.py +0 -0
  67. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/config_auth.py +0 -0
  68. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/config_cold.py +0 -0
  69. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/config_gc.py +0 -0
  70. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/config_loader.py +0 -0
  71. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/config_loop.py +0 -0
  72. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/config_producer.py +0 -0
  73. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/config_retry.py +0 -0
  74. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/config_types.py +0 -0
  75. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/diff_filter.py +0 -0
  76. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/dispatcher.py +0 -0
  77. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/doctor.py +0 -0
  78. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/doctor_env.py +0 -0
  79. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/doctor_preview.py +0 -0
  80. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/doctor_types.py +0 -0
  81. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/exit_codes.py +0 -0
  82. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/findings.py +0 -0
  83. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/findings_json.py +0 -0
  84. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/gc.py +0 -0
  85. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/gc_execute.py +0 -0
  86. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/gc_protection.py +0 -0
  87. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/gc_types.py +0 -0
  88. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/gc_worktrees.py +0 -0
  89. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/git_object_id.py +0 -0
  90. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/git_preconditions.py +0 -0
  91. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/logging.py +0 -0
  92. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/metrics/__init__.py +0 -0
  93. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/metrics/aggregate.py +0 -0
  94. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/metrics/schema.py +0 -0
  95. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/orchestrator/__init__.py +0 -0
  96. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/orchestrator/_runs_dir.py +0 -0
  97. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/orchestrator/branch_advance.py +0 -0
  98. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/orchestrator/branch_guard.py +0 -0
  99. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/orchestrator/budget.py +0 -0
  100. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/orchestrator/escalation_coverage.py +0 -0
  101. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/orchestrator/loop.py +0 -0
  102. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/orchestrator/loop_dispatch_check.py +0 -0
  103. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/orchestrator/loop_finalize.py +0 -0
  104. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/orchestrator/loop_preflight.py +0 -0
  105. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/orchestrator/loop_resume.py +0 -0
  106. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/orchestrator/loop_rmtree.py +0 -0
  107. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/orchestrator/loop_round_step.py +0 -0
  108. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/orchestrator/prior_round.py +0 -0
  109. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/orchestrator/producer_phase.py +0 -0
  110. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/orchestrator/results.py +0 -0
  111. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/orchestrator/resume.py +0 -0
  112. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/orchestrator/resume_load.py +0 -0
  113. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/orchestrator/resume_plan.py +0 -0
  114. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/orchestrator/resume_target.py +0 -0
  115. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/orchestrator/resume_types.py +0 -0
  116. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/orchestrator/reviewer_template_failure.py +0 -0
  117. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/orchestrator/round.py +0 -0
  118. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/orchestrator/round_checks.py +0 -0
  119. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/orchestrator/round_no_changes.py +0 -0
  120. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/orchestrator/round_predispatch.py +0 -0
  121. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/orchestrator/verdict.py +0 -0
  122. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/persistence/__init__.py +0 -0
  123. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/persistence/_atomic.py +0 -0
  124. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/persistence/_clusters.py +0 -0
  125. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/persistence/_findings_verdict.py +0 -0
  126. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/persistence/_markdown.py +0 -0
  127. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/persistence/_validation.py +0 -0
  128. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/persistence/checks.py +0 -0
  129. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/persistence/decision_needed.py +0 -0
  130. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/persistence/findings_md.py +0 -0
  131. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/persistence/handoff.py +0 -0
  132. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/persistence/handoff_classify.py +0 -0
  133. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/persistence/last_reviewed.py +0 -0
  134. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/persistence/loop_manifest.py +0 -0
  135. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/persistence/loop_summary.py +0 -0
  136. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/persistence/loop_summary_text.py +0 -0
  137. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/persistence/producer.py +0 -0
  138. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/persistence/reviewer.py +0 -0
  139. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/persistence/round_manifest.py +0 -0
  140. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/persistence/run_init.py +0 -0
  141. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/persistence/run_summary.py +0 -0
  142. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/persistence/run_summary_next_steps.py +0 -0
  143. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/persistence/synth.py +0 -0
  144. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/persistence/test_run.py +0 -0
  145. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/presets.py +0 -0
  146. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/pricing_config.py +0 -0
  147. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/process.py +0 -0
  148. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/producer.py +0 -0
  149. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/producer_attempt.py +0 -0
  150. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/producer_escalation.py +0 -0
  151. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/producer_git.py +0 -0
  152. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/producer_result.py +0 -0
  153. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/prompts.py +0 -0
  154. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/prompts_loader.py +0 -0
  155. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/retry.py +0 -0
  156. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/run_inputs.py +0 -0
  157. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/run_status.py +0 -0
  158. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/selfcheck.py +0 -0
  159. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/snapshot.py +0 -0
  160. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/spec_audit.py +0 -0
  161. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/spec_audit_schema.py +0 -0
  162. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/spec_draft.py +0 -0
  163. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/spec_source.py +0 -0
  164. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/synthesis.py +0 -0
  165. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/synthesis_clusters.py +0 -0
  166. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/synthesis_repair.py +0 -0
  167. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/synthesis_schema.py +0 -0
  168. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/synthesizer/__init__.py +0 -0
  169. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/synthesizer/constants.py +0 -0
  170. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/synthesizer/driver.py +0 -0
  171. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/synthesizer/rendering.py +0 -0
  172. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/synthesizer/result.py +0 -0
  173. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/synthesizer/validation.py +0 -0
  174. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/synthesizer/workspace.py +0 -0
  175. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/templates/presets/balanced.toml +0 -0
  176. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/templates/presets/cheap.toml +0 -0
  177. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/templates/presets/thorough.toml +0 -0
  178. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/templates/producer.md +0 -0
  179. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/templates/reviewer.md +0 -0
  180. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/templates/reviewer_adversarial.md +0 -0
  181. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/templates/reviewer_codex.md +0 -0
  182. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/templates/spec_audit.md +0 -0
  183. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/templates/spec_draft.md +0 -0
  184. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/templates/synthesizer.md +0 -0
  185. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/test_runner.py +0 -0
  186. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/test_runner_classify.py +0 -0
  187. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/transcript.py +0 -0
  188. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/usage.py +0 -0
  189. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/worktree.py +0 -0
  190. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/worktree_env.py +0 -0
  191. {syncade-0.6.2 → syncade-0.6.3}/src/syncade/worktree_paths.py +0 -0
  192. {syncade-0.6.2 → syncade-0.6.3}/src/syncade.egg-info/dependency_links.txt +0 -0
  193. {syncade-0.6.2 → syncade-0.6.3}/src/syncade.egg-info/entry_points.txt +0 -0
  194. {syncade-0.6.2 → syncade-0.6.3}/src/syncade.egg-info/requires.txt +0 -0
  195. {syncade-0.6.2 → syncade-0.6.3}/src/syncade.egg-info/top_level.txt +0 -0
  196. {syncade-0.6.2 → syncade-0.6.3}/tests/__init__.py +0 -0
  197. {syncade-0.6.2 → syncade-0.6.3}/tests/adapters/__init__.py +0 -0
  198. {syncade-0.6.2 → syncade-0.6.3}/tests/adapters/_anthropic_helpers.py +0 -0
  199. {syncade-0.6.2 → syncade-0.6.3}/tests/adapters/_openai_helpers.py +0 -0
  200. {syncade-0.6.2 → syncade-0.6.3}/tests/adapters/test_anthropic.py +0 -0
  201. {syncade-0.6.2 → syncade-0.6.3}/tests/adapters/test_anthropic_more.py +0 -0
  202. {syncade-0.6.2 → syncade-0.6.3}/tests/adapters/test_openai.py +0 -0
  203. {syncade-0.6.2 → syncade-0.6.3}/tests/adapters/test_openai_more.py +0 -0
  204. {syncade-0.6.2 → syncade-0.6.3}/tests/adapters/test_producer.py +0 -0
  205. {syncade-0.6.2 → syncade-0.6.3}/tests/adapters/test_producer_more.py +0 -0
  206. {syncade-0.6.2 → syncade-0.6.3}/tests/adapters/test_prompt_transport.py +0 -0
  207. {syncade-0.6.2 → syncade-0.6.3}/tests/auth_check/__init__.py +0 -0
  208. {syncade-0.6.2 → syncade-0.6.3}/tests/auth_check/test_probes.py +0 -0
  209. {syncade-0.6.2 → syncade-0.6.3}/tests/auth_check/test_run_auth_check.py +0 -0
  210. {syncade-0.6.2 → syncade-0.6.3}/tests/cli/__init__.py +0 -0
  211. {syncade-0.6.2 → syncade-0.6.3}/tests/cli/_helpers.py +0 -0
  212. {syncade-0.6.2 → syncade-0.6.3}/tests/cli/_undo_helpers.py +0 -0
  213. {syncade-0.6.2 → syncade-0.6.3}/tests/cli/test_basic_and_mutex.py +0 -0
  214. {syncade-0.6.2 → syncade-0.6.3}/tests/cli/test_cli_library_divergence.py +0 -0
  215. {syncade-0.6.2 → syncade-0.6.3}/tests/cli/test_config_keys.py +0 -0
  216. {syncade-0.6.2 → syncade-0.6.3}/tests/cli/test_config_list.py +0 -0
  217. {syncade-0.6.2 → syncade-0.6.3}/tests/cli/test_config_menu_rows.py +0 -0
  218. {syncade-0.6.2 → syncade-0.6.3}/tests/cli/test_config_mode.py +0 -0
  219. {syncade-0.6.2 → syncade-0.6.3}/tests/cli/test_config_overrides.py +0 -0
  220. {syncade-0.6.2 → syncade-0.6.3}/tests/cli/test_config_tui.py +0 -0
  221. {syncade-0.6.2 → syncade-0.6.3}/tests/cli/test_config_tui_pty.py +0 -0
  222. {syncade-0.6.2 → syncade-0.6.3}/tests/cli/test_documented_exit_codes.py +0 -0
  223. {syncade-0.6.2 → syncade-0.6.3}/tests/cli/test_documented_flags_exist.py +0 -0
  224. {syncade-0.6.2 → syncade-0.6.3}/tests/cli/test_flags.py +0 -0
  225. {syncade-0.6.2 → syncade-0.6.3}/tests/cli/test_gc.py +0 -0
  226. {syncade-0.6.2 → syncade-0.6.3}/tests/cli/test_help_retention_contract.py +0 -0
  227. {syncade-0.6.2 → syncade-0.6.3}/tests/cli/test_mode_flag_guards.py +0 -0
  228. {syncade-0.6.2 → syncade-0.6.3}/tests/cli/test_resume_reports_hard_kill.py +0 -0
  229. {syncade-0.6.2 → syncade-0.6.3}/tests/cli/test_run_and_config.py +0 -0
  230. {syncade-0.6.2 → syncade-0.6.3}/tests/cli/test_run_status_cli.py +0 -0
  231. {syncade-0.6.2 → syncade-0.6.3}/tests/cli/test_scope_openspec_draft.py +0 -0
  232. {syncade-0.6.2 → syncade-0.6.3}/tests/cli/test_toml_writer.py +0 -0
  233. {syncade-0.6.2 → syncade-0.6.3}/tests/cli/test_undo_auto_init.py +0 -0
  234. {syncade-0.6.2 → syncade-0.6.3}/tests/cli/test_undo_scope.py +0 -0
  235. {syncade-0.6.2 → syncade-0.6.3}/tests/cli/test_validate_before_mutate.py +0 -0
  236. {syncade-0.6.2 → syncade-0.6.3}/tests/config/__init__.py +0 -0
  237. {syncade-0.6.2 → syncade-0.6.3}/tests/config/test_auth_enforcement.py +0 -0
  238. {syncade-0.6.2 → syncade-0.6.3}/tests/config/test_auth_modes.py +0 -0
  239. {syncade-0.6.2 → syncade-0.6.3}/tests/config/test_auth_preflight.py +0 -0
  240. {syncade-0.6.2 → syncade-0.6.3}/tests/config/test_auth_report_scope.py +0 -0
  241. {syncade-0.6.2 → syncade-0.6.3}/tests/config/test_budget_config.py +0 -0
  242. {syncade-0.6.2 → syncade-0.6.3}/tests/config/test_cold_actors.py +0 -0
  243. {syncade-0.6.2 → syncade-0.6.3}/tests/config/test_config_checks.py +0 -0
  244. {syncade-0.6.2 → syncade-0.6.3}/tests/config/test_config_reference_drift.py +0 -0
  245. {syncade-0.6.2 → syncade-0.6.3}/tests/config/test_config_schema.py +0 -0
  246. {syncade-0.6.2 → syncade-0.6.3}/tests/config/test_gc_config.py +0 -0
  247. {syncade-0.6.2 → syncade-0.6.3}/tests/config/test_global_config_layer.py +0 -0
  248. {syncade-0.6.2 → syncade-0.6.3}/tests/config/test_presets.py +0 -0
  249. {syncade-0.6.2 → syncade-0.6.3}/tests/config/test_retry_config.py +0 -0
  250. {syncade-0.6.2 → syncade-0.6.3}/tests/config/test_reviewer_config.py +0 -0
  251. {syncade-0.6.2 → syncade-0.6.3}/tests/config/test_reviewer_permissions.py +0 -0
  252. {syncade-0.6.2 → syncade-0.6.3}/tests/config/test_worktree_base_config.py +0 -0
  253. {syncade-0.6.2 → syncade-0.6.3}/tests/conftest.py +0 -0
  254. {syncade-0.6.2 → syncade-0.6.3}/tests/dispatcher/__init__.py +0 -0
  255. {syncade-0.6.2 → syncade-0.6.3}/tests/dispatcher/_helpers.py +0 -0
  256. {syncade-0.6.2 → syncade-0.6.3}/tests/dispatcher/test_dispatch_core.py +0 -0
  257. {syncade-0.6.2 → syncade-0.6.3}/tests/dispatcher/test_dispatch_misc.py +0 -0
  258. {syncade-0.6.2 → syncade-0.6.3}/tests/dispatcher/test_dispatch_usage.py +0 -0
  259. {syncade-0.6.2 → syncade-0.6.3}/tests/doctor/__init__.py +0 -0
  260. {syncade-0.6.2 → syncade-0.6.3}/tests/doctor/_helpers.py +0 -0
  261. {syncade-0.6.2 → syncade-0.6.3}/tests/doctor/conftest.py +0 -0
  262. {syncade-0.6.2 → syncade-0.6.3}/tests/doctor/test_doctor.py +0 -0
  263. {syncade-0.6.2 → syncade-0.6.3}/tests/doctor/test_doctor_diff_size.py +0 -0
  264. {syncade-0.6.2 → syncade-0.6.3}/tests/doctor/test_doctor_malformed_diff.py +0 -0
  265. {syncade-0.6.2 → syncade-0.6.3}/tests/doctor/test_doctor_preview.py +0 -0
  266. {syncade-0.6.2 → syncade-0.6.3}/tests/findings/__init__.py +0 -0
  267. {syncade-0.6.2 → syncade-0.6.3}/tests/findings/_helpers.py +0 -0
  268. {syncade-0.6.2 → syncade-0.6.3}/tests/findings/test_parser_and_fields.py +0 -0
  269. {syncade-0.6.2 → syncade-0.6.3}/tests/findings/test_parser_resilience.py +0 -0
  270. {syncade-0.6.2 → syncade-0.6.3}/tests/findings/test_verdict_block_selection.py +0 -0
  271. {syncade-0.6.2 → syncade-0.6.3}/tests/gc/__init__.py +0 -0
  272. {syncade-0.6.2 → syncade-0.6.3}/tests/gc/_helpers.py +0 -0
  273. {syncade-0.6.2 → syncade-0.6.3}/tests/gc/test_autoprune.py +0 -0
  274. {syncade-0.6.2 → syncade-0.6.3}/tests/gc/test_execute.py +0 -0
  275. {syncade-0.6.2 → syncade-0.6.3}/tests/gc/test_plan.py +0 -0
  276. {syncade-0.6.2 → syncade-0.6.3}/tests/gc/test_plan_execution_races.py +0 -0
  277. {syncade-0.6.2 → syncade-0.6.3}/tests/gc/test_reaping_scope.py +0 -0
  278. {syncade-0.6.2 → syncade-0.6.3}/tests/gc/test_robustness.py +0 -0
  279. {syncade-0.6.2 → syncade-0.6.3}/tests/gc/test_self_orphan_protection.py +0 -0
  280. {syncade-0.6.2 → syncade-0.6.3}/tests/gc/test_smoke.py +0 -0
  281. {syncade-0.6.2 → syncade-0.6.3}/tests/gc/test_two_tier_retention.py +0 -0
  282. {syncade-0.6.2 → syncade-0.6.3}/tests/logging/__init__.py +0 -0
  283. {syncade-0.6.2 → syncade-0.6.3}/tests/logging/_helpers.py +0 -0
  284. {syncade-0.6.2 → syncade-0.6.3}/tests/logging/test_logging.py +0 -0
  285. {syncade-0.6.2 → syncade-0.6.3}/tests/logging/test_logging_summary.py +0 -0
  286. {syncade-0.6.2 → syncade-0.6.3}/tests/metrics/conftest.py +0 -0
  287. {syncade-0.6.2 → syncade-0.6.3}/tests/metrics/test_actor_spend.py +0 -0
  288. {syncade-0.6.2 → syncade-0.6.3}/tests/metrics/test_aggregate.py +0 -0
  289. {syncade-0.6.2 → syncade-0.6.3}/tests/metrics/test_billing_reconciliation.py +0 -0
  290. {syncade-0.6.2 → syncade-0.6.3}/tests/metrics/test_billing_surfaces_agree.py +0 -0
  291. {syncade-0.6.2 → syncade-0.6.3}/tests/metrics/test_budget_metrics.py +0 -0
  292. {syncade-0.6.2 → syncade-0.6.3}/tests/metrics/test_cost_honesty.py +0 -0
  293. {syncade-0.6.2 → syncade-0.6.3}/tests/metrics/test_metrics_cli.py +0 -0
  294. {syncade-0.6.2 → syncade-0.6.3}/tests/metrics/test_metrics_mode.py +0 -0
  295. {syncade-0.6.2 → syncade-0.6.3}/tests/metrics/test_schema.py +0 -0
  296. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/__init__.py +0 -0
  297. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/_helpers.py +0 -0
  298. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/_resume_fixtures.py +0 -0
  299. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/conftest.py +0 -0
  300. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/test_budget.py +0 -0
  301. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/test_budget_resume_e2e.py +0 -0
  302. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/test_budget_stop_message.py +0 -0
  303. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/test_budget_warning.py +0 -0
  304. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/test_checks_and_recording.py +0 -0
  305. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/test_contracts.py +0 -0
  306. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/test_cross_round.py +0 -0
  307. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/test_cross_round_more.py +0 -0
  308. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/test_default_branch_guard.py +0 -0
  309. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/test_diff_size_cap.py +0 -0
  310. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/test_dispatch_record.py +0 -0
  311. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/test_empty_diff.py +0 -0
  312. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/test_empty_diff_no_side_effects.py +0 -0
  313. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/test_escalation.py +0 -0
  314. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/test_last_reviewed_truthful.py +0 -0
  315. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/test_loop_audit_remediation.py +0 -0
  316. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/test_matrix_and_e2e.py +0 -0
  317. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/test_multiround.py +0 -0
  318. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/test_multiround_dirty.py +0 -0
  319. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/test_multiround_more.py +0 -0
  320. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/test_paraphrase_escalation.py +0 -0
  321. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/test_pipeline.py +0 -0
  322. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/test_provider_usage_limit.py +0 -0
  323. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/test_rename_boundary.py +0 -0
  324. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/test_resume.py +0 -0
  325. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/test_resume_base_oid.py +0 -0
  326. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/test_reviewer_blindness_is_structural.py +0 -0
  327. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/test_round_input_boundaries.py +0 -0
  328. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/test_run_layout.py +0 -0
  329. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/test_run_status_wiring.py +0 -0
  330. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/test_safety_warning_survives_quiet.py +0 -0
  331. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/test_snapshot_and_base.py +0 -0
  332. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/test_synth_phase.py +0 -0
  333. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/test_synth_phase_more.py +0 -0
  334. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/test_synth_provenance.py +0 -0
  335. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/test_synth_validation.py +0 -0
  336. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/test_template_robustness.py +0 -0
  337. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/test_termination_reason_drift.py +0 -0
  338. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/test_testleg.py +0 -0
  339. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/test_timeout_is_per_subprocess.py +0 -0
  340. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/test_usage_limit_message.py +0 -0
  341. {syncade-0.6.2 → syncade-0.6.3}/tests/orchestrator/test_worktree_cleanup.py +0 -0
  342. {syncade-0.6.2 → syncade-0.6.3}/tests/persistence/__init__.py +0 -0
  343. {syncade-0.6.2 → syncade-0.6.3}/tests/persistence/_helpers.py +0 -0
  344. {syncade-0.6.2 → syncade-0.6.3}/tests/persistence/test_atomic_writes.py +0 -0
  345. {syncade-0.6.2 → syncade-0.6.3}/tests/persistence/test_checks_surfacing.py +0 -0
  346. {syncade-0.6.2 → syncade-0.6.3}/tests/persistence/test_diff_size_recording.py +0 -0
  347. {syncade-0.6.2 → syncade-0.6.3}/tests/persistence/test_findings_md.py +0 -0
  348. {syncade-0.6.2 → syncade-0.6.3}/tests/persistence/test_findings_md_more.py +0 -0
  349. {syncade-0.6.2 → syncade-0.6.3}/tests/persistence/test_findings_md_verdict.py +0 -0
  350. {syncade-0.6.2 → syncade-0.6.3}/tests/persistence/test_handoff.py +0 -0
  351. {syncade-0.6.2 → syncade-0.6.3}/tests/persistence/test_loop_manifest_checks.py +0 -0
  352. {syncade-0.6.2 → syncade-0.6.3}/tests/persistence/test_loop_summary.py +0 -0
  353. {syncade-0.6.2 → syncade-0.6.3}/tests/persistence/test_loop_summary_more.py +0 -0
  354. {syncade-0.6.2 → syncade-0.6.3}/tests/persistence/test_md_safety.py +0 -0
  355. {syncade-0.6.2 → syncade-0.6.3}/tests/persistence/test_next_steps.py +0 -0
  356. {syncade-0.6.2 → syncade-0.6.3}/tests/persistence/test_producer_persist.py +0 -0
  357. {syncade-0.6.2 → syncade-0.6.3}/tests/persistence/test_reviewer.py +0 -0
  358. {syncade-0.6.2 → syncade-0.6.3}/tests/persistence/test_round_manifest.py +0 -0
  359. {syncade-0.6.2 → syncade-0.6.3}/tests/persistence/test_run_init.py +0 -0
  360. {syncade-0.6.2 → syncade-0.6.3}/tests/persistence/test_run_summary.py +0 -0
  361. {syncade-0.6.2 → syncade-0.6.3}/tests/persistence/test_run_summary_more.py +0 -0
  362. {syncade-0.6.2 → syncade-0.6.3}/tests/persistence/test_run_summary_producer.py +0 -0
  363. {syncade-0.6.2 → syncade-0.6.3}/tests/persistence/test_synth_persist.py +0 -0
  364. {syncade-0.6.2 → syncade-0.6.3}/tests/persistence/test_test_run.py +0 -0
  365. {syncade-0.6.2 → syncade-0.6.3}/tests/producer/__init__.py +0 -0
  366. {syncade-0.6.2 → syncade-0.6.3}/tests/producer/_helpers.py +0 -0
  367. {syncade-0.6.2 → syncade-0.6.3}/tests/producer/test_producer.py +0 -0
  368. {syncade-0.6.2 → syncade-0.6.3}/tests/producer/test_producer_confinement.py +0 -0
  369. {syncade-0.6.2 → syncade-0.6.3}/tests/producer/test_producer_retry.py +0 -0
  370. {syncade-0.6.2 → syncade-0.6.3}/tests/producer/test_producer_run.py +0 -0
  371. {syncade-0.6.2 → syncade-0.6.3}/tests/producer/test_producer_timeout.py +0 -0
  372. {syncade-0.6.2 → syncade-0.6.3}/tests/producer/test_producer_usage.py +0 -0
  373. {syncade-0.6.2 → syncade-0.6.3}/tests/prompts/__init__.py +0 -0
  374. {syncade-0.6.2 → syncade-0.6.3}/tests/prompts/test_load_template.py +0 -0
  375. {syncade-0.6.2 → syncade-0.6.3}/tests/prompts/test_misc.py +0 -0
  376. {syncade-0.6.2 → syncade-0.6.3}/tests/prompts/test_producer.py +0 -0
  377. {syncade-0.6.2 → syncade-0.6.3}/tests/prompts/test_render_synthesizer.py +0 -0
  378. {syncade-0.6.2 → syncade-0.6.3}/tests/prompts/test_synthesizer.py +0 -0
  379. {syncade-0.6.2 → syncade-0.6.3}/tests/resume/__init__.py +0 -0
  380. {syncade-0.6.2 → syncade-0.6.3}/tests/resume/_helpers.py +0 -0
  381. {syncade-0.6.2 → syncade-0.6.3}/tests/resume/test_budget_resume.py +0 -0
  382. {syncade-0.6.2 → syncade-0.6.3}/tests/resume/test_checks_phase_failure.py +0 -0
  383. {syncade-0.6.2 → syncade-0.6.3}/tests/resume/test_cli_malformed_rehydration.py +0 -0
  384. {syncade-0.6.2 → syncade-0.6.3}/tests/resume/test_find_resolve_plan.py +0 -0
  385. {syncade-0.6.2 → syncade-0.6.3}/tests/resume/test_plan_resume.py +0 -0
  386. {syncade-0.6.2 → syncade-0.6.3}/tests/resume/test_round_trip.py +0 -0
  387. {syncade-0.6.2 → syncade-0.6.3}/tests/resume/test_tree_drift_and_escalation.py +0 -0
  388. {syncade-0.6.2 → syncade-0.6.3}/tests/skills/test_config_section.py +0 -0
  389. {syncade-0.6.2 → syncade-0.6.3}/tests/skills/test_install_skill.py +0 -0
  390. {syncade-0.6.2 → syncade-0.6.3}/tests/skills/test_install_skill_multi_harness.py +0 -0
  391. {syncade-0.6.2 → syncade-0.6.3}/tests/skills/test_install_skill_ownership.py +0 -0
  392. {syncade-0.6.2 → syncade-0.6.3}/tests/skills/test_install_skill_refuses.py +0 -0
  393. {syncade-0.6.2 → syncade-0.6.3}/tests/skills/test_skill_drift.py +0 -0
  394. {syncade-0.6.2 → syncade-0.6.3}/tests/smoke/__init__.py +0 -0
  395. {syncade-0.6.2 → syncade-0.6.3}/tests/smoke/test_anthropic_smoke.py +0 -0
  396. {syncade-0.6.2 → syncade-0.6.3}/tests/smoke/test_auth_check_smoke.py +0 -0
  397. {syncade-0.6.2 → syncade-0.6.3}/tests/smoke/test_codex_smoke.py +0 -0
  398. {syncade-0.6.2 → syncade-0.6.3}/tests/smoke/test_loop_smoke.py +0 -0
  399. {syncade-0.6.2 → syncade-0.6.3}/tests/smoke/test_orchestrator_smoke.py +0 -0
  400. {syncade-0.6.2 → syncade-0.6.3}/tests/smoke/test_orchestrator_smoke_more.py +0 -0
  401. {syncade-0.6.2 → syncade-0.6.3}/tests/smoke/test_producer_smoke.py +0 -0
  402. {syncade-0.6.2 → syncade-0.6.3}/tests/smoke/test_resume_reap_smoke.py +0 -0
  403. {syncade-0.6.2 → syncade-0.6.3}/tests/smoke/test_selfcheck_smoke.py +0 -0
  404. {syncade-0.6.2 → syncade-0.6.3}/tests/smoke/test_synthesizer_smoke.py +0 -0
  405. {syncade-0.6.2 → syncade-0.6.3}/tests/smoke/test_test_runner_smoke.py +0 -0
  406. {syncade-0.6.2 → syncade-0.6.3}/tests/snapshot/__init__.py +0 -0
  407. {syncade-0.6.2 → syncade-0.6.3}/tests/snapshot/_helpers.py +0 -0
  408. {syncade-0.6.2 → syncade-0.6.3}/tests/snapshot/conftest.py +0 -0
  409. {syncade-0.6.2 → syncade-0.6.3}/tests/snapshot/test_snapshot.py +0 -0
  410. {syncade-0.6.2 → syncade-0.6.3}/tests/snapshot/test_snapshot_diff_identity.py +0 -0
  411. {syncade-0.6.2 → syncade-0.6.3}/tests/snapshot/test_snapshot_dirty.py +0 -0
  412. {syncade-0.6.2 → syncade-0.6.3}/tests/snapshot/test_snapshot_warning.py +0 -0
  413. {syncade-0.6.2 → syncade-0.6.3}/tests/snapshot/test_three_dot_diff.py +0 -0
  414. {syncade-0.6.2 → syncade-0.6.3}/tests/synthesis/__init__.py +0 -0
  415. {syncade-0.6.2 → syncade-0.6.3}/tests/synthesis/_helpers.py +0 -0
  416. {syncade-0.6.2 → syncade-0.6.3}/tests/synthesis/test_cluster_repair.py +0 -0
  417. {syncade-0.6.2 → syncade-0.6.3}/tests/synthesis/test_clusters.py +0 -0
  418. {syncade-0.6.2 → syncade-0.6.3}/tests/synthesis/test_models.py +0 -0
  419. {syncade-0.6.2 → syncade-0.6.3}/tests/synthesis/test_models_distinct_keying.py +0 -0
  420. {syncade-0.6.2 → syncade-0.6.3}/tests/synthesis/test_parsing.py +0 -0
  421. {syncade-0.6.2 → syncade-0.6.3}/tests/synthesis/test_unanimous_blocker_downgrade.py +0 -0
  422. {syncade-0.6.2 → syncade-0.6.3}/tests/synthesizer/test_provenance_fabrication.py +0 -0
  423. {syncade-0.6.2 → syncade-0.6.3}/tests/synthesizer/test_provenance_repair.py +0 -0
  424. {syncade-0.6.2 → syncade-0.6.3}/tests/test_adapter_registry.py +0 -0
  425. {syncade-0.6.2 → syncade-0.6.3}/tests/test_adapters_fake.py +0 -0
  426. {syncade-0.6.2 → syncade-0.6.3}/tests/test_auto_init_policy.py +0 -0
  427. {syncade-0.6.2 → syncade-0.6.3}/tests/test_base_resolution.py +0 -0
  428. {syncade-0.6.2 → syncade-0.6.3}/tests/test_check_loc_script.py +0 -0
  429. {syncade-0.6.2 → syncade-0.6.3}/tests/test_checks.py +0 -0
  430. {syncade-0.6.2 → syncade-0.6.3}/tests/test_config_loader.py +0 -0
  431. {syncade-0.6.2 → syncade-0.6.3}/tests/test_cross_model_claim.py +0 -0
  432. {syncade-0.6.2 → syncade-0.6.3}/tests/test_diff_filter.py +0 -0
  433. {syncade-0.6.2 → syncade-0.6.3}/tests/test_diff_filter_binary.py +0 -0
  434. {syncade-0.6.2 → syncade-0.6.3}/tests/test_diff_filter_quoted_paths.py +0 -0
  435. {syncade-0.6.2 → syncade-0.6.3}/tests/test_escalation_coverage.py +0 -0
  436. {syncade-0.6.2 → syncade-0.6.3}/tests/test_exit_codes.py +0 -0
  437. {syncade-0.6.2 → syncade-0.6.3}/tests/test_git_choke_point.py +0 -0
  438. {syncade-0.6.2 → syncade-0.6.3}/tests/test_git_config_isolation.py +0 -0
  439. {syncade-0.6.2 → syncade-0.6.3}/tests/test_git_preconditions.py +0 -0
  440. {syncade-0.6.2 → syncade-0.6.3}/tests/test_last_reviewed.py +0 -0
  441. {syncade-0.6.2 → syncade-0.6.3}/tests/test_parse_repair_extra_keys.py +0 -0
  442. {syncade-0.6.2 → syncade-0.6.3}/tests/test_pricing.py +0 -0
  443. {syncade-0.6.2 → syncade-0.6.3}/tests/test_private_identifier_gate.py +0 -0
  444. {syncade-0.6.2 → syncade-0.6.3}/tests/test_process.py +0 -0
  445. {syncade-0.6.2 → syncade-0.6.3}/tests/test_process_teardown.py +0 -0
  446. {syncade-0.6.2 → syncade-0.6.3}/tests/test_producer_escalation.py +0 -0
  447. {syncade-0.6.2 → syncade-0.6.3}/tests/test_regression_acme.py +0 -0
  448. {syncade-0.6.2 → syncade-0.6.3}/tests/test_retry.py +0 -0
  449. {syncade-0.6.2 → syncade-0.6.3}/tests/test_run_status.py +0 -0
  450. {syncade-0.6.2 → syncade-0.6.3}/tests/test_run_status_signals.py +0 -0
  451. {syncade-0.6.2 → syncade-0.6.3}/tests/test_runner/__init__.py +0 -0
  452. {syncade-0.6.2 → syncade-0.6.3}/tests/test_runner/test_test_runner.py +0 -0
  453. {syncade-0.6.2 → syncade-0.6.3}/tests/test_runner/test_test_runner_more.py +0 -0
  454. {syncade-0.6.2 → syncade-0.6.3}/tests/test_safety_disclosures_survive_quiet.py +0 -0
  455. {syncade-0.6.2 → syncade-0.6.3}/tests/test_selfcheck.py +0 -0
  456. {syncade-0.6.2 → syncade-0.6.3}/tests/test_spec_audit.py +0 -0
  457. {syncade-0.6.2 → syncade-0.6.3}/tests/test_spec_draft.py +0 -0
  458. {syncade-0.6.2 → syncade-0.6.3}/tests/test_spec_source.py +0 -0
  459. {syncade-0.6.2 → syncade-0.6.3}/tests/test_streamed_capture.py +0 -0
  460. {syncade-0.6.2 → syncade-0.6.3}/tests/test_strip_list_semantics.py +0 -0
  461. {syncade-0.6.2 → syncade-0.6.3}/tests/test_synthesizer.py +0 -0
  462. {syncade-0.6.2 → syncade-0.6.3}/tests/test_synthesizer_passthrough.py +0 -0
  463. {syncade-0.6.2 → syncade-0.6.3}/tests/test_synthesizer_provenance.py +0 -0
  464. {syncade-0.6.2 → syncade-0.6.3}/tests/test_transcript.py +0 -0
  465. {syncade-0.6.2 → syncade-0.6.3}/tests/test_usage.py +0 -0
  466. {syncade-0.6.2 → syncade-0.6.3}/tests/test_usage_persistence.py +0 -0
  467. {syncade-0.6.2 → syncade-0.6.3}/tests/test_workflow_action_pins.py +0 -0
  468. {syncade-0.6.2 → syncade-0.6.3}/tests/test_worktree_env.py +0 -0
  469. {syncade-0.6.2 → syncade-0.6.3}/tests/test_worktree_strip_guard.py +0 -0
  470. {syncade-0.6.2 → syncade-0.6.3}/tests/worktree/__init__.py +0 -0
  471. {syncade-0.6.2 → syncade-0.6.3}/tests/worktree/_helpers.py +0 -0
  472. {syncade-0.6.2 → syncade-0.6.3}/tests/worktree/conftest.py +0 -0
  473. {syncade-0.6.2 → syncade-0.6.3}/tests/worktree/test_worktree.py +0 -0
  474. {syncade-0.6.2 → syncade-0.6.3}/tests/worktree/test_worktree_cleanup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: syncade
3
- Version: 0.6.2
3
+ Version: 0.6.3
4
4
  Summary: External blind multi-judge review orchestrator for AI-assisted coding
5
5
  Author-email: Syncade <build@syncade.ai>
6
6
  License-Expression: Apache-2.0
@@ -87,9 +87,15 @@ terminal or copy-paste between tools.
87
87
  ## Install
88
88
 
89
89
  ```bash
90
- uv tool install git+https://github.com/syncade-ai/syncade-ai.git # recommended
90
+ uv tool install syncade # recommended
91
91
  # or with pip:
92
- pip install git+https://github.com/syncade-ai/syncade-ai.git
92
+ pip install syncade
93
+ ```
94
+
95
+ To track unreleased `main` instead of the latest release:
96
+
97
+ ```bash
98
+ uv tool install git+https://github.com/syncade-ai/syncade-ai.git
93
99
  ```
94
100
 
95
101
  Then install the harness integration so you can invoke it as `/syncade`:
@@ -104,6 +110,24 @@ touching the directory and names every file at risk. Ordinary upgrades need no f
104
110
  recognises its own past output by content, not by filename. Use `--force-install` to overwrite
105
111
  deliberately; it still lists what it destroys.
106
112
 
113
+ ### Staying up to date
114
+
115
+ The first time you run syncade in a new terminal or harness window, it checks once whether a
116
+ newer release exists and prints a line if so. It only ever prints — it never blocks a run,
117
+ changes an exit code, or fails one, and every error (offline, bad response) is silent. Nothing
118
+ about your repo, diff, or run is sent. Turn it off with `syncade --config set update.check false`,
119
+ or set `CI` in automation.
120
+
121
+ ```bash
122
+ syncade --update # upgrade, then re-run your command
123
+ ```
124
+
125
+ `--update` works out how syncade was installed by looking for a marker inside its own install
126
+ tree, and **refuses rather than guessing** if it cannot tell — printing the manual `pip` command
127
+ instead. It also refuses while a review is still running, and declines to touch a source
128
+ checkout. A running process cannot switch to the version it just installed, so it exits and asks
129
+ you to re-run.
130
+
107
131
  **Requirements**
108
132
  - **Python 3.11+**, on **macOS or Linux** (on Windows, use WSL).
109
133
  - **`codex`** (OpenAI), on your `PATH` and authenticated — required for the default
@@ -57,9 +57,15 @@ terminal or copy-paste between tools.
57
57
  ## Install
58
58
 
59
59
  ```bash
60
- uv tool install git+https://github.com/syncade-ai/syncade-ai.git # recommended
60
+ uv tool install syncade # recommended
61
61
  # or with pip:
62
- pip install git+https://github.com/syncade-ai/syncade-ai.git
62
+ pip install syncade
63
+ ```
64
+
65
+ To track unreleased `main` instead of the latest release:
66
+
67
+ ```bash
68
+ uv tool install git+https://github.com/syncade-ai/syncade-ai.git
63
69
  ```
64
70
 
65
71
  Then install the harness integration so you can invoke it as `/syncade`:
@@ -74,6 +80,24 @@ touching the directory and names every file at risk. Ordinary upgrades need no f
74
80
  recognises its own past output by content, not by filename. Use `--force-install` to overwrite
75
81
  deliberately; it still lists what it destroys.
76
82
 
83
+ ### Staying up to date
84
+
85
+ The first time you run syncade in a new terminal or harness window, it checks once whether a
86
+ newer release exists and prints a line if so. It only ever prints — it never blocks a run,
87
+ changes an exit code, or fails one, and every error (offline, bad response) is silent. Nothing
88
+ about your repo, diff, or run is sent. Turn it off with `syncade --config set update.check false`,
89
+ or set `CI` in automation.
90
+
91
+ ```bash
92
+ syncade --update # upgrade, then re-run your command
93
+ ```
94
+
95
+ `--update` works out how syncade was installed by looking for a marker inside its own install
96
+ tree, and **refuses rather than guessing** if it cannot tell — printing the manual `pip` command
97
+ instead. It also refuses while a review is still running, and declines to touch a source
98
+ checkout. A running process cannot switch to the version it just installed, so it exits and asks
99
+ you to re-run.
100
+
77
101
  **Requirements**
78
102
  - **Python 3.11+**, on **macOS or Linux** (on Windows, use WSL).
79
103
  - **`codex`** (OpenAI), on your `PATH` and authenticated — required for the default
@@ -99,6 +99,7 @@ below; `worktree_base` is the one top-level scalar.
99
99
  | `review` | `[review]` table | — | What reviewers see. See [`[review]`](#review--reviewconfig). |
100
100
  | `retry` | `[retry]` table | — | Transient-error retry bound. See [`[retry]`](#retry--retryconfig). |
101
101
  | `gc` | `[gc]` table | — | Run-artifact retention. See [`[gc]`](#gc--gcconfig). |
102
+ | `update` | `[update]` table | — | The once-per-session update check. See [`[update]`](#update--updateconfig). |
102
103
  | `worktree_base` | path | `/tmp/syncade` | Base dir for per-run git worktrees. Overridable with `--worktree-base`; point it at a fast local disk if `/tmp` is small or slow. |
103
104
  | `checks` | `[[checks]]` list | none | Mechanical exit-code gates. See [`[[checks]]`](#checks--checkconfig). |
104
105
  | `pricing` | `[pricing]` table | packaged price table | Per-model token pricing for cost estimation. See [`[pricing]`](#pricing--pricingconfig). |
@@ -229,6 +230,21 @@ deleted — only bulky subprocess transcripts. CLI `--gc-keep` / `--gc-max-age-d
229
230
  | `keep` | int ≥ 0 | `20` | Newest N runs whose transcripts are always kept. |
230
231
  | `max_age_days` | int ≥ 0 | `0` | Additional age floor: a beyond-`keep` run is pruned only if also older than this. `0` disables the age floor. |
231
232
 
233
+ ## `[update]` — `UpdateConfig`
234
+
235
+ Whether syncade checks for a newer release. The check only ever **prints**: it cannot block a run,
236
+ change an exit code, or fail one — every error (offline, timeout, bad JSON) is silent. It fires at
237
+ most once per window, at the first syncade invocation, and is skipped entirely whenever `CI` is
238
+ set. Nothing about your repo, diff, run, or identity is sent.
239
+
240
+ The advisory source is deliberately not configurable — a repo-local file that could repoint or
241
+ silence a security notice would be a hole, not a feature.
242
+
243
+ <!-- config-fields: UpdateConfig -->
244
+ | Field | Type | Default | What it does |
245
+ |---|---|---|---|
246
+ | `check` | bool | `true` | Check once per session whether a newer syncade is published. Set `false` to never make the request. |
247
+
232
248
  ## `[[checks]]` — `CheckConfig`
233
249
 
234
250
  Each `[[checks]]` block is one mechanical command gate run after the review. `blocking` checks can
@@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta"
9
9
 
10
10
  [project]
11
11
  name = "syncade"
12
- version = "0.6.2"
12
+ version = "0.6.3"
13
13
  description = "External blind multi-judge review orchestrator for AI-assisted coding"
14
14
  readme = "README.md"
15
15
  requires-python = ">=3.11"
@@ -1,3 +1,3 @@
1
1
  """Syncade — external blind multi-judge review orchestrator."""
2
2
 
3
- __version__ = "0.6.2"
3
+ __version__ = "0.6.3"
@@ -492,11 +492,35 @@ def main(argv: list[str] | None = None) -> int:
492
492
  # re-inject after parsing. Also detect the forbidden prefix form (--repo before --config).
493
493
  _config_operands, _argv_parsed, _repo_prefix = _extract_config_operands(_argv)
494
494
  parser = build_parser()
495
+
496
+ # NOTE `--version` / `--help` never reach the notice below: argparse handles and exits
497
+ # during parse_args(). That is deliberate, and the brief says so rather than the code
498
+ # working around it. A pre-parse emit was tried and REVERTED — it could not pass `--quiet`
499
+ # through, could not honour a prompt answer (argparse exits first), and matched only the
500
+ # literal spellings, so argparse's own abbreviations (`--ver`, `--he`) still bypassed it.
501
+ # Matching those means reimplementing argparse's prefix matching: an enumeration, for the
502
+ # two flags least worth notifying on. `--version` in particular is what scripts call.
495
503
  args = parser.parse_args(_argv_parsed)
496
504
  if _config_operands is not None:
497
505
  # Merge any trailing non-flag operands that argparse still captured (e.g. `--config list
498
506
  # extra`) with the dash-prefixed ones we extracted before parsing.
499
507
  args.config = _config_operands + (args.config or [])
508
+
509
+ # FIRST thing after argv is parsed, deliberately — before the `--repo` shape check and
510
+ # before `validate_command_shape`, both of which return early. Placing it after them made
511
+ # every parsed-but-invalid invocation a silent third exception to "every invocation is
512
+ # eligible": `syncade --update --gc` reached syncade code, emitted nothing, marked nothing,
513
+ # and the NEXT valid command in that window printed the stale notice instead. This is the
514
+ # second blocker that one sentence produced, so the position is now structural rather than
515
+ # remembered: nothing can return ahead of it. It can only print — no exit code, no gate.
516
+ # Returns True in a terminal TTY when the operator chose to update now.
517
+ from syncade.cli.update_notice import emit_update_notice
518
+
519
+ _notice_root = Path(args.repo_root).expanduser() if args.repo_root else Path.cwd()
520
+ if emit_update_notice(quiet=args.quiet, repo_root=_notice_root):
521
+ from syncade.cli.update_mode import run_update
522
+
523
+ return run_update(cwd=_notice_root)
500
524
  # Reject the prefix form: --repo before --config set violates the suffix-only contract (D1).
501
525
  # Only applies to "set" — for other verbs (list/get) or no verb, _reject_config_mode_conflicts
502
526
  # will catch the --repo misuse with the appropriate "meaningful only with --config set" message.
@@ -515,6 +539,13 @@ def main(argv: list[str] | None = None) -> int:
515
539
  # --install-skill is a standalone local operation — copies bundled skill files into the
516
540
  # harness dirs. Dispatched after command-shape validation so --doctor/--quick combinations
517
541
  # are rejected before the filesystem mutation.
542
+ if args.update:
543
+ from syncade.cli.update_mode import run_update
544
+
545
+ # Pass the --repo-root hint so run_update() can resolve the actual git root and check
546
+ # for a live run there, rather than always defaulting to Path.cwd().
547
+ _update_cwd = Path(args.repo_root).expanduser() if args.repo_root else Path.cwd()
548
+ return run_update(cwd=_update_cwd)
518
549
  if args.install_skill is not None:
519
550
  from syncade.cli.install_skill import install_skill
520
551
 
@@ -162,6 +162,22 @@ def _is_plain_name(key: str) -> bool:
162
162
  return bool(key) and key not in (".", "..") and "/" not in key and not Path(key).is_absolute()
163
163
 
164
164
 
165
+ def resolve_homes(home: Path | None, codex_home: Path | None) -> tuple[Path, Path]:
166
+ """The ONE place harness home directories are resolved.
167
+
168
+ Extracted in PR-h-field-07 because it was not one place: the installer honoured
169
+ ``CODEX_HOME`` while the new runtime skill-status path re-derived ``~/.codex`` by hand. A
170
+ Codex skill installed under a custom home then read as ABSENT, so no stale notice fired and
171
+ ``--update`` skipped re-installing it — the exact drift this PR exists to close, hidden by
172
+ the copy. Both reviewers flagged it independently.
173
+ """
174
+ home = home or Path.home()
175
+ if codex_home is None:
176
+ env_cx = os.environ.get("CODEX_HOME")
177
+ codex_home = Path(env_cx) if env_cx else home / ".codex"
178
+ return home, codex_home
179
+
180
+
165
181
  def _dest(harness: str, home: Path, codex_home: Path) -> Path:
166
182
  if harness == "claude":
167
183
  return home / ".claude" / "skills" / "syncade"
@@ -336,10 +352,7 @@ def install_skill(
336
352
  )
337
353
  return CLI_USAGE_ERROR
338
354
 
339
- home = home or Path.home()
340
- if codex_home is None:
341
- env_cx = os.environ.get("CODEX_HOME")
342
- codex_home = Path(env_cx) if env_cx else home / ".codex"
355
+ home, codex_home = resolve_homes(home, codex_home)
343
356
 
344
357
  harnesses = ["claude", "codex"] if target == "all" else [target]
345
358
  plan = [
@@ -306,6 +306,16 @@ def build_parser() -> argparse.ArgumentParser:
306
306
  "Leaves the instant config / PATH / worktree / branch / plan / cost checks. The "
307
307
  "skipped legs are reported as 'skipped', never as passed.",
308
308
  )
309
+ parser.add_argument(
310
+ "--update",
311
+ action="store_true",
312
+ help="Upgrade syncade to the latest published release, then exit. Detects how syncade "
313
+ "was installed by finding a marker inside its own install tree (uv tool / pipx) and "
314
+ "REFUSES rather than guessing when it cannot tell, printing the manual pip command "
315
+ "instead. Refuses while a run is still going. Re-installs any skill you already have, "
316
+ "using the upgraded code. A running process cannot switch to the new version, so this "
317
+ "exits and asks you to re-run your command.",
318
+ )
309
319
  parser.add_argument(
310
320
  "--install-skill",
311
321
  nargs="?",
@@ -0,0 +1,142 @@
1
+ """Is the operator's INSTALLED skill still the one this package ships? — PR-h-field-07 item 3.
2
+
3
+ Read-only. It classifies; it never writes, and it never touches the destination beyond reading
4
+ the files the current bundle names.
5
+
6
+ **Staleness is decided by CONTENT, not by a recorded version number**, which is a deliberate
7
+ departure from the brief and the safer of the two designs for three separate reasons:
8
+
9
+ 1. *The record cannot carry a version without being reshaped.* ``.syncade-install.json`` is
10
+ exactly ``{"files": {…}}`` and is protected by a self-hash over its canonical BYTES; any extra
11
+ top-level key fails it closed (measured). Failing closed means "we can prove nothing", which
12
+ makes every differing file the operator's — so the very installs this feature exists to help
13
+ would start REFUSING to upgrade.
14
+ 2. *It would break every existing install.* Records already on disk are the old shape. Accepting
15
+ both shapes means two canonical forms and two self-hash paths, in the one structure that took
16
+ three consecutive dogfood rounds to make bypass-proof.
17
+ 3. *Content is a strictly better signal anyway.* The documented install has been an unpinned
18
+ ``git+https://`` URL, so two installs reporting the same ``__version__`` routinely ship
19
+ different skill files. A version compare is blind to exactly that case; hashes are not.
20
+
21
+ The three states the brief asks for fall straight out of data that already exists — what the
22
+ package would write now, versus what is on disk, versus what the record says we wrote.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from dataclasses import dataclass
28
+ from pathlib import Path
29
+ from typing import Literal
30
+
31
+ from syncade.cli.install_skill import (
32
+ _MANIFEST,
33
+ _bundle,
34
+ _dest,
35
+ _read_manifest,
36
+ _sha,
37
+ resolve_homes,
38
+ )
39
+
40
+ HARNESSES = ("claude", "codex")
41
+
42
+ #: ``absent`` nothing installed here — not a problem, and never reported.
43
+ #: ``in_sync`` every packaged file is present and byte-identical.
44
+ #: ``stale`` ours, unmodified, but from an older package. THE actionable state.
45
+ #: ``modified`` at least one file is not what we wrote — the operator's. Never nagged about.
46
+ #: ``unknown`` files differ and there is no usable record, so ownership is unproven.
47
+ Status = Literal["absent", "in_sync", "stale", "modified", "unknown"]
48
+
49
+
50
+ @dataclass(frozen=True)
51
+ class SkillStatus:
52
+ harness: str
53
+ status: Status
54
+ path: Path
55
+
56
+
57
+ def _classify(dest: Path, bundle: dict[str, bytes], record: dict[str, str]) -> Status:
58
+ # The self-hash entry is the record vouching for itself, not a file we wrote; excluding it
59
+ # is what makes `known` mean "files we can actually account for".
60
+ known = {k: v for k, v in record.items() if k != _MANIFEST}
61
+ differing: list[str] = []
62
+
63
+ for name, packaged in bundle.items():
64
+ try:
65
+ on_disk = (dest / name).read_bytes()
66
+ except OSError:
67
+ # Missing or unreadable. If we recorded writing it, someone removed it — theirs.
68
+ # If we did not, the file is new in this package version — that is staleness.
69
+ if name in known:
70
+ return "modified"
71
+ differing.append(name)
72
+ continue
73
+ if _sha(on_disk) != _sha(packaged):
74
+ differing.append(name)
75
+ # `known and` is load-bearing. An ABSENT record proves nothing, so it cannot
76
+ # convict the operator — without this guard every recordless install (which is
77
+ # every install predating the current installer, including both of mine) is
78
+ # misreported as operator-edited and silently skipped. Only a record that
79
+ # CONTRADICTS the file on disk is evidence of an edit.
80
+ if known and known.get(name) != _sha(on_disk):
81
+ # Returns immediately so a mixed tree is "modified" rather than "stale": an
82
+ # update notice aimed at someone's own edits is worse than no notice at all.
83
+ return "modified"
84
+
85
+ # Any entry in the destination that neither the current bundle nor the installer manifest
86
+ # accounts for is the operator's. A mixed tree — bundled files that look stale PLUS an
87
+ # extra file we never wrote — resolves to "modified" (silent) rather than "stale"
88
+ # (actionable): pointing at --install-skill for a tree it would immediately refuse is
89
+ # worse than no notice. The conservative direction misses a stale notice; the other
90
+ # costs the operator's trust.
91
+ #
92
+ # Exception: a file proven ours by the install record (hash matches the recorded hash) is
93
+ # syncade-owned — it was in an older bundle but was since removed or renamed. Treat it as
94
+ # a stale difference, not an operator modification, so --install-skill can fix it.
95
+ try:
96
+ for entry in dest.iterdir():
97
+ if entry.name not in bundle and entry.name != _MANIFEST:
98
+ if entry.is_file() and known:
99
+ try:
100
+ on_disk_hash = _sha(entry.read_bytes())
101
+ except OSError:
102
+ return "modified"
103
+ if known.get(entry.name) == on_disk_hash:
104
+ differing.append(entry.name)
105
+ continue
106
+ return "modified"
107
+ except OSError:
108
+ pass # cannot scan dest; treat as clean and continue
109
+
110
+ if not differing:
111
+ return "in_sync"
112
+ # Every difference is explained by the record, so this is our own older output — unless
113
+ # there is no record, in which case we can prove nothing and must not claim it is ours.
114
+ return "stale" if known else "unknown"
115
+
116
+
117
+ def skill_status(
118
+ harness: str, *, home: Path | None = None, codex_home: Path | None = None
119
+ ) -> SkillStatus:
120
+ """Classify one harness's installed skill. Never raises."""
121
+ # Resolved by the installer's own function, never re-derived here: an independent default
122
+ # is how CODEX_HOME got ignored at runtime while being honoured at install time.
123
+ home, codex_home = resolve_homes(home, codex_home)
124
+ dest = _dest(harness, home, codex_home)
125
+ try:
126
+ if not dest.is_dir():
127
+ return SkillStatus(harness, "absent", dest)
128
+ return SkillStatus(harness, _classify(dest, _bundle(harness), _read_manifest(dest)), dest)
129
+ except Exception: # noqa: BLE001
130
+ # Same contract as the update check: a skill-drift notice must never be able to fail a
131
+ # run. An unreadable destination or a missing packaged bundle reads as "nothing to say".
132
+ return SkillStatus(harness, "absent", dest)
133
+
134
+
135
+ def stale_skills(*, home: Path | None = None, codex_home: Path | None = None) -> list[SkillStatus]:
136
+ """Every installed harness whose skill is out of date or of unproven origin.
137
+
138
+ ``absent``/``in_sync``/``modified`` are all silent — the first two have nothing to say, and
139
+ the third is the operator's own work.
140
+ """
141
+ found = [skill_status(h, home=home, codex_home=codex_home) for h in HARNESSES]
142
+ return [s for s in found if s.status in ("stale", "unknown")]
@@ -0,0 +1,226 @@
1
+ """``syncade --update`` — PR-h-field-07 item 5.
2
+
3
+ **A process cannot upgrade itself.** Its modules are already imported; if the files change
4
+ underneath it, anything imported LATER comes back new while everything already loaded stays old.
5
+ A half-and-half process is worse than either version, so this upgrades and then EXITS, asking to
6
+ be re-run. `os.execv` was considered and rejected: replacing an executable mid-replacement, plus
7
+ argv preservation and loop-guarding, for a cosmetic gain. In a terminal the re-run is one
8
+ keystroke; in a harness the skill does it, which is what makes it seamless from the chat side.
9
+
10
+ **Install-method detection PROVES or REFUSES; it never guesses.** The method is read from where
11
+ ``syncade`` actually lives — a marker file inside its own install tree — not from a table of
12
+ default paths, which vary with ``UV_TOOL_DIR``, ``PIPX_HOME``, XDG settings and platform. When no
13
+ marker is found, this prints the manual command and says plainly that it could not tell. That is
14
+ the PR-h-04.5 shape one layer up: refuse rather than act on an unproven belief.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import subprocess
20
+ import sys
21
+ from dataclasses import dataclass
22
+ from pathlib import Path
23
+
24
+ import syncade
25
+ from syncade.exit_codes import SUCCESS, WORKTREE_ERROR
26
+ from syncade.process import SubprocessError, run_subprocess
27
+
28
+ _UPGRADE_TIMEOUT = 300.0
29
+ #: How far up from ``syncade/__init__.py`` an install marker can sit. A venv layout is
30
+ #: ``<root>/lib/python3.x/site-packages/syncade/__init__.py`` — five parents to the root; the
31
+ #: extra headroom costs nothing and covers layouts that nest one deeper.
32
+ _MARKER_DEPTH = 7
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class InstallMethod:
37
+ kind: str # "uv" | "pipx" | "source" | "unknown"
38
+ command: list[str] | None # the upgrade to run, or None when we must not run anything
39
+ where: Path | None
40
+
41
+
42
+ def detect_install() -> InstallMethod:
43
+ """Prove how syncade got here by finding a marker inside its own install tree."""
44
+ start = Path(syncade.__file__).resolve().parent
45
+ for parent in [start, *start.parents][:_MARKER_DEPTH]:
46
+ if (parent / "uv-receipt.toml").is_file():
47
+ return InstallMethod("uv", ["uv", "tool", "upgrade", "syncade"], parent)
48
+ if (parent / "pipx_metadata.json").is_file():
49
+ return InstallMethod("pipx", ["pipx", "upgrade", "syncade"], parent)
50
+ # An ENVIRONMENT ends the walk before any enclosing checkout can claim it. A plain
51
+ # venv install commonly lives at `<checkout>/.venv/lib/pythonX/site-packages/syncade`,
52
+ # and without this the ancestor `pyproject.toml` made it read as a source checkout — so
53
+ # `--update` refused with "use git", which would not update the installed package at
54
+ # all. Checked AFTER the uv/pipx markers, which sit at this same root and are more
55
+ # specific; reaching here means the environment exists but its manager is unproven,
56
+ # which is exactly what "unknown" is for.
57
+ if (parent / "pyvenv.cfg").is_file():
58
+ return InstallMethod("unknown", None, parent)
59
+ # A checkout is the one case where upgrading would be actively wrong: the operator's
60
+ # own working tree is the install, and `uv tool upgrade` would neither touch it nor
61
+ # tell them why nothing changed. Detected by the project file naming THIS project, so a
62
+ # coincidental pyproject.toml higher up cannot claim it.
63
+ pyproject = parent / "pyproject.toml"
64
+ if pyproject.is_file() and 'name = "syncade"' in _read(pyproject):
65
+ return InstallMethod("source", None, parent)
66
+ return InstallMethod("unknown", None, None)
67
+
68
+
69
+ def _read(path: Path) -> str:
70
+ try:
71
+ return path.read_text(encoding="utf-8", errors="replace")
72
+ except OSError:
73
+ return ""
74
+
75
+
76
+ def live_run(cwd: Path) -> Path | None:
77
+ """A run in ``cwd`` that is still going, or ``None``.
78
+
79
+ Upgrading underneath a running review is the same half-and-half hazard from outside the
80
+ process: the reviewers and producer it has yet to spawn would load the new code while the
81
+ orchestrator that planned them is the old one. Scoped to this repo's ``.syncade/runs/``,
82
+ which is where the breadcrumb lives; a run in some other checkout is not visible here and
83
+ the message says so rather than implying a global guarantee.
84
+ """
85
+ import json
86
+
87
+ from syncade.run_status import STATUS_FILENAME, is_stale_running
88
+
89
+ runs = cwd / ".syncade" / "runs"
90
+ if not runs.is_dir():
91
+ return None
92
+ for status_path in sorted(runs.glob(f"*/{STATUS_FILENAME}")):
93
+ try:
94
+ status = json.loads(status_path.read_text(encoding="utf-8"))
95
+ except (OSError, ValueError):
96
+ continue
97
+ # `is_stale_running` is true for a HARD-KILLED run, i.e. running-but-dead. A live run is
98
+ # running and NOT stale — reusing the existing predicate rather than re-deriving
99
+ # liveness, so the two cannot drift apart on what "still going" means.
100
+ if isinstance(status, dict) and status.get("state") == "running":
101
+ if not is_stale_running(status):
102
+ return status_path.parent
103
+ return None
104
+
105
+
106
+ def _syncade_argv() -> list[str]:
107
+ # Use sys.executable rather than shutil.which("syncade"): the latter resolves against
108
+ # ambient PATH and would invoke a shadow when the operator ran syncade by absolute path
109
+ # or from a venv while a different syncade is earlier on PATH. sys.executable is the
110
+ # interpreter of the upgraded install, so the subprocess always runs the right package.
111
+ return [sys.executable, "-m", "syncade"]
112
+
113
+
114
+ def _resync_skills(out) -> bool:
115
+ """Re-install every skill the operator ALREADY has, using the UPGRADED code.
116
+
117
+ A subprocess, not an in-process call: this process is still running the OLD package, so
118
+ installing from here would faithfully write the OLD skill files — the exact drift the
119
+ upgrade is meant to close. Harnesses with no skill installed are skipped rather than gaining
120
+ one they never asked for.
121
+
122
+ Returns True when every installed skill was successfully re-synced, False when at least one
123
+ was refused (so the caller can propagate the partial failure rather than claiming success).
124
+ """
125
+ from syncade.cli.skill_status import HARNESSES, skill_status
126
+
127
+ all_ok = True
128
+ for harness in HARNESSES:
129
+ if skill_status(harness).status == "absent":
130
+ continue
131
+ result = subprocess.run( # noqa: S603
132
+ [*_syncade_argv(), "--install-skill", harness],
133
+ capture_output=True,
134
+ text=True,
135
+ )
136
+ if result.returncode == SUCCESS:
137
+ print(f"[syncade] re-installed the {harness} skill", file=out)
138
+ else:
139
+ all_ok = False
140
+ # `--install-skill` refuses rather than destroying — a non-zero here usually means
141
+ # the operator edited their copy or the skill has no install record (unknown state).
142
+ # Tell them the exact command to force a resync with their explicit consent.
143
+ print(
144
+ f"[syncade] the {harness} skill was NOT re-installed — syncade refused rather "
145
+ f"than overwrite it. Run `syncade --install-skill {harness} --force-install` "
146
+ "to sync it regardless.",
147
+ file=out,
148
+ )
149
+ return all_ok
150
+
151
+
152
+ def run_update(*, cwd: Path | None = None, out=None) -> int:
153
+ """Upgrade syncade in place, then exit asking to be re-run."""
154
+ out = out or sys.stderr
155
+ cwd = cwd or Path.cwd()
156
+
157
+ # Resolve to the actual git repo root so live_run() does not miss a run when invoked
158
+ # from a subdirectory — `.syncade/runs/` is at the root, not at the invocation directory.
159
+ # Fails open: if discovery raises (not in a git repo, git not on PATH, etc.) we fall back
160
+ # to cwd and check there, which is correct behaviour for a non-repo invocation.
161
+ try:
162
+ from syncade.snapshot import discover_repo_root
163
+
164
+ cwd = discover_repo_root(cwd)
165
+ except Exception: # noqa: BLE001
166
+ pass
167
+
168
+ running = live_run(cwd)
169
+ if running is not None:
170
+ print(
171
+ f"[syncade] refusing to update: a run is still going ({running.name}).\n"
172
+ " Upgrading underneath it would leave the run half-old and half-new.\n"
173
+ " Wait for it to finish, then run `syncade --update` again.",
174
+ file=out,
175
+ )
176
+ return WORKTREE_ERROR
177
+
178
+ method = detect_install()
179
+ if method.kind == "source":
180
+ print(
181
+ f"[syncade] this syncade runs from a source checkout at {method.where}.\n"
182
+ " Update it with git, not a package manager.",
183
+ file=out,
184
+ )
185
+ return WORKTREE_ERROR
186
+ if method.command is None:
187
+ print(
188
+ "[syncade] could not determine how syncade was installed, so nothing was run.\n"
189
+ " If you used pip: pip install -U syncade",
190
+ file=out,
191
+ )
192
+ return WORKTREE_ERROR
193
+
194
+ print(f"[syncade] {' '.join(method.command)}", file=out)
195
+ # `SubprocessResult.returncode` — NOT `exit_code`, which does not exist. The first version
196
+ # read `exit_code` and every test passed, because the test double was hand-written with that
197
+ # attribute: the fake encoded the belief instead of the type, so the suite and the mutation
198
+ # run both validated the same wrong contract. A real upgrade would have raised AttributeError
199
+ # AFTER upgrading, losing the skill re-sync and the re-run instruction. The tests now build
200
+ # the real SubprocessResult, so a rename breaks them instead of hiding in them.
201
+ try:
202
+ result = run_subprocess(method.command, timeout=_UPGRADE_TIMEOUT)
203
+ except SubprocessError as exc:
204
+ # `uv`/`pipx` absent from PATH, or unlaunchable. Reported, not escaped: an operator who
205
+ # ran --update deserves to be told the package manager is missing.
206
+ print(f"[syncade] update failed: {exc}", file=out)
207
+ return WORKTREE_ERROR
208
+ if result.returncode != 0:
209
+ print(
210
+ f"[syncade] update failed (exit {result.returncode}). Nothing was changed.\n"
211
+ + (result.stderr.strip() or result.stdout.strip())[:800],
212
+ file=out,
213
+ )
214
+ return WORKTREE_ERROR
215
+
216
+ skills_ok = _resync_skills(out)
217
+ print(
218
+ "[syncade] updated. Re-run your command to use the new version — this process is\n"
219
+ " still running the old one and cannot switch to it.",
220
+ file=out,
221
+ )
222
+ if not skills_ok:
223
+ # The package upgrade succeeded but at least one skill resync was refused. The operator
224
+ # now has a fresh CLI and a stale or recordless skill — not a clean update end to end.
225
+ return WORKTREE_ERROR
226
+ return SUCCESS