claude-code-conductor 2.62.0__tar.gz → 2.63.0__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.
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/docs/config-policy.md +28 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/settings.json +2 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/skills/start/SKILL.md +18 -6
- claude_code_conductor-2.63.0/.claude/skills/start/scripts/archive_reports.py +513 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/CHANGELOG.md +57 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/PKG-INFO +1 -1
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/src/c3/__init__.py +1 -1
- claude_code_conductor-2.63.0/tests/test_archive_reports.py +1524 -0
- claude_code_conductor-2.63.0/tests/test_ci_workflows.py +205 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_refgraph.py +4 -4
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_refgraph_structural.py +3 -3
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_scripts_src_bootstrap.py +262 -12
- claude_code_conductor-2.63.0/tests/test_start_skill_archive_contract.py +304 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/.gitignore +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/CLAUDE.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/agents/architect.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/agents/code-reviewer.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/agents/design-critic.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/agents/developer.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/agents/doc-writer.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/agents/interviewer.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/agents/planner.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/agents/project-setup.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/agents/security-reviewer.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/agents/systematic-debugger.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/agents/tester.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/agents/wt_developer.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/agents/wt_systematic-debugger.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/agents/wt_tester.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/breaking-changes.txt +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/deletions.txt +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/docs/autonomous-mode-onboarding.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/docs/nul-boundary.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/docs/parallel-agents-setup.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/docs/platform-adapters.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/docs/settings.json.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/hooks/_hook_utils.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/hooks/check_agent_invocation.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/hooks/consolidate_memory.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/hooks/patterns_guard.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/hooks/permission_handler.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/hooks/permission_handler_toast.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/hooks/planner_check.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/hooks/post_tool.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/hooks/pre_compact.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/hooks/pre_tool.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/hooks/recall_autorebuild.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/hooks/recall_inject.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/hooks/report_contract_check.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/hooks/restore_session.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/hooks/select_tier.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/hooks/session_mode_watch.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/hooks/session_start.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/hooks/session_stop.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/hooks/session_utils.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/hooks/statusline.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/hooks/stop.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/hooks/tier_autoapply.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/hooks/tier_gap_check.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/hooks/worktree_guard.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/memory/.gitkeep +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/permission_rules.json +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/rules/promoted/index.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/skills/autonomous-mode/SKILL.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/skills/autonomous-mode/scripts/mode_line.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/skills/brainstorm/SKILL.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/skills/codex-review/SKILL.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/skills/dev-workflow/SKILL.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/skills/dev-workflow/references/code-review-checklist.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/skills/dev-workflow/references/design-critic-rubric.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/skills/dev-workflow/references/design-rubric.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/skills/dev-workflow/references/interview-rubric.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/skills/dev-workflow/references/plan-design-guidelines.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/skills/dev-workflow/references/security-review-checklist.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/skills/dev-workflow/scripts/detect_execution_verification.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/skills/dev-workflow/scripts/record_agent_outcome.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/skills/dev-workflow/scripts/record_review_decision.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/skills/dev-workflow/scripts/review_hint_inject.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/skills/develop/SKILL.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/skills/doc/SKILL.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/skills/extract-lib/SKILL.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/skills/init-session/SKILL.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/skills/init-session/scripts/session_guard.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/skills/mcp-config/SKILL.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/skills/parallel-agents/SKILL.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/skills/pattern-status/SKILL.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/skills/promote-pattern/SKILL.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/skills/recall/SKILL.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/skills/report-timestamp/SKILL.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/skills/report-timestamp/scripts/get_timestamp.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/skills/review-phase/SKILL.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/skills/setup/SKILL.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/skills/setup/reference.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/skills/setup/templates/coding-standards-template.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/skills/setup/templates/project-conventions-template.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.claude/state/.gitkeep +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/.gitignore +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/ARCHITECTURE.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/LICENSE +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/LICENSES/fastembed-LICENSE +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/LICENSES/fastembed-NOTICE +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/LICENSES/numpy-LICENSE +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/LICENSES/onnxruntime-LICENSE +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/LICENSES/paraphrase-multilingual-MiniLM-L12-v2-LICENSE +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/README.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/hatch_build.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/pyproject.toml +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/src/c3/__main__.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/src/c3/_db_params.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/src/c3/_excludes.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/src/c3/_terminal.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/src/c3/adapters.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/src/c3/cli.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/src/c3/cli_ask.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/src/c3/cli_doctor.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/src/c3/cli_init.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/src/c3/cli_list.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/src/c3/cli_metrics.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/src/c3/cli_plan.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/src/c3/cli_recall.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/src/c3/cli_run.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/src/c3/cli_tier.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/src/c3/cli_update.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/src/c3/db.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/src/c3/embedding.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/src/c3/gitutil.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/src/c3/mcp_server.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/src/c3/migrate.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/src/c3/migrations/001_initial.sql +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/src/c3/migrations/002_agent_cost_runs.sql +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/src/c3/migrations/003_tier_cost.sql +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/src/c3/migrations/004_agent_outcomes.sql +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/src/c3/migrations/005_drop_agent_tier_bandit.sql +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/src/c3/migrations/006_review_decisions_severity.sql +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/src/c3/migrations/007_review_decisions_resolution.sql +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/src/c3/migrations/008_drop_agent_runs.sql +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/src/c3/migrations/README.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/src/c3/migrations/__init__.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/src/c3/paths.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/src/c3/plan_validator.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/src/c3/platforms.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/src/c3/pricing.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/src/c3/question.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/src/c3/recall_chunker.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/src/c3/recall_index.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/src/c3/refgraph.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/src/c3/usage_ingester.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/__init__.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/_pre_compact_helpers.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/conftest.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/fixtures/usage/README.md +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/fixtures/usage/mainline.jsonl +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/fixtures/usage/subagents/agent-deadbeef.jsonl +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/fixtures/usage/subagents/agent-deadbeef.meta.json +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/hooks/__init__.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/hooks/test_check_agent_invocation.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/hooks/test_consolidate_memory.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/hooks/test_detect_execution_verification.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/hooks/test_hook_utils.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/hooks/test_permission_handler.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/hooks/test_permission_handler_toast.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/hooks/test_pip_reinstall_reminder.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/hooks/test_planner_check.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/hooks/test_planner_check_dev.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/hooks/test_post_tool.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/hooks/test_pre_tool.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/hooks/test_recall_autorebuild.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/hooks/test_recall_inject.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/hooks/test_record_agent_outcome.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/hooks/test_record_review_decision.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/hooks/test_restore_session.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/hooks/test_review_hint_inject.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/hooks/test_select_tier.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/hooks/test_select_tier_escalation.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/hooks/test_session_start.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/hooks/test_session_stop.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/hooks/test_session_utils.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/hooks/test_settings_local_absolute_paths.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/hooks/test_settings_tier_autoapply_registration.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/hooks/test_similarity_boost.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/hooks/test_statusline.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/hooks/test_statusline_template_sync.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/hooks/test_sync_check.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/hooks/test_template_guard.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/hooks/test_tier_autoapply.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/hooks/test_tier_gap_check.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/hooks/test_tier_security_fixes.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/skills/__init__.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/skills/_skill_helpers.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/skills/test_dev_workflow_no_task_type.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/skills/test_init_session_no_task_type.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/skills/test_marker_compliance.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/skills/test_parallel_agents_skill_tier.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/skills/test_planner_lightweight.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/skills/test_recall_skill.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/skills/test_session_backlog_reconciliation.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/skills/test_session_guard.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/skills/test_setup_templates.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/skills/test_start_skill_bugfix_flow.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/skills/test_start_skill_new_flow.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/skills/test_start_skill_security_audit_phase.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_adapters.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_audit_review_decisions.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_check_deletions.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_claude_gitignore.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_cli_ask.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_cli_doctor.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_cli_doctor_launcher.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_cli_entry.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_cli_init.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_cli_list.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_cli_metrics.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_cli_plan.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_cli_recall.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_cli_run.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_cli_tier.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_cli_update_breaking_changes.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_cli_update_deletions.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_db.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_db_connect_lint.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_docstring_consistency.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_embedding.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_excludes.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_extract_breaking_changes.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_gitutil.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_hooks_stdin_idiom.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_init_only_patterns.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_mcp_server_elicit.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_migrate.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_mode_line.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_no_bare_python_launcher.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_no_passonly_tests.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_nul_boundary_lint.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_paths.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_patterns_guard.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_plan_validator.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_pre_compact.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_pre_tool_hook.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_precompact_additional.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_precompact_toctou_fixes.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_pricing.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_recall_chunker.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_recall_index.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_references_migration.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_refgraph_query.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_report_contract_check.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_scripts_stdout_encoding.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_session_mode_watch.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_session_utils_additional.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_settings_hooks_registration.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_skill_no_builtin_conflict.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_statusline.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_stop_additional.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_stop_agent_memory_warn.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_stop_hook.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_stop_precompact_fixes.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_sync_template_stop.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_template_pre_tool_hook.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_usage_ingester.py +0 -0
- {claude_code_conductor-2.62.0 → claude_code_conductor-2.63.0}/tests/test_worktree_guard.py +0 -0
|
@@ -422,6 +422,34 @@ Claude Code 側で `deny` 判定が出た後に発火するため、`permission_
|
|
|
422
422
|
定期的にレビューすること(`c3 run <固定スクリプトパス>` 形式のみを許可し、`../` を含みうる可変パスを
|
|
423
423
|
プレフィックス許可しない)。
|
|
424
424
|
|
|
425
|
+
**追記(SR-AI-001): 破壊操作を伴うスクリプトは追記系と同列に扱わない**
|
|
426
|
+
|
|
427
|
+
上記のプレフィックス一致という性質は、許可対象のスクリプトが**何をするか**によって危険度が変わる。
|
|
428
|
+
`c3 run <script>*` の末尾ワイルドカード 1 本は、そのスクリプトが取りうる**あらゆる引数列**を
|
|
429
|
+
無確認で許可する。追記系スクリプト(記録・注入・インデックス更新)ならこれで実害は限定的だが、
|
|
430
|
+
**ファイルの移動・削除・上書きを行うスクリプトでは、引数 1 つで対象ディレクトリごと差し替えられる**。
|
|
431
|
+
実例: `archive_reports.py` は `--reports-dir <任意ディレクトリ>` を取り、その直下の `*.md` を
|
|
432
|
+
移動して移動元を削除する。末尾ワイルドカードで allow すると、プロンプトインジェクション経由で
|
|
433
|
+
任意ディレクトリの `*.md` が無確認で移動・削除されうる。
|
|
434
|
+
|
|
435
|
+
**対処**(新しい allow を追加する際・レビューで拾う際のチェック):
|
|
436
|
+
- そのスクリプトが破壊操作(削除・移動・上書き)を伴うかをまず判定する
|
|
437
|
+
- 伴う場合、allow は**正規経路が実際に使う形だけ**に絞る(引数なしの完全一致 + 正規のオプションで
|
|
438
|
+
始まる形)。末尾ワイルドカード 1 本にしない
|
|
439
|
+
- allow の絞り込みだけでは足りない場合がある。`<script> --opt *` 形のパターンは、後続に別の
|
|
440
|
+
オプションが続く文字列とも前方一致しうるため、**危険な引数自体に env ゲートを課す**
|
|
441
|
+
(例: `archive_reports.py` の `--reports-dir` は `C3_ARCHIVE_REPORTS_DIR_OK=1` がある場合のみ有効)。
|
|
442
|
+
env は allow パターンの前方一致では満たせないため、単一のコマンド文字列では越えられない関所になる
|
|
443
|
+
|
|
444
|
+
**追記(SR-NEW-2): env ゲートの射程は単一コマンド文字列に対する関所であり、セッション跨ぎの env 継承は射程外**
|
|
445
|
+
|
|
446
|
+
上記の env ゲート(例: `C3_ARCHIVE_REPORTS_DIR_OK=1`)が防いでいるのは「1 本の許可済みコマンド文字列の
|
|
447
|
+
前方一致だけでは越えられない」という点であり、**シェルセッションを跨いだ環境変数の永続化・継承は
|
|
448
|
+
別の話**である。`export C3_ARCHIVE_REPORTS_DIR_OK=1` を `.bashrc` 等の起動ファイルに書く・
|
|
449
|
+
CI のジョブ環境変数として設定する・親プロセスから子プロセスへ継承させる、といった経路で
|
|
450
|
+
ゲートが「常時開いた状態」になれば、env ゲート自体は無力化される。ゲートは「未設定が既定」
|
|
451
|
+
という運用を前提にした関所であり、恒久的な env 設定と組み合わせて使わないこと。
|
|
452
|
+
|
|
425
453
|
---
|
|
426
454
|
|
|
427
455
|
## 8. 参照先
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
"Bash(c3 run .claude/skills/dev-workflow/scripts/record_review_decision.py*)",
|
|
14
14
|
"Bash(c3 run .claude/skills/dev-workflow/scripts/record_agent_outcome.py*)",
|
|
15
15
|
"Bash(c3 run .claude/skills/init-session/scripts/session_guard.py*)",
|
|
16
|
+
"Bash(c3 run .claude/skills/start/scripts/archive_reports.py)",
|
|
17
|
+
"Bash(c3 run .claude/skills/start/scripts/archive_reports.py --phase *)",
|
|
16
18
|
"Read(**)",
|
|
17
19
|
"Grep(**)",
|
|
18
20
|
"Edit(.claude/reports/**)",
|
|
@@ -53,10 +53,14 @@ Glob で `.claude/reports/*.md` を検索する(`archive/` 配下は含まな
|
|
|
53
53
|
}
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
+
アーカイブは専用スクリプトが行う。このコマンドは、移動先に同名のファイルが既にある場合は
|
|
57
|
+
移動元を `{元の名前}-archived-{元ファイルの更新時刻}.md` へリネームして**両方を残す**(既存ファイルは 1 バイトも変更しない)。
|
|
58
|
+
アーカイブ先ディレクトリが無ければ作成する。移動できなかったファイルがあれば stderr に一覧を出して exit 1 になる。
|
|
59
|
+
|
|
56
60
|
**「全てアーカイブして新しく始める」の場合:**
|
|
57
61
|
Bash ツールで実行する:
|
|
58
62
|
```bash
|
|
59
|
-
|
|
63
|
+
c3 run .claude/skills/start/scripts/archive_reports.py
|
|
60
64
|
```
|
|
61
65
|
|
|
62
66
|
**「アーカイブするフェーズを選ぶ」の場合:**
|
|
@@ -77,11 +81,19 @@ AskUserQuestion で対象フェーズを確認する:
|
|
|
77
81
|
}
|
|
78
82
|
```
|
|
79
83
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
選択された各ラベルを次の表で `--phase` の値に読み替え、Bash ツールで 1 回だけ実行する
|
|
85
|
+
(`--phase` は繰り返し指定でき、指定した分の和が対象になる。対象ファイルが 1 件も無くても成功する):
|
|
86
|
+
|
|
87
|
+
| 選択肢のラベル | `--phase` の値 | 対象になるファイル |
|
|
88
|
+
|---|---|---|
|
|
89
|
+
| 要件定義 | `requirements` | `requirements-report-*.md` |
|
|
90
|
+
| 設計 | `architecture` | `architecture-report-*.md` |
|
|
91
|
+
| 計画 | `plan` | `plan-report-*.md` |
|
|
92
|
+
| レビュー | `review` | `code-review-report-*.md` / `security-review-report-*.md` / `design-review-report-*.md` |
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
c3 run .claude/skills/start/scripts/archive_reports.py --phase requirements --phase review
|
|
96
|
+
```
|
|
85
97
|
|
|
86
98
|
---
|
|
87
99
|
|
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""`.claude/reports/*.md` を `.claude/reports/archive/` へ安全に移動する。
|
|
3
|
+
|
|
4
|
+
`/start` の Step 0(レポート整理)から `c3 run` 経由で呼ばれる。
|
|
5
|
+
|
|
6
|
+
設計(SSOT): `.claude/reports/architecture-report-20260809-154124.md`(改訂 4)
|
|
7
|
+
- INV-1: 既存ファイルを 1 バイトも変更しない(移動先の名前を排他生成で確保する)
|
|
8
|
+
- INV-2: `archive/` はフラットのまま(サブディレクトリを作らない)
|
|
9
|
+
- INV-3: アーカイブの実装はこの 1 箇所(SKILL.md 側にロジックを置かない)
|
|
10
|
+
- INV-4: 移動先はバイト同一(読み書きはバイナリ I/O のみ・テキスト変換を経由しない)
|
|
11
|
+
|
|
12
|
+
挙動:
|
|
13
|
+
- 移動先に同名が既にある場合は、移動元を
|
|
14
|
+
`{stem}-archived-{移動元 mtime:YYYYMMDD-HHMMSS}{suffix}` へリネームして移動する(ADR-2)。
|
|
15
|
+
その名前も埋まっていれば `-2` / `-3` … と空きを探す(上限 `MAX_CANDIDATE_NAMES`)。
|
|
16
|
+
既存ファイルは決して上書きしない
|
|
17
|
+
- 移動先の mtime は移動元の値へ復元する(`recall` の再構築誘発と版情報の消失を防ぐ)
|
|
18
|
+
- 1 件失敗しても残りは処理し、末尾で stderr に失敗一覧を出して exit 1 にする(ロールバックしない)
|
|
19
|
+
- 失敗した 1 件は移動元を消さない(残す方に倒す)
|
|
20
|
+
|
|
21
|
+
改訂 4(フェーズ E レビュー差し戻し)で加わった規定:
|
|
22
|
+
- SR-V-002: `{reports-dir}/archive` が対象ディレクトリの外へ解決されるなら 1 件も処理せず exit 1
|
|
23
|
+
(`mkdir` の前と `os.open` の前の 2 箇所で検査する)
|
|
24
|
+
- SR-NEW-2: `src` を読む直前に `is_symlink()` を再判定し、真ならその 1 件を失敗にする
|
|
25
|
+
- SR-AI-001: `--reports-dir` は環境変数 `C3_ARCHIVE_REPORTS_DIR_OK=1` がある場合のみ受け付ける
|
|
26
|
+
- CR-E-005: 「コピー成功・移動元の削除だけ失敗」は `FAILED` ではなく `SOURCE_KEPT` として
|
|
27
|
+
別分類で報告し、**移動先は削除しない**
|
|
28
|
+
- SR-V-001: 衝突退避の候補生成に上限を設け、超過はその 1 件の失敗として扱う
|
|
29
|
+
- SR-NEW-1: stdout / stderr に出す全てのファイル名・パス・失敗理由を `_safe()` に通す
|
|
30
|
+
|
|
31
|
+
改訂 5(フェーズ E 再レビュー・CR-NEW / SR-V-002 の再発)で加わった規定:
|
|
32
|
+
- `archive_dir` の判定を `is_symlink()` から **realpath 封じ込め**へ置き換えた。
|
|
33
|
+
`Path.is_symlink()` は NTFS ディレクトリジャンクション(`mklink /J` で管理者権限なしに
|
|
34
|
+
作成できる)に `False` を返し素通りするため(実測: exit 0 の成功扱いで外部ディレクトリへ
|
|
35
|
+
移動し移動元が削除された)。判定は「`os.path.realpath(archive_dir)` が
|
|
36
|
+
`os.path.realpath(reports_dir)` 直下の `archive` と一致すること」(reparse の種別を問わない)。
|
|
37
|
+
既存の解決パターン(`mode_line.py` の plan-path 検査)と同じ realpath 封じ込め方式に揃えた
|
|
38
|
+
- `src` 側(ファイル)の `is_symlink()` 判定は据え置き(ジャンクションはディレクトリ専用で
|
|
39
|
+
ファイルには張れないため影響を受けない)
|
|
40
|
+
|
|
41
|
+
Exit code(設計 §3-3。**戻り値で表し例外では表さない**):
|
|
42
|
+
0: 全対象の移動を確認できた(対象 0 件も 0)
|
|
43
|
+
1: 1 件でも移動に失敗した、コピー済み・移動元が残存した、または引数が不正
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
from __future__ import annotations
|
|
47
|
+
|
|
48
|
+
import argparse
|
|
49
|
+
import os
|
|
50
|
+
import sys
|
|
51
|
+
from datetime import datetime
|
|
52
|
+
from pathlib import Path, PurePath
|
|
53
|
+
|
|
54
|
+
if sys.stdout and hasattr(sys.stdout, "reconfigure"):
|
|
55
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
56
|
+
if sys.stderr and hasattr(sys.stderr, "reconfigure"):
|
|
57
|
+
sys.stderr.reconfigure(encoding="utf-8")
|
|
58
|
+
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
# 定数
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
ARCHIVE_DIR_NAME = "archive"
|
|
64
|
+
|
|
65
|
+
# ADR-2: 衝突退避で付く接尾辞。`-archived-` の識別子を挟むことで、
|
|
66
|
+
# タイムスタンプ命名の正規レポート名と字面で区別できるようにする。
|
|
67
|
+
ARCHIVED_MARKER = "-archived-"
|
|
68
|
+
|
|
69
|
+
# ADR-2 / 未確定事項 1: 接尾辞は **ローカル時刻** で組む(既存レポート名が日本時間のため)。
|
|
70
|
+
STAMP_FORMAT = "%Y%m%d-%H%M%S"
|
|
71
|
+
|
|
72
|
+
# 設計 §3-3【改訂 4・SR-V-001】: 移動先名の候補生成は無限ではなく上限を持つ。
|
|
73
|
+
# 上限なしだと、退避名を大量に事前配置されたときに 1 件の移動が O(N) の `os.open` 試行を要する。
|
|
74
|
+
MAX_CANDIDATE_NAMES = 1000
|
|
75
|
+
|
|
76
|
+
# 設計 §3-3c【改訂 4・SR-AI-001】: `--reports-dir` を開ける env ゲート。
|
|
77
|
+
# `--reports-dir` は任意ディレクトリへの破壊操作(移動元の削除)を開く唯一の入口であり、
|
|
78
|
+
# `permissions.allow` の前方一致では塞ぎきれないため、env を実効的な関所にする。
|
|
79
|
+
REPORTS_DIR_OK_ENV = "C3_ARCHIVE_REPORTS_DIR_OK"
|
|
80
|
+
REPORTS_DIR_OK_VALUE = "1"
|
|
81
|
+
|
|
82
|
+
# 設計 §3-2: フェーズ名 → ファイル名接頭辞(このスクリプトが SSOT)。
|
|
83
|
+
PHASE_PREFIXES: dict[str, tuple[str, ...]] = {
|
|
84
|
+
"requirements": ("requirements-report-",),
|
|
85
|
+
"architecture": ("architecture-report-",),
|
|
86
|
+
"plan": ("plan-report-",),
|
|
87
|
+
"review": (
|
|
88
|
+
"code-review-report-",
|
|
89
|
+
"security-review-report-",
|
|
90
|
+
"design-review-report-",
|
|
91
|
+
),
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# ADR-3: 名前の確保を原子的に行う。O_EXCL があれば既存を潰すことは起こりえない。
|
|
95
|
+
# O_BINARY は Windows のみ存在する(POSIX では 0 相当)。
|
|
96
|
+
_EXCL_FLAGS = os.O_CREAT | os.O_EXCL | os.O_WRONLY | getattr(os, "O_BINARY", 0)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
# 出力の無害化(設計 §3-3【改訂 4・SR-NEW-1】)
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _safe(value: object) -> str:
|
|
105
|
+
"""出力に載せる文字列から C0 制御文字と DEL を取り除く。
|
|
106
|
+
|
|
107
|
+
stdout / stderr に出す**すべての**ファイル名・パス・失敗理由をこの helper に通す。
|
|
108
|
+
`--reports-dir` は任意ディレクトリを指せるため、そこにあるファイル名は攻撃者が制御でき、
|
|
109
|
+
生の ANSI エスケープが端末表示を偽装しうる。
|
|
110
|
+
|
|
111
|
+
除去ではなく `\\x1b` 形の可視表現へ置換するのは、どの名前だったかの手掛かりを残すため。
|
|
112
|
+
改行・タブも対象に含む: `FAILED` / `SOURCE_KEPT` 行は「1 件 1 行・タブ区切り」の契約であり、
|
|
113
|
+
生の改行・タブが混ざると行とフィールドの構造そのものが壊れる。
|
|
114
|
+
"""
|
|
115
|
+
text = str(value)
|
|
116
|
+
return "".join(
|
|
117
|
+
f"\\x{ord(char):02x}" if ord(char) < 0x20 or ord(char) == 0x7F else char
|
|
118
|
+
for char in text
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _one_line(exc: BaseException) -> str:
|
|
123
|
+
"""失敗理由を 1 行に潰す(失敗一覧は 1 件 1 行の契約のため)。
|
|
124
|
+
|
|
125
|
+
`_safe()` が改行・タブを含む制御文字を可視表現へ置換するため、この戻り値は
|
|
126
|
+
そのまま `FAILED` / `SOURCE_KEPT` 行のフィールドとして使える。
|
|
127
|
+
"""
|
|
128
|
+
return _safe(f"{type(exc).__name__}: {exc}")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# ---------------------------------------------------------------------------
|
|
132
|
+
# パス解決
|
|
133
|
+
# ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _archive_dir_is_contained(archive_dir: Path) -> bool:
|
|
137
|
+
"""`archive_dir` が対象ディレクトリ(その親)の直下の `archive` へ実際に解決されるか(設計 §3-3c 改訂 5)。
|
|
138
|
+
|
|
139
|
+
`is_symlink()` は NTFS ディレクトリジャンクションに `False` を返し素通りするため判定に使えない
|
|
140
|
+
(実測済み)。reparse point の種別を問わず塞ぐには、**実際に辿り着く先**を見るしかない。
|
|
141
|
+
`archive_dir` は常に `reports_dir / ARCHIVE_DIR_NAME` として構築されるため、
|
|
142
|
+
`archive_dir.parent` を対象ディレクトリとして扱える(`_move_one` は設計 §3-3b で
|
|
143
|
+
引数を `src` / `archive_dir` の 2 個に固定しているため、`reports_dir` を別引数で渡さない)。
|
|
144
|
+
|
|
145
|
+
既存の解決パターン(`mode_line.py` の plan-path 検査)と同じ `os.path.realpath` を使う。
|
|
146
|
+
例外の扱いも引用元に揃える(`mode_line.py:132-136` は `realpath` を `try/except OSError` で
|
|
147
|
+
包み、失敗を無効側へ倒している)。判定関数が例外を送出すると設計 §3-3 の
|
|
148
|
+
「exit code は戻り値で表し例外で表さない」契約を破るため、**解決できない時点で
|
|
149
|
+
「収まっていない」と扱う**(安全側・SR-NEW-1)。
|
|
150
|
+
"""
|
|
151
|
+
try:
|
|
152
|
+
expected = os.path.join(os.path.realpath(archive_dir.parent), ARCHIVE_DIR_NAME)
|
|
153
|
+
actual = os.path.realpath(archive_dir)
|
|
154
|
+
except OSError:
|
|
155
|
+
return False
|
|
156
|
+
return actual == expected
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _default_reports_dir() -> Path:
|
|
160
|
+
"""`--reports-dir` 省略時の対象ディレクトリ(設計 §3-1)。
|
|
161
|
+
|
|
162
|
+
cwd 起点にしない: `c3 run` は runpy による同一プロセス実行で cwd をシェルから
|
|
163
|
+
引き継ぐため、worktree cwd リーク(Issue #28017)の状況で対象を取り違える。
|
|
164
|
+
|
|
165
|
+
**この解決は import 時ではなく呼び出し時に行う**(設計 §3-3b)。浅い一時ディレクトリへ
|
|
166
|
+
スクリプトの写しを置いた場合、import 時に評価すると `IndexError` で落ちてしまう。
|
|
167
|
+
"""
|
|
168
|
+
return Path(__file__).resolve().parents[4] / ".claude" / "reports"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# ---------------------------------------------------------------------------
|
|
172
|
+
# 移動先の名前
|
|
173
|
+
# ---------------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _stamp(mtime: float) -> str:
|
|
177
|
+
"""mtime をローカル時刻の `YYYYMMDD-HHMMSS` に整形する。"""
|
|
178
|
+
return datetime.fromtimestamp(mtime).strftime(STAMP_FORMAT)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _candidate_names(name: str, mtime: float):
|
|
182
|
+
"""移動先ファイル名の候補を優先順に produce する(ADR-2)。
|
|
183
|
+
|
|
184
|
+
1. 元の名前そのもの
|
|
185
|
+
2. `{stem}-archived-{mtime}{suffix}`
|
|
186
|
+
3. `{stem}-archived-{mtime}-2{suffix}` / `-3` …(同秒衝突の退避)
|
|
187
|
+
|
|
188
|
+
【改訂 4・SR-V-001】候補は合計 `MAX_CANDIDATE_NAMES` 件で打ち切る(無限に produce しない)。
|
|
189
|
+
枯渇した場合の扱いは呼び出し側(`_move_one`)が決める。
|
|
190
|
+
"""
|
|
191
|
+
yield name
|
|
192
|
+
pure = PurePath(name)
|
|
193
|
+
base = f"{pure.stem}{ARCHIVED_MARKER}{_stamp(mtime)}"
|
|
194
|
+
suffix = pure.suffix
|
|
195
|
+
yield f"{base}{suffix}"
|
|
196
|
+
for index in range(2, MAX_CANDIDATE_NAMES):
|
|
197
|
+
yield f"{base}-{index}{suffix}"
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# ---------------------------------------------------------------------------
|
|
201
|
+
# 1 件分の移動
|
|
202
|
+
# ---------------------------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class _SourceKeptError(Exception):
|
|
206
|
+
"""コピーは成功したが移動元の削除だけが失敗した(設計 §3-3【改訂 4・CR-E-005】)。
|
|
207
|
+
|
|
208
|
+
この経路を通常の失敗(`FAILED`)に丸めると、**移動先には正しい内容が既に存在する**という
|
|
209
|
+
実態を報告できず、再実行のたびに同一内容が別名で `archive/` に積み上がる。
|
|
210
|
+
移動先は削除せず(バイト同一のコピーを捨てない)、`SOURCE_KEPT` として別分類で報告する。
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
def __init__(self, dst: Path, cause: BaseException) -> None:
|
|
214
|
+
super().__init__(str(cause))
|
|
215
|
+
self.dst = dst
|
|
216
|
+
self.cause = cause
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _move_one(src: Path, archive_dir: Path) -> Path:
|
|
220
|
+
"""`src` を `archive_dir` へ移動し、実際に書いた移動先パスを返す。
|
|
221
|
+
|
|
222
|
+
設計 §3-3b は引数 2 個(`src` / `archive_dir`)を契約として固定している。戻り値は
|
|
223
|
+
同 §3-3b では `None` と書かれているが、§3-3 の stdout 契約(リネーム時に旧名→新名を出す)を
|
|
224
|
+
命名ロジックの重複なく満たすため移動先を返す形にした(INV-3: 命名の実装を 2 箇所に持たない)。
|
|
225
|
+
|
|
226
|
+
失敗時は例外を送出し、**移動元を残す**(ADR-3)。中途半端に生成した移動先は削除する。
|
|
227
|
+
ただしコピー完了後の `src.unlink()` だけが失敗した場合は `_SourceKeptError`(CR-E-005)。
|
|
228
|
+
"""
|
|
229
|
+
# 【改訂 4・SR-NEW-2】列挙(`_collect_targets`)と読み取りの間に `src` が symlink へ
|
|
230
|
+
# 差し替えられる窓を狭める。読む直前に再判定し、真ならこの 1 件を失敗にする。
|
|
231
|
+
if src.is_symlink():
|
|
232
|
+
raise RuntimeError(
|
|
233
|
+
f"読み取り直前に対象がシンボリックリンクへ差し替えられた: {_safe(src.name)}"
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
info = src.stat()
|
|
237
|
+
# INV-4: バイナリ I/O のみ。テキストモードを経由すると改行・encoding が変換される。
|
|
238
|
+
data = src.read_bytes()
|
|
239
|
+
|
|
240
|
+
# 【改訂 5・CR-NEW / SR-V-002 再発】`os.open` の前にも移動先を検査する。`O_EXCL` は
|
|
241
|
+
# 最終コンポーネントにしか効かないため、親が reparse point だと移動先をすり替えたうえで
|
|
242
|
+
# 移動元が削除される。`is_symlink()` は NTFS ジャンクションに素通りされるため
|
|
243
|
+
# realpath 封じ込めで判定する(種別を問わない)。
|
|
244
|
+
if not _archive_dir_is_contained(archive_dir):
|
|
245
|
+
raise RuntimeError(
|
|
246
|
+
f"アーカイブ先が対象ディレクトリの外へ解決される: {_safe(archive_dir)}"
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
dst: Path | None = None
|
|
250
|
+
fd = -1
|
|
251
|
+
for candidate in _candidate_names(src.name, info.st_mtime):
|
|
252
|
+
target = archive_dir / candidate
|
|
253
|
+
try:
|
|
254
|
+
fd = os.open(str(target), _EXCL_FLAGS, 0o644)
|
|
255
|
+
except FileExistsError:
|
|
256
|
+
continue
|
|
257
|
+
dst = target
|
|
258
|
+
break
|
|
259
|
+
|
|
260
|
+
if dst is None or fd < 0:
|
|
261
|
+
# 【改訂 4・SR-V-001】候補は有限(`MAX_CANDIDATE_NAMES` 件)。枯渇はこの 1 件の失敗。
|
|
262
|
+
# `assert` は `python -O` で無効化されるため `RuntimeError` で明示的に投げる
|
|
263
|
+
# (`record_agent_outcome.py` / `review_hint_inject.py` と同じ理由)。
|
|
264
|
+
raise RuntimeError(
|
|
265
|
+
f"移動先の候補が上限 {MAX_CANDIDATE_NAMES} 件で尽きた: {_safe(src.name)}"
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
try:
|
|
269
|
+
with os.fdopen(fd, "wb") as handle:
|
|
270
|
+
handle.write(data)
|
|
271
|
+
# コピー+削除では移動時刻に置き換わるため、移動元の値を復元する。
|
|
272
|
+
os.utime(dst, (info.st_atime, info.st_mtime))
|
|
273
|
+
except BaseException:
|
|
274
|
+
try:
|
|
275
|
+
os.unlink(str(dst))
|
|
276
|
+
except OSError:
|
|
277
|
+
pass
|
|
278
|
+
raise
|
|
279
|
+
|
|
280
|
+
try:
|
|
281
|
+
src.unlink()
|
|
282
|
+
except OSError as exc:
|
|
283
|
+
# 【改訂 4・CR-E-005】ここまでで移動先にはバイト同一のコピーが存在する。
|
|
284
|
+
# 移動先は削除せず、`SOURCE_KEPT` として運用者が手で後始末できる形で報告する。
|
|
285
|
+
raise _SourceKeptError(dst, exc) from exc
|
|
286
|
+
return dst
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
# ---------------------------------------------------------------------------
|
|
290
|
+
# 対象の列挙
|
|
291
|
+
# ---------------------------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _selected_prefixes(phases: list[str]) -> tuple[str, ...] | None:
|
|
295
|
+
"""`--phase` の指定を接頭辞の集合へ畳む。指定が無ければ `None`(全件)。"""
|
|
296
|
+
if not phases:
|
|
297
|
+
return None
|
|
298
|
+
prefixes: list[str] = []
|
|
299
|
+
for phase in phases:
|
|
300
|
+
for prefix in PHASE_PREFIXES[phase]:
|
|
301
|
+
if prefix not in prefixes:
|
|
302
|
+
prefixes.append(prefix)
|
|
303
|
+
return tuple(prefixes)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _collect_targets(
|
|
307
|
+
reports_dir: Path, prefixes: tuple[str, ...] | None
|
|
308
|
+
) -> tuple[list[Path], list[Path]]:
|
|
309
|
+
"""移動対象と、シンボリックリンクゆえに除外したエントリを返す(設計 §3-3c)。
|
|
310
|
+
|
|
311
|
+
破壊操作の対象は `{reports-dir}` 直下の `*.md` に限る(再帰しない・`archive/` を見ない)。
|
|
312
|
+
"""
|
|
313
|
+
targets: list[Path] = []
|
|
314
|
+
skipped_links: list[Path] = []
|
|
315
|
+
for entry in sorted(reports_dir.glob("*.md")):
|
|
316
|
+
# シンボリックリンクは辿らない。`is_file()` はリンクを辿るため先に判定する。
|
|
317
|
+
if entry.is_symlink():
|
|
318
|
+
skipped_links.append(entry)
|
|
319
|
+
continue
|
|
320
|
+
if not entry.is_file():
|
|
321
|
+
continue
|
|
322
|
+
if prefixes is not None and not entry.name.startswith(prefixes):
|
|
323
|
+
continue
|
|
324
|
+
targets.append(entry)
|
|
325
|
+
return targets, skipped_links
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
# ---------------------------------------------------------------------------
|
|
329
|
+
# 移動の実行と集計(CR-Q-002: `main()` は「検証 → 委譲 → 出力」に絞る)
|
|
330
|
+
# ---------------------------------------------------------------------------
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
class _ArchiveOutcome:
|
|
334
|
+
"""`_archive_all()` の結果。件数と、報告に必要な名前の組を持つ。
|
|
335
|
+
|
|
336
|
+
`@dataclass` を使わないのは、本スクリプトが `importlib.util.spec_from_file_location` で
|
|
337
|
+
`sys.modules` に登録せずロードされる(テスト・confirm のスタブ検査がこの形)ため。
|
|
338
|
+
`from __future__ import annotations` と組み合わせると `dataclasses` が
|
|
339
|
+
`sys.modules[cls.__module__]` を引けず `AttributeError` でロードごと落ちる(実測)。
|
|
340
|
+
"""
|
|
341
|
+
|
|
342
|
+
def __init__(self) -> None:
|
|
343
|
+
self.moved: int = 0
|
|
344
|
+
self.renamed: list[tuple[str, str]] = []
|
|
345
|
+
self.failures: list[tuple[str, str]] = []
|
|
346
|
+
self.kept: list[tuple[str, str, str]] = []
|
|
347
|
+
|
|
348
|
+
def has_problem(self) -> bool:
|
|
349
|
+
"""exit 1 にすべき状態か(失敗、またはコピー済み・移動元残存)。"""
|
|
350
|
+
return bool(self.failures or self.kept)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _archive_all(targets: list[Path], archive_dir: Path) -> _ArchiveOutcome:
|
|
354
|
+
"""対象を 1 件ずつ移動して集計する。
|
|
355
|
+
|
|
356
|
+
ADR-4「1 件ごとの確認」/設計 §3-3「1 件失敗しても残りの対象は処理を続ける」。
|
|
357
|
+
ロールバックはしない。
|
|
358
|
+
"""
|
|
359
|
+
outcome = _ArchiveOutcome()
|
|
360
|
+
for src in targets:
|
|
361
|
+
try:
|
|
362
|
+
dst = _move_one(src, archive_dir)
|
|
363
|
+
except _SourceKeptError as exc:
|
|
364
|
+
# CR-E-005: コピーは成功している。`FAILED` に丸めない。
|
|
365
|
+
outcome.kept.append((src.name, exc.dst.name, _one_line(exc.cause)))
|
|
366
|
+
continue
|
|
367
|
+
except Exception as exc: # noqa: BLE001 - 1 件の失敗で残りを打ち切らない(ADR-4)
|
|
368
|
+
outcome.failures.append((src.name, _one_line(exc)))
|
|
369
|
+
continue
|
|
370
|
+
outcome.moved += 1
|
|
371
|
+
if dst.name != src.name:
|
|
372
|
+
outcome.renamed.append((src.name, dst.name))
|
|
373
|
+
return outcome
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _report(outcome: _ArchiveOutcome, archive_dir: Path, skipped_links: list[Path]) -> None:
|
|
377
|
+
"""集計を stdout へ、失敗一覧を stderr へ出す(設計 §3-3)。
|
|
378
|
+
|
|
379
|
+
出力に載る名前・パス・理由は例外なく `_safe()` を通す(SR-NEW-1)。
|
|
380
|
+
"""
|
|
381
|
+
print(f"アーカイブ先: {_safe(archive_dir)}")
|
|
382
|
+
print(
|
|
383
|
+
f"移動: {outcome.moved} 件 / リネーム: {len(outcome.renamed)} 件"
|
|
384
|
+
f" / 失敗: {len(outcome.failures)} 件"
|
|
385
|
+
f" / コピー済み・移動元が残存: {len(outcome.kept)} 件"
|
|
386
|
+
)
|
|
387
|
+
for old_name, new_name in outcome.renamed:
|
|
388
|
+
print(f" リネーム: {_safe(old_name)} -> {_safe(new_name)}")
|
|
389
|
+
for link in skipped_links:
|
|
390
|
+
print(f" スキップ(シンボリックリンク): {_safe(link.name)}")
|
|
391
|
+
|
|
392
|
+
for name, reason in outcome.failures:
|
|
393
|
+
print(f"FAILED\t{_safe(name)}\t{reason}", file=sys.stderr)
|
|
394
|
+
for name, dst_name, reason in outcome.kept:
|
|
395
|
+
print(f"SOURCE_KEPT\t{_safe(name)}\t{_safe(dst_name)}\t{reason}", file=sys.stderr)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
# ---------------------------------------------------------------------------
|
|
399
|
+
# エントリポイント
|
|
400
|
+
# ---------------------------------------------------------------------------
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
404
|
+
parser = argparse.ArgumentParser(
|
|
405
|
+
description="Move .claude/reports/*.md into .claude/reports/archive/ without overwriting.",
|
|
406
|
+
)
|
|
407
|
+
# choices= は使わない: 未知値で argparse が SystemExit(2) を送出し、
|
|
408
|
+
# 「exit code は main の戻り値で表す」(設計 §3-3b)に反するため main 内で検証する。
|
|
409
|
+
parser.add_argument(
|
|
410
|
+
"--phase",
|
|
411
|
+
action="append",
|
|
412
|
+
default=None,
|
|
413
|
+
metavar="NAME",
|
|
414
|
+
help="target phase (repeatable). omit to target every report",
|
|
415
|
+
)
|
|
416
|
+
parser.add_argument(
|
|
417
|
+
"--reports-dir",
|
|
418
|
+
default=None,
|
|
419
|
+
metavar="PATH",
|
|
420
|
+
help=(
|
|
421
|
+
"reports directory (archive dir is always <PATH>/archive)."
|
|
422
|
+
f" requires {REPORTS_DIR_OK_ENV}={REPORTS_DIR_OK_VALUE}"
|
|
423
|
+
),
|
|
424
|
+
)
|
|
425
|
+
return parser
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _resolve_reports_dir(raw: str | None) -> Path | None:
|
|
429
|
+
"""`--reports-dir` を解決する。ゲート未通過・不正なら `None` を返す(設計 §3-3c)。
|
|
430
|
+
|
|
431
|
+
エラー内容は stderr へ出す。呼び出し側は `None` を exit 1 として扱う。
|
|
432
|
+
"""
|
|
433
|
+
if raw is None:
|
|
434
|
+
reports_dir = _default_reports_dir()
|
|
435
|
+
else:
|
|
436
|
+
# 【改訂 4・SR-AI-001】`--reports-dir` はテスト・検証専用であり、正規経路
|
|
437
|
+
# (`start/SKILL.md`)は一切使わない。明示的な opt-in を関所にする。
|
|
438
|
+
if os.environ.get(REPORTS_DIR_OK_ENV, "").strip() != REPORTS_DIR_OK_VALUE:
|
|
439
|
+
print(
|
|
440
|
+
f"--reports-dir は環境変数 {REPORTS_DIR_OK_ENV}={REPORTS_DIR_OK_VALUE} が"
|
|
441
|
+
"設定されている場合のみ受け付ける(テスト・検証専用の引数)",
|
|
442
|
+
file=sys.stderr,
|
|
443
|
+
)
|
|
444
|
+
return None
|
|
445
|
+
reports_dir = Path(raw).resolve()
|
|
446
|
+
|
|
447
|
+
# 設計 §3-3c: 存在しない / ディレクトリでない場合はエラー(作らない)。
|
|
448
|
+
if not reports_dir.is_dir():
|
|
449
|
+
print(
|
|
450
|
+
f"対象ディレクトリがディレクトリとして存在しない: {_safe(reports_dir)}",
|
|
451
|
+
file=sys.stderr,
|
|
452
|
+
)
|
|
453
|
+
return None
|
|
454
|
+
return reports_dir
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def main(argv: list[str] | None = None) -> int:
|
|
458
|
+
parser = _build_parser()
|
|
459
|
+
try:
|
|
460
|
+
args = parser.parse_args(argv)
|
|
461
|
+
except SystemExit as exc:
|
|
462
|
+
# --help は 0、引数不正は 1(設計 §3-3)。例外のまま抜けさせない。
|
|
463
|
+
return 0 if exc.code in (0, None) else 1
|
|
464
|
+
|
|
465
|
+
# --- 検証 -------------------------------------------------------------
|
|
466
|
+
phases: list[str] = list(args.phase or [])
|
|
467
|
+
unknown = sorted({phase for phase in phases if phase not in PHASE_PREFIXES})
|
|
468
|
+
if unknown:
|
|
469
|
+
# 引数不正は検証段階で弾き、1 件も移動しない(部分適用を作らない・ADR-3)。
|
|
470
|
+
# 一覧は list の repr のまま出す(人間可読の 1 行。機械可読な行集合ではない)。
|
|
471
|
+
print(f"unknown --phase value(s): {_safe(unknown)}", file=sys.stderr)
|
|
472
|
+
print(f"valid values: {sorted(PHASE_PREFIXES)}", file=sys.stderr)
|
|
473
|
+
return 1
|
|
474
|
+
|
|
475
|
+
reports_dir = _resolve_reports_dir(args.reports_dir)
|
|
476
|
+
if reports_dir is None:
|
|
477
|
+
return 1
|
|
478
|
+
|
|
479
|
+
archive_dir = reports_dir / ARCHIVE_DIR_NAME
|
|
480
|
+
# 【改訂 5・CR-NEW / SR-V-002 再発】`mkdir` の前に検査する。`mkdir(exist_ok=True)` は
|
|
481
|
+
# リンク先が実在ディレクトリなら素通りするため、ここで止めないと移動先ごとすり替えられる。
|
|
482
|
+
# `is_symlink()` は NTFS ジャンクションに `False` を返し素通りするため、
|
|
483
|
+
# realpath 封じ込めで判定する(reparse の種別を問わない)。
|
|
484
|
+
if not _archive_dir_is_contained(archive_dir):
|
|
485
|
+
print(
|
|
486
|
+
f"アーカイブ先が対象ディレクトリの外へ解決されるため 1 件も処理しない: {_safe(archive_dir)}",
|
|
487
|
+
file=sys.stderr,
|
|
488
|
+
)
|
|
489
|
+
return 1
|
|
490
|
+
|
|
491
|
+
targets, skipped_links = _collect_targets(reports_dir, _selected_prefixes(phases))
|
|
492
|
+
|
|
493
|
+
if targets:
|
|
494
|
+
# `c3 init` 直後の利用先には archive/ が無い。作るのは archive/ 自身のみ(INV-2)。
|
|
495
|
+
try:
|
|
496
|
+
archive_dir.mkdir(parents=True, exist_ok=True)
|
|
497
|
+
except OSError as exc:
|
|
498
|
+
print(
|
|
499
|
+
f"アーカイブ先を作成できない: {_safe(archive_dir)} ({_one_line(exc)})",
|
|
500
|
+
file=sys.stderr,
|
|
501
|
+
)
|
|
502
|
+
return 1
|
|
503
|
+
|
|
504
|
+
# --- 委譲 -------------------------------------------------------------
|
|
505
|
+
outcome = _archive_all(targets, archive_dir)
|
|
506
|
+
|
|
507
|
+
# --- 出力 -------------------------------------------------------------
|
|
508
|
+
_report(outcome, archive_dir, skipped_links)
|
|
509
|
+
return 1 if outcome.has_problem() else 0
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
if __name__ == "__main__":
|
|
513
|
+
sys.exit(main())
|
|
@@ -1,5 +1,62 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.63.0] - 2026-08-09
|
|
4
|
+
|
|
5
|
+
### 追加
|
|
6
|
+
|
|
7
|
+
- **レポートのアーカイブを `\.claude/skills/start/scripts/archive_reports.py` へ集約した**(配布物)。
|
|
8
|
+
`/start` Step 0 のアーカイブは `mv` を 5 箇所・7 個に分散して持っており、**移動先に同名ファイルが
|
|
9
|
+
あると黙って上書きしていた**。reviewer のレポート名は task_id ベースの固定名で、task id は
|
|
10
|
+
スライスをまたいで再利用されるため、平坦な `archive/` では衝突が必然だった。実際に
|
|
11
|
+
2026-08-09 にレビューレポート 2 本を復元不能な形で失っている(`.claude/reports/` は gitignored)
|
|
12
|
+
- **移動先の名前を `O_CREAT|O_EXCL` で先に確保してから書き、成功後に移動元を削除する**。
|
|
13
|
+
`shutil.move` / `os.replace` は黙って上書きし、`os.rename` は POSIX でのみ上書きするため使わない。
|
|
14
|
+
「存在するか調べてから移動する」形も TOCTOU があるため採らない
|
|
15
|
+
- 衝突時は移動元を `{stem}-archived-{元の mtime}{suffix}` へリネームし、**既存と新規の両方を残す**。
|
|
16
|
+
`-archived-` の識別子を挟むのは、タイムスタンプ命名の正規レポート名と字面で区別するため
|
|
17
|
+
- 移動は**バイナリ I/O でバイト同一**を保つ(テキスト I/O は Windows 既定の cp932 で例外になり、
|
|
18
|
+
encoding を指定しても改行変換でバイト列が変わる)。移動先の mtime は `os.utime` で復元する
|
|
19
|
+
(復元しないと `c3 recall` の staleness 判定がアーカイブのたびに全件を「新しい」と見なす)
|
|
20
|
+
- `archive/` は**フラットのまま**維持する。`recall_index.py` が非再帰 glob で走査しており、
|
|
21
|
+
サブディレクトリ化すると既存のアーカイブが `c3 recall` の対象から丸ごと外れる
|
|
22
|
+
- 1 件失敗しても残りは処理し、stderr に失敗一覧を出して exit 1。**コピーは成功したが移動元の
|
|
23
|
+
削除だけ失敗した場合**は `FAILED` と別分類(`SOURCE_KEPT`)にし、移動先は削除しない
|
|
24
|
+
- `--phase requirements|architecture|plan|review` で対象を絞れる。`--reports-dir` は
|
|
25
|
+
テスト・検証専用で、環境変数 `C3_ARCHIVE_REPORTS_DIR_OK=1` がある場合のみ受け付ける
|
|
26
|
+
- **`check_deletions --check` を CI 化した**(`deletions-check` job)。リリース前の関所 4 つのうち
|
|
27
|
+
CI 化されていたのは `extract_breaking_changes --check` の 1 つだけで、残り 3 つは人手だった
|
|
28
|
+
- **未 push の先行コミットを検知する hook を追加した**(配布元専用・`.dev/hooks/_push_lag_watch.py`)。
|
|
29
|
+
3 コミット以上、または最古が 24 時間以上前で SessionStart 時に通知する。v2.61.0 以降 50 コミットを
|
|
30
|
+
溜めて CI が止まり、公開サイトの停止と回帰の検出漏れが同時に露見したことへの対処
|
|
31
|
+
|
|
32
|
+
### 修正
|
|
33
|
+
|
|
34
|
+
- **アーカイブ先ディレクトリ自体が reparse point の場合に、移動先をすり替えられる穴を塞いだ**。
|
|
35
|
+
当初 `Path.is_symlink()` で検査していたが、**NTFS ディレクトリジャンクションには `False` を返し、
|
|
36
|
+
`mklink /J` は管理者権限なしに作成できる**。`os.open` の `O_EXCL` は最終コンポーネントにしか
|
|
37
|
+
効かないため、親が reparse point なら追従する。実測では exit 0 の成功扱いでファイルが外部
|
|
38
|
+
ディレクトリへ移動し、移動元が削除された
|
|
39
|
+
- 判定を **`os.path.realpath` による封じ込め**へ置き換えた。アーカイブ先が対象ディレクトリ直下へ
|
|
40
|
+
解決されなければ、reparse の種別を問わず 1 件も処理せず exit 1 とする。`mode_line.py` の
|
|
41
|
+
plan-path 検査が既に採っている方式に揃えており、新しい機構は足していない
|
|
42
|
+
- この欠陥クラスは本リポジトリで 5 回目の再発。**知見は v2.62.0 の CHANGELOG にも書かれていた**が、
|
|
43
|
+
設計時に参照されなかった
|
|
44
|
+
- **`permissions.allow` の末尾ワイルドカードを正規経路 2 本へ絞った**。削除を伴うスクリプトを
|
|
45
|
+
追記系と同列に扱うと、`--reports-dir` を含む任意引数が無確認で通る。`--phase *` 形は後続の
|
|
46
|
+
`--reports-dir` とも前方一致しうるため、allow の絞り込みと env ゲートの二段で塞いでいる
|
|
47
|
+
- **stdout / stderr に出すファイル名・パス・失敗理由の制御文字を一元的に無害化した**(C0 と DEL)
|
|
48
|
+
- **GitHub Pages のビルドを止めていた docs ツリー外への相対リンクを修正した**。mkdocs `--strict` が
|
|
49
|
+
警告 1 件でビルドを中断するため、公開サイトが 7/24 の内容で停止していた
|
|
50
|
+
- **ブートストラップ検査が editable install で fail-open になる欠陥を修正した**。hatchling の `.pth` が
|
|
51
|
+
`src` を `sys.path` へ載せるため、スクリプト自身の bootstrap が無くても解決できてしまっていた
|
|
52
|
+
|
|
53
|
+
### 内部
|
|
54
|
+
|
|
55
|
+
- `.claude/docs/config-policy.md` に、`c3 run <script>*` 型の allow について**破壊操作を伴う
|
|
56
|
+
スクリプトを追記系と同列に扱わない**旨と、env ゲートが単一コマンド文字列への関所であり
|
|
57
|
+
セッションを跨いだ env 継承は射程外である旨を追記した
|
|
58
|
+
- テストを 41 件追加した(アーカイブの振る舞い・`start/SKILL.md` の静的契約・CI ワークフローのガード)
|
|
59
|
+
|
|
3
60
|
## [2.62.0] - 2026-08-09
|
|
4
61
|
|
|
5
62
|
### 追加
|