cc-session-tools 2.12.0__tar.gz → 2.12.2__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.
- {cc_session_tools-2.12.0/src/cc_session_tools.egg-info → cc_session_tools-2.12.2}/PKG-INFO +9 -2
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/README.md +8 -1
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/pyproject.toml +1 -1
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/cli/ccd.py +4 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/cli/ccr.py +4 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/cli/ccsched.py +38 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/cli/ccst.py +34 -0
- cc_session_tools-2.12.2/src/cc_session_tools/lib/occupancy.py +168 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/pdata/init_service.py +83 -20
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/pdata/repository.py +17 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/scheduler/jobspec.py +5 -2
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/scheduler/ledger.py +17 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/scheduler/registry.py +25 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/scheduler/worker.py +8 -2
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/skills/manage-recurring-cc-jobs-using-ccsched/SKILL.md +3 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2/src/cc_session_tools.egg-info}/PKG-INFO +9 -2
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cccs_hooks/pdata_sync.py +50 -22
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_ccst_pdata_init_cli.py +66 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_ccst_pdata_verify_cli.py +37 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_cli_ccd.py +6 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_cli_ccr.py +3 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_occupancy.py +79 -0
- cc_session_tools-2.12.0/src/cc_session_tools/lib/occupancy.py +0 -83
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/LICENSE +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/setup.cfg +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/__init__.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/cli/__init__.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/cli/ccmsg.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/cli/ccs.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/cli/migrate_ccmsg.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/cli/migrate_ccsched.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/cli/migrate_sessions_db.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/cli/migrate_telemetry.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/config/hooks-bundle.json +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/config/session-tag-hook.json +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/hooks_install.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/__init__.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/claude_flags.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/claude_md_install.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/context_overrides.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/db.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/debug.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/doctor.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/doctor_mutes.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/hook_registry.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/install_sync.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/levenshtein.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/machine_identity.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/messaging/__init__.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/messaging/addressing.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/messaging/cursor.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/messaging/lock.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/messaging/message.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/messaging/move_safety.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/messaging/repository.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/messaging/retention.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/messaging/service.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/messaging/store.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/paths.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/pdata/__init__.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/pdata/backup.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/pdata/classify.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/pdata/cutover.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/pdata/dump.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/pdata/formatting.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/pdata/importers.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/pdata/init_paths.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/pdata/manifest.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/pdata/naming.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/pdata/rehydrate.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/pdata/rename_group.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/pdata/reorganize.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/pdata/resolve.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/pdata/service.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/pdata/session_output.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/pdata/store.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/pdata/sync_check.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/pdata/sync_lock.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/pdata/sync_notify.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/pdata/vector_clock.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/pdata/vector_clock_store.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/pdata/verify.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/pdata/write_log.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/picker.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/proc_lock.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/prompts.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/roots.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/rules.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/scheduler/__init__.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/scheduler/bundled_jobs.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/scheduler/cadence.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/scheduler/cursor.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/scheduler/digest.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/scheduler/due.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/scheduler/duration.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/scheduler/lock.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/scheduler/notify.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/scheduler/reconcile.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/scheduler/runner.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/scheduler/state.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/scheduler/store.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/scheduler/surface.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/scheduler/throttle.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/session_gc.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/sessions.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/sessions_db.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/sessions_repair.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/shell_install.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/tasklist.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/telemetry_store.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/prompts/pdata-migration-claude-md-update.md +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/prompts/pdata-migration-skills-update.md +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/skills/analyse-cc-usage/SKILL.md +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/skills/clean-hook-sessions/SKILL.md +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/skills/clean-hook-sessions/scripts/clean-hook-sessions.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/skills/context-override/SKILL.md +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/skills/delete-sessions/SKILL.md +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/skills/delete-sessions/scripts/delete_sessions.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/skills/do-executor-critic-assessor-loop/SKILL.md +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/skills/find-claude-code-session/SKILL.md +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/skills/generate-8digit-code/SKILL.md +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/skills/generate-8digit-code/scripts/generate_8digit_code.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/skills/list-empty-sessions/SKILL.md +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/skills/list-empty-sessions/scripts/list_empty_sessions.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/skills/move-session/SKILL.md +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/skills/move-session/hooks/sessionstart-pending-rename.sh +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/skills/move-session/scripts/cc_session_rules.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/skills/move-session/scripts/move_session.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/skills/pm-pdata-conflict-resolution/SKILL.md +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/skills/pm-pdata-schema-design/SKILL.md +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/skills/pm-project-init/SKILL.md +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/skills/pm-project-layout-reference/SKILL.md +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/skills/pm-update-central-files/SKILL.md +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/skills/reduce-persistent-context/CALIBRATION.md +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/skills/reduce-persistent-context/SKILL.md +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/skills/reduce-persistent-context/scripts/analyze_context.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/skills/reduce-persistent-context/scripts/duplicates.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/skills/reduce-persistent-context/scripts/measure.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/skills/reduce-persistent-context/scripts/report.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/skills/reduce-persistent-context/scripts/tokens.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/skills/reduce-persistent-context/scripts/usage.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/skills/select-agent-model/SKILL.md +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/skills/send-session-message/SKILL.md +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/skills/update-command-cache/SKILL.md +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/skills/update-command-cache/scripts/update_command_cache.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools.egg-info/SOURCES.txt +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools.egg-info/dependency_links.txt +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools.egg-info/entry_points.txt +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools.egg-info/requires.txt +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools.egg-info/top_level.txt +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cccs_hooks/__init__.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cccs_hooks/after_response.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cccs_hooks/bash_hard_deny.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cccs_hooks/bash_security_review.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cccs_hooks/cache.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cccs_hooks/catchup.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cccs_hooks/confirm_8digit.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cccs_hooks/context_window_warning.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cccs_hooks/last_screenshot.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cccs_hooks/marker_allow.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cccs_hooks/markers.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cccs_hooks/messaging_deliver.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cccs_hooks/model_info.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cccs_hooks/normalise.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cccs_hooks/pending_migration.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cccs_hooks/pending_rename.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cccs_hooks/session_tag.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cccs_hooks/stats.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cccs_hooks/telemetry.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cccs_hooks/telemetry_query.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cccs_hooks/telemetry_trim.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cccs_hooks/transcript.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cccs_hooks/worklog_guard.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/claude_code_usage/__init__.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/claude_code_usage/attribution.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/claude_code_usage/cache.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/claude_code_usage/ccusage_wrapper.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/claude_code_usage/cli.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/claude_code_usage/data/pricing.json +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/claude_code_usage/parent_inference.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/claude_code_usage/parser.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/claude_code_usage/pricing.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/claude_code_usage/query.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/claude_code_usage/report.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/claude_code_usage/schema.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/claude_code_usage/session_names.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_after_response.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_attribution.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_bash_hard_deny.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_bash_security_review.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_cache.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_cache_sqlite.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_ccr_orphans.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_ccs_emptiness.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_ccs_eta.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_ccs_list_mode.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_ccs_scope_flags.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_ccs_sentinel_sort.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_ccs_session_counts.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_ccst_bundle_discovery.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_ccst_ccsched_jobs_cli.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_ccst_cli.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_ccst_context_override_cli.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_ccst_doctor.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_ccst_gc_prune.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_ccst_gc_report.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_ccst_hook_dispatcher.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_ccst_hooks_install_prune.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_ccst_hooks_install_zero_arg.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_ccst_hooks_uninstall.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_ccst_install_everything.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_ccst_machine_identity_cli.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_ccst_main_gate.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_ccst_pdata_cli.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_ccst_pdata_reconcile_cli.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_ccst_pdata_rename_group_cli.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_ccst_pdata_reorganize_cli.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_ccst_pdata_sync_cli.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_ccst_repair_sessions_cli.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_ccst_sessions_cli.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_ccst_shell_install.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_ccst_skills_install.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_ccst_skills_uninstall.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_ccst_telemetry_query.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_ccst_telemetry_trim.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_ccusage_wrapper.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_claude_flags.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_claude_md_install.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_cli.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_cli_ccd_dry_run.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_cli_ccs.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_cli_version.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_confirm_8digit.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_context_overrides.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_context_window_warning.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_db.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_debug.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_doctor_mutes.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_empty_session.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_hook_cache.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_hooks_install.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_install_sync.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_last_screenshot.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_levenshtein.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_machine_identity.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_marker_allow.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_migrate_ccmsg.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_migrate_ccsched.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_migrate_sessions_db.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_migrate_telemetry.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_model_info.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_normalise.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_parent_inference.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_parser.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_paths.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_pending_migration_hook.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_picker.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_pricing.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_proc_lock.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_prompts.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_query.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_report.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_roots.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_rules.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_scheduler_bundled_jobs.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_schema.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_session_names.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_session_tag.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_sessions.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_sessions_db.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_sessions_repair.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_tasklist.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_telemetry.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_telemetry_store.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_transcript.py +0 -0
- {cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/tests/test_worklog_guard.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cc-session-tools
|
|
3
|
-
Version: 2.12.
|
|
3
|
+
Version: 2.12.2
|
|
4
4
|
Summary: Claude Code session management CLI tools (ccd, ccr, ccs, claude-code-usage, ccst) and hook library (cccs_hooks)
|
|
5
5
|
Author: raffishquartan
|
|
6
6
|
License: MIT License
|
|
@@ -80,7 +80,7 @@ bundled scheduled jobs:
|
|
|
80
80
|
| **`claude-code-usage`** | Multi-dimensional usage analytics CLI: query/group/filter by project, session, model, MCP server, plugin, tool, day/week/month/year. Reconciles dollar totals against `ccusage`. |
|
|
81
81
|
| **`ccst <noun> <verb>`** | Umbrella CLI for hook and skill management: install, uninstall, health-check, shell helpers, telemetry trim, global CLAUDE.md messaging block. |
|
|
82
82
|
| **`ccmsg <command>`** | Inter-session messaging CLI: send, deliver, read, list, claim, and archive durable messages between Claude Code sessions. |
|
|
83
|
-
| **`ccsched <command>`** | Scheduled-task CLI: register, list, edit, enable/disable, and remove recurring jobs; inspect status; one-shot sweep. |
|
|
83
|
+
| **`ccsched <command>`** | Scheduled-task CLI: register, list, edit, rename, enable/disable, and remove recurring jobs; inspect status; one-shot sweep. |
|
|
84
84
|
| **`ccl` (shell fn)** | Shell function wrapping `ccs` for list-mode usage. Installed by `ccst shell install`. |
|
|
85
85
|
|
|
86
86
|
**Bundled skills** (installed via `ccst skills install`)
|
|
@@ -630,6 +630,7 @@ loser exits).
|
|
|
630
630
|
| `edit` | Edit a job field (cadence, command, coalesce, etc.) in-place. |
|
|
631
631
|
| `enable` / `disable` | Toggle a job on or off without removing it. |
|
|
632
632
|
| `remove` | Delete a job from the registry. |
|
|
633
|
+
| `rename` | Rename a job's id; its run state and ledger history carry over to the new id. Refuses while the job is running. |
|
|
633
634
|
| `run` | Run a job immediately (foreground; bypasses cadence). |
|
|
634
635
|
| `status` | Show recent ledger history for a job (or all jobs). |
|
|
635
636
|
| `sweep` | One-shot reconcile + launch from the shell. |
|
|
@@ -1073,6 +1074,12 @@ Claude Code lets multiple sessions share a single task list if they all set the
|
|
|
1073
1074
|
|
|
1074
1075
|
This means you can pick up a task created in yesterday's session from today's session in the same project, without any extra setup.
|
|
1075
1076
|
|
|
1077
|
+
Both `ccd` and `ccr` also unconditionally set `CLAUDE_CODE_ENABLE_TODO_TOOLS=1` in the launched
|
|
1078
|
+
session's environment. Claude Code >=2.1.233 hides the TodoWrite/TaskCreate/TaskGet/TaskUpdate/
|
|
1079
|
+
TaskList tools by default on Opus 4.8, Sonnet 5, Fable 5, Mythos 5 and later unless this is set -
|
|
1080
|
+
without it, the task list machinery above would be wired up but the tools that use it would be
|
|
1081
|
+
invisible to the model.
|
|
1082
|
+
|
|
1076
1083
|
## Typo protection (strict root only)
|
|
1077
1084
|
|
|
1078
1085
|
When you start a session under the **strict** (`PROJ_ROOT`) root, `ccd` checks whether your tag's first dash-separated term looks like a typo of the project directory name (Levenshtein distance ≤ 2):
|
|
@@ -23,7 +23,7 @@ bundled scheduled jobs:
|
|
|
23
23
|
| **`claude-code-usage`** | Multi-dimensional usage analytics CLI: query/group/filter by project, session, model, MCP server, plugin, tool, day/week/month/year. Reconciles dollar totals against `ccusage`. |
|
|
24
24
|
| **`ccst <noun> <verb>`** | Umbrella CLI for hook and skill management: install, uninstall, health-check, shell helpers, telemetry trim, global CLAUDE.md messaging block. |
|
|
25
25
|
| **`ccmsg <command>`** | Inter-session messaging CLI: send, deliver, read, list, claim, and archive durable messages between Claude Code sessions. |
|
|
26
|
-
| **`ccsched <command>`** | Scheduled-task CLI: register, list, edit, enable/disable, and remove recurring jobs; inspect status; one-shot sweep. |
|
|
26
|
+
| **`ccsched <command>`** | Scheduled-task CLI: register, list, edit, rename, enable/disable, and remove recurring jobs; inspect status; one-shot sweep. |
|
|
27
27
|
| **`ccl` (shell fn)** | Shell function wrapping `ccs` for list-mode usage. Installed by `ccst shell install`. |
|
|
28
28
|
|
|
29
29
|
**Bundled skills** (installed via `ccst skills install`)
|
|
@@ -573,6 +573,7 @@ loser exits).
|
|
|
573
573
|
| `edit` | Edit a job field (cadence, command, coalesce, etc.) in-place. |
|
|
574
574
|
| `enable` / `disable` | Toggle a job on or off without removing it. |
|
|
575
575
|
| `remove` | Delete a job from the registry. |
|
|
576
|
+
| `rename` | Rename a job's id; its run state and ledger history carry over to the new id. Refuses while the job is running. |
|
|
576
577
|
| `run` | Run a job immediately (foreground; bypasses cadence). |
|
|
577
578
|
| `status` | Show recent ledger history for a job (or all jobs). |
|
|
578
579
|
| `sweep` | One-shot reconcile + launch from the shell. |
|
|
@@ -1016,6 +1017,12 @@ Claude Code lets multiple sessions share a single task list if they all set the
|
|
|
1016
1017
|
|
|
1017
1018
|
This means you can pick up a task created in yesterday's session from today's session in the same project, without any extra setup.
|
|
1018
1019
|
|
|
1020
|
+
Both `ccd` and `ccr` also unconditionally set `CLAUDE_CODE_ENABLE_TODO_TOOLS=1` in the launched
|
|
1021
|
+
session's environment. Claude Code >=2.1.233 hides the TodoWrite/TaskCreate/TaskGet/TaskUpdate/
|
|
1022
|
+
TaskList tools by default on Opus 4.8, Sonnet 5, Fable 5, Mythos 5 and later unless this is set -
|
|
1023
|
+
without it, the task list machinery above would be wired up but the tools that use it would be
|
|
1024
|
+
invisible to the model.
|
|
1025
|
+
|
|
1019
1026
|
## Typo protection (strict root only)
|
|
1020
1027
|
|
|
1021
1028
|
When you start a session under the **strict** (`PROJ_ROOT`) root, `ccd` checks whether your tag's first dash-separated term looks like a typo of the project directory name (Levenshtein distance ≤ 2):
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cc-session-tools"
|
|
7
|
-
version = "2.12.
|
|
7
|
+
version = "2.12.2"
|
|
8
8
|
description = "Claude Code session management CLI tools (ccd, ccr, ccs, claude-code-usage, ccst) and hook library (cccs_hooks)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { file = "LICENSE" }
|
|
@@ -200,6 +200,10 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
200
200
|
env["CLD_SESSION_TAG"] = tag
|
|
201
201
|
env["CLD_SESSION_DIR"] = str(session_dir)
|
|
202
202
|
env["CLD_SESSION_MODE"] = "new"
|
|
203
|
+
# Claude Code >=2.1.233 hides TodoWrite/TaskCreate/TaskGet/TaskUpdate/TaskList
|
|
204
|
+
# on Opus 4.8, Sonnet 5, Fable 5, Mythos 5+ unless this is set, regardless of
|
|
205
|
+
# whether a task list ID resolves below.
|
|
206
|
+
env["CLAUDE_CODE_ENABLE_TODO_TOOLS"] = "1"
|
|
203
207
|
task_list_id = id_for_project(real_pwd)
|
|
204
208
|
if task_list_id is not None:
|
|
205
209
|
env["CLAUDE_CODE_TASK_LIST_ID"] = task_list_id
|
|
@@ -221,6 +221,10 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
221
221
|
env["CLD_SESSION_TAG"] = tag
|
|
222
222
|
env["CLD_SESSION_DIR"] = str(m.session_dir)
|
|
223
223
|
env["CLD_SESSION_MODE"] = "resume"
|
|
224
|
+
# Claude Code >=2.1.233 hides TodoWrite/TaskCreate/TaskGet/TaskUpdate/TaskList
|
|
225
|
+
# on Opus 4.8, Sonnet 5, Fable 5, Mythos 5+ unless this is set, regardless of
|
|
226
|
+
# whether a task list ID resolves below.
|
|
227
|
+
env["CLAUDE_CODE_ENABLE_TODO_TOOLS"] = "1"
|
|
224
228
|
task_list_id = id_for_project(m.project_dir)
|
|
225
229
|
if task_list_id is not None:
|
|
226
230
|
env["CLAUDE_CODE_TASK_LIST_ID"] = task_list_id
|
|
@@ -25,9 +25,11 @@ from cc_session_tools.lib.scheduler.duration import parse_duration
|
|
|
25
25
|
from cc_session_tools.lib.scheduler.jobspec import (
|
|
26
26
|
JobSpec,
|
|
27
27
|
JobValidationError,
|
|
28
|
+
check_job_id,
|
|
28
29
|
parse_success_exit_codes,
|
|
29
30
|
validate_job_fields,
|
|
30
31
|
)
|
|
32
|
+
from cc_session_tools.lib.scheduler.lock import InFlightLockHeld, in_flight_lock
|
|
31
33
|
from cc_session_tools.lib.scheduler.runner import run_command
|
|
32
34
|
|
|
33
35
|
|
|
@@ -137,6 +139,18 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
137
139
|
sp = sub.add_parser(verb, help=f"{verb.capitalize()} a job.")
|
|
138
140
|
sp.add_argument("id")
|
|
139
141
|
|
|
142
|
+
rename_p = sub.add_parser(
|
|
143
|
+
"rename",
|
|
144
|
+
help="Rename a job, keeping its run history under the new id.",
|
|
145
|
+
)
|
|
146
|
+
rename_p.add_argument("old_id", metavar="old-id", help="The job's current id.")
|
|
147
|
+
rename_p.add_argument(
|
|
148
|
+
"new_id", metavar="new-id",
|
|
149
|
+
help="The new id, lowercase kebab-case [a-z0-9-]. Run state (last_success, "
|
|
150
|
+
"consecutive_failures, suspended, ...) and ledger history (`ccsched status`) "
|
|
151
|
+
"carry over. Refuses while the job is currently running.",
|
|
152
|
+
)
|
|
153
|
+
|
|
140
154
|
run_p = sub.add_parser("run", help="Run one job now.")
|
|
141
155
|
run_p.add_argument("id")
|
|
142
156
|
|
|
@@ -303,6 +317,25 @@ def _cmd_remove(job_id: str) -> int:
|
|
|
303
317
|
return 0
|
|
304
318
|
|
|
305
319
|
|
|
320
|
+
def _cmd_rename(args: argparse.Namespace) -> int:
|
|
321
|
+
if args.new_id == args.old_id:
|
|
322
|
+
return _err("new id is the same as the current id")
|
|
323
|
+
try:
|
|
324
|
+
check_job_id(args.new_id)
|
|
325
|
+
except JobValidationError as exc:
|
|
326
|
+
return _err(str(exc))
|
|
327
|
+
try:
|
|
328
|
+
with in_flight_lock(args.old_id):
|
|
329
|
+
registry.rename_job(args.old_id, args.new_id)
|
|
330
|
+
ledger.rename_job(args.old_id, args.new_id)
|
|
331
|
+
except InFlightLockHeld:
|
|
332
|
+
return _err(f"job {args.old_id!r} is currently running; try again once it finishes")
|
|
333
|
+
except registry.RegistryError as exc:
|
|
334
|
+
return _err(str(exc))
|
|
335
|
+
print(f"renamed {args.old_id} -> {args.new_id}")
|
|
336
|
+
return 0
|
|
337
|
+
|
|
338
|
+
|
|
306
339
|
def _cmd_run(
|
|
307
340
|
args: argparse.Namespace, *, notify_push: worker.NotifyPush = notify.push_outcome,
|
|
308
341
|
) -> int:
|
|
@@ -340,6 +373,9 @@ def _cmd_status(args: argparse.Namespace) -> int:
|
|
|
340
373
|
for r in rows:
|
|
341
374
|
print(f"{r.get('ts','')} {r.get('job_id',''):<24} {r.get('event',''):<12} "
|
|
342
375
|
f"ran={r.get('ran')} exit={r.get('exit_code')}")
|
|
376
|
+
error = r.get("error")
|
|
377
|
+
if error:
|
|
378
|
+
print(f" {error}")
|
|
343
379
|
return 0
|
|
344
380
|
|
|
345
381
|
|
|
@@ -380,6 +416,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
380
416
|
return _cmd_set_enabled(args.id, False)
|
|
381
417
|
if args.command == "remove":
|
|
382
418
|
return _cmd_remove(args.id)
|
|
419
|
+
if args.command == "rename":
|
|
420
|
+
return _cmd_rename(args)
|
|
383
421
|
if args.command == "run":
|
|
384
422
|
return _cmd_run(args)
|
|
385
423
|
if args.command == "status":
|
|
@@ -1156,6 +1156,11 @@ def _cmd_pdata_init(args: argparse.Namespace) -> int:
|
|
|
1156
1156
|
print(f"ccst pdata: {exc}", file=sys.stderr)
|
|
1157
1157
|
return 2
|
|
1158
1158
|
print(result.report)
|
|
1159
|
+
if result.would_adopt_from_dump:
|
|
1160
|
+
# No proposal was written and there's nothing to classify - the reminders below are
|
|
1161
|
+
# about updating project docs after a real classify/import cutover, which doesn't
|
|
1162
|
+
# apply here.
|
|
1163
|
+
return 0
|
|
1159
1164
|
print(f"\nProposal: {result.proposal_path}")
|
|
1160
1165
|
_print_migration_prompt_reminders(
|
|
1161
1166
|
result.proposal_path.parent,
|
|
@@ -1407,6 +1412,35 @@ def _cmd_pdata_verify(args: argparse.Namespace) -> int:
|
|
|
1407
1412
|
print(f"ccst pdata verify: {project}: {exc}", file=sys.stderr)
|
|
1408
1413
|
worst = max(worst, 2)
|
|
1409
1414
|
continue
|
|
1415
|
+
except Exception as exc:
|
|
1416
|
+
# run_verify crashed before persisting anything for this project - a
|
|
1417
|
+
# per-project issue (its own except ValueError above) is the only
|
|
1418
|
+
# *expected* failure shape; anything else is a bug or a transient
|
|
1419
|
+
# race (e.g. a concurrent writer) worth surfacing in full rather than
|
|
1420
|
+
# letting it silently take the whole --all-projects sweep down with
|
|
1421
|
+
# no diagnostic trail. Rerun once, in --full mode, as a one-shot
|
|
1422
|
+
# diagnostic: exactly one retry, never chained further even if the
|
|
1423
|
+
# retry crashes too, so a persistently broken project reports once
|
|
1424
|
+
# and moves on instead of turning into a retry loop.
|
|
1425
|
+
print(f"ccst pdata verify: {project}: crashed: {exc}", file=sys.stderr)
|
|
1426
|
+
worst = max(worst, 1)
|
|
1427
|
+
flagged.append(project)
|
|
1428
|
+
try:
|
|
1429
|
+
diag = verify.run_verify(project=project, full=True)
|
|
1430
|
+
except Exception as diag_exc:
|
|
1431
|
+
print(
|
|
1432
|
+
f"ccst pdata verify: {project}: diagnostic rerun also crashed: {diag_exc}",
|
|
1433
|
+
file=sys.stderr,
|
|
1434
|
+
)
|
|
1435
|
+
else:
|
|
1436
|
+
print(
|
|
1437
|
+
f"ccst pdata verify: {project}: diagnostic rerun: {diag.status} "
|
|
1438
|
+
f"({len(diag.issues)} issue(s))",
|
|
1439
|
+
file=sys.stderr,
|
|
1440
|
+
)
|
|
1441
|
+
for issue in diag.issues:
|
|
1442
|
+
print(f" [{issue.severity}] {issue.check}: {issue.message}", file=sys.stderr)
|
|
1443
|
+
continue
|
|
1410
1444
|
if summary.status != "OK":
|
|
1411
1445
|
worst = max(worst, 1)
|
|
1412
1446
|
flagged.append(project)
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Is a live `claude` process currently working in a given project (spec: "Process safety").
|
|
2
|
+
|
|
3
|
+
Fails safe: any error enumerating `claude` processes, or resolving a PID's cwd, counts as
|
|
4
|
+
occupied, never as clear - see is_occupied()'s single try/except around both steps. Letting
|
|
5
|
+
either failure fall through to "not occupied" would be indistinguishable from the legitimate
|
|
6
|
+
"no claude process right now" case to every caller, which is exactly the false "clear to
|
|
7
|
+
rehydrate" signal this module exists to prevent.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _claude_pids() -> list[int]:
|
|
18
|
+
"""Every running `claude` process's PID. Raises (OSError, subprocess.SubprocessError) on any
|
|
19
|
+
genuine failure to enumerate them - does not swallow these into an empty list, since an
|
|
20
|
+
empty list here is indistinguishable from "pgrep ran fine and found nothing" to is_occupied().
|
|
21
|
+
A *clean* "no matches" from pgrep itself (exit code 1, empty stdout, no exception) is not a
|
|
22
|
+
failure and correctly produces []."""
|
|
23
|
+
out = subprocess.run(
|
|
24
|
+
["pgrep", "-x", "claude"],
|
|
25
|
+
capture_output=True,
|
|
26
|
+
text=True,
|
|
27
|
+
timeout=5,
|
|
28
|
+
).stdout
|
|
29
|
+
# str.split() with no args already splits on whitespace and drops empty
|
|
30
|
+
# tokens, so each token is already stripped — no further .strip() needed.
|
|
31
|
+
return [int(p) for p in out.split() if p.isdigit()]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _cwd_of_pid(pid: int) -> Path:
|
|
35
|
+
"""pid's current working directory. Raises (OSError, subprocess.SubprocessError) on any
|
|
36
|
+
failure - callers must catch both, not just OSError (a nonzero lsof exit, e.g. the ordinary
|
|
37
|
+
race where the process has already exited, raises CalledProcessError, not OSError)."""
|
|
38
|
+
if sys.platform == "darwin":
|
|
39
|
+
# lsof's field-output mode ("man lsof", OUTPUT FOR OTHER PROGRAMS)
|
|
40
|
+
# always emits the PID ('p') field, then whichever other fields were
|
|
41
|
+
# requested — here just the name ('n') field — each on its own line,
|
|
42
|
+
# prefixed by its field character. So scanning for the line that
|
|
43
|
+
# starts with 'n' and stripping that one character is documented
|
|
44
|
+
# lsof behaviour, not a guess.
|
|
45
|
+
out = subprocess.run(
|
|
46
|
+
["lsof", "-a", "-p", str(pid), "-d", "cwd", "-Fn"],
|
|
47
|
+
capture_output=True,
|
|
48
|
+
text=True,
|
|
49
|
+
timeout=5,
|
|
50
|
+
check=True,
|
|
51
|
+
).stdout
|
|
52
|
+
for line in out.splitlines():
|
|
53
|
+
if line.startswith("n"):
|
|
54
|
+
return Path(line[1:])
|
|
55
|
+
raise OSError(f"lsof returned no cwd line for pid {pid}")
|
|
56
|
+
if sys.platform.startswith("linux"):
|
|
57
|
+
return Path(os.readlink(f"/proc/{pid}/cwd"))
|
|
58
|
+
# A third platform (e.g. Windows-native Python, not WSL) has no /proc and isn't darwin -
|
|
59
|
+
# raise explicitly rather than silently falling into the Linux branch and failing with a
|
|
60
|
+
# confusing FileNotFoundError. Still caught by is_occupied()'s fail-safe handler below.
|
|
61
|
+
raise OSError(f"occupancy check not implemented for platform {sys.platform!r}")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _ppid_of(pid: int) -> int | None:
|
|
65
|
+
"""`pid`'s parent PID, or None if it can't be determined (already exited, permission error,
|
|
66
|
+
or an unsupported platform) - every failure degrades to None rather than raising, since the
|
|
67
|
+
only caller, `launching_claude_pid`, already treats "can't climb any further" as a normal
|
|
68
|
+
stopping condition, not an error."""
|
|
69
|
+
if sys.platform.startswith("linux"):
|
|
70
|
+
try:
|
|
71
|
+
status = Path(f"/proc/{pid}/status").read_text()
|
|
72
|
+
except OSError:
|
|
73
|
+
return None
|
|
74
|
+
for line in status.splitlines():
|
|
75
|
+
if line.startswith("PPid:"):
|
|
76
|
+
return int(line.split()[1])
|
|
77
|
+
return None
|
|
78
|
+
if sys.platform == "darwin":
|
|
79
|
+
try:
|
|
80
|
+
out = subprocess.run(
|
|
81
|
+
["ps", "-o", "ppid=", "-p", str(pid)],
|
|
82
|
+
capture_output=True, text=True, timeout=5, check=True,
|
|
83
|
+
).stdout.strip()
|
|
84
|
+
except (OSError, subprocess.SubprocessError):
|
|
85
|
+
return None
|
|
86
|
+
return int(out) if out.isdigit() else None
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _comm_of(pid: int) -> str | None:
|
|
91
|
+
"""`pid`'s process name (`/proc/<pid>/comm`'s exact contents on Linux - the same field
|
|
92
|
+
`pgrep -x` matches against), or None if it can't be read."""
|
|
93
|
+
if sys.platform.startswith("linux"):
|
|
94
|
+
try:
|
|
95
|
+
return Path(f"/proc/{pid}/comm").read_text().strip()
|
|
96
|
+
except OSError:
|
|
97
|
+
return None
|
|
98
|
+
if sys.platform == "darwin":
|
|
99
|
+
try:
|
|
100
|
+
out = subprocess.run(
|
|
101
|
+
["ps", "-o", "comm=", "-p", str(pid)],
|
|
102
|
+
capture_output=True, text=True, timeout=5, check=True,
|
|
103
|
+
).stdout.strip()
|
|
104
|
+
except (OSError, subprocess.SubprocessError):
|
|
105
|
+
return None
|
|
106
|
+
return out or None
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def launching_claude_pid(start_pid: int, *, max_hops: int = 5) -> int | None:
|
|
111
|
+
"""The nearest ancestor of `start_pid` (inclusive) whose process name is `claude`, or None if
|
|
112
|
+
none is found within `max_hops` steps.
|
|
113
|
+
|
|
114
|
+
Exists because a hook's `command` is not run as a direct child of the `claude` process: Claude
|
|
115
|
+
Code spawns every command hook via `/bin/sh -c "<command>"`, and on any system where `/bin/sh`
|
|
116
|
+
is dash rather than bash - the Debian/Ubuntu/WSL2 default - dash does not replace itself with
|
|
117
|
+
the command it's running the way bash does for a single simple `-c` script. That leaves a real
|
|
118
|
+
`sh` process between `claude` and the hook's own process, confirmed empirically (not assumed)
|
|
119
|
+
by installing a diagnostic `SessionStart` hook in a scratch project and inspecting
|
|
120
|
+
`/proc/<pid>/{comm,status}` for the whole ancestor chain: `ccst` (the hook) -> `sh -c ...` ->
|
|
121
|
+
`claude` -> the shell that launched it, exactly two hops from `os.getppid()`'s immediate
|
|
122
|
+
parent to the real `claude` process, not one. A caller that naively used `os.getppid()` as
|
|
123
|
+
"the launching claude process's PID" (as this hook's `main()` used to) would therefore be
|
|
124
|
+
excluding the wrong PID from `is_occupied()`'s check every time - one that will never actually
|
|
125
|
+
appear in `_claude_pids()`'s `pgrep -x claude` results - so the exclusion silently never
|
|
126
|
+
matches anything, and a SessionStart hook checking its own just-started project reports it as
|
|
127
|
+
occupied by itself on every single invocation.
|
|
128
|
+
|
|
129
|
+
Bounded to `max_hops` (comfortably above the 2 hops seen in the confirmed case, to tolerate
|
|
130
|
+
extra wrapping such as a `timeout` command) so an unrecognised process-tree shape degrades to
|
|
131
|
+
None rather than looping forever; a caller receiving None should exclude nothing, matching
|
|
132
|
+
this module's existing bias toward reporting occupied over silently under-excluding.
|
|
133
|
+
|
|
134
|
+
Unconfirmed on macOS's own `/bin/sh` (Apple's bash 3.2 build) - it may already perform the
|
|
135
|
+
single-command replacement bash does elsewhere, making the extra hop unnecessary there; this
|
|
136
|
+
function is safe either way since it stops as soon as it finds a `claude` ancestor, however
|
|
137
|
+
many hops that takes."""
|
|
138
|
+
pid = start_pid
|
|
139
|
+
for _ in range(max_hops):
|
|
140
|
+
if _comm_of(pid) == "claude":
|
|
141
|
+
return pid
|
|
142
|
+
parent = _ppid_of(pid)
|
|
143
|
+
if parent is None or parent == pid:
|
|
144
|
+
return None
|
|
145
|
+
pid = parent
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def is_occupied(project_root: Path, *, exclude_pid: int | None = None) -> bool:
|
|
150
|
+
"""True if any live `claude` process (other than `exclude_pid`) has its cwd resolved to
|
|
151
|
+
exactly `project_root` - matching Claude Code's own behaviour of keeping a session's cwd
|
|
152
|
+
fixed at the project root for its whole lifetime (see e.g. ccd.py's os.chdir(real_pwd)),
|
|
153
|
+
never a cc-sessions/<tag>/ subdirectory."""
|
|
154
|
+
resolved_root = project_root.resolve()
|
|
155
|
+
try:
|
|
156
|
+
pids = _claude_pids()
|
|
157
|
+
except (OSError, subprocess.SubprocessError):
|
|
158
|
+
return True # can't even enumerate claude processes - fail safe, never fail open
|
|
159
|
+
for pid in pids:
|
|
160
|
+
if pid == exclude_pid:
|
|
161
|
+
continue
|
|
162
|
+
try:
|
|
163
|
+
cwd = _cwd_of_pid(pid).resolve()
|
|
164
|
+
except (OSError, subprocess.SubprocessError):
|
|
165
|
+
return True # fail safe, never fail open
|
|
166
|
+
if cwd == resolved_root:
|
|
167
|
+
return True
|
|
168
|
+
return False
|
{cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/pdata/init_service.py
RENAMED
|
@@ -32,11 +32,43 @@ class DryRunResult:
|
|
|
32
32
|
manifest: Manifest
|
|
33
33
|
report: str
|
|
34
34
|
proposal_path: Path
|
|
35
|
+
# True iff --write would adopt from an existing sync dump rather than classify/import this
|
|
36
|
+
# machine's flat files - see _pending_adoption. When True, `report` already describes the
|
|
37
|
+
# adoption and no proposal file was written; `proposal_path` is still populated (where one
|
|
38
|
+
# *would* go) for callers that print it unconditionally, but shouldn't be treated as pointing
|
|
39
|
+
# at a real file.
|
|
40
|
+
would_adopt_from_dump: bool = False
|
|
35
41
|
|
|
36
42
|
|
|
37
43
|
def dry_run(*, project: str, rehearse: Path | None = None) -> DryRunResult:
|
|
38
44
|
project_root = init_paths.resolve_project_root(project, rehearse=rehearse)
|
|
45
|
+
proposal_path = project_root / init_paths.PROPOSAL_FILENAME
|
|
39
46
|
with init_paths.project_db_dir_override(rehearse):
|
|
47
|
+
# Checked before anything below opens/creates this project's .db (repository.connect()'s
|
|
48
|
+
# own DDL does that as a side effect - see _already_locally_migrated): a dry run must
|
|
49
|
+
# never create persistent state that could itself go on to poison a later adopt-from-dump
|
|
50
|
+
# decision, which is exactly what happened before this check existed - see that
|
|
51
|
+
# function's docstring for the real incident.
|
|
52
|
+
pending = _pending_adoption(project=project, project_root=project_root)
|
|
53
|
+
if pending is not None:
|
|
54
|
+
latest_path = project_root / ".pdata-db-dump" / "latest.sql"
|
|
55
|
+
published_at = _format_published_at(pending, latest_path)
|
|
56
|
+
report = (
|
|
57
|
+
f"ccst pdata init --project {project}: a published sync dump already exists "
|
|
58
|
+
f"(from {pending.machine_id}, at {published_at}) and this machine has no local "
|
|
59
|
+
f"pdata content for this project yet.\n\n`--write` will adopt that dump directly "
|
|
60
|
+
f"(same mechanism as the ongoing sync trigger) instead of classifying/importing "
|
|
61
|
+
f"this machine's current flat files, which would create a second, disconnected "
|
|
62
|
+
f"history for this project.\n\nRun `ccst pdata init --project {project} --write` "
|
|
63
|
+
f"to adopt it."
|
|
64
|
+
)
|
|
65
|
+
return DryRunResult(
|
|
66
|
+
manifest=Manifest(project=project, entries=[]),
|
|
67
|
+
report=report,
|
|
68
|
+
proposal_path=proposal_path,
|
|
69
|
+
would_adopt_from_dump=True,
|
|
70
|
+
)
|
|
71
|
+
|
|
40
72
|
# repository.connect() runs the base-schema DDL (CREATE TABLE IF NOT
|
|
41
73
|
# EXISTS) on every call — this is what "safe to run against an empty
|
|
42
74
|
# folder... also how a genuinely new project gets its .db" (spec §5) means.
|
|
@@ -50,7 +82,6 @@ def dry_run(*, project: str, rehearse: Path | None = None) -> DryRunResult:
|
|
|
50
82
|
existing_record_groups = frozenset(
|
|
51
83
|
str(group["record_group"]) for group in service.schema_list(project=project)
|
|
52
84
|
)
|
|
53
|
-
proposal_path = project_root / init_paths.PROPOSAL_FILENAME
|
|
54
85
|
m = manifest.load_or_create(
|
|
55
86
|
project_root, project, proposal_path,
|
|
56
87
|
existing_record_groups=existing_record_groups,
|
|
@@ -209,26 +240,40 @@ def _format_published_at(info: dump.DumpInfo, latest_path: Path) -> str:
|
|
|
209
240
|
return dump.format_dumped_at(int(latest_path.stat().st_mtime))
|
|
210
241
|
|
|
211
242
|
|
|
212
|
-
def
|
|
213
|
-
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
243
|
+
def _already_locally_migrated(project: str) -> bool:
|
|
244
|
+
"""True iff this machine already has real pdata content for `project` - not merely an empty
|
|
245
|
+
schema-only `.db` file. `repository.connect()`'s own DDL (`CREATE TABLE IF NOT EXISTS`)
|
|
246
|
+
creates that file as a side effect of any read-only operation that opens a connection -
|
|
247
|
+
`dry_run()`'s own schema lookup, `ccst pdata verify`, and others - well before any real
|
|
248
|
+
migration ever happens on this machine. A bare `store.db_path(project).exists()` check
|
|
249
|
+
can't tell that empty file apart from a genuine prior migration, and would misread it as
|
|
250
|
+
"already migrated here" - silently and permanently skipping adopt-from-dump. Confirmed live,
|
|
251
|
+
not hypothetical: a second machine's `ccst pdata init` (no `--write`) created exactly this
|
|
252
|
+
empty file, and the very next `--write` on the same machine then skipped adoption because of
|
|
253
|
+
it, falling through to an ordinary classify/import that failed for unrelated reasons."""
|
|
254
|
+
if not store.db_path(project).exists():
|
|
255
|
+
return False
|
|
256
|
+
conn = repository.connect(project)
|
|
257
|
+
try:
|
|
258
|
+
return repository.has_any_records(conn)
|
|
259
|
+
finally:
|
|
260
|
+
conn.close()
|
|
261
|
+
|
|
222
262
|
|
|
223
|
-
|
|
224
|
-
there
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
263
|
+
def _pending_adoption(*, project: str, project_root: Path) -> dump.DumpInfo | None:
|
|
264
|
+
"""Is there a published dump this machine should adopt instead of classifying/importing its
|
|
265
|
+
own flat files? Returns the validated `DumpInfo` when adoption applies; `None` when there is
|
|
266
|
+
nothing to adopt (no dump has ever been published for this project - the ordinary
|
|
267
|
+
classify/import flow proceeds exactly as before this feature existed) or this machine is
|
|
268
|
+
already genuinely migrated (see `_already_locally_migrated` - ccst pdata rehydrate, not ccst
|
|
269
|
+
pdata init, is the ongoing sync path for that case).
|
|
228
270
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
271
|
+
Shared by `dry_run()` (reports what `--write` would do) and `_adopt_from_dump()` (does it) -
|
|
272
|
+
one check, so the two can never disagree about whether adoption applies.
|
|
273
|
+
|
|
274
|
+
Checked via the dump file's own existence, not `DumpInfo.checksum_valid` alone: `read_latest`
|
|
275
|
+
returns `checksum_valid=False` both when no dump was ever published and when one exists but is
|
|
276
|
+
corrupt, and those two cases must not be treated the same - the first is the ordinary
|
|
232
277
|
first-ever migration, the second is a real problem that must be surfaced, never silently
|
|
233
278
|
papered over by falling through to classification."""
|
|
234
279
|
latest_path = project_root / ".pdata-db-dump" / "latest.sql"
|
|
@@ -243,8 +288,26 @@ def _adopt_from_dump(
|
|
|
243
288
|
"project. Publish a fresh dump from a known-good machine (ccst pdata dump --force) "
|
|
244
289
|
"or reconcile manually (ccst pdata resolve) once those commands exist."
|
|
245
290
|
)
|
|
246
|
-
if
|
|
291
|
+
if _already_locally_migrated(project):
|
|
247
292
|
return None
|
|
293
|
+
return info
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _adopt_from_dump(
|
|
297
|
+
*, project: str, project_root: Path, on_progress: Callable[[str], None] | None,
|
|
298
|
+
) -> WriteResult | None:
|
|
299
|
+
"""Spec "ccst pdata init on a second machine (adopt-from-dump)": a project already migrated
|
|
300
|
+
to pdata on another machine publishes a dump into <project_root>/.pdata-db-dump/latest.sql —
|
|
301
|
+
a machine with no local pdata content for this project yet must adopt that dump directly
|
|
302
|
+
(same mechanism as the ongoing rehydrate trigger) rather than classifying/importing this
|
|
303
|
+
machine's current flat files as if this were a first-ever migration, which would produce a DB
|
|
304
|
+
disconnected from the dump's vector-clock history and likely duplicate content already
|
|
305
|
+
captured elsewhere. Returns the early WriteResult to hand straight back from write() when
|
|
306
|
+
adoption ran; None when `_pending_adoption` finds nothing to adopt."""
|
|
307
|
+
info = _pending_adoption(project=project, project_root=project_root)
|
|
308
|
+
if info is None:
|
|
309
|
+
return None
|
|
310
|
+
latest_path = project_root / ".pdata-db-dump" / "latest.sql"
|
|
248
311
|
published_at = _format_published_at(info, latest_path)
|
|
249
312
|
message = (
|
|
250
313
|
f"Adopting existing pdata from sync dump (published by {info.machine_id}, "
|
{cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/pdata/repository.py
RENAMED
|
@@ -108,6 +108,23 @@ def get_base_record(conn: sqlite3.Connection, record_id: int) -> sqlite3.Row | N
|
|
|
108
108
|
return row
|
|
109
109
|
|
|
110
110
|
|
|
111
|
+
def has_any_records(conn: sqlite3.Connection) -> bool:
|
|
112
|
+
"""True iff `records` has at least one row NOT soft-deleted. Existence check, not a count -
|
|
113
|
+
`LIMIT 1` so this stays cheap regardless of table size. Distinguishes a genuinely-populated
|
|
114
|
+
.db from an empty schema-only one that `connect()`'s own DDL can create as an incidental side
|
|
115
|
+
effect of a read-only operation, well before any real content is ever written.
|
|
116
|
+
|
|
117
|
+
Excludes soft-deleted rows deliberately, not an oversight: a rolled-back `ccst pdata init
|
|
118
|
+
--write` (`init_service._rollback`) soft-deletes every row it inserted rather than hard
|
|
119
|
+
deleting them - "no hard delete, full auditability" - so a failed write attempt leaves the
|
|
120
|
+
`records` table non-empty with nothing but tombstones. Counting those as "real content" would
|
|
121
|
+
make a failed, fully-rolled-back --write permanently block adopt-from-dump on this exact
|
|
122
|
+
machine, which is the opposite of what a rollback is supposed to mean."""
|
|
123
|
+
return conn.execute(
|
|
124
|
+
"SELECT 1 FROM records WHERE deleted_at IS NULL LIMIT 1"
|
|
125
|
+
).fetchone() is not None
|
|
126
|
+
|
|
127
|
+
|
|
111
128
|
def ensure_extension_table(conn: sqlite3.Connection, record_group: str) -> None:
|
|
112
129
|
"""CREATE ext_<group> (record_id INTEGER PRIMARY KEY REFERENCES records(id)) if it doesn't
|
|
113
130
|
exist yet — record_group is validated by naming.extension_table_name(). Caller owns the
|
{cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/scheduler/jobspec.py
RENAMED
|
@@ -38,7 +38,10 @@ class JobSpec:
|
|
|
38
38
|
success_exit_codes: tuple[int, ...] = (0,)
|
|
39
39
|
|
|
40
40
|
|
|
41
|
-
def
|
|
41
|
+
def check_job_id(job_id: str) -> None:
|
|
42
|
+
"""Boundary validator for a bare job id, used both by validate_job_fields
|
|
43
|
+
(a full spec) and by callers that only need to validate an id on its own
|
|
44
|
+
- e.g. ccsched's `rename` command, which never builds a JobSpec."""
|
|
42
45
|
if not _KEBAB_RE.match(job_id):
|
|
43
46
|
raise JobValidationError(
|
|
44
47
|
f"invalid job id {job_id!r}: must be lowercase kebab-case [a-z0-9-], "
|
|
@@ -107,7 +110,7 @@ def validate_job_fields(
|
|
|
107
110
|
timeout: str,
|
|
108
111
|
success_exit_codes: tuple[int, ...] = (0,),
|
|
109
112
|
) -> JobSpec:
|
|
110
|
-
|
|
113
|
+
check_job_id(job_id)
|
|
111
114
|
try:
|
|
112
115
|
parse_cadence(cadence)
|
|
113
116
|
except CadenceError as exc:
|
{cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/scheduler/ledger.py
RENAMED
|
@@ -78,6 +78,23 @@ def record(entry: LedgerEntry) -> None:
|
|
|
78
78
|
print(f"[telemetry-warn] ledger write failed: {e}", file=sys.stderr)
|
|
79
79
|
|
|
80
80
|
|
|
81
|
+
def rename_job(old_id: str, new_id: str) -> None:
|
|
82
|
+
"""Repoint every catchup_events row from old_id to new_id, so `ccsched
|
|
83
|
+
status` and the surfacing pass keep showing continuous history under a
|
|
84
|
+
job's new name. Unlike record(), this raises on failure rather than
|
|
85
|
+
swallowing it - it's a foreground CLI operation (ccsched rename), not
|
|
86
|
+
best-effort logging, so a caller that already renamed the job in
|
|
87
|
+
ccsched.db needs to know if history did not follow."""
|
|
88
|
+
conn = telemetry_store.connect()
|
|
89
|
+
try:
|
|
90
|
+
conn.execute(
|
|
91
|
+
"UPDATE catchup_events SET job_id=? WHERE job_id=?", (new_id, old_id)
|
|
92
|
+
)
|
|
93
|
+
conn.commit()
|
|
94
|
+
finally:
|
|
95
|
+
conn.close()
|
|
96
|
+
|
|
97
|
+
|
|
81
98
|
def read_recent(job_id: str | None = None, *, limit: int = 50) -> list[dict[str, object]]:
|
|
82
99
|
"""Return up to ``limit`` recent catch-up rows, oldest-first within that
|
|
83
100
|
slice, optionally filtered by job_id."""
|
{cc_session_tools-2.12.0 → cc_session_tools-2.12.2}/src/cc_session_tools/lib/scheduler/registry.py
RENAMED
|
@@ -112,6 +112,31 @@ def set_enabled(job_id: str, enabled: bool) -> None:
|
|
|
112
112
|
conn.close()
|
|
113
113
|
|
|
114
114
|
|
|
115
|
+
def rename_job(old_id: str, new_id: str) -> None:
|
|
116
|
+
"""Repoint a job's id everywhere it's a row key in ccsched.db - the `jobs`
|
|
117
|
+
spec row, its `job_state` row (so registered_at/last_success/etc. carry
|
|
118
|
+
over), and its `bundled_job_installs` row if it has one - in a single
|
|
119
|
+
transaction. Callers that also want run history to follow (telemetry.db's
|
|
120
|
+
catchup_events) call ledger.rename_job separately; the two stores are
|
|
121
|
+
never truly atomic together, matching how the rest of this package treats
|
|
122
|
+
them as independent."""
|
|
123
|
+
conn = store.connect()
|
|
124
|
+
try:
|
|
125
|
+
cur = conn.execute("UPDATE jobs SET job_id=? WHERE job_id=?", (new_id, old_id))
|
|
126
|
+
if cur.rowcount == 0:
|
|
127
|
+
conn.commit()
|
|
128
|
+
raise RegistryError(f"unknown job id: {old_id!r}")
|
|
129
|
+
conn.execute("UPDATE job_state SET job_id=? WHERE job_id=?", (new_id, old_id))
|
|
130
|
+
conn.execute(
|
|
131
|
+
"UPDATE bundled_job_installs SET job_id=? WHERE job_id=?", (new_id, old_id)
|
|
132
|
+
)
|
|
133
|
+
conn.commit()
|
|
134
|
+
except sqlite3.IntegrityError as exc:
|
|
135
|
+
raise RegistryError(f"job id already exists: {new_id!r}") from exc
|
|
136
|
+
finally:
|
|
137
|
+
conn.close()
|
|
138
|
+
|
|
139
|
+
|
|
115
140
|
def mark_bundled_installed(job_id: str, installed_at: str) -> None:
|
|
116
141
|
"""Record that a CCST-bundled job (lib/scheduler/bundled_jobs.py) has been installed on this
|
|
117
142
|
machine, so a later `ccsched remove` can be told apart from "never installed" by
|