syncade 0.7.0__tar.gz → 0.7.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {syncade-0.7.0/src/syncade.egg-info → syncade-0.7.3}/PKG-INFO +28 -4
- {syncade-0.7.0 → syncade-0.7.3}/README.md +27 -3
- {syncade-0.7.0 → syncade-0.7.3}/docs/config-reference.md +13 -6
- {syncade-0.7.0 → syncade-0.7.3}/pyproject.toml +1 -1
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/__init__.py +1 -1
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/parser.py +2 -2
- syncade-0.7.3/src/syncade/cli/update_mode.py +473 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/config.py +3 -2
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/config_update.py +6 -5
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/doctor.py +11 -3
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/doctor_env.py +47 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/update_check.py +38 -6
- {syncade-0.7.0 → syncade-0.7.3/src/syncade.egg-info}/PKG-INFO +28 -4
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade.egg-info/SOURCES.txt +1 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_documented_flags_exist.py +8 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_update_mode.py +161 -3
- syncade-0.7.3/tests/cli/test_update_post_condition.py +656 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/conftest.py +16 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/doctor/conftest.py +10 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_update_check.py +10 -0
- syncade-0.7.0/src/syncade/cli/update_mode.py +0 -226
- {syncade-0.7.0 → syncade-0.7.3}/LICENSE +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/MANIFEST.in +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/docs/how-to-use.md +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/setup.cfg +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/__main__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/adapters/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/adapters/anthropic.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/adapters/base.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/adapters/fake.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/adapters/fake_common.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/adapters/fake_producer_audit_draft.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/adapters/fake_reviewer_synth.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/adapters/openai.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/adapters/openai_parsing.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/adapters/producer.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/adapters/producer_anthropic.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/adapters/producer_openai.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/adapters/registry.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/auth_check.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/auth_preflight.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/base_resolution.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/billing.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/checks_config.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/auth_gate.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/config_keys.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/config_list.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/config_menu_rows.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/config_mode.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/config_overrides.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/config_tui.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/doctor_mode.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/gc_mode.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/install_skill.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/metrics_mode.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/modes.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/parser_types.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/paths.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/preflight_paths.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/resolve.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/resume_mode.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/skill_status.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/toml_writer.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/update_notice.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/cli/validate.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/config_auth.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/config_cold.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/config_gc.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/config_loader.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/config_loop.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/config_producer.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/config_retry.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/config_types.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/diff_filter.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/dispatcher.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/doctor_preview.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/doctor_types.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/exit_codes.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/findings.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/findings_json.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/gc.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/gc_execute.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/gc_protection.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/gc_types.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/gc_worktrees.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/git_object_id.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/git_preconditions.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/logging.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/metrics/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/metrics/aggregate.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/metrics/findings.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/metrics/schema.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/_runs_dir.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/branch_advance.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/branch_guard.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/budget.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/escalation_coverage.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/loop.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/loop_dispatch_check.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/loop_finalize.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/loop_preflight.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/loop_resume.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/loop_rmtree.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/loop_round_step.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/prior_round.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/producer_phase.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/results.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/resume.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/resume_load.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/resume_plan.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/resume_target.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/resume_types.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/reviewer_template_failure.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/round.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/round_checks.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/round_no_changes.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/round_predispatch.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/orchestrator/verdict.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/_atomic.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/_clusters.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/_findings_verdict.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/_markdown.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/_validation.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/checks.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/decision_needed.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/findings_md.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/handoff.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/handoff_classify.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/last_reviewed.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/loop_manifest.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/loop_summary.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/loop_summary_text.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/producer.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/reviewer.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/round_manifest.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/run_init.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/run_summary.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/run_summary_next_steps.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/synth.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/persistence/test_run.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/presets.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/pricing_config.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/process.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/producer.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/producer_attempt.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/producer_escalation.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/producer_git.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/producer_result.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/prompts.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/prompts_loader.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/retry.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/run_inputs.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/run_status.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/selfcheck.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/skills/claude/README.md +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/skills/claude/SKILL.md +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/skills/codex/README.md +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/skills/codex/SKILL.md +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/snapshot.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/spec_audit.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/spec_audit_schema.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/spec_draft.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/spec_source.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/synthesis.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/synthesis_clusters.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/synthesis_repair.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/synthesis_schema.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/synthesizer/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/synthesizer/constants.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/synthesizer/driver.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/synthesizer/rendering.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/synthesizer/result.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/synthesizer/validation.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/synthesizer/workspace.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/templates/presets/balanced.toml +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/templates/presets/cheap.toml +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/templates/presets/thorough.toml +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/templates/producer.md +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/templates/reviewer.md +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/templates/reviewer_adversarial.md +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/templates/reviewer_codex.md +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/templates/spec_audit.md +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/templates/spec_draft.md +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/templates/synthesizer.md +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/test_runner.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/test_runner_classify.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/transcript.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/usage.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/worktree.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/worktree_env.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade/worktree_paths.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade.egg-info/dependency_links.txt +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade.egg-info/entry_points.txt +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade.egg-info/requires.txt +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/src/syncade.egg-info/top_level.txt +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/adapters/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/adapters/_anthropic_helpers.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/adapters/_openai_helpers.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/adapters/test_anthropic.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/adapters/test_anthropic_more.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/adapters/test_openai.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/adapters/test_openai_more.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/adapters/test_producer.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/adapters/test_producer_more.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/adapters/test_prompt_transport.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/auth_check/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/auth_check/test_probes.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/auth_check/test_run_auth_check.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/cli/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/cli/_helpers.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/cli/_undo_helpers.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_basic_and_mutex.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_cli_library_divergence.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_config_keys.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_config_list.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_config_menu_rows.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_config_mode.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_config_overrides.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_config_tui.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_config_tui_pty.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_documented_exit_codes.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_flags.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_gc.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_help_retention_contract.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_mode_exclusivity_matrix.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_mode_flag_guards.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_resume_reports_hard_kill.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_run_and_config.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_run_status_cli.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_scope_openspec_draft.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_toml_writer.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_undo_auto_init.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_undo_scope.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_update_notice.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/cli/test_validate_before_mutate.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/config/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/config/test_auth_enforcement.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/config/test_auth_modes.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/config/test_auth_preflight.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/config/test_auth_report_scope.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/config/test_budget_config.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/config/test_cold_actors.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/config/test_config_checks.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/config/test_config_reference_drift.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/config/test_config_schema.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/config/test_gc_config.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/config/test_global_config_layer.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/config/test_presets.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/config/test_retry_config.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/config/test_reviewer_config.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/config/test_reviewer_permissions.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/config/test_worktree_base_config.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/dispatcher/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/dispatcher/_helpers.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/dispatcher/test_dispatch_core.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/dispatcher/test_dispatch_misc.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/dispatcher/test_dispatch_usage.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/doctor/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/doctor/_helpers.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/doctor/test_doctor.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/doctor/test_doctor_diff_size.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/doctor/test_doctor_malformed_diff.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/doctor/test_doctor_preview.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/findings/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/findings/_helpers.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/findings/test_parser_and_fields.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/findings/test_parser_resilience.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/findings/test_verdict_block_selection.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/gc/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/gc/_helpers.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/gc/test_autoprune.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/gc/test_execute.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/gc/test_plan.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/gc/test_plan_execution_races.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/gc/test_reaping_scope.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/gc/test_retention_tiers.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/gc/test_robustness.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/gc/test_self_orphan_protection.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/gc/test_smoke.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/gc/test_two_tier_retention.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/gc/test_worktree_age_bound.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/logging/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/logging/_helpers.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/logging/test_logging.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/logging/test_logging_summary.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/metrics/conftest.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/metrics/test_actor_spend.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/metrics/test_aggregate.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/metrics/test_billing_reconciliation.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/metrics/test_billing_surfaces_agree.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/metrics/test_budget_metrics.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/metrics/test_consensus_report.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/metrics/test_cost_honesty.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/metrics/test_findings_capture.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/metrics/test_metrics_cli.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/metrics/test_metrics_mode.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/metrics/test_schema.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/metrics/test_unknown_panel.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/_helpers.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/_resume_fixtures.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/conftest.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_budget.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_budget_resume_e2e.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_budget_stop_message.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_budget_warning.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_checks_and_recording.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_contracts.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_cross_round.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_cross_round_more.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_default_branch_guard.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_diff_size_cap.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_dispatch_record.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_empty_diff.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_empty_diff_no_side_effects.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_escalation.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_last_reviewed_truthful.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_loop_audit_remediation.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_matrix_and_e2e.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_multiround.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_multiround_dirty.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_multiround_more.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_paraphrase_escalation.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_pipeline.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_provider_usage_limit.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_rename_boundary.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_resume.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_resume_base_oid.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_reviewer_blindness_is_structural.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_round_input_boundaries.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_run_layout.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_run_status_wiring.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_safety_warning_survives_quiet.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_snapshot_and_base.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_synth_phase.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_synth_phase_more.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_synth_provenance.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_synth_validation.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_template_robustness.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_termination_reason_drift.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_testleg.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_timeout_is_per_subprocess.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_usage_limit_message.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/orchestrator/test_worktree_cleanup.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/_helpers.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/test_atomic_writes.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/test_checks_surfacing.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/test_diff_size_recording.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/test_findings_md.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/test_findings_md_more.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/test_findings_md_verdict.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/test_handoff.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/test_loop_manifest_checks.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/test_loop_summary.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/test_loop_summary_more.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/test_md_safety.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/test_next_steps.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/test_producer_persist.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/test_reviewer.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/test_round_manifest.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/test_run_init.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/test_run_summary.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/test_run_summary_more.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/test_run_summary_producer.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/test_synth_persist.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/persistence/test_test_run.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/producer/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/producer/_helpers.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/producer/test_producer.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/producer/test_producer_confinement.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/producer/test_producer_retry.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/producer/test_producer_run.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/producer/test_producer_timeout.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/producer/test_producer_usage.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/prompts/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/prompts/test_load_template.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/prompts/test_misc.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/prompts/test_producer.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/prompts/test_render_synthesizer.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/prompts/test_synthesizer.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/resume/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/resume/_helpers.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/resume/test_budget_resume.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/resume/test_checks_phase_failure.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/resume/test_cli_malformed_rehydration.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/resume/test_find_resolve_plan.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/resume/test_plan_resume.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/resume/test_round_trip.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/resume/test_tree_drift_and_escalation.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/skills/test_config_section.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/skills/test_install_skill.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/skills/test_install_skill_multi_harness.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/skills/test_install_skill_ownership.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/skills/test_install_skill_refuses.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/skills/test_skill_drift.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/skills/test_skill_status.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/smoke/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/smoke/test_anthropic_smoke.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/smoke/test_auth_check_smoke.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/smoke/test_codex_smoke.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/smoke/test_loop_smoke.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/smoke/test_orchestrator_smoke.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/smoke/test_orchestrator_smoke_more.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/smoke/test_producer_smoke.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/smoke/test_resume_reap_smoke.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/smoke/test_selfcheck_smoke.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/smoke/test_synthesizer_smoke.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/smoke/test_test_runner_smoke.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/snapshot/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/snapshot/_helpers.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/snapshot/conftest.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/snapshot/test_snapshot.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/snapshot/test_snapshot_diff_identity.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/snapshot/test_snapshot_dirty.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/snapshot/test_snapshot_warning.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/snapshot/test_three_dot_diff.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/synthesis/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/synthesis/_helpers.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/synthesis/test_cluster_repair.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/synthesis/test_clusters.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/synthesis/test_models.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/synthesis/test_models_distinct_keying.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/synthesis/test_parsing.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/synthesis/test_unanimous_blocker_downgrade.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/synthesizer/test_provenance_fabrication.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/synthesizer/test_provenance_repair.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_adapter_registry.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_adapters_fake.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_auto_init_policy.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_base_resolution.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_check_loc_script.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_checks.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_config_loader.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_cross_model_claim.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_diff_filter.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_diff_filter_binary.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_diff_filter_quoted_paths.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_escalation_coverage.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_exit_codes.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_git_choke_point.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_git_config_isolation.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_git_preconditions.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_last_reviewed.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_parse_repair_extra_keys.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_pricing.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_private_identifier_gate.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_process.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_process_teardown.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_producer_escalation.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_regression_acme.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_retry.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_run_status.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_run_status_signals.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_runner/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_runner/test_test_runner.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_runner/test_test_runner_more.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_safety_disclosures_survive_quiet.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_selfcheck.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_spec_audit.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_spec_draft.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_spec_source.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_streamed_capture.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_strip_list_semantics.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_synthesizer.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_synthesizer_passthrough.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_synthesizer_provenance.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_transcript.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_usage.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_usage_persistence.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_workflow_action_pins.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_worktree_base_isolation.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_worktree_env.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/test_worktree_strip_guard.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/worktree/__init__.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/worktree/_helpers.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/worktree/conftest.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/worktree/test_worktree.py +0 -0
- {syncade-0.7.0 → syncade-0.7.3}/tests/worktree/test_worktree_cleanup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: syncade
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.3
|
|
4
4
|
Summary: External blind multi-judge review orchestrator for AI-assisted coding
|
|
5
5
|
Author-email: Syncade <build@syncade.ai>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -123,11 +123,35 @@ syncade --update # upgrade, then re-run your command
|
|
|
123
123
|
```
|
|
124
124
|
|
|
125
125
|
`--update` works out how syncade was installed by looking for a marker inside its own install
|
|
126
|
-
tree
|
|
127
|
-
instead. It also refuses while a review is still running, and declines to touch a source
|
|
126
|
+
tree — `uv`, `pipx`, or the `INSTALLER` record pip leaves — and **refuses rather than guessing**
|
|
127
|
+
if no marker proves one, printing the manual command instead. It also refuses while a review is still running, and declines to touch a source
|
|
128
128
|
checkout. A running process cannot switch to the version it just installed, so it exits and asks
|
|
129
129
|
you to re-run.
|
|
130
130
|
|
|
131
|
+
> **Installed before 0.6.3? You will never be told about updates.** The check itself arrived in
|
|
132
|
+
> **0.6.3**; every release before it — `0.1.0` through `0.6.2` — has no such code, so it cannot
|
|
133
|
+
> announce anything and `--update` is not a flag it knows. There is no way for us to reach those
|
|
134
|
+
> installs, which is why this paragraph exists. Check with `syncade --version`, and if it is below
|
|
135
|
+
> `0.6.3`, upgrade once by hand:
|
|
136
|
+
>
|
|
137
|
+
> ```bash
|
|
138
|
+
> uv tool install --force syncade # if you installed with uv
|
|
139
|
+
> pipx upgrade syncade # if you installed with pipx
|
|
140
|
+
> python3 -m pip install --user -U syncade # if you installed with pip --user
|
|
141
|
+
> python3 -m pip install -U syncade # if you installed with pip
|
|
142
|
+
> ```
|
|
143
|
+
>
|
|
144
|
+
> **Check `syncade --version` afterwards either way** — do not trust the success message. Each
|
|
145
|
+
> of these can report success while changing nothing: `pipx` refuses a package you have
|
|
146
|
+
> `pipx pin`ned, `pip` may target a different interpreter than the one `syncade` resolves to,
|
|
147
|
+
> and on `0.6.3`/`0.7.0` `--update` itself could claim success having installed nothing (fixed
|
|
148
|
+
> in `0.7.1`). The `uv` line is the exception: it upgrades *and* clears any pin.
|
|
149
|
+
>
|
|
150
|
+
> From `0.6.3` on syncade tells you about updates itself — **when it can reach the manifest**.
|
|
151
|
+
> That check is silent on failure by design, so a machine that cannot reach it looks exactly
|
|
152
|
+
> like a machine that is up to date. If you want certainty, compare `syncade --version` against
|
|
153
|
+
> the [releases page](https://github.com/syncade-ai/syncade-ai/releases).
|
|
154
|
+
|
|
131
155
|
**Requirements**
|
|
132
156
|
- **Python 3.11+**, on **macOS or Linux** (on Windows, use WSL).
|
|
133
157
|
- **`codex`** (OpenAI), on your `PATH` and authenticated — required for the default
|
|
@@ -287,7 +311,7 @@ syncade runs AI coding-agent CLIs with **elevated tool access on your repo**:
|
|
|
287
311
|
- **Reviewers** run headless inside throwaway worktree copies under `/tmp/syncade/` (Codex reviewers are additionally sandboxed to theirs).
|
|
288
312
|
- **The producer** runs unsandboxed and **commits to your current branch** (syncade refuses the default branch by default).
|
|
289
313
|
- Full LLM transcripts — including source a reviewer read — are written to `.syncade/runs/` (gitignored, auto-pruned). **A secret in a file a reviewer opens lands there in plaintext.**
|
|
290
|
-
- syncade makes
|
|
314
|
+
- syncade makes a small number of network calls of its own: a **once-per-session** GET to its public update manifest at the first invocation in each terminal or harness window (suppressible via `[update] check = false`, or set `CI`). `syncade --update` and `syncade --doctor` check every time you run them — operator-requested, so not session-gated. **At most one manifest GET per invocation** either way: a single `syncade` process fetches once and shares it, so `--doctor` does not add a request on top of the session check. `syncade --update` also invokes your package manager (`uv tool upgrade`, `pipx upgrade`, or your own interpreter's `-m pip install -U`) — only when you run that flag. All other egress is the provider CLIs you already authenticated and the test/check commands you configure.
|
|
291
315
|
|
|
292
316
|
Running syncade is comparable to running the target repo's `Makefile` — it executes code
|
|
293
317
|
with your permissions. See **[SECURITY.md](SECURITY.md)** for the full threat model and how
|
|
@@ -93,11 +93,35 @@ syncade --update # upgrade, then re-run your command
|
|
|
93
93
|
```
|
|
94
94
|
|
|
95
95
|
`--update` works out how syncade was installed by looking for a marker inside its own install
|
|
96
|
-
tree
|
|
97
|
-
instead. It also refuses while a review is still running, and declines to touch a source
|
|
96
|
+
tree — `uv`, `pipx`, or the `INSTALLER` record pip leaves — and **refuses rather than guessing**
|
|
97
|
+
if no marker proves one, printing the manual command instead. It also refuses while a review is still running, and declines to touch a source
|
|
98
98
|
checkout. A running process cannot switch to the version it just installed, so it exits and asks
|
|
99
99
|
you to re-run.
|
|
100
100
|
|
|
101
|
+
> **Installed before 0.6.3? You will never be told about updates.** The check itself arrived in
|
|
102
|
+
> **0.6.3**; every release before it — `0.1.0` through `0.6.2` — has no such code, so it cannot
|
|
103
|
+
> announce anything and `--update` is not a flag it knows. There is no way for us to reach those
|
|
104
|
+
> installs, which is why this paragraph exists. Check with `syncade --version`, and if it is below
|
|
105
|
+
> `0.6.3`, upgrade once by hand:
|
|
106
|
+
>
|
|
107
|
+
> ```bash
|
|
108
|
+
> uv tool install --force syncade # if you installed with uv
|
|
109
|
+
> pipx upgrade syncade # if you installed with pipx
|
|
110
|
+
> python3 -m pip install --user -U syncade # if you installed with pip --user
|
|
111
|
+
> python3 -m pip install -U syncade # if you installed with pip
|
|
112
|
+
> ```
|
|
113
|
+
>
|
|
114
|
+
> **Check `syncade --version` afterwards either way** — do not trust the success message. Each
|
|
115
|
+
> of these can report success while changing nothing: `pipx` refuses a package you have
|
|
116
|
+
> `pipx pin`ned, `pip` may target a different interpreter than the one `syncade` resolves to,
|
|
117
|
+
> and on `0.6.3`/`0.7.0` `--update` itself could claim success having installed nothing (fixed
|
|
118
|
+
> in `0.7.1`). The `uv` line is the exception: it upgrades *and* clears any pin.
|
|
119
|
+
>
|
|
120
|
+
> From `0.6.3` on syncade tells you about updates itself — **when it can reach the manifest**.
|
|
121
|
+
> That check is silent on failure by design, so a machine that cannot reach it looks exactly
|
|
122
|
+
> like a machine that is up to date. If you want certainty, compare `syncade --version` against
|
|
123
|
+
> the [releases page](https://github.com/syncade-ai/syncade-ai/releases).
|
|
124
|
+
|
|
101
125
|
**Requirements**
|
|
102
126
|
- **Python 3.11+**, on **macOS or Linux** (on Windows, use WSL).
|
|
103
127
|
- **`codex`** (OpenAI), on your `PATH` and authenticated — required for the default
|
|
@@ -257,7 +281,7 @@ syncade runs AI coding-agent CLIs with **elevated tool access on your repo**:
|
|
|
257
281
|
- **Reviewers** run headless inside throwaway worktree copies under `/tmp/syncade/` (Codex reviewers are additionally sandboxed to theirs).
|
|
258
282
|
- **The producer** runs unsandboxed and **commits to your current branch** (syncade refuses the default branch by default).
|
|
259
283
|
- Full LLM transcripts — including source a reviewer read — are written to `.syncade/runs/` (gitignored, auto-pruned). **A secret in a file a reviewer opens lands there in plaintext.**
|
|
260
|
-
- syncade makes
|
|
284
|
+
- syncade makes a small number of network calls of its own: a **once-per-session** GET to its public update manifest at the first invocation in each terminal or harness window (suppressible via `[update] check = false`, or set `CI`). `syncade --update` and `syncade --doctor` check every time you run them — operator-requested, so not session-gated. **At most one manifest GET per invocation** either way: a single `syncade` process fetches once and shares it, so `--doctor` does not add a request on top of the session check. `syncade --update` also invokes your package manager (`uv tool upgrade`, `pipx upgrade`, or your own interpreter's `-m pip install -U`) — only when you run that flag. All other egress is the provider CLIs you already authenticated and the test/check commands you configure.
|
|
261
285
|
|
|
262
286
|
Running syncade is comparable to running the target repo's `Makefile` — it executes code
|
|
263
287
|
with your permissions. See **[SECURITY.md](SECURITY.md)** for the full threat model and how
|
|
@@ -99,7 +99,7 @@ below; `worktree_base` is the one top-level scalar.
|
|
|
99
99
|
| `review` | `[review]` table | — | What reviewers see. See [`[review]`](#review--reviewconfig). |
|
|
100
100
|
| `retry` | `[retry]` table | — | Transient-error retry bound. See [`[retry]`](#retry--retryconfig). |
|
|
101
101
|
| `gc` | `[gc]` table | — | Run-artifact retention. See [`[gc]`](#gc--gcconfig). |
|
|
102
|
-
| `update` | `[update]` table | — |
|
|
102
|
+
| `update` | `[update]` table | — | Background update check and opt-out. See [`[update]`](#update--updateconfig). |
|
|
103
103
|
| `worktree_base` | path | `/tmp/syncade` | Base dir for per-run git worktrees. Overridable with `--worktree-base`; point it at a fast local disk if `/tmp` is small or slow. |
|
|
104
104
|
| `checks` | `[[checks]]` list | none | Mechanical exit-code gates. See [`[[checks]]`](#checks--checkconfig). |
|
|
105
105
|
| `pricing` | `[pricing]` table | packaged price table | Per-model token pricing for cost estimation. See [`[pricing]`](#pricing--pricingconfig). |
|
|
@@ -237,10 +237,17 @@ the first two; the worktree bound is config-only (set it with
|
|
|
237
237
|
|
|
238
238
|
## `[update]` — `UpdateConfig`
|
|
239
239
|
|
|
240
|
-
Whether syncade checks for a newer release.
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
240
|
+
Whether syncade checks for a newer release. Nothing about your repo, diff, run, or identity is
|
|
241
|
+
sent. The check has two distinct modes with different behaviors:
|
|
242
|
+
|
|
243
|
+
**Background check** (automatic, at first invocation): fires at most once per session window,
|
|
244
|
+
only ever **prints** — it cannot block a run, change an exit code, or fail one. Every error
|
|
245
|
+
(offline, timeout, bad JSON) is silent. Skipped entirely when `CI` is set.
|
|
246
|
+
|
|
247
|
+
**Explicit check** (`--doctor` and `--update`): each makes its own manifest fetch (one per
|
|
248
|
+
process, independent of the background check). `--doctor` displays the result as a row in the
|
|
249
|
+
preflight table and exits 60 if the manifest is unreachable. `--update` surfaces an unreachable
|
|
250
|
+
manifest as an explicit message. Both are also skipped when `check = false` or `CI` is set.
|
|
244
251
|
|
|
245
252
|
The advisory source is deliberately not configurable — a repo-local file that could repoint or
|
|
246
253
|
silence a security notice would be a hole, not a feature.
|
|
@@ -248,7 +255,7 @@ silence a security notice would be a hole, not a feature.
|
|
|
248
255
|
<!-- config-fields: UpdateConfig -->
|
|
249
256
|
| Field | Type | Default | What it does |
|
|
250
257
|
|---|---|---|---|
|
|
251
|
-
| `check` | bool | `true` |
|
|
258
|
+
| `check` | bool | `true` | Enable update checks. Set `false` to suppress all manifest fetches — background, `--doctor`, and `--update`. Also skipped whenever `CI` is set. At most one fetch per invocation: the background check is once per session, `--doctor` and `--update` check whenever you run them, and a single process never fetches twice. |
|
|
252
259
|
|
|
253
260
|
## `[[checks]]` — `CheckConfig`
|
|
254
261
|
|
|
@@ -310,8 +310,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
310
310
|
"--update",
|
|
311
311
|
action="store_true",
|
|
312
312
|
help="Upgrade syncade to the latest published release, then exit. Detects how syncade "
|
|
313
|
-
"was installed by finding a marker inside its own install tree (uv tool / pipx)
|
|
314
|
-
"REFUSES rather than guessing when it cannot tell, printing the manual pip command "
|
|
313
|
+
"was installed by finding a marker inside its own install tree (uv tool / pipx / pip) "
|
|
314
|
+
"and REFUSES rather than guessing when it cannot tell, printing the manual pip command "
|
|
315
315
|
"instead. Refuses while a run is still going. Re-installs any skill you already have, "
|
|
316
316
|
"using the upgraded code. A running process cannot switch to the new version, so this "
|
|
317
317
|
"exits and asks you to re-run your command.",
|
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
"""``syncade --update`` — PR-h-field-07 item 5.
|
|
2
|
+
|
|
3
|
+
**A process cannot upgrade itself.** Its modules are already imported; if the files change
|
|
4
|
+
underneath it, anything imported LATER comes back new while everything already loaded stays old.
|
|
5
|
+
A half-and-half process is worse than either version, so this upgrades and then EXITS, asking to
|
|
6
|
+
be re-run. `os.execv` was considered and rejected: replacing an executable mid-replacement, plus
|
|
7
|
+
argv preservation and loop-guarding, for a cosmetic gain. In a terminal the re-run is one
|
|
8
|
+
keystroke; in a harness the skill does it, which is what makes it seamless from the chat side.
|
|
9
|
+
|
|
10
|
+
**Install-method detection PROVES or REFUSES; it never guesses.** The method is read from where
|
|
11
|
+
``syncade`` actually lives — a marker file inside its own install tree — not from a table of
|
|
12
|
+
default paths, which vary with ``UV_TOOL_DIR``, ``PIPX_HOME``, XDG settings and platform. When no
|
|
13
|
+
marker is found, this prints the manual command and says plainly that it could not tell. That is
|
|
14
|
+
the PR-h-04.5 shape one layer up: refuse rather than act on an unproven belief.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
import site
|
|
21
|
+
import subprocess
|
|
22
|
+
import sys
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
import syncade
|
|
27
|
+
from syncade.exit_codes import SUCCESS, WORKTREE_ERROR
|
|
28
|
+
from syncade.process import SubprocessError, run_subprocess
|
|
29
|
+
from syncade.update_check import _parse, _text, manifest_once
|
|
30
|
+
|
|
31
|
+
_UPGRADE_TIMEOUT = 300.0
|
|
32
|
+
#: Reading one version string out of a fresh interpreter. Generous, because a cold venv
|
|
33
|
+
#: import is slower than it looks; short, because we already hold the operator's terminal.
|
|
34
|
+
_VERSION_TIMEOUT = 60.0
|
|
35
|
+
#: How far up from ``syncade/__init__.py`` an install marker can sit. A venv layout is
|
|
36
|
+
#: ``<root>/lib/python3.x/site-packages/syncade/__init__.py`` — five parents to the root; the
|
|
37
|
+
#: extra headroom costs nothing and covers layouts that nest one deeper.
|
|
38
|
+
_MARKER_DEPTH = 7
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class InstallMethod:
|
|
43
|
+
kind: str # "uv" | "pipx" | "pip" | "source" | "unknown"
|
|
44
|
+
command: list[str] | None # the upgrade to run, or None when we must not run anything
|
|
45
|
+
where: Path | None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _in_user_site() -> bool:
|
|
49
|
+
"""Whether THIS syncade lives in the per-user site directory.
|
|
50
|
+
|
|
51
|
+
Decides one thing: whether the pip upgrade needs ``--user``. A user-site install requires it
|
|
52
|
+
and a system-site install rejects it, so guessing either way breaks half the population. The
|
|
53
|
+
answer is derivable from paths we already hold, so it is derived rather than assumed.
|
|
54
|
+
"""
|
|
55
|
+
try:
|
|
56
|
+
user_site = Path(site.getusersitepackages()).resolve()
|
|
57
|
+
except (AttributeError, OSError): # no user site on this interpreter
|
|
58
|
+
return False
|
|
59
|
+
return user_site in Path(syncade.__file__).resolve().parents
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _read_scoped_installer() -> str:
|
|
63
|
+
"""Read INSTALLER from the dist-info directory that BELONGS to this syncade install.
|
|
64
|
+
|
|
65
|
+
Path-scoped to the site-packages directory containing ``syncade/__init__.py``, so an
|
|
66
|
+
unrelated dist-info for a different interpreter's syncade cannot prove this tree as pip.
|
|
67
|
+
Returns the installer name (e.g. ``"pip"``), or empty string when not found or ambiguous.
|
|
68
|
+
|
|
69
|
+
Contradictory duplicate dist-info directories (a broken packaging state) are refused
|
|
70
|
+
conservatively: if two ``syncade-*.dist-info`` entries disagree on INSTALLER, the result
|
|
71
|
+
is empty string — the same as not found. First-match would misclassify in that state,
|
|
72
|
+
which contradicts the PROVE-or-refuse posture the rest of this module maintains.
|
|
73
|
+
"""
|
|
74
|
+
site_packages = Path(syncade.__file__).resolve().parent.parent
|
|
75
|
+
found: list[str] = []
|
|
76
|
+
try:
|
|
77
|
+
for entry in site_packages.iterdir():
|
|
78
|
+
if (
|
|
79
|
+
entry.name.startswith("syncade-")
|
|
80
|
+
and entry.name.endswith(".dist-info")
|
|
81
|
+
and entry.is_dir()
|
|
82
|
+
):
|
|
83
|
+
try:
|
|
84
|
+
found.append((entry / "INSTALLER").read_text(encoding="utf-8").strip())
|
|
85
|
+
except OSError:
|
|
86
|
+
pass # No INSTALLER file — this dist-info cannot prove anything.
|
|
87
|
+
except OSError:
|
|
88
|
+
pass
|
|
89
|
+
if not found:
|
|
90
|
+
return ""
|
|
91
|
+
# Contradictory entries in a broken packaging state are not proof of anything.
|
|
92
|
+
return found[0] if len(set(found)) == 1 else ""
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _pip_or_unknown(where: Path | None) -> InstallMethod:
|
|
96
|
+
"""``pip`` when the distribution records pip as its installer, else ``unknown``.
|
|
97
|
+
|
|
98
|
+
Reached ONLY after the marker walk has failed to prove uv, pipx or a checkout. That ordering
|
|
99
|
+
is load-bearing, not tidiness: uv and pipx write ``dist-info/INSTALLER`` naming THEMSELVES, so
|
|
100
|
+
consulting it earlier would run pip against a uv tool. Their own markers are the specific ones
|
|
101
|
+
and they are checked first.
|
|
102
|
+
|
|
103
|
+
``INSTALLER`` is a marker file naming its writer, exactly like ``uv-receipt.toml`` — reading it
|
|
104
|
+
keeps this PROVING rather than guessing, which is the distinction the refusal exists to
|
|
105
|
+
protect. Before this, a plain pip install was ``unknown`` and ``--update`` refused: correct
|
|
106
|
+
under the rule, and wrong about the facts, because pip had left proof all along and syncade
|
|
107
|
+
never looked. Found by an operator whose real install was exactly this shape.
|
|
108
|
+
|
|
109
|
+
The lookup is PATH-SCOPED to the site-packages directory containing ``syncade/__init__.py``.
|
|
110
|
+
Reading by project name from ambient ``importlib.metadata`` would match an unrelated
|
|
111
|
+
dist-info (e.g. from a different interpreter's install) and classify a markerless tree as
|
|
112
|
+
pip when it is not — measured in a worktree where ``syncade.__file__`` was the reviewed
|
|
113
|
+
source while metadata pointed at a real installed dist-info.
|
|
114
|
+
|
|
115
|
+
``sys.executable -m pip``, never a bare ``pip`` from PATH — that resolves to whichever pip
|
|
116
|
+
comes first and would upgrade some other interpreter's syncade as often as this one's.
|
|
117
|
+
"""
|
|
118
|
+
installer = _read_scoped_installer()
|
|
119
|
+
if installer != "pip":
|
|
120
|
+
# Some other installer, or a name we do not recognise. Unproven is still unproven.
|
|
121
|
+
return InstallMethod("unknown", None, where)
|
|
122
|
+
user = ["--user"] if _in_user_site() else []
|
|
123
|
+
return InstallMethod(
|
|
124
|
+
"pip", [sys.executable, "-m", "pip", "install", "-U", *user, "syncade"], where
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def detect_install() -> InstallMethod:
|
|
129
|
+
"""Prove how syncade got here by finding a marker inside its own install tree."""
|
|
130
|
+
start = Path(syncade.__file__).resolve().parent
|
|
131
|
+
for parent in [start, *start.parents][:_MARKER_DEPTH]:
|
|
132
|
+
if (parent / "uv-receipt.toml").is_file():
|
|
133
|
+
return InstallMethod("uv", ["uv", "tool", "upgrade", "syncade"], parent)
|
|
134
|
+
if (parent / "pipx_metadata.json").is_file():
|
|
135
|
+
return InstallMethod("pipx", ["pipx", "upgrade", "syncade"], parent)
|
|
136
|
+
# An ENVIRONMENT ends the walk before any enclosing checkout can claim it. A plain
|
|
137
|
+
# venv install commonly lives at `<checkout>/.venv/lib/pythonX/site-packages/syncade`,
|
|
138
|
+
# and without this the ancestor `pyproject.toml` made it read as a source checkout — so
|
|
139
|
+
# `--update` refused with "use git", which would not update the installed package at
|
|
140
|
+
# all. Checked AFTER the uv/pipx markers, which sit at this same root and are more
|
|
141
|
+
# specific; reaching here means the environment exists but its manager is unproven,
|
|
142
|
+
# which is exactly what "unknown" is for.
|
|
143
|
+
if (parent / "pyvenv.cfg").is_file():
|
|
144
|
+
return _pip_or_unknown(parent)
|
|
145
|
+
# A checkout is the one case where upgrading would be actively wrong: the operator's
|
|
146
|
+
# own working tree is the install, and `uv tool upgrade` would neither touch it nor
|
|
147
|
+
# tell them why nothing changed. Detected by the project file naming THIS project, so a
|
|
148
|
+
# coincidental pyproject.toml higher up cannot claim it.
|
|
149
|
+
pyproject = parent / "pyproject.toml"
|
|
150
|
+
if pyproject.is_file() and 'name = "syncade"' in _read(pyproject):
|
|
151
|
+
return InstallMethod("source", None, parent)
|
|
152
|
+
return _pip_or_unknown(None)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _read(path: Path) -> str:
|
|
156
|
+
try:
|
|
157
|
+
return path.read_text(encoding="utf-8", errors="replace")
|
|
158
|
+
except OSError:
|
|
159
|
+
return ""
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def live_run(cwd: Path) -> Path | None:
|
|
163
|
+
"""A run in ``cwd`` that is still going, or ``None``.
|
|
164
|
+
|
|
165
|
+
Upgrading underneath a running review is the same half-and-half hazard from outside the
|
|
166
|
+
process: the reviewers and producer it has yet to spawn would load the new code while the
|
|
167
|
+
orchestrator that planned them is the old one. Scoped to this repo's ``.syncade/runs/``,
|
|
168
|
+
which is where the breadcrumb lives; a run in some other checkout is not visible here and
|
|
169
|
+
the message says so rather than implying a global guarantee.
|
|
170
|
+
"""
|
|
171
|
+
import json
|
|
172
|
+
|
|
173
|
+
from syncade.run_status import STATUS_FILENAME, is_stale_running
|
|
174
|
+
|
|
175
|
+
runs = cwd / ".syncade" / "runs"
|
|
176
|
+
if not runs.is_dir():
|
|
177
|
+
return None
|
|
178
|
+
for status_path in sorted(runs.glob(f"*/{STATUS_FILENAME}")):
|
|
179
|
+
try:
|
|
180
|
+
status = json.loads(status_path.read_text(encoding="utf-8"))
|
|
181
|
+
except (OSError, ValueError):
|
|
182
|
+
continue
|
|
183
|
+
# `is_stale_running` is true for a HARD-KILLED run, i.e. running-but-dead. A live run is
|
|
184
|
+
# running and NOT stale — reusing the existing predicate rather than re-deriving
|
|
185
|
+
# liveness, so the two cannot drift apart on what "still going" means.
|
|
186
|
+
if isinstance(status, dict) and status.get("state") == "running":
|
|
187
|
+
if not is_stale_running(status):
|
|
188
|
+
return status_path.parent
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _syncade_argv() -> list[str]:
|
|
193
|
+
# Use sys.executable rather than shutil.which("syncade"): the latter resolves against
|
|
194
|
+
# ambient PATH and would invoke a shadow when the operator ran syncade by absolute path
|
|
195
|
+
# or from a venv while a different syncade is earlier on PATH. sys.executable is the
|
|
196
|
+
# interpreter of the upgraded install, so the subprocess always runs the right package.
|
|
197
|
+
return [sys.executable, "-m", "syncade"]
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _resync_skills(out) -> bool:
|
|
201
|
+
"""Re-install every skill the operator ALREADY has, using the UPGRADED code.
|
|
202
|
+
|
|
203
|
+
A subprocess, not an in-process call: this process is still running the OLD package, so
|
|
204
|
+
installing from here would faithfully write the OLD skill files — the exact drift the
|
|
205
|
+
upgrade is meant to close. Harnesses with no skill installed are skipped rather than gaining
|
|
206
|
+
one they never asked for.
|
|
207
|
+
|
|
208
|
+
Returns True when every installed skill was successfully re-synced, False when at least one
|
|
209
|
+
was refused (so the caller can propagate the partial failure rather than claiming success).
|
|
210
|
+
"""
|
|
211
|
+
from syncade.cli.skill_status import HARNESSES, skill_status
|
|
212
|
+
|
|
213
|
+
all_ok = True
|
|
214
|
+
for harness in HARNESSES:
|
|
215
|
+
if skill_status(harness).status == "absent":
|
|
216
|
+
continue
|
|
217
|
+
result = subprocess.run( # noqa: S603
|
|
218
|
+
[*_syncade_argv(), "--install-skill", harness],
|
|
219
|
+
capture_output=True,
|
|
220
|
+
text=True,
|
|
221
|
+
)
|
|
222
|
+
if result.returncode == SUCCESS:
|
|
223
|
+
print(f"[syncade] re-installed the {harness} skill", file=out)
|
|
224
|
+
else:
|
|
225
|
+
all_ok = False
|
|
226
|
+
# `--install-skill` refuses rather than destroying — a non-zero here usually means
|
|
227
|
+
# the operator edited their copy or the skill has no install record (unknown state).
|
|
228
|
+
# Tell them the exact command to force a resync with their explicit consent.
|
|
229
|
+
print(
|
|
230
|
+
f"[syncade] the {harness} skill was NOT re-installed — syncade refused rather "
|
|
231
|
+
f"than overwrite it. Run `syncade --install-skill {harness} --force-install` "
|
|
232
|
+
"to sync it regardless.",
|
|
233
|
+
file=out,
|
|
234
|
+
)
|
|
235
|
+
return all_ok
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _installed_version() -> str | None:
|
|
239
|
+
"""The version on disk NOW, or ``None`` when it cannot be established.
|
|
240
|
+
|
|
241
|
+
It CANNOT be ``syncade.__version__``: this process imported that before the upgrade ran, so
|
|
242
|
+
it is by definition the old number. A fresh interpreter re-reads the metadata the upgrade
|
|
243
|
+
just rewrote, and ``sys.executable`` is the tool venv's own python under both uv and pipx —
|
|
244
|
+
precisely the environment that changed.
|
|
245
|
+
|
|
246
|
+
``None`` means "could not verify", which is deliberately NOT the same as "did not change":
|
|
247
|
+
the caller must not turn an unreadable version into a failure report.
|
|
248
|
+
"""
|
|
249
|
+
try:
|
|
250
|
+
result = run_subprocess(
|
|
251
|
+
# `-I` (isolated) is load-bearing, not tidiness. Without it the probe inherits
|
|
252
|
+
# PYTHONPATH and the user site dir, so a `sitecustomize` on that path runs inside
|
|
253
|
+
# the answer and can emit stdout before OR after the version print.
|
|
254
|
+
# `-I` keeps the venv's own site-packages, so `importlib.metadata` still finds
|
|
255
|
+
# the install (verified) — but it does NOT suppress `sitecustomize.py` that lives
|
|
256
|
+
# in those site-packages. An atexit hook registered there (syncade ships such a
|
|
257
|
+
# shim for worktree imports) can append a version-shaped line AFTER the real one.
|
|
258
|
+
# `os._exit(0)` bypasses all atexit handlers: the process terminates immediately
|
|
259
|
+
# after `os.write`, so nothing can append to our stdout (reproduced: a hostile
|
|
260
|
+
# sitecustomize turned "0.7.0" into "0.7.0\n99.0.0" before this fix).
|
|
261
|
+
# `os.write` is used instead of `print` because `os._exit` skips stdio flushing;
|
|
262
|
+
# an unbuffered syscall guarantees the version byte reaches the pipe.
|
|
263
|
+
[
|
|
264
|
+
sys.executable,
|
|
265
|
+
"-I",
|
|
266
|
+
"-c",
|
|
267
|
+
"import importlib.metadata as m, os; "
|
|
268
|
+
"os.write(1, (m.version('syncade') + '\\n').encode()); "
|
|
269
|
+
"os._exit(0)",
|
|
270
|
+
],
|
|
271
|
+
timeout=_VERSION_TIMEOUT,
|
|
272
|
+
)
|
|
273
|
+
except SubprocessError:
|
|
274
|
+
return None
|
|
275
|
+
if result.returncode != 0:
|
|
276
|
+
return None
|
|
277
|
+
raw = result.stdout.strip()
|
|
278
|
+
if not raw:
|
|
279
|
+
return None
|
|
280
|
+
# sitecustomize from PYTHONPATH (suppressed by -I) could emit noise before -c runs;
|
|
281
|
+
# the venv's own sitecustomize can still do so. Take the LAST line — atexit is now
|
|
282
|
+
# impossible (os._exit), so the last line is what os.write put there — and validate it
|
|
283
|
+
# parses as a version. Noise that accidentally ends in a dotted string still fails _parse.
|
|
284
|
+
candidate = raw.splitlines()[-1].strip()
|
|
285
|
+
return candidate if _parse(candidate) is not None else None
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _is_newest(installed: str, *, enabled: bool = True) -> bool | None:
|
|
289
|
+
"""Whether ``installed`` is at or beyond the newest the manifest advertises, or ``None``.
|
|
290
|
+
|
|
291
|
+
THREE answers, because the caller has three outcomes and two states could not express
|
|
292
|
+
them. ``None`` means the manifest could not be read AT ALL, which is NOT "a newer version
|
|
293
|
+
exists" — collapsing those is a shipped defect: on a machine whose TLS verification fails,
|
|
294
|
+
an already-current install was told "nothing was installed, most often the install pins a
|
|
295
|
+
version" and sent hunting a pin that did not exist.
|
|
296
|
+
|
|
297
|
+
Only ever consulted on the unhappy path — the version failed to move — so the happy path
|
|
298
|
+
stays offline. Anything that leaves the question unanswered (unreachable manifest, no usable
|
|
299
|
+
``latest``, an installed version we cannot parse) returns ``None`` — the third state meaning
|
|
300
|
+
"could not determine". That is the safe direction when used with care: the caller must render
|
|
301
|
+
``None`` as "manifest unreachable" rather than a pin diagnosis, because collapsing the two was
|
|
302
|
+
the shipped defect this three-valued return exists to fix.
|
|
303
|
+
|
|
304
|
+
**This compares versions itself and must never delegate to ``evaluate()``.** That function
|
|
305
|
+
answers a DIFFERENT question — "should the operator see a notice?" — and the two answers
|
|
306
|
+
only usually coincide. They part company in the documented critical-no-fix state: when
|
|
307
|
+
``critical_below`` is above the installed version and no newer release exists, ``evaluate()``
|
|
308
|
+
returns a notice while the operator is genuinely current, so reading its ``None``-ness told a
|
|
309
|
+
fully up-to-date install that its upgrade had failed and it was probably pinned. Two dogfood
|
|
310
|
+
rounds landed on this function for the same underlying reason — a near-miss reuse of a
|
|
311
|
+
neighbouring answer — and the fix both times was to ask the actual question.
|
|
312
|
+
|
|
313
|
+
There is deliberately no special case for an unreachable manifest: ``{}`` has no usable
|
|
314
|
+
``latest``, so the one parse rule already answers it. An explicit ``if manifest is None``
|
|
315
|
+
branch has been added and removed TWICE, both times proved dead by a mutation run — the
|
|
316
|
+
second time by the same person who wrote this sentence, four lines below it. If you are about
|
|
317
|
+
to add it again, mutate it first: replace ``manifest_once(...) or {}`` and watch nothing
|
|
318
|
+
fail.
|
|
319
|
+
|
|
320
|
+
``enabled=False`` fetches NOTHING and yields ``None`` — an operator who set
|
|
321
|
+
``[update] check = false`` gets no egress from the explicit path either, which is what
|
|
322
|
+
makes README's "one network call of its own, suppressible" true. The caller already
|
|
323
|
+
reports ``None`` as "could not check", so honouring the opt-out costs no new branch.
|
|
324
|
+
"""
|
|
325
|
+
manifest = manifest_once(enabled=enabled) or {}
|
|
326
|
+
latest = _parse(_text(manifest.get("latest")))
|
|
327
|
+
current = _parse(installed)
|
|
328
|
+
if latest is None or current is None:
|
|
329
|
+
return None
|
|
330
|
+
return current >= latest
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def run_update(*, cwd: Path | None = None, out=None) -> int:
|
|
334
|
+
"""Upgrade syncade in place, then exit asking to be re-run."""
|
|
335
|
+
out = out or sys.stderr
|
|
336
|
+
cwd = cwd or Path.cwd()
|
|
337
|
+
|
|
338
|
+
# Resolve to the actual git repo root so live_run() does not miss a run when invoked
|
|
339
|
+
# from a subdirectory — `.syncade/runs/` is at the root, not at the invocation directory.
|
|
340
|
+
# Fails open: if discovery raises (not in a git repo, git not on PATH, etc.) we fall back
|
|
341
|
+
# to cwd and check there, which is correct behaviour for a non-repo invocation.
|
|
342
|
+
try:
|
|
343
|
+
from syncade.snapshot import discover_repo_root
|
|
344
|
+
|
|
345
|
+
cwd = discover_repo_root(cwd)
|
|
346
|
+
except Exception: # noqa: BLE001
|
|
347
|
+
pass
|
|
348
|
+
|
|
349
|
+
running = live_run(cwd)
|
|
350
|
+
if running is not None:
|
|
351
|
+
print(
|
|
352
|
+
f"[syncade] refusing to update: a run is still going ({running.name}).\n"
|
|
353
|
+
" Upgrading underneath it would leave the run half-old and half-new.\n"
|
|
354
|
+
" Wait for it to finish, then run `syncade --update` again.",
|
|
355
|
+
file=out,
|
|
356
|
+
)
|
|
357
|
+
return WORKTREE_ERROR
|
|
358
|
+
|
|
359
|
+
method = detect_install()
|
|
360
|
+
if method.kind == "source":
|
|
361
|
+
print(
|
|
362
|
+
f"[syncade] this syncade runs from a source checkout at {method.where}.\n"
|
|
363
|
+
" Update it with git, not a package manager.",
|
|
364
|
+
file=out,
|
|
365
|
+
)
|
|
366
|
+
return WORKTREE_ERROR
|
|
367
|
+
if method.command is None:
|
|
368
|
+
print(
|
|
369
|
+
"[syncade] could not determine how syncade was installed, so nothing was run.\n"
|
|
370
|
+
f" If you used pip: {sys.executable} -m pip install -U syncade",
|
|
371
|
+
file=out,
|
|
372
|
+
)
|
|
373
|
+
return WORKTREE_ERROR
|
|
374
|
+
|
|
375
|
+
print(f"[syncade] {' '.join(method.command)}", file=out)
|
|
376
|
+
# `SubprocessResult.returncode` — NOT `exit_code`, which does not exist. The first version
|
|
377
|
+
# read `exit_code` and every test passed, because the test double was hand-written with that
|
|
378
|
+
# attribute: the fake encoded the belief instead of the type, so the suite and the mutation
|
|
379
|
+
# run both validated the same wrong contract. A real upgrade would have raised AttributeError
|
|
380
|
+
# AFTER upgrading, losing the skill re-sync and the re-run instruction. The tests now build
|
|
381
|
+
# the real SubprocessResult, so a rename breaks them instead of hiding in them.
|
|
382
|
+
try:
|
|
383
|
+
result = run_subprocess(method.command, timeout=_UPGRADE_TIMEOUT)
|
|
384
|
+
except SubprocessError as exc:
|
|
385
|
+
# `uv`/`pipx` absent from PATH, or unlaunchable. Reported, not escaped: an operator who
|
|
386
|
+
# ran --update deserves to be told the package manager is missing.
|
|
387
|
+
print(f"[syncade] update failed: {exc}", file=out)
|
|
388
|
+
return WORKTREE_ERROR
|
|
389
|
+
if result.returncode != 0:
|
|
390
|
+
print(
|
|
391
|
+
f"[syncade] update failed (exit {result.returncode}). Nothing was changed.\n"
|
|
392
|
+
+ (result.stderr.strip() or result.stdout.strip())[:800],
|
|
393
|
+
file=out,
|
|
394
|
+
)
|
|
395
|
+
return WORKTREE_ERROR
|
|
396
|
+
|
|
397
|
+
# THE POST-CONDITION. Exit 0 from the package manager means "the command ran", NOT "the
|
|
398
|
+
# version moved", and the two come apart routinely: `uv tool install syncade==0.6.3` records
|
|
399
|
+
# `specifier = "==0.6.3"` in its receipt, so `uv tool upgrade` honours the pin, prints
|
|
400
|
+
# "Nothing to upgrade" and exits 0. Held packages, resolver conflicts and a manifest that is
|
|
401
|
+
# ahead of PyPI all land in the same place. Without this check syncade says "updated. Re-run
|
|
402
|
+
# your command", the operator re-runs, gets the same version, and is told again next session
|
|
403
|
+
# — forever. Measured live on the 0.7.0 release, which is why item 5 of that release existed.
|
|
404
|
+
now = _installed_version()
|
|
405
|
+
if now is not None and now == syncade.__version__:
|
|
406
|
+
# The version did not move. There are TWO reasons for that and they need opposite
|
|
407
|
+
# answers, so ask the manifest which one this is rather than guessing. Guessing was the
|
|
408
|
+
# first version of this guard and it told an operator who was simply already current
|
|
409
|
+
# that their install was pinned and broken — a fresh false claim inside the fix for a
|
|
410
|
+
# false claim, caught by testing the path instead of reasoning about it.
|
|
411
|
+
# The opt-out reaches HERE too, not just the startup notice: `[update] check = false`
|
|
412
|
+
# means no manifest request from any path, which is the documented contract.
|
|
413
|
+
from syncade.cli.update_notice import _check_enabled
|
|
414
|
+
|
|
415
|
+
# CI is honoured here for the same reason check_for_update honours it: no human is
|
|
416
|
+
# reading the output, so a manifest request adds no value and contradicts the
|
|
417
|
+
# documented "also skipped whenever CI is set" guarantee for [update] check.
|
|
418
|
+
newest = _is_newest(now, enabled=_check_enabled(cwd) and not bool(os.environ.get("CI")))
|
|
419
|
+
if newest:
|
|
420
|
+
skills_ok = _resync_skills(out)
|
|
421
|
+
print(f"[syncade] already up to date ({now}).", file=out)
|
|
422
|
+
return SUCCESS if skills_ok else WORKTREE_ERROR
|
|
423
|
+
if newest is None:
|
|
424
|
+
# Cannot read the manifest, so whether an upgrade was even due is unknown. Rendering
|
|
425
|
+
# the pin diagnosis here sends the operator hunting a pin that may not exist —
|
|
426
|
+
# measured on macOS framework Python, where a missing CA bundle fails TLS in 0.05s
|
|
427
|
+
# and leaves every update check silently dead. The manifest may also be reachable
|
|
428
|
+
# but malformed; either way, the CA remedy is a conditional suggestion, not a
|
|
429
|
+
# diagnosis.
|
|
430
|
+
print(
|
|
431
|
+
f"[syncade] syncade is still {now}, and the update manifest could not be\n"
|
|
432
|
+
" read, so whether that is correct is unknown. Compare your version\n"
|
|
433
|
+
" against https://github.com/syncade-ai/syncade-ai/releases\n"
|
|
434
|
+
" On macOS python.org builds a missing CA bundle can cause this —\n"
|
|
435
|
+
" run `Install Certificates.command` from your Python's Applications\n"
|
|
436
|
+
" folder if you see TLS errors.",
|
|
437
|
+
file=out,
|
|
438
|
+
)
|
|
439
|
+
return WORKTREE_ERROR
|
|
440
|
+
if method.kind == "pip":
|
|
441
|
+
user_flag = " --user" if _in_user_site() else ""
|
|
442
|
+
print(
|
|
443
|
+
f"[syncade] the upgrade command succeeded but syncade is still "
|
|
444
|
+
f"{syncade.__version__} — nothing was installed.\n"
|
|
445
|
+
" Most often a resolver constraint or pinned requirement prevented "
|
|
446
|
+
"the upgrade.\n"
|
|
447
|
+
" Reinstall unpinned: "
|
|
448
|
+
f"{sys.executable} -m pip install -U{user_flag} --force-reinstall syncade",
|
|
449
|
+
file=out,
|
|
450
|
+
)
|
|
451
|
+
else:
|
|
452
|
+
print(
|
|
453
|
+
f"[syncade] the upgrade command succeeded but syncade is still "
|
|
454
|
+
f"{syncade.__version__} — nothing was installed.\n"
|
|
455
|
+
f" Most often the install pins a version: check `uv tool list` / "
|
|
456
|
+
f"`pipx list`, then\n"
|
|
457
|
+
f" reinstall unpinned (e.g. `uv tool install --force syncade`).",
|
|
458
|
+
file=out,
|
|
459
|
+
)
|
|
460
|
+
return WORKTREE_ERROR
|
|
461
|
+
|
|
462
|
+
skills_ok = _resync_skills(out)
|
|
463
|
+
moved = f" to {now}" if now else ""
|
|
464
|
+
print(
|
|
465
|
+
f"[syncade] updated{moved}. Re-run your command to use the new version — this process is\n"
|
|
466
|
+
" still running the old one and cannot switch to it.",
|
|
467
|
+
file=out,
|
|
468
|
+
)
|
|
469
|
+
if not skills_ok:
|
|
470
|
+
# The package upgrade succeeded but at least one skill resync was refused. The operator
|
|
471
|
+
# now has a fresh CLI and a stale or recordless skill — not a clean update end to end.
|
|
472
|
+
return WORKTREE_ERROR
|
|
473
|
+
return SUCCESS
|