syncade 0.7.1__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.1/src/syncade.egg-info → syncade-0.7.3}/PKG-INFO +28 -4
- {syncade-0.7.1 → syncade-0.7.3}/README.md +27 -3
- {syncade-0.7.1 → syncade-0.7.3}/docs/config-reference.md +13 -6
- {syncade-0.7.1 → syncade-0.7.3}/pyproject.toml +1 -1
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/__init__.py +1 -1
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/parser.py +2 -2
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/update_mode.py +159 -25
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/config.py +3 -2
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/config_update.py +6 -5
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/doctor.py +11 -3
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/doctor_env.py +47 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/update_check.py +38 -6
- {syncade-0.7.1 → syncade-0.7.3/src/syncade.egg-info}/PKG-INFO +28 -4
- {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_documented_flags_exist.py +8 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_update_mode.py +160 -2
- {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_update_post_condition.py +282 -24
- {syncade-0.7.1 → syncade-0.7.3}/tests/conftest.py +16 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/doctor/conftest.py +10 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_update_check.py +10 -0
- {syncade-0.7.1 → syncade-0.7.3}/LICENSE +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/MANIFEST.in +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/docs/how-to-use.md +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/setup.cfg +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/__main__.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/adapters/__init__.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/adapters/anthropic.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/adapters/base.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/adapters/fake.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/adapters/fake_common.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/adapters/fake_producer_audit_draft.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/adapters/fake_reviewer_synth.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/adapters/openai.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/adapters/openai_parsing.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/adapters/producer.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/adapters/producer_anthropic.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/adapters/producer_openai.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/adapters/registry.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/auth_check.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/auth_preflight.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/base_resolution.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/billing.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/checks_config.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/__init__.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/auth_gate.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/config_keys.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/config_list.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/config_menu_rows.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/config_mode.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/config_overrides.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/config_tui.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/doctor_mode.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/gc_mode.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/install_skill.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/metrics_mode.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/modes.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/parser_types.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/paths.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/preflight_paths.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/resolve.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/resume_mode.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/skill_status.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/toml_writer.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/update_notice.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/cli/validate.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/config_auth.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/config_cold.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/config_gc.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/config_loader.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/config_loop.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/config_producer.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/config_retry.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/config_types.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/diff_filter.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/dispatcher.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/doctor_preview.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/doctor_types.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/exit_codes.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/findings.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/findings_json.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/gc.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/gc_execute.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/gc_protection.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/gc_types.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/gc_worktrees.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/git_object_id.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/git_preconditions.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/logging.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/metrics/__init__.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/metrics/aggregate.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/metrics/findings.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/metrics/schema.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/__init__.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/_runs_dir.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/branch_advance.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/branch_guard.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/budget.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/escalation_coverage.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/loop.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/loop_dispatch_check.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/loop_finalize.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/loop_preflight.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/loop_resume.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/loop_rmtree.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/loop_round_step.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/prior_round.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/producer_phase.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/results.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/resume.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/resume_load.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/resume_plan.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/resume_target.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/resume_types.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/reviewer_template_failure.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/round.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/round_checks.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/round_no_changes.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/round_predispatch.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/orchestrator/verdict.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/__init__.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/_atomic.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/_clusters.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/_findings_verdict.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/_markdown.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/_validation.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/checks.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/decision_needed.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/findings_md.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/handoff.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/handoff_classify.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/last_reviewed.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/loop_manifest.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/loop_summary.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/loop_summary_text.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/producer.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/reviewer.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/round_manifest.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/run_init.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/run_summary.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/run_summary_next_steps.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/synth.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/persistence/test_run.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/presets.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/pricing_config.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/process.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/producer.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/producer_attempt.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/producer_escalation.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/producer_git.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/producer_result.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/prompts.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/prompts_loader.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/retry.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/run_inputs.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/run_status.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/selfcheck.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/skills/claude/README.md +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/skills/claude/SKILL.md +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/skills/codex/README.md +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/skills/codex/SKILL.md +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/snapshot.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/spec_audit.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/spec_audit_schema.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/spec_draft.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/spec_source.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/synthesis.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/synthesis_clusters.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/synthesis_repair.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/synthesis_schema.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/synthesizer/__init__.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/synthesizer/constants.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/synthesizer/driver.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/synthesizer/rendering.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/synthesizer/result.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/synthesizer/validation.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/synthesizer/workspace.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/templates/presets/balanced.toml +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/templates/presets/cheap.toml +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/templates/presets/thorough.toml +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/templates/producer.md +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/templates/reviewer.md +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/templates/reviewer_adversarial.md +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/templates/reviewer_codex.md +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/templates/spec_audit.md +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/templates/spec_draft.md +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/templates/synthesizer.md +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/test_runner.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/test_runner_classify.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/transcript.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/usage.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/worktree.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/worktree_env.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade/worktree_paths.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade.egg-info/SOURCES.txt +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade.egg-info/dependency_links.txt +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade.egg-info/entry_points.txt +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade.egg-info/requires.txt +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/src/syncade.egg-info/top_level.txt +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/__init__.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/adapters/__init__.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/adapters/_anthropic_helpers.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/adapters/_openai_helpers.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/adapters/test_anthropic.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/adapters/test_anthropic_more.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/adapters/test_openai.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/adapters/test_openai_more.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/adapters/test_producer.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/adapters/test_producer_more.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/adapters/test_prompt_transport.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/auth_check/__init__.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/auth_check/test_probes.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/auth_check/test_run_auth_check.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/cli/__init__.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/cli/_helpers.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/cli/_undo_helpers.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_basic_and_mutex.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_cli_library_divergence.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_config_keys.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_config_list.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_config_menu_rows.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_config_mode.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_config_overrides.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_config_tui.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_config_tui_pty.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_documented_exit_codes.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_flags.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_gc.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_help_retention_contract.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_mode_exclusivity_matrix.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_mode_flag_guards.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_resume_reports_hard_kill.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_run_and_config.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_run_status_cli.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_scope_openspec_draft.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_toml_writer.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_undo_auto_init.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_undo_scope.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_update_notice.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/cli/test_validate_before_mutate.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/config/__init__.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/config/test_auth_enforcement.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/config/test_auth_modes.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/config/test_auth_preflight.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/config/test_auth_report_scope.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/config/test_budget_config.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/config/test_cold_actors.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/config/test_config_checks.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/config/test_config_reference_drift.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/config/test_config_schema.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/config/test_gc_config.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/config/test_global_config_layer.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/config/test_presets.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/config/test_retry_config.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/config/test_reviewer_config.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/config/test_reviewer_permissions.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/config/test_worktree_base_config.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/dispatcher/__init__.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/dispatcher/_helpers.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/dispatcher/test_dispatch_core.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/dispatcher/test_dispatch_misc.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/dispatcher/test_dispatch_usage.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/doctor/__init__.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/doctor/_helpers.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/doctor/test_doctor.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/doctor/test_doctor_diff_size.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/doctor/test_doctor_malformed_diff.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/doctor/test_doctor_preview.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/findings/__init__.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/findings/_helpers.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/findings/test_parser_and_fields.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/findings/test_parser_resilience.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/findings/test_verdict_block_selection.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/gc/__init__.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/gc/_helpers.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/gc/test_autoprune.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/gc/test_execute.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/gc/test_plan.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/gc/test_plan_execution_races.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/gc/test_reaping_scope.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/gc/test_retention_tiers.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/gc/test_robustness.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/gc/test_self_orphan_protection.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/gc/test_smoke.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/gc/test_two_tier_retention.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/gc/test_worktree_age_bound.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/logging/__init__.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/logging/_helpers.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/logging/test_logging.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/logging/test_logging_summary.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/metrics/conftest.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/metrics/test_actor_spend.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/metrics/test_aggregate.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/metrics/test_billing_reconciliation.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/metrics/test_billing_surfaces_agree.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/metrics/test_budget_metrics.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/metrics/test_consensus_report.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/metrics/test_cost_honesty.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/metrics/test_findings_capture.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/metrics/test_metrics_cli.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/metrics/test_metrics_mode.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/metrics/test_schema.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/metrics/test_unknown_panel.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/__init__.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/_helpers.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/_resume_fixtures.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/conftest.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_budget.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_budget_resume_e2e.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_budget_stop_message.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_budget_warning.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_checks_and_recording.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_contracts.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_cross_round.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_cross_round_more.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_default_branch_guard.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_diff_size_cap.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_dispatch_record.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_empty_diff.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_empty_diff_no_side_effects.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_escalation.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_last_reviewed_truthful.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_loop_audit_remediation.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_matrix_and_e2e.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_multiround.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_multiround_dirty.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_multiround_more.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_paraphrase_escalation.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_pipeline.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_provider_usage_limit.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_rename_boundary.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_resume.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_resume_base_oid.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_reviewer_blindness_is_structural.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_round_input_boundaries.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_run_layout.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_run_status_wiring.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_safety_warning_survives_quiet.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_snapshot_and_base.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_synth_phase.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_synth_phase_more.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_synth_provenance.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_synth_validation.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_template_robustness.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_termination_reason_drift.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_testleg.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_timeout_is_per_subprocess.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_usage_limit_message.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/orchestrator/test_worktree_cleanup.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/__init__.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/_helpers.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/test_atomic_writes.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/test_checks_surfacing.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/test_diff_size_recording.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/test_findings_md.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/test_findings_md_more.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/test_findings_md_verdict.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/test_handoff.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/test_loop_manifest_checks.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/test_loop_summary.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/test_loop_summary_more.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/test_md_safety.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/test_next_steps.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/test_producer_persist.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/test_reviewer.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/test_round_manifest.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/test_run_init.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/test_run_summary.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/test_run_summary_more.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/test_run_summary_producer.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/test_synth_persist.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/persistence/test_test_run.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/producer/__init__.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/producer/_helpers.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/producer/test_producer.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/producer/test_producer_confinement.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/producer/test_producer_retry.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/producer/test_producer_run.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/producer/test_producer_timeout.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/producer/test_producer_usage.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/prompts/__init__.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/prompts/test_load_template.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/prompts/test_misc.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/prompts/test_producer.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/prompts/test_render_synthesizer.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/prompts/test_synthesizer.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/resume/__init__.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/resume/_helpers.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/resume/test_budget_resume.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/resume/test_checks_phase_failure.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/resume/test_cli_malformed_rehydration.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/resume/test_find_resolve_plan.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/resume/test_plan_resume.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/resume/test_round_trip.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/resume/test_tree_drift_and_escalation.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/skills/test_config_section.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/skills/test_install_skill.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/skills/test_install_skill_multi_harness.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/skills/test_install_skill_ownership.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/skills/test_install_skill_refuses.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/skills/test_skill_drift.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/skills/test_skill_status.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/smoke/__init__.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/smoke/test_anthropic_smoke.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/smoke/test_auth_check_smoke.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/smoke/test_codex_smoke.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/smoke/test_loop_smoke.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/smoke/test_orchestrator_smoke.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/smoke/test_orchestrator_smoke_more.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/smoke/test_producer_smoke.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/smoke/test_resume_reap_smoke.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/smoke/test_selfcheck_smoke.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/smoke/test_synthesizer_smoke.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/smoke/test_test_runner_smoke.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/snapshot/__init__.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/snapshot/_helpers.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/snapshot/conftest.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/snapshot/test_snapshot.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/snapshot/test_snapshot_diff_identity.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/snapshot/test_snapshot_dirty.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/snapshot/test_snapshot_warning.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/snapshot/test_three_dot_diff.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/synthesis/__init__.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/synthesis/_helpers.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/synthesis/test_cluster_repair.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/synthesis/test_clusters.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/synthesis/test_models.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/synthesis/test_models_distinct_keying.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/synthesis/test_parsing.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/synthesis/test_unanimous_blocker_downgrade.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/synthesizer/test_provenance_fabrication.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/synthesizer/test_provenance_repair.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_adapter_registry.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_adapters_fake.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_auto_init_policy.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_base_resolution.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_check_loc_script.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_checks.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_config_loader.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_cross_model_claim.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_diff_filter.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_diff_filter_binary.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_diff_filter_quoted_paths.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_escalation_coverage.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_exit_codes.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_git_choke_point.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_git_config_isolation.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_git_preconditions.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_last_reviewed.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_parse_repair_extra_keys.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_pricing.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_private_identifier_gate.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_process.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_process_teardown.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_producer_escalation.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_regression_acme.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_retry.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_run_status.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_run_status_signals.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_runner/__init__.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_runner/test_test_runner.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_runner/test_test_runner_more.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_safety_disclosures_survive_quiet.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_selfcheck.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_spec_audit.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_spec_draft.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_spec_source.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_streamed_capture.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_strip_list_semantics.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_synthesizer.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_synthesizer_passthrough.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_synthesizer_provenance.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_transcript.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_usage.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_usage_persistence.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_workflow_action_pins.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_worktree_base_isolation.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_worktree_env.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/test_worktree_strip_guard.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/worktree/__init__.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/worktree/_helpers.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/worktree/conftest.py +0 -0
- {syncade-0.7.1 → syncade-0.7.3}/tests/worktree/test_worktree.py +0 -0
- {syncade-0.7.1 → 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.",
|
|
@@ -16,6 +16,8 @@ the PR-h-04.5 shape one layer up: refuse rather than act on an unproven belief.
|
|
|
16
16
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
|
+
import os
|
|
20
|
+
import site
|
|
19
21
|
import subprocess
|
|
20
22
|
import sys
|
|
21
23
|
from dataclasses import dataclass
|
|
@@ -24,7 +26,7 @@ from pathlib import Path
|
|
|
24
26
|
import syncade
|
|
25
27
|
from syncade.exit_codes import SUCCESS, WORKTREE_ERROR
|
|
26
28
|
from syncade.process import SubprocessError, run_subprocess
|
|
27
|
-
from syncade.update_check import
|
|
29
|
+
from syncade.update_check import _parse, _text, manifest_once
|
|
28
30
|
|
|
29
31
|
_UPGRADE_TIMEOUT = 300.0
|
|
30
32
|
#: Reading one version string out of a fresh interpreter. Generous, because a cold venv
|
|
@@ -38,11 +40,91 @@ _MARKER_DEPTH = 7
|
|
|
38
40
|
|
|
39
41
|
@dataclass(frozen=True)
|
|
40
42
|
class InstallMethod:
|
|
41
|
-
kind: str # "uv" | "pipx" | "source" | "unknown"
|
|
43
|
+
kind: str # "uv" | "pipx" | "pip" | "source" | "unknown"
|
|
42
44
|
command: list[str] | None # the upgrade to run, or None when we must not run anything
|
|
43
45
|
where: Path | None
|
|
44
46
|
|
|
45
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
|
+
|
|
46
128
|
def detect_install() -> InstallMethod:
|
|
47
129
|
"""Prove how syncade got here by finding a marker inside its own install tree."""
|
|
48
130
|
start = Path(syncade.__file__).resolve().parent
|
|
@@ -59,7 +141,7 @@ def detect_install() -> InstallMethod:
|
|
|
59
141
|
# specific; reaching here means the environment exists but its manager is unproven,
|
|
60
142
|
# which is exactly what "unknown" is for.
|
|
61
143
|
if (parent / "pyvenv.cfg").is_file():
|
|
62
|
-
return
|
|
144
|
+
return _pip_or_unknown(parent)
|
|
63
145
|
# A checkout is the one case where upgrading would be actively wrong: the operator's
|
|
64
146
|
# own working tree is the install, and `uv tool upgrade` would neither touch it nor
|
|
65
147
|
# tell them why nothing changed. Detected by the project file naming THIS project, so a
|
|
@@ -67,7 +149,7 @@ def detect_install() -> InstallMethod:
|
|
|
67
149
|
pyproject = parent / "pyproject.toml"
|
|
68
150
|
if pyproject.is_file() and 'name = "syncade"' in _read(pyproject):
|
|
69
151
|
return InstallMethod("source", None, parent)
|
|
70
|
-
return
|
|
152
|
+
return _pip_or_unknown(None)
|
|
71
153
|
|
|
72
154
|
|
|
73
155
|
def _read(path: Path) -> str:
|
|
@@ -203,16 +285,21 @@ def _installed_version() -> str | None:
|
|
|
203
285
|
return candidate if _parse(candidate) is not None else None
|
|
204
286
|
|
|
205
287
|
|
|
206
|
-
def _is_newest(installed: str) -> bool:
|
|
207
|
-
"""Whether ``installed`` is at or beyond the newest
|
|
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.
|
|
208
296
|
|
|
209
297
|
Only ever consulted on the unhappy path — the version failed to move — so the happy path
|
|
210
298
|
stays offline. Anything that leaves the question unanswered (unreachable manifest, no usable
|
|
211
|
-
``latest``, an installed version we cannot parse) returns ``
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
the new one.
|
|
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.
|
|
216
303
|
|
|
217
304
|
**This compares versions itself and must never delegate to ``evaluate()``.** That function
|
|
218
305
|
answers a DIFFERENT question — "should the operator see a notice?" — and the two answers
|
|
@@ -224,13 +311,23 @@ def _is_newest(installed: str) -> bool:
|
|
|
224
311
|
neighbouring answer — and the fix both times was to ask the actual question.
|
|
225
312
|
|
|
226
313
|
There is deliberately no special case for an unreachable manifest: ``{}`` has no usable
|
|
227
|
-
``latest``, so the one parse rule already answers it. An explicit
|
|
228
|
-
|
|
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.
|
|
229
324
|
"""
|
|
230
|
-
manifest =
|
|
325
|
+
manifest = manifest_once(enabled=enabled) or {}
|
|
231
326
|
latest = _parse(_text(manifest.get("latest")))
|
|
232
327
|
current = _parse(installed)
|
|
233
|
-
|
|
328
|
+
if latest is None or current is None:
|
|
329
|
+
return None
|
|
330
|
+
return current >= latest
|
|
234
331
|
|
|
235
332
|
|
|
236
333
|
def run_update(*, cwd: Path | None = None, out=None) -> int:
|
|
@@ -270,7 +367,7 @@ def run_update(*, cwd: Path | None = None, out=None) -> int:
|
|
|
270
367
|
if method.command is None:
|
|
271
368
|
print(
|
|
272
369
|
"[syncade] could not determine how syncade was installed, so nothing was run.\n"
|
|
273
|
-
" If you used pip: pip install -U syncade",
|
|
370
|
+
f" If you used pip: {sys.executable} -m pip install -U syncade",
|
|
274
371
|
file=out,
|
|
275
372
|
)
|
|
276
373
|
return WORKTREE_ERROR
|
|
@@ -311,18 +408,55 @@ def run_update(*, cwd: Path | None = None, out=None) -> int:
|
|
|
311
408
|
# first version of this guard and it told an operator who was simply already current
|
|
312
409
|
# that their install was pinned and broken — a fresh false claim inside the fix for a
|
|
313
410
|
# false claim, caught by testing the path instead of reasoning about it.
|
|
314
|
-
|
|
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:
|
|
315
420
|
skills_ok = _resync_skills(out)
|
|
316
421
|
print(f"[syncade] already up to date ({now}).", file=out)
|
|
317
422
|
return SUCCESS if skills_ok else WORKTREE_ERROR
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
+
)
|
|
326
460
|
return WORKTREE_ERROR
|
|
327
461
|
|
|
328
462
|
skills_ok = _resync_skills(out)
|
|
@@ -355,8 +355,9 @@ class SyncadeConfig(BaseModel):
|
|
|
355
355
|
# Run-artifact retention (PR-v2-9). Defaults reproduce gc.DEFAULT_KEEP / DEFAULT_MAX_AGE_DAYS;
|
|
356
356
|
# governs BOTH the per-loop auto-prune and an explicit ``syncade --gc``.
|
|
357
357
|
gc: GcConfig = Field(default_factory=GcConfig)
|
|
358
|
-
# One-key
|
|
359
|
-
#
|
|
358
|
+
# One-key opt-out for update manifest fetches (PR-h-field-07). Suppresses background
|
|
359
|
+
# check, --update, and --doctor's manifest row when false. --doctor's red manifest row
|
|
360
|
+
# exits 60; the background check only prints, silent errors.
|
|
360
361
|
update: UpdateConfig = Field(default_factory=UpdateConfig)
|
|
361
362
|
# Where per-run git worktrees are provisioned (PR-v2-9). A single top-level value (not a
|
|
362
363
|
# ``[worktree]`` block — Q5); default reproduces worktree.DEFAULT_WORKTREE_BASE.
|
|
@@ -19,11 +19,12 @@ class UpdateConfig(BaseModel):
|
|
|
19
19
|
check: bool = Field(
|
|
20
20
|
default=True,
|
|
21
21
|
description=(
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
22
|
+
"Enable update checks (default true). An HTTPS GET to the public repo's update "
|
|
23
|
+
"manifest, 1.5s timeout. Nothing about your repo, diff, run, or identity is sent. "
|
|
24
|
+
"Set false to suppress all fetches — background, --update, and --doctor. "
|
|
25
|
+
"Background: fires at most once per session window, only prints, every error is "
|
|
26
|
+
"silent. Explicit (--update, --doctor): each makes its own fetch per process; "
|
|
27
|
+
"--doctor exits 60 when the manifest is unreachable. Also skipped whenever CI "
|
|
27
28
|
"is set."
|
|
28
29
|
),
|
|
29
30
|
)
|
|
@@ -37,15 +37,16 @@ from syncade.config import SyncadeConfig
|
|
|
37
37
|
from syncade.config_auth import ALL_BLOCKS
|
|
38
38
|
|
|
39
39
|
# Re-exported for import stability after the PR-h-field-01 split (Decomposition Rule): callers
|
|
40
|
-
# keep importing these from `syncade.doctor`. MONKEYPATCH TARGETS ARE `
|
|
41
|
-
#
|
|
42
|
-
#
|
|
40
|
+
# keep importing these from `syncade.doctor`. MONKEYPATCH TARGETS ARE `syncade.doctor`, NOT
|
|
41
|
+
# `doctor_env` — `collect_checks()` calls these names through the binding HERE, so patching
|
|
42
|
+
# `doctor_env` does not affect what the function body reads.
|
|
43
43
|
from syncade.doctor_env import ( # noqa: F401
|
|
44
44
|
_CLI_LAUNCH_TIMEOUT,
|
|
45
45
|
_MIN_FREE_DISK_BYTES,
|
|
46
46
|
_PROVIDER_CLI,
|
|
47
47
|
_check_config,
|
|
48
48
|
_check_provider_clis,
|
|
49
|
+
_check_update_manifest,
|
|
49
50
|
_check_worktree_root,
|
|
50
51
|
_configured_providers,
|
|
51
52
|
_probe_cli_launch,
|
|
@@ -330,6 +331,10 @@ def collect_checks(
|
|
|
330
331
|
check_cost(config, repo_root, max_rounds=max_rounds),
|
|
331
332
|
check_budget(config),
|
|
332
333
|
]
|
|
334
|
+
# The update-manifest check is informational: an unreachable manifest does not refuse a real
|
|
335
|
+
# run, so it must NOT be in `cheap` (whose reds gate the live legs). It is run after the
|
|
336
|
+
# gating decision, and its red status does not suppress auth or producer-commit.
|
|
337
|
+
update_check = _check_update_manifest(enabled=config.update.check)
|
|
333
338
|
checks = list(cheap)
|
|
334
339
|
if quick:
|
|
335
340
|
checks.append(DoctorCheck("auth", _SKIP, "skipped (--quick): credential probe not run"))
|
|
@@ -355,6 +360,9 @@ def collect_checks(
|
|
|
355
360
|
)
|
|
356
361
|
else:
|
|
357
362
|
checks.append(_check_producer_commit(config, timeout_seconds=timeout_seconds))
|
|
363
|
+
# Append the update-manifest check after the live-legs decision: it is informational and
|
|
364
|
+
# its red status must not appear to gate the live legs (which were decided above without it).
|
|
365
|
+
checks.append(update_check)
|
|
358
366
|
return checks
|
|
359
367
|
|
|
360
368
|
|
|
@@ -216,3 +216,50 @@ def _check_worktree_root(worktree_base: Path) -> DoctorCheck:
|
|
|
216
216
|
fix="free up disk space; each reviewer/producer/test leg checks out a worktree",
|
|
217
217
|
)
|
|
218
218
|
return DoctorCheck("worktree", _OK, f"{probe_dir} writable, {free_gib:.1f} GiB free")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _check_update_manifest(*, enabled: bool = True) -> DoctorCheck:
|
|
222
|
+
"""Can this machine reach the update manifest at all?
|
|
223
|
+
|
|
224
|
+
A red row here does not affect a review — the background check is silent by design and must
|
|
225
|
+
never slow or fail a run. It is worth a row because its failure is INVISIBLE everywhere else:
|
|
226
|
+
an unreachable manifest and an up-to-date syncade look identical, forever, so an operator can
|
|
227
|
+
sit years behind believing they are current. Doctor exists to surface exactly that kind of
|
|
228
|
+
quietly-broken setup for $0.
|
|
229
|
+
|
|
230
|
+
Measured cause on the reporting machine: python.org's macOS framework build ships no CA
|
|
231
|
+
bundle, its `openssl_cafile` points at a `cert.pem` that does not exist, and TLS verification
|
|
232
|
+
fails in 0.05s. Not a timeout — no timeout value would have changed it.
|
|
233
|
+
|
|
234
|
+
Costs one bounded GET (1.5s ceiling, fails open), the same request the first run of any
|
|
235
|
+
session already makes, so this adds no capability doctor did not already exercise.
|
|
236
|
+
|
|
237
|
+
Skipped (with a SKIP row, not red) when ``enabled`` is False or when the ``CI`` environment
|
|
238
|
+
variable is set — the same gate ``check_for_update`` applies to the background notice.
|
|
239
|
+
A disabled check cannot be meaningfully diagnosed, and CI environments rarely have a human
|
|
240
|
+
to act on the result.
|
|
241
|
+
"""
|
|
242
|
+
import os
|
|
243
|
+
|
|
244
|
+
from syncade.update_check import _parse, _text, manifest_once
|
|
245
|
+
|
|
246
|
+
if not enabled or os.environ.get("CI"):
|
|
247
|
+
return DoctorCheck(
|
|
248
|
+
"update check",
|
|
249
|
+
_SKIP,
|
|
250
|
+
"skipped: update check is disabled (check=false or CI is set)",
|
|
251
|
+
)
|
|
252
|
+
manifest = manifest_once(enabled=True) # `enabled` already gated above
|
|
253
|
+
if manifest is None:
|
|
254
|
+
return DoctorCheck(
|
|
255
|
+
"update check",
|
|
256
|
+
_RED,
|
|
257
|
+
"cannot reach the update manifest — new releases will never be announced",
|
|
258
|
+
"on macOS python.org builds, run `Install Certificates.command` from your "
|
|
259
|
+
"Python's Applications folder; otherwise check network/proxy access to "
|
|
260
|
+
"raw.githubusercontent.com",
|
|
261
|
+
)
|
|
262
|
+
latest = _text(manifest.get("latest"))
|
|
263
|
+
if _parse(latest) is None:
|
|
264
|
+
return DoctorCheck("update check", _RED, f"manifest reachable but unusable: {latest!r}")
|
|
265
|
+
return DoctorCheck("update check", _OK, f"reachable; latest release is {latest}")
|
|
@@ -18,11 +18,13 @@ could stop a run would be a remote kill switch, and a cheap one — one JSON lin
|
|
|
18
18
|
write plus a release plus a publish, and needing no action from the operator to take effect.
|
|
19
19
|
Print-only means the worst a compromised manifest can do is lie, which is visible and survivable.
|
|
20
20
|
|
|
21
|
-
**
|
|
22
|
-
rule is that a later invocation in the same
|
|
23
|
-
checked BEFORE the fetch, deliberately — marking
|
|
24
|
-
network cost a 1.5s stall on every invocation for
|
|
25
|
-
at most one missed notice.
|
|
21
|
+
**The background check pings ONCE PER SESSION via ``check_for_update``.** Not once per
|
|
22
|
+
invocation and not once per N hours: the operator's rule is that a later invocation in the same
|
|
23
|
+
window must not re-ping. The session is marked checked BEFORE the fetch, deliberately — marking
|
|
24
|
+
after a *successful* fetch would make a flaky network cost a 1.5s stall on every invocation for
|
|
25
|
+
the whole session, where marking first costs at most one missed notice. Explicit operator
|
|
26
|
+
invocations (``--update``, ``--doctor``) bypass the session gate and make their own fetch — they
|
|
27
|
+
are diagnostic tools, not background checks, and the docs say so.
|
|
26
28
|
|
|
27
29
|
Severity cannot come from PyPI's JSON, which has no such field. It comes from a manifest we host,
|
|
28
30
|
so a version can be marked critical the day a hole is found rather than when the fix ships.
|
|
@@ -239,6 +241,36 @@ def _exclusive_check_and_mark(path: Path, key: str) -> bool:
|
|
|
239
241
|
return _mark_checked(path, key)
|
|
240
242
|
|
|
241
243
|
|
|
244
|
+
#: Sentinel distinguishing "not fetched yet" from a fetch that legitimately returned ``None``.
|
|
245
|
+
_UNFETCHED = object()
|
|
246
|
+
_manifest_cache: object = _UNFETCHED
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def manifest_once(*, enabled: bool) -> dict | None:
|
|
250
|
+
"""The update manifest, fetched AT MOST ONCE per process. ``None`` when disabled or unreadable.
|
|
251
|
+
|
|
252
|
+
THE single egress point for the manifest. Three callers want it — the startup notice,
|
|
253
|
+
``--update`` and ``--doctor`` — and before this each fetched independently, so a CLI
|
|
254
|
+
``--doctor`` made two requests and ``--update`` fetched even with ``[update] check = false``.
|
|
255
|
+
That made `README`'s "one network call of its own, suppressible" FALSE, which is a published
|
|
256
|
+
privacy claim rather than a tidiness point. A blind panel raised it in all four rounds of one
|
|
257
|
+
dogfood and three producer attempts each fixed a different facet, because the fix is to remove
|
|
258
|
+
the duplication, not to patch each site.
|
|
259
|
+
|
|
260
|
+
``enabled`` is a PARAMETER, not read here: this module is a stdlib-only leaf and importing the
|
|
261
|
+
config surface to answer it would end that. Callers hold the config; this holds the socket.
|
|
262
|
+
|
|
263
|
+
A failed fetch is cached too. A syncade process is short-lived, and retrying inside one would
|
|
264
|
+
reintroduce exactly the multiplication this exists to prevent.
|
|
265
|
+
"""
|
|
266
|
+
global _manifest_cache
|
|
267
|
+
if not enabled:
|
|
268
|
+
return None
|
|
269
|
+
if _manifest_cache is _UNFETCHED:
|
|
270
|
+
_manifest_cache = _fetch(MANIFEST_URL)
|
|
271
|
+
return _manifest_cache
|
|
272
|
+
|
|
273
|
+
|
|
242
274
|
def check_for_update(installed: str, *, enabled: bool = True) -> UpdateNotice | None:
|
|
243
275
|
"""One ping per session, or ``None``. Never raises, never blocks, never touches exit codes.
|
|
244
276
|
|
|
@@ -255,7 +287,7 @@ def check_for_update(installed: str, *, enabled: bool = True) -> UpdateNotice |
|
|
|
255
287
|
# first invocations from all fetching simultaneously.
|
|
256
288
|
if not _exclusive_check_and_mark(path, key):
|
|
257
289
|
return None
|
|
258
|
-
return evaluate(
|
|
290
|
+
return evaluate(manifest_once(enabled=True), installed)
|
|
259
291
|
except Exception: # noqa: BLE001
|
|
260
292
|
# The outermost guarantee, not a duplicate of the inner ones: `Path.home()` raises
|
|
261
293
|
# RuntimeError with no $HOME and no passwd entry, and this runs on the review path where
|