syncade 0.6.3__tar.gz → 0.7.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {syncade-0.6.3/src/syncade.egg-info → syncade-0.7.1}/PKG-INFO +8 -8
- {syncade-0.6.3 → syncade-0.7.1}/README.md +7 -7
- {syncade-0.6.3 → syncade-0.7.1}/docs/config-reference.md +6 -1
- {syncade-0.6.3 → syncade-0.7.1}/docs/how-to-use.md +4 -3
- {syncade-0.6.3 → syncade-0.7.1}/pyproject.toml +2 -1
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/__init__.py +1 -1
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/cli/gc_mode.py +9 -1
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/cli/metrics_mode.py +81 -2
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/cli/parser.py +18 -15
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/cli/update_mode.py +114 -1
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/config_gc.py +18 -1
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/gc.py +58 -1
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/gc_execute.py +12 -1
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/gc_types.py +13 -2
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/metrics/aggregate.py +42 -21
- syncade-0.7.1/src/syncade/metrics/findings.py +358 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/metrics/schema.py +134 -2
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/orchestrator/reviewer_template_failure.py +1 -0
- {syncade-0.6.3 → syncade-0.7.1/src/syncade.egg-info}/PKG-INFO +8 -8
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade.egg-info/SOURCES.txt +8 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/cli/test_config_menu_rows.py +5 -1
- {syncade-0.6.3 → syncade-0.7.1}/tests/cli/test_config_overrides.py +1 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/cli/test_documented_flags_exist.py +34 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/cli/test_gc.py +3 -3
- {syncade-0.6.3 → syncade-0.7.1}/tests/cli/test_help_retention_contract.py +16 -4
- {syncade-0.6.3 → syncade-0.7.1}/tests/cli/test_update_mode.py +1 -1
- syncade-0.7.1/tests/cli/test_update_post_condition.py +398 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/config/test_worktree_base_config.py +1 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/conftest.py +36 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/gc/test_execute.py +17 -17
- {syncade-0.6.3 → syncade-0.7.1}/tests/gc/test_plan.py +13 -13
- syncade-0.7.1/tests/gc/test_retention_tiers.py +121 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/gc/test_self_orphan_protection.py +10 -6
- syncade-0.7.1/tests/gc/test_worktree_age_bound.py +300 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/metrics/test_actor_spend.py +1 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/metrics/test_aggregate.py +19 -4
- syncade-0.7.1/tests/metrics/test_consensus_report.py +428 -0
- syncade-0.7.1/tests/metrics/test_findings_capture.py +437 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/metrics/test_schema.py +70 -0
- syncade-0.7.1/tests/metrics/test_unknown_panel.py +286 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/conftest.py +8 -35
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/test_template_robustness.py +55 -0
- syncade-0.7.1/tests/test_worktree_base_isolation.py +58 -0
- {syncade-0.6.3 → syncade-0.7.1}/LICENSE +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/MANIFEST.in +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/setup.cfg +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/__main__.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/adapters/__init__.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/adapters/anthropic.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/adapters/base.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/adapters/fake.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/adapters/fake_common.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/adapters/fake_producer_audit_draft.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/adapters/fake_reviewer_synth.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/adapters/openai.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/adapters/openai_parsing.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/adapters/producer.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/adapters/producer_anthropic.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/adapters/producer_openai.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/adapters/registry.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/auth_check.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/auth_preflight.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/base_resolution.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/billing.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/checks_config.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/cli/__init__.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/cli/auth_gate.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/cli/config_keys.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/cli/config_list.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/cli/config_menu_rows.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/cli/config_mode.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/cli/config_overrides.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/cli/config_tui.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/cli/doctor_mode.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/cli/install_skill.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/cli/modes.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/cli/parser_types.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/cli/paths.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/cli/preflight_paths.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/cli/resolve.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/cli/resume_mode.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/cli/skill_status.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/cli/toml_writer.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/cli/update_notice.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/cli/validate.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/config.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/config_auth.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/config_cold.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/config_loader.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/config_loop.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/config_producer.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/config_retry.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/config_types.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/config_update.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/diff_filter.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/dispatcher.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/doctor.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/doctor_env.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/doctor_preview.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/doctor_types.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/exit_codes.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/findings.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/findings_json.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/gc_protection.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/gc_worktrees.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/git_object_id.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/git_preconditions.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/logging.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/metrics/__init__.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/orchestrator/__init__.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/orchestrator/_runs_dir.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/orchestrator/branch_advance.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/orchestrator/branch_guard.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/orchestrator/budget.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/orchestrator/escalation_coverage.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/orchestrator/loop.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/orchestrator/loop_dispatch_check.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/orchestrator/loop_finalize.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/orchestrator/loop_preflight.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/orchestrator/loop_resume.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/orchestrator/loop_rmtree.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/orchestrator/loop_round_step.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/orchestrator/prior_round.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/orchestrator/producer_phase.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/orchestrator/results.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/orchestrator/resume.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/orchestrator/resume_load.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/orchestrator/resume_plan.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/orchestrator/resume_target.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/orchestrator/resume_types.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/orchestrator/round.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/orchestrator/round_checks.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/orchestrator/round_no_changes.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/orchestrator/round_predispatch.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/orchestrator/verdict.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/persistence/__init__.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/persistence/_atomic.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/persistence/_clusters.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/persistence/_findings_verdict.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/persistence/_markdown.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/persistence/_validation.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/persistence/checks.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/persistence/decision_needed.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/persistence/findings_md.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/persistence/handoff.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/persistence/handoff_classify.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/persistence/last_reviewed.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/persistence/loop_manifest.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/persistence/loop_summary.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/persistence/loop_summary_text.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/persistence/producer.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/persistence/reviewer.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/persistence/round_manifest.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/persistence/run_init.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/persistence/run_summary.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/persistence/run_summary_next_steps.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/persistence/synth.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/persistence/test_run.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/presets.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/pricing_config.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/process.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/producer.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/producer_attempt.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/producer_escalation.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/producer_git.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/producer_result.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/prompts.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/prompts_loader.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/retry.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/run_inputs.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/run_status.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/selfcheck.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/skills/claude/README.md +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/skills/claude/SKILL.md +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/skills/codex/README.md +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/skills/codex/SKILL.md +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/snapshot.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/spec_audit.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/spec_audit_schema.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/spec_draft.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/spec_source.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/synthesis.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/synthesis_clusters.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/synthesis_repair.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/synthesis_schema.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/synthesizer/__init__.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/synthesizer/constants.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/synthesizer/driver.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/synthesizer/rendering.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/synthesizer/result.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/synthesizer/validation.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/synthesizer/workspace.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/templates/presets/balanced.toml +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/templates/presets/cheap.toml +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/templates/presets/thorough.toml +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/templates/producer.md +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/templates/reviewer.md +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/templates/reviewer_adversarial.md +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/templates/reviewer_codex.md +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/templates/spec_audit.md +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/templates/spec_draft.md +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/templates/synthesizer.md +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/test_runner.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/test_runner_classify.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/transcript.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/update_check.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/usage.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/worktree.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/worktree_env.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade/worktree_paths.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade.egg-info/dependency_links.txt +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade.egg-info/entry_points.txt +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade.egg-info/requires.txt +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/src/syncade.egg-info/top_level.txt +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/__init__.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/adapters/__init__.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/adapters/_anthropic_helpers.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/adapters/_openai_helpers.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/adapters/test_anthropic.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/adapters/test_anthropic_more.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/adapters/test_openai.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/adapters/test_openai_more.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/adapters/test_producer.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/adapters/test_producer_more.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/adapters/test_prompt_transport.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/auth_check/__init__.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/auth_check/test_probes.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/auth_check/test_run_auth_check.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/cli/__init__.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/cli/_helpers.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/cli/_undo_helpers.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/cli/test_basic_and_mutex.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/cli/test_cli_library_divergence.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/cli/test_config_keys.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/cli/test_config_list.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/cli/test_config_mode.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/cli/test_config_tui.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/cli/test_config_tui_pty.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/cli/test_documented_exit_codes.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/cli/test_flags.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/cli/test_mode_exclusivity_matrix.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/cli/test_mode_flag_guards.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/cli/test_resume_reports_hard_kill.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/cli/test_run_and_config.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/cli/test_run_status_cli.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/cli/test_scope_openspec_draft.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/cli/test_toml_writer.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/cli/test_undo_auto_init.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/cli/test_undo_scope.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/cli/test_update_notice.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/cli/test_validate_before_mutate.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/config/__init__.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/config/test_auth_enforcement.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/config/test_auth_modes.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/config/test_auth_preflight.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/config/test_auth_report_scope.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/config/test_budget_config.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/config/test_cold_actors.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/config/test_config_checks.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/config/test_config_reference_drift.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/config/test_config_schema.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/config/test_gc_config.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/config/test_global_config_layer.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/config/test_presets.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/config/test_retry_config.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/config/test_reviewer_config.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/config/test_reviewer_permissions.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/dispatcher/__init__.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/dispatcher/_helpers.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/dispatcher/test_dispatch_core.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/dispatcher/test_dispatch_misc.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/dispatcher/test_dispatch_usage.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/doctor/__init__.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/doctor/_helpers.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/doctor/conftest.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/doctor/test_doctor.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/doctor/test_doctor_diff_size.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/doctor/test_doctor_malformed_diff.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/doctor/test_doctor_preview.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/findings/__init__.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/findings/_helpers.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/findings/test_parser_and_fields.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/findings/test_parser_resilience.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/findings/test_verdict_block_selection.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/gc/__init__.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/gc/_helpers.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/gc/test_autoprune.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/gc/test_plan_execution_races.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/gc/test_reaping_scope.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/gc/test_robustness.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/gc/test_smoke.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/gc/test_two_tier_retention.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/logging/__init__.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/logging/_helpers.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/logging/test_logging.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/logging/test_logging_summary.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/metrics/conftest.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/metrics/test_billing_reconciliation.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/metrics/test_billing_surfaces_agree.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/metrics/test_budget_metrics.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/metrics/test_cost_honesty.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/metrics/test_metrics_cli.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/metrics/test_metrics_mode.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/__init__.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/_helpers.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/_resume_fixtures.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/test_budget.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/test_budget_resume_e2e.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/test_budget_stop_message.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/test_budget_warning.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/test_checks_and_recording.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/test_contracts.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/test_cross_round.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/test_cross_round_more.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/test_default_branch_guard.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/test_diff_size_cap.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/test_dispatch_record.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/test_empty_diff.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/test_empty_diff_no_side_effects.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/test_escalation.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/test_last_reviewed_truthful.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/test_loop_audit_remediation.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/test_matrix_and_e2e.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/test_multiround.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/test_multiround_dirty.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/test_multiround_more.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/test_paraphrase_escalation.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/test_pipeline.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/test_provider_usage_limit.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/test_rename_boundary.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/test_resume.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/test_resume_base_oid.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/test_reviewer_blindness_is_structural.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/test_round_input_boundaries.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/test_run_layout.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/test_run_status_wiring.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/test_safety_warning_survives_quiet.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/test_snapshot_and_base.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/test_synth_phase.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/test_synth_phase_more.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/test_synth_provenance.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/test_synth_validation.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/test_termination_reason_drift.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/test_testleg.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/test_timeout_is_per_subprocess.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/test_usage_limit_message.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/orchestrator/test_worktree_cleanup.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/persistence/__init__.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/persistence/_helpers.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/persistence/test_atomic_writes.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/persistence/test_checks_surfacing.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/persistence/test_diff_size_recording.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/persistence/test_findings_md.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/persistence/test_findings_md_more.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/persistence/test_findings_md_verdict.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/persistence/test_handoff.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/persistence/test_loop_manifest_checks.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/persistence/test_loop_summary.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/persistence/test_loop_summary_more.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/persistence/test_md_safety.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/persistence/test_next_steps.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/persistence/test_producer_persist.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/persistence/test_reviewer.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/persistence/test_round_manifest.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/persistence/test_run_init.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/persistence/test_run_summary.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/persistence/test_run_summary_more.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/persistence/test_run_summary_producer.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/persistence/test_synth_persist.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/persistence/test_test_run.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/producer/__init__.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/producer/_helpers.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/producer/test_producer.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/producer/test_producer_confinement.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/producer/test_producer_retry.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/producer/test_producer_run.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/producer/test_producer_timeout.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/producer/test_producer_usage.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/prompts/__init__.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/prompts/test_load_template.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/prompts/test_misc.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/prompts/test_producer.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/prompts/test_render_synthesizer.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/prompts/test_synthesizer.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/resume/__init__.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/resume/_helpers.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/resume/test_budget_resume.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/resume/test_checks_phase_failure.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/resume/test_cli_malformed_rehydration.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/resume/test_find_resolve_plan.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/resume/test_plan_resume.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/resume/test_round_trip.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/resume/test_tree_drift_and_escalation.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/skills/test_config_section.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/skills/test_install_skill.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/skills/test_install_skill_multi_harness.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/skills/test_install_skill_ownership.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/skills/test_install_skill_refuses.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/skills/test_skill_drift.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/skills/test_skill_status.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/smoke/__init__.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/smoke/test_anthropic_smoke.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/smoke/test_auth_check_smoke.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/smoke/test_codex_smoke.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/smoke/test_loop_smoke.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/smoke/test_orchestrator_smoke.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/smoke/test_orchestrator_smoke_more.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/smoke/test_producer_smoke.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/smoke/test_resume_reap_smoke.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/smoke/test_selfcheck_smoke.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/smoke/test_synthesizer_smoke.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/smoke/test_test_runner_smoke.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/snapshot/__init__.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/snapshot/_helpers.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/snapshot/conftest.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/snapshot/test_snapshot.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/snapshot/test_snapshot_diff_identity.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/snapshot/test_snapshot_dirty.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/snapshot/test_snapshot_warning.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/snapshot/test_three_dot_diff.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/synthesis/__init__.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/synthesis/_helpers.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/synthesis/test_cluster_repair.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/synthesis/test_clusters.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/synthesis/test_models.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/synthesis/test_models_distinct_keying.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/synthesis/test_parsing.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/synthesis/test_unanimous_blocker_downgrade.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/synthesizer/test_provenance_fabrication.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/synthesizer/test_provenance_repair.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_adapter_registry.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_adapters_fake.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_auto_init_policy.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_base_resolution.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_check_loc_script.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_checks.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_config_loader.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_cross_model_claim.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_diff_filter.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_diff_filter_binary.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_diff_filter_quoted_paths.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_escalation_coverage.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_exit_codes.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_git_choke_point.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_git_config_isolation.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_git_preconditions.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_last_reviewed.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_parse_repair_extra_keys.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_pricing.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_private_identifier_gate.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_process.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_process_teardown.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_producer_escalation.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_regression_acme.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_retry.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_run_status.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_run_status_signals.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_runner/__init__.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_runner/test_test_runner.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_runner/test_test_runner_more.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_safety_disclosures_survive_quiet.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_selfcheck.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_spec_audit.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_spec_draft.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_spec_source.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_streamed_capture.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_strip_list_semantics.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_synthesizer.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_synthesizer_passthrough.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_synthesizer_provenance.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_transcript.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_update_check.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_usage.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_usage_persistence.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_workflow_action_pins.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_worktree_env.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/test_worktree_strip_guard.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/worktree/__init__.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/worktree/_helpers.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/worktree/conftest.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/worktree/test_worktree.py +0 -0
- {syncade-0.6.3 → syncade-0.7.1}/tests/worktree/test_worktree_cleanup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: syncade
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.1
|
|
4
4
|
Summary: External blind multi-judge review orchestrator for AI-assisted coding
|
|
5
5
|
Author-email: Syncade <build@syncade.ai>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -287,7 +287,7 @@ syncade runs AI coding-agent CLIs with **elevated tool access on your repo**:
|
|
|
287
287
|
- **Reviewers** run headless inside throwaway worktree copies under `/tmp/syncade/` (Codex reviewers are additionally sandboxed to theirs).
|
|
288
288
|
- **The producer** runs unsandboxed and **commits to your current branch** (syncade refuses the default branch by default).
|
|
289
289
|
- 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 **
|
|
290
|
+
- syncade makes **one network call of its own**: a once-per-session GET to its public update manifest to check for a newer version (suppressible via `[update] check = false`). `syncade --update` additionally invokes your package manager (`uv tool upgrade`, `pipx upgrade`, or `pip install -U`) — operator-requested and only when you run that flag. All other egress is the provider CLIs you already authenticated and the test/check commands you configure.
|
|
291
291
|
|
|
292
292
|
Running syncade is comparable to running the target repo's `Makefile` — it executes code
|
|
293
293
|
with your permissions. See **[SECURITY.md](SECURITY.md)** for the full threat model and how
|
|
@@ -302,12 +302,12 @@ Early access. These are measured, not suspected.
|
|
|
302
302
|
Both findings were real. If a SHIP arrives over code you did not change since a NO-SHIP, treat
|
|
303
303
|
it as the weaker signal — compare the verdict to what the *code* changed, not to the previous
|
|
304
304
|
verdict.
|
|
305
|
-
- **NO-SHIP runs keep their worktrees
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
directory does not remove git's registration of it.
|
|
305
|
+
- **Worktrees accumulate under `<worktree_base>`.** NO-SHIP runs keep their worktrees for
|
|
306
|
+
inspection. `syncade --gc` removes worktrees once a run can no longer plausibly be resumed —
|
|
307
|
+
controlled by `gc.worktree_max_age_days` (default 14 days), which applies even to runs that are
|
|
308
|
+
technically still resume-eligible. It reached 4.4 GB on this machine before a cleanup. Point
|
|
309
|
+
`worktree_base` somewhere you do not mind, and run `git worktree prune` after manual removals:
|
|
310
|
+
removing the directory does not remove git's registration of it.
|
|
311
311
|
- **A hard-killed run keeps almost all of what its reviewers had written.** Reviewer stdout and
|
|
312
312
|
stderr are copied to `<round>/<name>.stdout` / `<round>/<name>.stderr` as the child produces
|
|
313
313
|
them, rather than held in memory until it exits, so a run ended by `SIGKILL` (or a machine
|
|
@@ -257,7 +257,7 @@ syncade runs AI coding-agent CLIs with **elevated tool access on your repo**:
|
|
|
257
257
|
- **Reviewers** run headless inside throwaway worktree copies under `/tmp/syncade/` (Codex reviewers are additionally sandboxed to theirs).
|
|
258
258
|
- **The producer** runs unsandboxed and **commits to your current branch** (syncade refuses the default branch by default).
|
|
259
259
|
- 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 **
|
|
260
|
+
- syncade makes **one network call of its own**: a once-per-session GET to its public update manifest to check for a newer version (suppressible via `[update] check = false`). `syncade --update` additionally invokes your package manager (`uv tool upgrade`, `pipx upgrade`, or `pip install -U`) — operator-requested and only when you run that flag. All other egress is the provider CLIs you already authenticated and the test/check commands you configure.
|
|
261
261
|
|
|
262
262
|
Running syncade is comparable to running the target repo's `Makefile` — it executes code
|
|
263
263
|
with your permissions. See **[SECURITY.md](SECURITY.md)** for the full threat model and how
|
|
@@ -272,12 +272,12 @@ Early access. These are measured, not suspected.
|
|
|
272
272
|
Both findings were real. If a SHIP arrives over code you did not change since a NO-SHIP, treat
|
|
273
273
|
it as the weaker signal — compare the verdict to what the *code* changed, not to the previous
|
|
274
274
|
verdict.
|
|
275
|
-
- **NO-SHIP runs keep their worktrees
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
directory does not remove git's registration of it.
|
|
275
|
+
- **Worktrees accumulate under `<worktree_base>`.** NO-SHIP runs keep their worktrees for
|
|
276
|
+
inspection. `syncade --gc` removes worktrees once a run can no longer plausibly be resumed —
|
|
277
|
+
controlled by `gc.worktree_max_age_days` (default 14 days), which applies even to runs that are
|
|
278
|
+
technically still resume-eligible. It reached 4.4 GB on this machine before a cleanup. Point
|
|
279
|
+
`worktree_base` somewhere you do not mind, and run `git worktree prune` after manual removals:
|
|
280
|
+
removing the directory does not remove git's registration of it.
|
|
281
281
|
- **A hard-killed run keeps almost all of what its reviewers had written.** Reviewer stdout and
|
|
282
282
|
stderr are copied to `<round>/<name>.stdout` / `<round>/<name>.stderr` as the child produces
|
|
283
283
|
them, rather than held in memory until it exits, so a run ended by `SIGKILL` (or a machine
|
|
@@ -222,13 +222,18 @@ any spend.
|
|
|
222
222
|
## `[gc]` — `GcConfig`
|
|
223
223
|
|
|
224
224
|
Governs both the per-loop auto-prune and an explicit `syncade --gc`. Run directories are never
|
|
225
|
-
deleted
|
|
225
|
+
deleted. `keep` / `max_age_days` prune bulky subprocess transcripts; `worktree_max_age_days`
|
|
226
|
+
removes stale worktrees under `worktree_base`, which is a separate retention tier because a
|
|
227
|
+
worktree is reconstructible from a recorded SHA. CLI `--gc-keep` / `--gc-max-age-days` override
|
|
228
|
+
the first two; the worktree bound is config-only (set it with
|
|
229
|
+
`syncade --config set gc.worktree_max_age_days N`).
|
|
226
230
|
|
|
227
231
|
<!-- config-fields: GcConfig -->
|
|
228
232
|
| Field | Type | Default | What it does |
|
|
229
233
|
|---|---|---|---|
|
|
230
234
|
| `keep` | int ≥ 0 | `20` | Newest N runs whose transcripts are always kept. |
|
|
231
235
|
| `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. |
|
|
236
|
+
| `worktree_max_age_days` | int ≥ 0 | `14` | Days a run's **worktree** is kept, after which it is removed — including while the run is still resume-eligible. This is the ONLY rule that selects a worktree: one is never removed because its transcripts became prunable, so a young worktree you are still inspecting survives a busy week of runs. Separate from `max_age_days`, which gates transcripts only — a worktree is reconstructible from the SHA the run records, so removing one costs a `git worktree add` and never history. `0` disables the bound and restores the previous never-ending protection. |
|
|
232
237
|
|
|
233
238
|
## `[update]` — `UpdateConfig`
|
|
234
239
|
|
|
@@ -450,9 +450,10 @@ below** before you commit anything yourself.
|
|
|
450
450
|
- **Loop mode refuses a tracked-modified dirty tree** unless you pass `--force-dirty`. Single-pass
|
|
451
451
|
review does not refuse: reviewers see committed HEAD, so uncommitted work is invisible to them.
|
|
452
452
|
- **`<worktree_base>` grows.** NO-SHIP runs keep their worktrees so you can inspect what a reviewer
|
|
453
|
-
saw
|
|
454
|
-
|
|
455
|
-
|
|
453
|
+
saw. `syncade --gc` removes worktrees once a run can no longer plausibly be resumed, controlled by
|
|
454
|
+
`gc.worktree_max_age_days` (default 14 days) — this applies even to runs that remain
|
|
455
|
+
resume-eligible. It reached 4.4 GB on this machine before a cleanup. Point `worktree_base`
|
|
456
|
+
somewhere you don't mind and run `git worktree prune` after manual removals — removing the
|
|
456
457
|
directory does not remove git's registration of it.
|
|
457
458
|
- **Run artifacts are never deleted**, only transcripts are pruned. `syncade --metrics` is derived
|
|
458
459
|
from that tree and would lose your history otherwise.
|
|
@@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta"
|
|
|
9
9
|
|
|
10
10
|
[project]
|
|
11
11
|
name = "syncade"
|
|
12
|
-
version = "0.
|
|
12
|
+
version = "0.7.1"
|
|
13
13
|
description = "External blind multi-judge review orchestrator for AI-assisted coding"
|
|
14
14
|
readme = "README.md"
|
|
15
15
|
requires-python = ">=3.11"
|
|
@@ -73,6 +73,7 @@ syncade = ["templates/*.md", "templates/presets/*.toml", "skills/*/*.md"]
|
|
|
73
73
|
testpaths = ["tests"]
|
|
74
74
|
markers = [
|
|
75
75
|
"smoke: end-to-end integration tests that shell out to real provider CLIs (opt-in via `pytest -m smoke`)",
|
|
76
|
+
"real_worktree_base: opt out of the autouse worktree-base isolation, for the drift tests that must assert the REAL default (PR-h-12 item 1b)",
|
|
76
77
|
]
|
|
77
78
|
addopts = "-m 'not smoke'"
|
|
78
79
|
|
|
@@ -70,7 +70,15 @@ def _run_gc(args) -> int:
|
|
|
70
70
|
# worktree_base: clean the base the runs actually use (config.worktree_base / --worktree-base),
|
|
71
71
|
# not the hardcoded default — else a relocated base leaves worktree leftovers uncollected.
|
|
72
72
|
plan = plan_gc(
|
|
73
|
-
repo_root,
|
|
73
|
+
repo_root,
|
|
74
|
+
keep=keep,
|
|
75
|
+
max_age_days=max_age_days,
|
|
76
|
+
# Tier 3 has no CLI flag on purpose: it is a machine-level retention preference set once,
|
|
77
|
+
# not a per-invocation choice, and `--config set gc.worktree_max_age_days N` already
|
|
78
|
+
# reaches it. Another flag would need parser, validation, docs and two drift-test entries
|
|
79
|
+
# for a knob nobody tunes twice.
|
|
80
|
+
worktree_max_age_days=config.gc.worktree_max_age_days,
|
|
81
|
+
worktree_base=config.worktree_base,
|
|
74
82
|
)
|
|
75
83
|
report = execute_gc(plan, dry_run=args.gc_dry_run, repo_root=repo_root)
|
|
76
84
|
|
|
@@ -17,6 +17,7 @@ from syncade import billing
|
|
|
17
17
|
from syncade.billing import Billing
|
|
18
18
|
from syncade.exit_codes import SUCCESS, WORKTREE_ERROR
|
|
19
19
|
from syncade.metrics.aggregate import backfill
|
|
20
|
+
from syncade.metrics.findings import blocker_curve, consensus_stats
|
|
20
21
|
from syncade.metrics.schema import open_db
|
|
21
22
|
from syncade.snapshot import SnapshotError, discover_repo_root
|
|
22
23
|
|
|
@@ -238,7 +239,10 @@ def render_report(conn: sqlite3.Connection, last_n: int | None = None) -> str:
|
|
|
238
239
|
"SELECT COALESCE(SUM(blockers),0), COALESCE(SUM(minors),0), COALESCE(SUM(nits),0), "
|
|
239
240
|
"COALESCE(SUM(dismissed),0) FROM runs"
|
|
240
241
|
).fetchone()
|
|
241
|
-
rounds
|
|
242
|
+
# From `rounds`, not `runs.rounds_executed`: the latter comes from loop-manifest.json, so an
|
|
243
|
+
# interrupted run with perfectly good round artifacts reported "rounds: 0 total" while the
|
|
244
|
+
# blocker curve simultaneously knew round 0 had happened.
|
|
245
|
+
rounds = conn.execute("SELECT COUNT(*) FROM rounds").fetchone()[0]
|
|
242
246
|
handoffs, decisions = conn.execute(
|
|
243
247
|
"SELECT COALESCE(SUM(handoff),0), COALESCE(SUM(decision_needed),0) FROM runs"
|
|
244
248
|
).fetchone()
|
|
@@ -294,11 +298,86 @@ def render_report(conn: sqlite3.Connection, last_n: int | None = None) -> str:
|
|
|
294
298
|
if reviewer_rows:
|
|
295
299
|
lines.append(" reviewers by model (findings/wall-clock):")
|
|
296
300
|
for provider, model, findings, run_count, wall, tok in reviewer_rows:
|
|
297
|
-
|
|
301
|
+
# A row with no model is reported as UNKNOWN, never as a bare provider. A bare
|
|
302
|
+
# provider reads like a panel that had no model, which is a claim we cannot make;
|
|
303
|
+
# "unknown" says the thing that is actually true. (No such row exists in this
|
|
304
|
+
# corpus today — every recorded panel carries both — but the renderer must not be
|
|
305
|
+
# the reason a future one silently looks answered.)
|
|
306
|
+
label = f"{provider}/{model}" if model else f"{provider or '?'}/unknown"
|
|
298
307
|
lines.append(
|
|
299
308
|
f" {label:24} findings={findings} runs={run_count} "
|
|
300
309
|
f"wall={int(wall)}s tok={int(tok)}"
|
|
301
310
|
)
|
|
311
|
+
# Rounds whose panel was never recorded are INVISIBLE in the table above, which is worse
|
|
312
|
+
# than being wrong: anyone dividing findings by runs gets a denominator quietly missing
|
|
313
|
+
# them. They come from runs interrupted before the loop terminator wrote a manifest, so
|
|
314
|
+
# nothing ever summarised their reviewers. Never guessed — the configured roster is not
|
|
315
|
+
# evidence of what a killed run actually dispatched.
|
|
316
|
+
#
|
|
317
|
+
# DO NOT write a measured count here. This comment said "all 49" until the release audit
|
|
318
|
+
# found the line below printing 22: excluding known-empty panels changed the number and
|
|
319
|
+
# the prose kept the old one. A count in a comment is a measurement with no test reading
|
|
320
|
+
# it, which is the same defect this release had to fix in its own CHANGELOG.
|
|
321
|
+
|
|
322
|
+
# OUTSIDE the `if reviewer_rows` block on purpose. Nested inside it, a corpus where EVERY
|
|
323
|
+
# panel is unknown printed no warning at all — the one corpus that most needs it, silent.
|
|
324
|
+
# panel_source='unknown' means no manifest had a `reviewers` key for that round. This is
|
|
325
|
+
# independent of blockers_source: a manifest can record a blocker count without a reviewer
|
|
326
|
+
# list, and a no-dispatch round (no_changes_to_review) has panel_source='recorded' and
|
|
327
|
+
# panel_size=0 — explicitly empty, not unknown.
|
|
328
|
+
unknown_rounds, total_rounds = conn.execute(
|
|
329
|
+
"SELECT COALESCE(SUM(CASE WHEN panel_source = 'unknown' THEN 1 ELSE 0 END), 0), "
|
|
330
|
+
"COUNT(*) FROM rounds"
|
|
331
|
+
).fetchone()
|
|
332
|
+
if unknown_rounds:
|
|
333
|
+
lines.append(
|
|
334
|
+
f" {unknown_rounds} of {total_rounds} rounds record no reviewer panel "
|
|
335
|
+
f"(no manifest evidence; excluded from consensus)"
|
|
336
|
+
)
|
|
337
|
+
manifest_only, unknown_only = conn.execute(
|
|
338
|
+
"SELECT "
|
|
339
|
+
"COALESCE(SUM(CASE WHEN blockers_source='manifest' THEN 1 ELSE 0 END), 0), "
|
|
340
|
+
"COALESCE(SUM(CASE WHEN blockers_source='unknown' THEN 1 ELSE 0 END), 0) "
|
|
341
|
+
"FROM rounds "
|
|
342
|
+
"WHERE NOT (panel_source='recorded' AND panel_size=0 AND blockers=0)"
|
|
343
|
+
).fetchone()
|
|
344
|
+
if manifest_only:
|
|
345
|
+
lines.append(
|
|
346
|
+
f" {manifest_only} of {total_rounds} rounds have no synthesizer artifact; their "
|
|
347
|
+
f"blocker counts come from round manifests and carry no provenance"
|
|
348
|
+
)
|
|
349
|
+
if unknown_only:
|
|
350
|
+
lines.append(
|
|
351
|
+
f" {unknown_only} of {total_rounds} rounds have no blocker evidence "
|
|
352
|
+
f"(killed/interrupted before any artifact or manifest); "
|
|
353
|
+
f"excluded from the blocker curve"
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
consensus = consensus_stats(conn)
|
|
357
|
+
if consensus is not None:
|
|
358
|
+
total, single = consensus
|
|
359
|
+
lines.append(
|
|
360
|
+
f" consensus: {single} of {total} active blockers ({single / total:.0%}) were "
|
|
361
|
+
f"raised by exactly ONE reviewer"
|
|
362
|
+
)
|
|
363
|
+
lines.append(" (over runs with a multi-reviewer panel; what a second reviewer earns)")
|
|
364
|
+
|
|
365
|
+
curve = blocker_curve(conn)
|
|
366
|
+
if curve:
|
|
367
|
+
# Density, never the raw total alone. The blocker count falls steeply across rounds and
|
|
368
|
+
# reads as convergence; it is SURVIVORSHIP — fewer runs reach each round, so the
|
|
369
|
+
# denominator falls with it. This repo published the raw sequence as convergence once
|
|
370
|
+
# and had to correct it, so the renderer shows both numbers and the per-round rate.
|
|
371
|
+
lines.append(
|
|
372
|
+
" blockers by round (artifact- or manifest-backed only; "
|
|
373
|
+
"killed runs with no evidence excluded):"
|
|
374
|
+
)
|
|
375
|
+
for round_index, blockers, rounds in curve:
|
|
376
|
+
density = blockers / rounds if rounds else 0.0
|
|
377
|
+
lines.append(
|
|
378
|
+
f" round {round_index}: {blockers} blockers over {rounds} rounds "
|
|
379
|
+
f"({density:.2f}/round)"
|
|
380
|
+
)
|
|
302
381
|
|
|
303
382
|
if last_n:
|
|
304
383
|
ids = [
|
|
@@ -381,22 +381,25 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
381
381
|
parser.add_argument(
|
|
382
382
|
"--gc",
|
|
383
383
|
action="store_true",
|
|
384
|
-
help="Run a one-shot maintenance pass. Retention is
|
|
384
|
+
help="Run a one-shot maintenance pass. Retention is FOUR-TIER: run "
|
|
385
385
|
"history is kept FOREVER (loop-manifest.json, findings.md, run-init.json, "
|
|
386
|
-
"round manifests, summaries)
|
|
387
|
-
"
|
|
388
|
-
"
|
|
389
|
-
"
|
|
390
|
-
"
|
|
391
|
-
"
|
|
392
|
-
"
|
|
393
|
-
"
|
|
394
|
-
"
|
|
395
|
-
"
|
|
396
|
-
"
|
|
397
|
-
"
|
|
398
|
-
"
|
|
399
|
-
"
|
|
386
|
+
"round manifests, summaries); transcripts (round-*/*.stdout, *.stderr - "
|
|
387
|
+
"90%% of the corpus) are pruned beyond --gc-keep and any --gc-max-age-days "
|
|
388
|
+
"floor; worktrees are removed once a run can no longer plausibly be resumed "
|
|
389
|
+
"(worktrees are reconstructible from the recorded SHA, so removing one "
|
|
390
|
+
"costs a git worktree add, never history) — resume-eligible runs keep their "
|
|
391
|
+
"worktree while younger than gc.worktree_max_age_days (default 14 days, "
|
|
392
|
+
"set via syncade --config set gc.worktree_max_age_days N); "
|
|
393
|
+
"and metrics.db is a droppable derived view. NO run directory is ever "
|
|
394
|
+
"deleted: .syncade/metrics.db is a derived view over .syncade/runs/, so "
|
|
395
|
+
"deleting a run would destroy its history the next time that view rebuilds. "
|
|
396
|
+
"Also removes matching <worktree_base>/<run-id>/ worktree leftovers whose "
|
|
397
|
+
"directory identity still matches the GC plan, and safely reaps orphaned "
|
|
398
|
+
"reviewer/producer subprocesses whose working dir is INSIDE a worktree "
|
|
399
|
+
"being removed. Runs also auto-prune their transcripts at the start of "
|
|
400
|
+
"every fresh loop, so --gc is for worktree/process cleanup and one-off "
|
|
401
|
+
"maintenance rather than routine disk hygiene. Non-run state "
|
|
402
|
+
"(config.toml, last-reviewed.json, draft-spec-*.md) is never touched. "
|
|
400
403
|
"Mutually exclusive with PR_DOC, --selfcheck, --auth-check, "
|
|
401
404
|
"--spec-audit, --draft-spec, --resume, --openspec.",
|
|
402
405
|
)
|
|
@@ -24,8 +24,12 @@ from pathlib import Path
|
|
|
24
24
|
import syncade
|
|
25
25
|
from syncade.exit_codes import SUCCESS, WORKTREE_ERROR
|
|
26
26
|
from syncade.process import SubprocessError, run_subprocess
|
|
27
|
+
from syncade.update_check import MANIFEST_URL, _fetch, _parse, _text
|
|
27
28
|
|
|
28
29
|
_UPGRADE_TIMEOUT = 300.0
|
|
30
|
+
#: Reading one version string out of a fresh interpreter. Generous, because a cold venv
|
|
31
|
+
#: import is slower than it looks; short, because we already hold the operator's terminal.
|
|
32
|
+
_VERSION_TIMEOUT = 60.0
|
|
29
33
|
#: How far up from ``syncade/__init__.py`` an install marker can sit. A venv layout is
|
|
30
34
|
#: ``<root>/lib/python3.x/site-packages/syncade/__init__.py`` — five parents to the root; the
|
|
31
35
|
#: extra headroom costs nothing and covers layouts that nest one deeper.
|
|
@@ -149,6 +153,86 @@ def _resync_skills(out) -> bool:
|
|
|
149
153
|
return all_ok
|
|
150
154
|
|
|
151
155
|
|
|
156
|
+
def _installed_version() -> str | None:
|
|
157
|
+
"""The version on disk NOW, or ``None`` when it cannot be established.
|
|
158
|
+
|
|
159
|
+
It CANNOT be ``syncade.__version__``: this process imported that before the upgrade ran, so
|
|
160
|
+
it is by definition the old number. A fresh interpreter re-reads the metadata the upgrade
|
|
161
|
+
just rewrote, and ``sys.executable`` is the tool venv's own python under both uv and pipx —
|
|
162
|
+
precisely the environment that changed.
|
|
163
|
+
|
|
164
|
+
``None`` means "could not verify", which is deliberately NOT the same as "did not change":
|
|
165
|
+
the caller must not turn an unreadable version into a failure report.
|
|
166
|
+
"""
|
|
167
|
+
try:
|
|
168
|
+
result = run_subprocess(
|
|
169
|
+
# `-I` (isolated) is load-bearing, not tidiness. Without it the probe inherits
|
|
170
|
+
# PYTHONPATH and the user site dir, so a `sitecustomize` on that path runs inside
|
|
171
|
+
# the answer and can emit stdout before OR after the version print.
|
|
172
|
+
# `-I` keeps the venv's own site-packages, so `importlib.metadata` still finds
|
|
173
|
+
# the install (verified) — but it does NOT suppress `sitecustomize.py` that lives
|
|
174
|
+
# in those site-packages. An atexit hook registered there (syncade ships such a
|
|
175
|
+
# shim for worktree imports) can append a version-shaped line AFTER the real one.
|
|
176
|
+
# `os._exit(0)` bypasses all atexit handlers: the process terminates immediately
|
|
177
|
+
# after `os.write`, so nothing can append to our stdout (reproduced: a hostile
|
|
178
|
+
# sitecustomize turned "0.7.0" into "0.7.0\n99.0.0" before this fix).
|
|
179
|
+
# `os.write` is used instead of `print` because `os._exit` skips stdio flushing;
|
|
180
|
+
# an unbuffered syscall guarantees the version byte reaches the pipe.
|
|
181
|
+
[
|
|
182
|
+
sys.executable,
|
|
183
|
+
"-I",
|
|
184
|
+
"-c",
|
|
185
|
+
"import importlib.metadata as m, os; "
|
|
186
|
+
"os.write(1, (m.version('syncade') + '\\n').encode()); "
|
|
187
|
+
"os._exit(0)",
|
|
188
|
+
],
|
|
189
|
+
timeout=_VERSION_TIMEOUT,
|
|
190
|
+
)
|
|
191
|
+
except SubprocessError:
|
|
192
|
+
return None
|
|
193
|
+
if result.returncode != 0:
|
|
194
|
+
return None
|
|
195
|
+
raw = result.stdout.strip()
|
|
196
|
+
if not raw:
|
|
197
|
+
return None
|
|
198
|
+
# sitecustomize from PYTHONPATH (suppressed by -I) could emit noise before -c runs;
|
|
199
|
+
# the venv's own sitecustomize can still do so. Take the LAST line — atexit is now
|
|
200
|
+
# impossible (os._exit), so the last line is what os.write put there — and validate it
|
|
201
|
+
# parses as a version. Noise that accidentally ends in a dotted string still fails _parse.
|
|
202
|
+
candidate = raw.splitlines()[-1].strip()
|
|
203
|
+
return candidate if _parse(candidate) is not None else None
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _is_newest(installed: str) -> bool:
|
|
207
|
+
"""Whether ``installed`` is at or beyond the newest version the manifest advertises.
|
|
208
|
+
|
|
209
|
+
Only ever consulted on the unhappy path — the version failed to move — so the happy path
|
|
210
|
+
stays offline. Anything that leaves the question unanswered (unreachable manifest, no usable
|
|
211
|
+
``latest``, an installed version we cannot parse) returns ``False``, i.e. "cannot prove you
|
|
212
|
+
are current", which routes to the louder message. That is the safe direction: telling someone
|
|
213
|
+
to check for a pin when they were merely current is a wasted minute; telling someone they are
|
|
214
|
+
up to date when the upgrade silently failed strands them on an old version believing it is
|
|
215
|
+
the new one.
|
|
216
|
+
|
|
217
|
+
**This compares versions itself and must never delegate to ``evaluate()``.** That function
|
|
218
|
+
answers a DIFFERENT question — "should the operator see a notice?" — and the two answers
|
|
219
|
+
only usually coincide. They part company in the documented critical-no-fix state: when
|
|
220
|
+
``critical_below`` is above the installed version and no newer release exists, ``evaluate()``
|
|
221
|
+
returns a notice while the operator is genuinely current, so reading its ``None``-ness told a
|
|
222
|
+
fully up-to-date install that its upgrade had failed and it was probably pinned. Two dogfood
|
|
223
|
+
rounds landed on this function for the same underlying reason — a near-miss reuse of a
|
|
224
|
+
neighbouring answer — and the fix both times was to ask the actual question.
|
|
225
|
+
|
|
226
|
+
There is deliberately no special case for an unreachable manifest: ``{}`` has no usable
|
|
227
|
+
``latest``, so the one parse rule already answers it. An explicit branch was there and a
|
|
228
|
+
mutation run proved it dead — every input reached the same verdict without it.
|
|
229
|
+
"""
|
|
230
|
+
manifest = _fetch(MANIFEST_URL) or {}
|
|
231
|
+
latest = _parse(_text(manifest.get("latest")))
|
|
232
|
+
current = _parse(installed)
|
|
233
|
+
return latest is not None and current is not None and current >= latest
|
|
234
|
+
|
|
235
|
+
|
|
152
236
|
def run_update(*, cwd: Path | None = None, out=None) -> int:
|
|
153
237
|
"""Upgrade syncade in place, then exit asking to be re-run."""
|
|
154
238
|
out = out or sys.stderr
|
|
@@ -213,9 +297,38 @@ def run_update(*, cwd: Path | None = None, out=None) -> int:
|
|
|
213
297
|
)
|
|
214
298
|
return WORKTREE_ERROR
|
|
215
299
|
|
|
300
|
+
# THE POST-CONDITION. Exit 0 from the package manager means "the command ran", NOT "the
|
|
301
|
+
# version moved", and the two come apart routinely: `uv tool install syncade==0.6.3` records
|
|
302
|
+
# `specifier = "==0.6.3"` in its receipt, so `uv tool upgrade` honours the pin, prints
|
|
303
|
+
# "Nothing to upgrade" and exits 0. Held packages, resolver conflicts and a manifest that is
|
|
304
|
+
# ahead of PyPI all land in the same place. Without this check syncade says "updated. Re-run
|
|
305
|
+
# your command", the operator re-runs, gets the same version, and is told again next session
|
|
306
|
+
# — forever. Measured live on the 0.7.0 release, which is why item 5 of that release existed.
|
|
307
|
+
now = _installed_version()
|
|
308
|
+
if now is not None and now == syncade.__version__:
|
|
309
|
+
# The version did not move. There are TWO reasons for that and they need opposite
|
|
310
|
+
# answers, so ask the manifest which one this is rather than guessing. Guessing was the
|
|
311
|
+
# first version of this guard and it told an operator who was simply already current
|
|
312
|
+
# that their install was pinned and broken — a fresh false claim inside the fix for a
|
|
313
|
+
# false claim, caught by testing the path instead of reasoning about it.
|
|
314
|
+
if _is_newest(now):
|
|
315
|
+
skills_ok = _resync_skills(out)
|
|
316
|
+
print(f"[syncade] already up to date ({now}).", file=out)
|
|
317
|
+
return SUCCESS if skills_ok else WORKTREE_ERROR
|
|
318
|
+
print(
|
|
319
|
+
f"[syncade] the upgrade command succeeded but syncade is still "
|
|
320
|
+
f"{syncade.__version__} — nothing was installed.\n"
|
|
321
|
+
f" Most often the install pins a version: check `uv tool list` / "
|
|
322
|
+
f"`pipx list`, then\n"
|
|
323
|
+
f" reinstall unpinned (e.g. `uv tool install --force syncade`).",
|
|
324
|
+
file=out,
|
|
325
|
+
)
|
|
326
|
+
return WORKTREE_ERROR
|
|
327
|
+
|
|
216
328
|
skills_ok = _resync_skills(out)
|
|
329
|
+
moved = f" to {now}" if now else ""
|
|
217
330
|
print(
|
|
218
|
-
"[syncade] updated. Re-run your command to use the new version — this process is\n"
|
|
331
|
+
f"[syncade] updated{moved}. Re-run your command to use the new version — this process is\n"
|
|
219
332
|
" still running the old one and cannot switch to it.",
|
|
220
333
|
file=out,
|
|
221
334
|
)
|
|
@@ -18,6 +18,9 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
|
18
18
|
# Drift-guarded literal mirrors of syncade.gc.DEFAULT_KEEP / DEFAULT_MAX_AGE_DAYS (see docstring).
|
|
19
19
|
_DEFAULT_KEEP = 20
|
|
20
20
|
_DEFAULT_MAX_AGE_DAYS = 0
|
|
21
|
+
#: Calibrated on 421 runs / 65 resume-protected: 14d releases 86% while keeping 9.
|
|
22
|
+
#: 7d buys 6% more for half the margin; 30d leaves 19 runs at ~130 MB/round accruing.
|
|
23
|
+
_DEFAULT_WORKTREE_MAX_AGE_DAYS = 14 # mirrors gc.DEFAULT_WORKTREE_MAX_AGE_DAYS
|
|
21
24
|
|
|
22
25
|
|
|
23
26
|
class GcConfig(BaseModel):
|
|
@@ -41,7 +44,21 @@ class GcConfig(BaseModel):
|
|
|
41
44
|
),
|
|
42
45
|
)
|
|
43
46
|
|
|
44
|
-
|
|
47
|
+
worktree_max_age_days: int = Field(
|
|
48
|
+
default=_DEFAULT_WORKTREE_MAX_AGE_DAYS,
|
|
49
|
+
ge=0,
|
|
50
|
+
description=(
|
|
51
|
+
"Days after which a run's WORKTREE is removed even though the run is still "
|
|
52
|
+
f"resume-eligible (default {_DEFAULT_WORKTREE_MAX_AGE_DAYS}; 0 disables the bound "
|
|
53
|
+
"and restores the previous never-ending protection). Tier 3 of the retention "
|
|
54
|
+
"policy: a worktree is reconstructible from the SHA the run already records, so "
|
|
55
|
+
"removing one costs a `git worktree add` and never history. Distinct from "
|
|
56
|
+
"`max_age_days`, which gates TRANSCRIPTS only — one key cannot be non-zero for "
|
|
57
|
+
"worktrees and zero for transcripts at once."
|
|
58
|
+
),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
@field_validator("keep", "max_age_days", "worktree_max_age_days", mode="before")
|
|
45
62
|
@classmethod
|
|
46
63
|
def _strict_int(cls, value: object) -> object:
|
|
47
64
|
# Pydantic's lax mode coerces a quoted number ("0"→0), an exact float (1.0→1), and a boolean
|
|
@@ -79,6 +79,12 @@ DEFAULT_KEEP: int = 20
|
|
|
79
79
|
"""Default number of most-recent non-protected runs to keep."""
|
|
80
80
|
|
|
81
81
|
DEFAULT_MAX_AGE_DAYS: int = 0
|
|
82
|
+
|
|
83
|
+
#: Tier 3 (PR-h-12 item 2). Calibrated on this repo's corpus rather than picked round: over 421
|
|
84
|
+
#: runs, 65 of them resume-protected, a 14-day floor releases 56 (86%) and keeps 9. A 7-day floor
|
|
85
|
+
#: buys 6% more for half the safety margin; 30 leaves 19 runs at ~130 MB/round still accruing,
|
|
86
|
+
#: which is the unbounded growth this exists to stop. ``0`` disables the bound entirely.
|
|
87
|
+
DEFAULT_WORKTREE_MAX_AGE_DAYS: int = 14
|
|
82
88
|
"""Default age floor in days. ``0`` disables the age gate."""
|
|
83
89
|
|
|
84
90
|
|
|
@@ -116,6 +122,7 @@ def plan_gc(
|
|
|
116
122
|
*,
|
|
117
123
|
keep: int = DEFAULT_KEEP,
|
|
118
124
|
max_age_days: int = DEFAULT_MAX_AGE_DAYS,
|
|
125
|
+
worktree_max_age_days: int = DEFAULT_WORKTREE_MAX_AGE_DAYS,
|
|
119
126
|
worktree_base: Path = DEFAULT_WORKTREE_BASE,
|
|
120
127
|
skip_worktrees: bool = False,
|
|
121
128
|
) -> GcPlan:
|
|
@@ -145,11 +152,27 @@ def plan_gc(
|
|
|
145
152
|
worktree_trees_to_remove=[],
|
|
146
153
|
orphan_worktree_trees=[],
|
|
147
154
|
worktree_tree_identities={},
|
|
155
|
+
worktree_age_released=[],
|
|
148
156
|
)
|
|
149
157
|
|
|
158
|
+
# TIER 3 (PR-h-12 item 2). Worktree removal is independent of tier-2 transcript slimming:
|
|
159
|
+
# `slim_names` is the transcript filter (beyond-keep AND old-enough), but a run INSIDE the
|
|
160
|
+
# keep window can still have an ancient worktree nobody is inspecting. The two tiers must be
|
|
161
|
+
# selected by their own criteria — `worktree_max_age_days` for tier 3, `keep`/`max_age_days`
|
|
162
|
+
# for tier 2 — so they can be non-zero for one and zero for the other simultaneously.
|
|
163
|
+
#
|
|
164
|
+
# `released` is the PROTECTED half: protected runs aged past the floor, with a TOCTOU bypass
|
|
165
|
+
# flag so execute_gc skips the on-disk protection re-check (the disk state hasn't changed —
|
|
166
|
+
# only age releases it). `worktree_aged` is the UNPROTECTED half: candidates aged past the
|
|
167
|
+
# floor, selected independently of slim_names. Both are empty when worktree_max_age_days=0
|
|
168
|
+
# (the opt-out that reproduces the pre-PR-h-12 behaviour).
|
|
169
|
+
# TIER 3, selected by its OWN rule — never derived from `slim_names`. See
|
|
170
|
+
# `_worktree_removable`: deriving tier-3 targets from tier-2's selection is what let a
|
|
171
|
+
# one-day-old worktree be deleted because its transcripts had aged out.
|
|
172
|
+
all_worktree_ids, released = _worktree_removable(run_dirs, protected, worktree_max_age_days)
|
|
150
173
|
worktree_trees = [
|
|
151
174
|
tree
|
|
152
|
-
for tree in existing_worktree_trees(worktree_base,
|
|
175
|
+
for tree in existing_worktree_trees(worktree_base, all_worktree_ids)
|
|
153
176
|
if not tree_contains_repo_root(tree, repo_root)
|
|
154
177
|
]
|
|
155
178
|
known_run_ids = {d.name for d in run_dirs} | protected
|
|
@@ -168,6 +191,7 @@ def plan_gc(
|
|
|
168
191
|
worktree_trees_to_remove=worktree_trees,
|
|
169
192
|
orphan_worktree_trees=orphan_trees,
|
|
170
193
|
worktree_tree_identities=tree_identities,
|
|
194
|
+
worktree_age_released=released,
|
|
171
195
|
)
|
|
172
196
|
|
|
173
197
|
|
|
@@ -197,6 +221,39 @@ def _started_at_timestamp(run_dir: Path) -> float | None:
|
|
|
197
221
|
return dt.timestamp()
|
|
198
222
|
|
|
199
223
|
|
|
224
|
+
def _worktree_removable(
|
|
225
|
+
run_dirs: list[Path], protected: set[str], worktree_max_age_days: int
|
|
226
|
+
) -> tuple[list[str], list[str]]:
|
|
227
|
+
"""``(removable_run_ids, of_those_that_were_protected)`` — TIER 3's only rule.
|
|
228
|
+
|
|
229
|
+
One predicate, because the coupling was the defect. Worktree selection used to be a union
|
|
230
|
+
with ``slim_names``, so a run beyond ``gc.keep`` lost its worktree the moment its TRANSCRIPTS
|
|
231
|
+
became eligible — a one-day-old inspection worktree deleted under normal run volume, with the
|
|
232
|
+
age floor bypassed entirely. That contradicted the four-tier policy in ``CLAUDE.md``, which
|
|
233
|
+
says tier 3 is removed on its own age rule; the document was right and the code never matched
|
|
234
|
+
it. Two dogfood rounds fixed half of it each, because each added a source instead of removing
|
|
235
|
+
the coupling.
|
|
236
|
+
|
|
237
|
+
A worktree is removable when it is OLDER than the floor, and — for a run that is still
|
|
238
|
+
resume-eligible — only when a floor is actually configured::
|
|
239
|
+
|
|
240
|
+
age > floor and (not protected or floor > 0)
|
|
241
|
+
|
|
242
|
+
``worktree_max_age_days = 0`` is the opt-out and keeps its meaning at both ends: an
|
|
243
|
+
unprotected run's worktree is collectable immediately, exactly as before PR-h-12, while a
|
|
244
|
+
resumable run's is never released. The second return value is the protected subset, which
|
|
245
|
+
``execute_gc`` needs to bypass its protection re-check for those specific runs.
|
|
246
|
+
"""
|
|
247
|
+
cutoff = datetime.now(UTC).timestamp() - (worktree_max_age_days * 86400)
|
|
248
|
+
removable = [d for d in run_dirs if _run_sort_key(d) < cutoff]
|
|
249
|
+
if worktree_max_age_days <= 0:
|
|
250
|
+
removable = [d for d in removable if d.name not in protected]
|
|
251
|
+
return (
|
|
252
|
+
sorted(d.name for d in removable),
|
|
253
|
+
sorted(d.name for d in removable if d.name in protected),
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
200
257
|
def _select_for_slimming(
|
|
201
258
|
candidates_newest_first: list[Path],
|
|
202
259
|
*,
|
|
@@ -28,10 +28,21 @@ def execute_gc(plan: GcPlan, *, dry_run: bool, repo_root: Path) -> GcReport:
|
|
|
28
28
|
pids_reaped: list[int] = []
|
|
29
29
|
worktrees_removed: list[Path] = []
|
|
30
30
|
protected = set(plan.protected_run_ids) | current_protected_run_ids(repo_root)
|
|
31
|
+
# Tier-3 age release (PR-h-12 item 2). Subtracting these from `protected` above does NOT
|
|
32
|
+
# work and the first version tried it: `run_id_protected_now` falls through to a DISK
|
|
33
|
+
# re-check that re-derives protection from the run directory, which has not changed —
|
|
34
|
+
# only the run's AGE releases it. So the bound computed correctly and then removed nothing.
|
|
35
|
+
# Unit tests over `plan_gc` could not see that; the end-to-end CLI run did.
|
|
36
|
+
#
|
|
37
|
+
# Bypassing the re-check here is safe precisely because age is the criterion: the TOCTOU
|
|
38
|
+
# guard exists for a run that became resume-eligible BETWEEN plan and execute, and these
|
|
39
|
+
# were already resume-eligible at plan time and released on an age that cannot change in
|
|
40
|
+
# the seconds since. Transcripts still go through the unmodified guard.
|
|
41
|
+
age_released = set(plan.worktree_age_released)
|
|
31
42
|
|
|
32
43
|
pruned_any = False
|
|
33
44
|
for tree in plan.worktree_trees_to_remove:
|
|
34
|
-
if run_id_protected_now(repo_root, tree.name, protected):
|
|
45
|
+
if tree.name not in age_released and run_id_protected_now(repo_root, tree.name, protected):
|
|
35
46
|
continue
|
|
36
47
|
if tree_contains_repo_root(tree, repo_root):
|
|
37
48
|
errors.append(f"skipping worktree tree {tree}: it contains repo root {repo_root}")
|
|
@@ -19,8 +19,13 @@ GC used to ``rmtree`` whole run directories. That is now a data-loss bug, not a
|
|
|
19
19
|
cleanup: ``.syncade/metrics.db`` is a *derived, rebuildable view* over
|
|
20
20
|
``.syncade/runs/`` — it drop-and-recreates itself on schema drift — so deleting a run
|
|
21
21
|
directory destroys that run's history permanently the next time the view rebuilds.
|
|
22
|
-
Retention is therefore
|
|
23
|
-
|
|
22
|
+
Retention is therefore **four-tier**: run history (``runs/``) kept forever; transcripts
|
|
23
|
+
disposable beyond the configured keep window; worktrees removed once a run can no longer
|
|
24
|
+
plausibly be resumed (age-bounded by ``gc.worktree_max_age_days``, default 14 days);
|
|
25
|
+
and ``metrics.db`` is a droppable derived view. The asymmetry between tiers 1 and 3 is
|
|
26
|
+
intentional: a worktree is reconstructible from the run's recorded SHA (costs a
|
|
27
|
+
``git worktree add``, never history), so GC can safely remove it; a run directory is
|
|
28
|
+
not reconstructible, so it is never deleted."""
|
|
24
29
|
|
|
25
30
|
|
|
26
31
|
@dataclass(frozen=True)
|
|
@@ -33,6 +38,12 @@ class GcPlan:
|
|
|
33
38
|
every structured artifact in it SURVIVE — see :data:`BULK_ARTIFACT_SUFFIXES`."""
|
|
34
39
|
worktree_trees_to_remove: list[Path]
|
|
35
40
|
orphan_worktree_trees: list[Path]
|
|
41
|
+
worktree_age_released: list[str] = field(default_factory=list)
|
|
42
|
+
"""Run-ids still resume-eligible whose WORKTREES have aged past
|
|
43
|
+
``gc.worktree_max_age_days``. Their transcripts and run directories stay protected — this
|
|
44
|
+
releases tier 3 only. Carried on the plan so ``execute_gc`` can keep its TOCTOU re-check
|
|
45
|
+
(a run may have become protected since planning) without that re-check silently undoing
|
|
46
|
+
the age bound."""
|
|
36
47
|
worktree_tree_identities: dict[Path, tuple[int, int, int]] = field(default_factory=dict)
|
|
37
48
|
|
|
38
49
|
|