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