cc-session-tools 2.8.2__tar.gz → 2.9.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.
- {cc_session_tools-2.8.2/src/cc_session_tools.egg-info → cc_session_tools-2.9.0}/PKG-INFO +1 -1
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/pyproject.toml +1 -1
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/cli/ccsched.py +14 -14
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/cli/ccst.py +72 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/pdata/init_service.py +57 -0
- cc_session_tools-2.9.0/src/cc_session_tools/lib/pdata/rename_group.py +161 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/scheduler/digest.py +35 -17
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/scheduler/ledger.py +18 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/scheduler/notify.py +35 -1
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/scheduler/surface.py +57 -23
- cc_session_tools-2.9.0/src/cc_session_tools/lib/scheduler/worker.py +235 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/skills/analyse-cc-usage/SKILL.md +17 -1
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0/src/cc_session_tools.egg-info}/PKG-INFO +1 -1
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools.egg-info/SOURCES.txt +2 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/catchup.py +17 -2
- cc_session_tools-2.9.0/tests/test_ccst_pdata_rename_group_cli.py +117 -0
- cc_session_tools-2.8.2/src/cc_session_tools/lib/scheduler/worker.py +0 -133
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/LICENSE +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/README.md +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/setup.cfg +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/__init__.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/cli/__init__.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/cli/ccd.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/cli/ccmsg.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/cli/ccr.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/cli/ccs.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/cli/migrate_ccmsg.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/cli/migrate_ccsched.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/cli/migrate_sessions_db.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/cli/migrate_telemetry.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/config/hooks-bundle.json +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/config/session-tag-hook.json +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/hooks_install.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/__init__.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/claude_flags.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/claude_md_install.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/context_overrides.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/db.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/debug.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/doctor.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/doctor_mutes.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/hook_registry.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/install_sync.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/levenshtein.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/messaging/__init__.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/messaging/addressing.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/messaging/cursor.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/messaging/lock.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/messaging/message.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/messaging/move_safety.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/messaging/repository.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/messaging/retention.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/messaging/service.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/messaging/store.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/paths.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/pdata/__init__.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/pdata/backup.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/pdata/classify.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/pdata/cutover.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/pdata/formatting.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/pdata/importers.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/pdata/init_paths.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/pdata/manifest.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/pdata/naming.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/pdata/reorganize.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/pdata/repository.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/pdata/service.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/pdata/session_output.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/pdata/store.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/pdata/verify.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/pdata/write_log.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/picker.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/proc_lock.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/prompts.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/roots.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/rules.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/scheduler/__init__.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/scheduler/bundled_jobs.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/scheduler/cadence.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/scheduler/cursor.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/scheduler/due.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/scheduler/duration.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/scheduler/jobspec.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/scheduler/lock.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/scheduler/reconcile.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/scheduler/registry.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/scheduler/runner.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/scheduler/state.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/scheduler/store.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/scheduler/throttle.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/session_gc.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/sessions.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/sessions_db.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/sessions_repair.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/shell_install.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/tasklist.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/telemetry_store.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/prompts/pdata-migration-claude-md-update.md +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/prompts/pdata-migration-skills-update.md +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/skills/context-override/SKILL.md +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/skills/delete-sessions/SKILL.md +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/skills/delete-sessions/scripts/delete_sessions.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/skills/do-executor-critic-assessor-loop/SKILL.md +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/skills/find-claude-code-session/SKILL.md +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/skills/generate-8digit-code/SKILL.md +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/skills/generate-8digit-code/scripts/generate_8digit_code.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/skills/list-empty-sessions/SKILL.md +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/skills/list-empty-sessions/scripts/list_empty_sessions.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/skills/manage-recurring-cc-jobs-using-ccsched/SKILL.md +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/skills/move-session/SKILL.md +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/skills/move-session/hooks/sessionstart-pending-rename.sh +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/skills/move-session/scripts/cc_session_rules.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/skills/move-session/scripts/move_session.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/skills/pm-pdata-conflict-resolution/SKILL.md +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/skills/pm-pdata-schema-design/SKILL.md +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/skills/pm-project-init/SKILL.md +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/skills/pm-project-layout-reference/SKILL.md +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/skills/pm-update-central-files/SKILL.md +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/skills/reduce-persistent-context/CALIBRATION.md +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/skills/reduce-persistent-context/SKILL.md +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/skills/reduce-persistent-context/scripts/analyze_context.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/skills/reduce-persistent-context/scripts/duplicates.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/skills/reduce-persistent-context/scripts/measure.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/skills/reduce-persistent-context/scripts/report.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/skills/reduce-persistent-context/scripts/tokens.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/skills/reduce-persistent-context/scripts/usage.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/skills/select-agent-model/SKILL.md +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/skills/send-session-message/SKILL.md +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/skills/update-command-cache/SKILL.md +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/skills/update-command-cache/scripts/update_command_cache.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools.egg-info/dependency_links.txt +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools.egg-info/entry_points.txt +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools.egg-info/requires.txt +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools.egg-info/top_level.txt +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/__init__.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/after_response.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/bash_hard_deny.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/bash_security_review.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/cache.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/confirm_8digit.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/context_window_warning.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/last_screenshot.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/marker_allow.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/markers.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/messaging_deliver.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/model_info.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/normalise.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/pending_migration.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/pending_rename.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/session_tag.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/stats.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/telemetry.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/telemetry_query.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/telemetry_trim.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/transcript.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/worklog_guard.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/claude_code_usage/__init__.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/claude_code_usage/attribution.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/claude_code_usage/cache.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/claude_code_usage/ccusage_wrapper.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/claude_code_usage/cli.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/claude_code_usage/data/pricing.json +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/claude_code_usage/parent_inference.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/claude_code_usage/parser.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/claude_code_usage/pricing.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/claude_code_usage/query.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/claude_code_usage/report.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/claude_code_usage/schema.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/claude_code_usage/session_names.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_after_response.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_attribution.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_bash_hard_deny.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_bash_security_review.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_cache.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_cache_sqlite.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccr_orphans.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccs_emptiness.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccs_eta.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccs_list_mode.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccs_scope_flags.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccs_sentinel_sort.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccs_session_counts.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_bundle_discovery.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_ccsched_jobs_cli.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_cli.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_context_override_cli.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_doctor.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_gc_report.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_hook_dispatcher.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_hooks_install_prune.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_hooks_install_zero_arg.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_hooks_uninstall.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_install_everything.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_main_gate.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_pdata_cli.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_pdata_init_cli.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_pdata_reconcile_cli.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_pdata_reorganize_cli.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_pdata_verify_cli.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_repair_sessions_cli.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_sessions_cli.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_shell_install.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_skills_install.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_skills_uninstall.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_telemetry_query.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_telemetry_trim.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccusage_wrapper.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_claude_flags.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_claude_md_install.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_cli.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_cli_ccd.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_cli_ccd_dry_run.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_cli_ccr.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_cli_ccs.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_cli_version.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_confirm_8digit.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_context_overrides.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_context_window_warning.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_db.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_debug.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_doctor_mutes.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_empty_session.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_hook_cache.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_hooks_install.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_install_sync.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_last_screenshot.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_levenshtein.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_marker_allow.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_migrate_ccmsg.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_migrate_ccsched.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_migrate_sessions_db.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_migrate_telemetry.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_model_info.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_normalise.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_parent_inference.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_parser.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_paths.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_pending_migration_hook.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_picker.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_pricing.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_proc_lock.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_prompts.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_query.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_report.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_roots.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_rules.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_scheduler_bundled_jobs.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_schema.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_session_names.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_session_tag.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_sessions.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_sessions_db.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_sessions_repair.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_tasklist.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_telemetry.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_telemetry_store.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_transcript.py +0 -0
- {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_worklog_guard.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cc-session-tools"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.9.0"
|
|
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" }
|
|
@@ -11,6 +11,7 @@ from cc_session_tools import __version__
|
|
|
11
11
|
from cc_session_tools.lib.scheduler import (
|
|
12
12
|
cursor,
|
|
13
13
|
ledger,
|
|
14
|
+
notify,
|
|
14
15
|
reconcile,
|
|
15
16
|
registry,
|
|
16
17
|
state,
|
|
@@ -240,7 +241,9 @@ def _cmd_remove(job_id: str) -> int:
|
|
|
240
241
|
return 0
|
|
241
242
|
|
|
242
243
|
|
|
243
|
-
def _cmd_run(
|
|
244
|
+
def _cmd_run(
|
|
245
|
+
args: argparse.Namespace, *, notify_push: worker.NotifyPush = notify.push_outcome,
|
|
246
|
+
) -> int:
|
|
244
247
|
specs = {s.job_id: s for s in registry.load_registry()}
|
|
245
248
|
spec: JobSpec | None = specs.get(args.id)
|
|
246
249
|
if spec is None:
|
|
@@ -249,25 +252,22 @@ def _cmd_run(args: argparse.Namespace) -> int:
|
|
|
249
252
|
now = datetime.now(timezone.utc)
|
|
250
253
|
attempt_ts = state.format_ts(now)
|
|
251
254
|
state.ensure_registered_db(spec.job_id, now)
|
|
252
|
-
|
|
253
|
-
|
|
255
|
+
# Manual runs always use record_manual_failure/record_success, not
|
|
256
|
+
# worker.py's suspend-threshold-aware record_failure - `ccsched run` never
|
|
257
|
+
# auto-suspends a job, unlike the scheduled `_run-job` path.
|
|
258
|
+
capture = worker.classify_outcome(spec, outcome, notify_push=notify_push)
|
|
259
|
+
if capture.crashed:
|
|
254
260
|
new_consecutive = state.record_manual_failure(spec.job_id, attempt_ts=attempt_ts)
|
|
255
261
|
else:
|
|
256
262
|
state.record_success(spec.job_id, new_success=attempt_ts, attempt_ts=attempt_ts)
|
|
257
263
|
new_consecutive = 0
|
|
258
|
-
findings = None
|
|
259
|
-
if not crashed and outcome.exit_code != 0:
|
|
260
|
-
findings = outcome.stdout.strip()[:1000] or None
|
|
261
264
|
ledger.record(ledger.LedgerEntry(
|
|
262
|
-
job_id=spec.job_id,
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
duration_ms=outcome.duration_ms,
|
|
266
|
-
error=(outcome.stderr.strip()[:200] or None) if crashed else findings,
|
|
267
|
-
consecutive_failures=new_consecutive if crashed else 0,
|
|
265
|
+
job_id=spec.job_id, event=capture.event, owed=1, ran=0 if capture.crashed else 1,
|
|
266
|
+
exit_code=outcome.exit_code, duration_ms=outcome.duration_ms, error=capture.detail,
|
|
267
|
+
consecutive_failures=new_consecutive if capture.crashed else 0,
|
|
268
268
|
))
|
|
269
|
-
print(f"{'failed' if crashed else 'ran'} {spec.job_id} (exit={outcome.exit_code})")
|
|
270
|
-
return 1 if crashed else 0
|
|
269
|
+
print(f"{'failed' if capture.crashed else 'ran'} {spec.job_id} (exit={outcome.exit_code})")
|
|
270
|
+
return 1 if capture.crashed else 0
|
|
271
271
|
|
|
272
272
|
|
|
273
273
|
def _cmd_status(args: argparse.Namespace) -> int:
|
|
@@ -1217,6 +1217,64 @@ def _cmd_pdata_reorganize(args: argparse.Namespace) -> int:
|
|
|
1217
1217
|
return 0
|
|
1218
1218
|
|
|
1219
1219
|
|
|
1220
|
+
def _cmd_pdata_rename_group(args: argparse.Namespace) -> int:
|
|
1221
|
+
from cc_session_tools.lib.pdata import init_paths, naming, rename_group
|
|
1222
|
+
|
|
1223
|
+
try:
|
|
1224
|
+
project_root = init_paths.resolve_project_root(args.project, rehearse=None)
|
|
1225
|
+
except ValueError as exc:
|
|
1226
|
+
print(f"ccst pdata: {exc}", file=sys.stderr)
|
|
1227
|
+
return 2
|
|
1228
|
+
|
|
1229
|
+
if not args.write:
|
|
1230
|
+
try:
|
|
1231
|
+
plan = rename_group.dry_run(
|
|
1232
|
+
project=args.project, project_root=project_root, old=args.from_, new=args.to,
|
|
1233
|
+
)
|
|
1234
|
+
except ValueError as exc:
|
|
1235
|
+
print(f"ccst pdata: {exc}", file=sys.stderr)
|
|
1236
|
+
return 2
|
|
1237
|
+
print(f"record_group: {plan.old} -> {plan.new}")
|
|
1238
|
+
print(f" {plan.row_count} row(s) in records to update")
|
|
1239
|
+
if plan.has_extension_table:
|
|
1240
|
+
print(f" ext table: {naming.extension_table_name(plan.old)} -> "
|
|
1241
|
+
f"{naming.extension_table_name(plan.new)}")
|
|
1242
|
+
else:
|
|
1243
|
+
print(" no extension table")
|
|
1244
|
+
if plan.manifest_entry_paths:
|
|
1245
|
+
n = len(plan.manifest_entry_paths)
|
|
1246
|
+
print(f" {n} manifest entr{'y' if n == 1 else 'ies'} to update:")
|
|
1247
|
+
for path in plan.manifest_entry_paths:
|
|
1248
|
+
print(f" {path}")
|
|
1249
|
+
else:
|
|
1250
|
+
print(" no matching manifest entries")
|
|
1251
|
+
return 0
|
|
1252
|
+
|
|
1253
|
+
try:
|
|
1254
|
+
result = rename_group.write(
|
|
1255
|
+
project=args.project, project_root=project_root, old=args.from_, new=args.to,
|
|
1256
|
+
)
|
|
1257
|
+
except ValueError as exc:
|
|
1258
|
+
print(f"ccst pdata: {exc}", file=sys.stderr)
|
|
1259
|
+
return 2
|
|
1260
|
+
|
|
1261
|
+
if result.failure is not None:
|
|
1262
|
+
print("ccst pdata rename-group: failed:", file=sys.stderr)
|
|
1263
|
+
for reason in result.failure.reasons:
|
|
1264
|
+
print(f" - {reason}", file=sys.stderr)
|
|
1265
|
+
return 1
|
|
1266
|
+
|
|
1267
|
+
plan = result.plan
|
|
1268
|
+
print(f"Renamed record_group {plan.old!r} -> {plan.new!r}: {plan.row_count} row(s)")
|
|
1269
|
+
print(f"Backup: {result.backup_path}")
|
|
1270
|
+
n = len(plan.manifest_entry_paths)
|
|
1271
|
+
if n:
|
|
1272
|
+
print(f"{n} manifest entr{'y' if n == 1 else 'ies'} updated - remember to update any "
|
|
1273
|
+
f"skills/docs that still reference the old group name.")
|
|
1274
|
+
print("SUCCESS")
|
|
1275
|
+
return 0
|
|
1276
|
+
|
|
1277
|
+
|
|
1220
1278
|
def _cmd_pdata_reconcile_session_output(args: argparse.Namespace) -> int:
|
|
1221
1279
|
from cc_session_tools.lib import roots
|
|
1222
1280
|
from cc_session_tools.lib.pdata import session_output
|
|
@@ -2247,6 +2305,18 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
2247
2305
|
help="Perform the move and update matching pdata records (default: dry-run only)",
|
|
2248
2306
|
)
|
|
2249
2307
|
|
|
2308
|
+
pdata_rename_group_parser = pdata_sub.add_parser(
|
|
2309
|
+
"rename-group", help="Rename a record_group in place (records, record_group_fields, "
|
|
2310
|
+
"its ext table, and any matching manifest entries)"
|
|
2311
|
+
)
|
|
2312
|
+
pdata_rename_group_parser.add_argument("--project", required=True, metavar="NAME")
|
|
2313
|
+
pdata_rename_group_parser.add_argument("--from", dest="from_", required=True, metavar="OLD")
|
|
2314
|
+
pdata_rename_group_parser.add_argument("--to", required=True, metavar="NEW")
|
|
2315
|
+
pdata_rename_group_parser.add_argument(
|
|
2316
|
+
"--write", action="store_true",
|
|
2317
|
+
help="Perform the rename and update .ccst-pdata-proposal.json (default: dry-run only)",
|
|
2318
|
+
)
|
|
2319
|
+
|
|
2250
2320
|
pdata_reconcile_parser = pdata_sub.add_parser(
|
|
2251
2321
|
"reconcile-session-output",
|
|
2252
2322
|
help="Backfill the session-output index from cc-sessions/*/out/ on disk (idempotent)",
|
|
@@ -2553,6 +2623,8 @@ def main() -> None:
|
|
|
2553
2623
|
sys.exit(_cmd_pdata_init(args))
|
|
2554
2624
|
if args.verb == "reorganize":
|
|
2555
2625
|
sys.exit(_cmd_pdata_reorganize(args))
|
|
2626
|
+
if args.verb == "rename-group":
|
|
2627
|
+
sys.exit(_cmd_pdata_rename_group(args))
|
|
2556
2628
|
if args.verb == "reconcile-session-output":
|
|
2557
2629
|
sys.exit(_cmd_pdata_reconcile_session_output(args))
|
|
2558
2630
|
if args.verb == "verify":
|
{cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/pdata/init_service.py
RENAMED
|
@@ -5,10 +5,12 @@ A's service.py — this module owns no SQL of its own.
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
7
|
import csv
|
|
8
|
+
import sqlite3
|
|
8
9
|
from collections.abc import Callable
|
|
9
10
|
from dataclasses import dataclass
|
|
10
11
|
from pathlib import Path
|
|
11
12
|
|
|
13
|
+
from cc_session_tools.lib import db
|
|
12
14
|
from cc_session_tools.lib.pdata import (
|
|
13
15
|
backup,
|
|
14
16
|
cutover,
|
|
@@ -16,6 +18,7 @@ from cc_session_tools.lib.pdata import (
|
|
|
16
18
|
manifest,
|
|
17
19
|
repository,
|
|
18
20
|
service,
|
|
21
|
+
store,
|
|
19
22
|
)
|
|
20
23
|
from cc_session_tools.lib.pdata.importers import ImportRow, count_source_rows, import_entry
|
|
21
24
|
from cc_session_tools.lib.pdata.manifest import Manifest, ManifestEntry
|
|
@@ -137,6 +140,58 @@ def _rollback(*, project: str, created_ids: list[int]) -> list[str]:
|
|
|
137
140
|
return rollback_failures
|
|
138
141
|
|
|
139
142
|
|
|
143
|
+
def _check_db_not_locked(project: str) -> None:
|
|
144
|
+
"""Pre-flight concurrency guard (inter-session message 20260819T114156Z-3ec7,
|
|
145
|
+
gap #2): refuse to start --write if another connection already holds a write
|
|
146
|
+
lock on the project's .db right now — e.g. a concurrent --write, or
|
|
147
|
+
pdata-verify-all's scheduled cadence firing against the same project — rather
|
|
148
|
+
than letting the first schema_add_field/add_record call inside the with-block
|
|
149
|
+
below block on db.py's passive 5s busy_timeout and eventually fail with
|
|
150
|
+
rows already mid-migration. Called as the very first thing inside write()'s
|
|
151
|
+
project_db_dir_override/backup_dir_override with-block so store.db_path()
|
|
152
|
+
resolves the rehearsal-sandboxed path when rehearsing, exactly like every
|
|
153
|
+
other db access in this function.
|
|
154
|
+
|
|
155
|
+
No-op if the project has no .db yet (a brand-new project's first --write) —
|
|
156
|
+
nothing to guard, and connecting would create one just to probe it.
|
|
157
|
+
|
|
158
|
+
The probe is a throwaway BEGIN IMMEDIATE / ROLLBACK on its own short-lived
|
|
159
|
+
connection, not a lock held across the rest of write() — this does not
|
|
160
|
+
protect against a second --write starting a few seconds *after* this check
|
|
161
|
+
passes (a TOCTOU race that is out of scope; the existing passive
|
|
162
|
+
busy_timeout is still the backstop for that narrower case). Its job is only
|
|
163
|
+
to catch the common case of something *already* actively using the .db,
|
|
164
|
+
immediately and with a clear message, instead of a raw sqlite3 error
|
|
165
|
+
surfacing minutes into the migration.
|
|
166
|
+
|
|
167
|
+
busy_timeout is overridden to 0 on this connection only — db.connect()'s
|
|
168
|
+
shared 5000ms default is exactly the passive wait this probe exists to
|
|
169
|
+
avoid; a probe that waits out the same timeout the mutation path already
|
|
170
|
+
relies on would defeat the point of checking up front. isolation_level is
|
|
171
|
+
set to None for the same reason repository.connect() sets it: so this
|
|
172
|
+
caller-issued BEGIN IMMEDIATE isn't wrapped in a second, implicit
|
|
173
|
+
transaction by sqlite3's own legacy transaction handling."""
|
|
174
|
+
db_path = store.db_path(project)
|
|
175
|
+
if not db_path.exists():
|
|
176
|
+
return
|
|
177
|
+
conn = db.connect(db_path)
|
|
178
|
+
conn.isolation_level = None
|
|
179
|
+
try:
|
|
180
|
+
conn.execute("PRAGMA busy_timeout=0")
|
|
181
|
+
try:
|
|
182
|
+
conn.execute("BEGIN IMMEDIATE")
|
|
183
|
+
except sqlite3.OperationalError as exc:
|
|
184
|
+
raise ValueError(
|
|
185
|
+
f"cannot start 'ccst pdata init --write' for project {project!r}: "
|
|
186
|
+
f"another process appears to be using {db_path} right now ({exc}) "
|
|
187
|
+
f"— wait for it to finish, or confirm it isn't stuck, then re-run --write"
|
|
188
|
+
) from exc
|
|
189
|
+
else:
|
|
190
|
+
conn.execute("ROLLBACK")
|
|
191
|
+
finally:
|
|
192
|
+
conn.close()
|
|
193
|
+
|
|
194
|
+
|
|
140
195
|
def write(
|
|
141
196
|
*, project: str, rehearse: Path | None = None,
|
|
142
197
|
on_progress: Callable[[str], None] | None = None,
|
|
@@ -171,6 +226,8 @@ def write(
|
|
|
171
226
|
init_paths.project_db_dir_override(rehearse),
|
|
172
227
|
init_paths.backup_dir_override(rehearse),
|
|
173
228
|
):
|
|
229
|
+
_check_db_not_locked(project)
|
|
230
|
+
|
|
174
231
|
db_owned = [e for e in m.entries if e.classification == "db-owned"]
|
|
175
232
|
_emit(on_progress, f"Importing {len(db_owned)} file(s)...")
|
|
176
233
|
for entry in m.entries:
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Record-group rename for `ccst pdata rename-group` (feature request from the `home` project's
|
|
2
|
+
migration-test session, message 20260822T114514Z-91ed).
|
|
3
|
+
|
|
4
|
+
A record_group rename touches three places that must move together in one transaction -
|
|
5
|
+
`records.record_group`, `record_group_fields.record_group`, and the `ext_<group>` table's own
|
|
6
|
+
name - plus a fourth, non-transactional place that is just as easy to forget: any matching entry
|
|
7
|
+
in `.ccst-pdata-proposal.json` (the classification manifest, spec §7.1). Skip that and
|
|
8
|
+
verify.check_row_count_parity cross-checks live row counts against the manifest's archived-file
|
|
9
|
+
entries by record_group; every renamed group then fails `ccst pdata verify` forever with a
|
|
10
|
+
"possible data loss" row-count-parity issue that isn't actually data loss - it's just comparing
|
|
11
|
+
the new name's row count against the old name's manifest entry.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import sqlite3
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from cc_session_tools.lib.pdata import backup, init_paths, manifest, naming, repository
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True, slots=True)
|
|
23
|
+
class RenameGroupPlan:
|
|
24
|
+
project: str
|
|
25
|
+
project_root: Path
|
|
26
|
+
old: str
|
|
27
|
+
new: str
|
|
28
|
+
row_count: int
|
|
29
|
+
has_extension_table: bool
|
|
30
|
+
manifest_entry_paths: list[str]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True, slots=True)
|
|
34
|
+
class RenameGroupFailure:
|
|
35
|
+
reasons: list[str]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True, slots=True)
|
|
39
|
+
class RenameGroupResult:
|
|
40
|
+
plan: RenameGroupPlan
|
|
41
|
+
backup_path: Path | None
|
|
42
|
+
failure: RenameGroupFailure | None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _plan(*, project: str, project_root: Path, old: str, new: str) -> RenameGroupPlan:
|
|
46
|
+
naming.validate_record_group(old)
|
|
47
|
+
naming.validate_record_group(new)
|
|
48
|
+
if old == new:
|
|
49
|
+
raise ValueError(f"--from and --to are the same record_group: {old!r}")
|
|
50
|
+
|
|
51
|
+
conn = repository.connect(project)
|
|
52
|
+
try:
|
|
53
|
+
old_row_count = conn.execute(
|
|
54
|
+
"SELECT COUNT(*) FROM records WHERE record_group=?", (old,)
|
|
55
|
+
).fetchone()[0]
|
|
56
|
+
old_has_ext = repository.extension_table_exists(conn, old)
|
|
57
|
+
if old_row_count == 0 and not old_has_ext:
|
|
58
|
+
raise ValueError(f"no such record_group {old!r} in project {project!r}")
|
|
59
|
+
|
|
60
|
+
# "has rows" is checked including soft-deleted rows, and an ext table with zero rows
|
|
61
|
+
# (a schema-only group) also counts - both would otherwise collide silently with the
|
|
62
|
+
# ALTER TABLE RENAME TO / UPDATE below instead of raising a clear refusal up front.
|
|
63
|
+
new_row_count = conn.execute(
|
|
64
|
+
"SELECT COUNT(*) FROM records WHERE record_group=?", (new,)
|
|
65
|
+
).fetchone()[0]
|
|
66
|
+
new_has_ext = repository.extension_table_exists(conn, new)
|
|
67
|
+
if new_row_count > 0 or new_has_ext:
|
|
68
|
+
raise ValueError(
|
|
69
|
+
f"record_group {new!r} already exists in project {project!r} - "
|
|
70
|
+
f"rename-group refuses to merge into an existing group"
|
|
71
|
+
)
|
|
72
|
+
finally:
|
|
73
|
+
conn.close()
|
|
74
|
+
|
|
75
|
+
proposal_path = project_root / init_paths.PROPOSAL_FILENAME
|
|
76
|
+
manifest_entry_paths: list[str] = []
|
|
77
|
+
if proposal_path.exists():
|
|
78
|
+
m = manifest.load(proposal_path)
|
|
79
|
+
manifest_entry_paths = [e.path for e in m.entries if e.record_group == old]
|
|
80
|
+
|
|
81
|
+
return RenameGroupPlan(
|
|
82
|
+
project=project, project_root=project_root, old=old, new=new,
|
|
83
|
+
row_count=old_row_count, has_extension_table=old_has_ext,
|
|
84
|
+
manifest_entry_paths=manifest_entry_paths,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def dry_run(*, project: str, project_root: Path, old: str, new: str) -> RenameGroupPlan:
|
|
89
|
+
return _plan(project=project, project_root=project_root, old=old, new=new)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _rename_in_db(project: str, *, old: str, new: str) -> None:
|
|
93
|
+
conn = repository.connect(project)
|
|
94
|
+
try:
|
|
95
|
+
with repository._immediate(conn):
|
|
96
|
+
if repository.extension_table_exists(conn, old):
|
|
97
|
+
old_table = naming.extension_table_name(old)
|
|
98
|
+
new_table = naming.extension_table_name(new)
|
|
99
|
+
conn.execute(f'ALTER TABLE "{old_table}" RENAME TO "{new_table}"')
|
|
100
|
+
conn.execute("UPDATE records SET record_group=? WHERE record_group=?", (new, old))
|
|
101
|
+
conn.execute(
|
|
102
|
+
"UPDATE record_group_fields SET record_group=? WHERE record_group=?", (new, old)
|
|
103
|
+
)
|
|
104
|
+
finally:
|
|
105
|
+
conn.close()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _rename_in_manifest(project_root: Path, *, old: str, new: str) -> int:
|
|
109
|
+
"""Returns the number of entries updated. No-op (returns 0) if the project was never
|
|
110
|
+
migrated - nothing to update against."""
|
|
111
|
+
proposal_path = project_root / init_paths.PROPOSAL_FILENAME
|
|
112
|
+
if not proposal_path.exists():
|
|
113
|
+
return 0
|
|
114
|
+
m = manifest.load(proposal_path)
|
|
115
|
+
updated = 0
|
|
116
|
+
for entry in m.entries:
|
|
117
|
+
if entry.record_group == old:
|
|
118
|
+
entry.record_group = new
|
|
119
|
+
updated += 1
|
|
120
|
+
if updated:
|
|
121
|
+
manifest.save(m, proposal_path)
|
|
122
|
+
return updated
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def write(*, project: str, project_root: Path, old: str, new: str) -> RenameGroupResult:
|
|
126
|
+
plan = _plan(project=project, project_root=project_root, old=old, new=new)
|
|
127
|
+
|
|
128
|
+
# Safety net before any write, matching reorganize.write() / init_service.write()'s own
|
|
129
|
+
# use of the same helper.
|
|
130
|
+
try:
|
|
131
|
+
backup_path = backup.create_backup(project=project, project_root=project_root)
|
|
132
|
+
except backup.BackupError as exc:
|
|
133
|
+
return RenameGroupResult(
|
|
134
|
+
plan=plan, backup_path=None, failure=RenameGroupFailure(reasons=[str(exc)]),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
_rename_in_db(project, old=old, new=new)
|
|
139
|
+
except sqlite3.Error as exc:
|
|
140
|
+
return RenameGroupResult(
|
|
141
|
+
plan=plan, backup_path=backup_path, failure=RenameGroupFailure(reasons=[str(exc)]),
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# The DB rename already committed by this point - a manifest-write failure here (a
|
|
145
|
+
# permissions error, a malformed manifest another process wrote concurrently) must be
|
|
146
|
+
# reported clearly rather than silently leaving the manifest pointing at the old name,
|
|
147
|
+
# since that's exactly the "possible data loss" verify failure this command exists to
|
|
148
|
+
# prevent.
|
|
149
|
+
try:
|
|
150
|
+
_rename_in_manifest(project_root, old=old, new=new)
|
|
151
|
+
except (OSError, ValueError) as exc:
|
|
152
|
+
return RenameGroupResult(
|
|
153
|
+
plan=plan, backup_path=backup_path,
|
|
154
|
+
failure=RenameGroupFailure(reasons=[
|
|
155
|
+
f"records/record_group_fields/ext table renamed successfully, but updating "
|
|
156
|
+
f"{init_paths.PROPOSAL_FILENAME} failed: {exc} - fix it by hand for these "
|
|
157
|
+
f"paths: {plan.manifest_entry_paths}"
|
|
158
|
+
]),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
return RenameGroupResult(plan=plan, backup_path=backup_path, failure=None)
|
{cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/scheduler/digest.py
RENAMED
|
@@ -31,9 +31,15 @@ class JobReport:
|
|
|
31
31
|
# SUMMARY only: number of routine entries folded into this one line.
|
|
32
32
|
count: int = 0
|
|
33
33
|
# RAN only: a successful-but-nonzero-exit run's captured stdout (e.g. a
|
|
34
|
-
# drift monitor's report). Always surfaced
|
|
35
|
-
# see surface.py.
|
|
34
|
+
# drift monitor's "found something" report). Always surfaced with a
|
|
35
|
+
# warning wording, never folded into SUMMARY - see surface.py.
|
|
36
36
|
findings: str | None = None
|
|
37
|
+
# RAN only: a successful, zero-exit run's captured stdout (e.g. a verify
|
|
38
|
+
# command's "all OK" confirmation). Always surfaced with neutral wording,
|
|
39
|
+
# distinct from `findings`' warning wording - see surface.py. Mutually
|
|
40
|
+
# exclusive with `findings`: a given run is either a clean 0-exit or a
|
|
41
|
+
# findings-bearing nonzero exit, never both.
|
|
42
|
+
output: str | None = None
|
|
37
43
|
|
|
38
44
|
|
|
39
45
|
def _ordinal(n: int) -> str:
|
|
@@ -64,24 +70,36 @@ def _line(report: JobReport) -> str | None:
|
|
|
64
70
|
f"({_ordinal(report.consecutive_failures)} consecutive{age_suffix}) — see "
|
|
65
71
|
f"`ccsched status {report.job_id}`"
|
|
66
72
|
)
|
|
67
|
-
if report.outcome is Outcome.RAN
|
|
68
|
-
#
|
|
69
|
-
#
|
|
73
|
+
if report.outcome is Outcome.RAN:
|
|
74
|
+
# A completed run is always shown, regardless of `surface` - unlike
|
|
75
|
+
# LAUNCHED below, "it ran" is never something a job can silence.
|
|
70
76
|
overdue = f" ({report.overdue} overdue)" if report.overdue else ""
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
77
|
+
if report.findings:
|
|
78
|
+
# Bypasses the surface gate deliberately, same as FAILED/SUSPENDED
|
|
79
|
+
# above: findings are signal, not routine noise a job can silence.
|
|
80
|
+
header = f"⚠ {report.job_id} ran with findings{overdue}:"
|
|
81
|
+
body = "\n".join(f" {line}" for line in report.findings.splitlines())
|
|
82
|
+
return f"{header}\n{body}"
|
|
83
|
+
if report.output:
|
|
84
|
+
# A clean (0-exit) run's captured stdout - neutral wording, kept
|
|
85
|
+
# distinct from the findings/warning case above so a passing check
|
|
86
|
+
# is never mistaken for one that found something. One line-group:
|
|
87
|
+
# never falls through to the bare "✓ ran" case below.
|
|
88
|
+
header = f"✓ ran {report.job_id}{overdue}:"
|
|
89
|
+
body = "\n".join(f" {line}" for line in report.output.splitlines())
|
|
90
|
+
return f"{header}\n{body}"
|
|
91
|
+
base = f"✓ ran {report.job_id}{overdue}"
|
|
92
|
+
if report.deferred:
|
|
93
|
+
base += f"\n⏳ {report.job_id}: {report.deferred} backfills deferred"
|
|
94
|
+
if report.expired:
|
|
95
|
+
base += f"\n ({report.expired} missed run(s) dropped as expired)"
|
|
96
|
+
return base
|
|
97
|
+
# Only Outcome.LAUNCHED can reach here - SUMMARY/SUSPENDED/FAILED/RAN are
|
|
98
|
+
# all fully handled above and always return. A "started" notice is not
|
|
99
|
+
# "it ran", so it is the one outcome that still respects `surface`.
|
|
74
100
|
if not report.surface:
|
|
75
101
|
return None
|
|
76
|
-
|
|
77
|
-
return f"▶ launched {report.job_id} (running in background)"
|
|
78
|
-
overdue = f" ({report.overdue} overdue)" if report.overdue else ""
|
|
79
|
-
base = f"✓ ran {report.job_id}{overdue}"
|
|
80
|
-
if report.deferred:
|
|
81
|
-
base += f"\n⏳ {report.job_id}: {report.deferred} backfills deferred"
|
|
82
|
-
if report.expired:
|
|
83
|
-
base += f"\n ({report.expired} missed run(s) dropped as expired)"
|
|
84
|
-
return base
|
|
102
|
+
return f"▶ launched {report.job_id} (running in background)"
|
|
85
103
|
|
|
86
104
|
|
|
87
105
|
def format_digest(reports: list[JobReport], *, parse_error: str | None = None) -> str:
|
{cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/scheduler/ledger.py
RENAMED
|
@@ -112,6 +112,24 @@ def read_since(offset: int) -> tuple[list[dict[str, object]], int]:
|
|
|
112
112
|
conn.close()
|
|
113
113
|
|
|
114
114
|
|
|
115
|
+
def offset_before_ts(cutoff_ts: str) -> int:
|
|
116
|
+
"""The highest catchup_events id whose ts is strictly earlier than
|
|
117
|
+
cutoff_ts (0 if none) - the offset such that read_since(this) yields
|
|
118
|
+
every row with ts >= cutoff_ts. Used by the SessionStart digest's 24h
|
|
119
|
+
rolling lookback (§9.3 widen fix) to compute a starting offset without
|
|
120
|
+
loading the whole ledger table into memory; idx_catchup_events_ts makes
|
|
121
|
+
this a single index range scan rather than a full-table filter."""
|
|
122
|
+
conn = telemetry_store.connect()
|
|
123
|
+
try:
|
|
124
|
+
row = conn.execute(
|
|
125
|
+
"SELECT COALESCE(MAX(id), 0) AS m FROM catchup_events WHERE ts < ?",
|
|
126
|
+
(cutoff_ts,),
|
|
127
|
+
).fetchone()
|
|
128
|
+
return int(row["m"])
|
|
129
|
+
finally:
|
|
130
|
+
conn.close()
|
|
131
|
+
|
|
132
|
+
|
|
115
133
|
def current_offset() -> int:
|
|
116
134
|
"""The current max catchup_events id (0 if empty). Used to seed a
|
|
117
135
|
brand-new session's cursor (§9.3) so its first digest reflects only
|
{cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/scheduler/notify.py
RENAMED
|
@@ -9,7 +9,12 @@ if already exported, else are parsed directly from ``~/.creds`` (override via
|
|
|
9
9
|
``CCCS_CREDS_PATH``) since a detached subprocess's inherited environment is not
|
|
10
10
|
guaranteed to have sourced a shell profile. Every failure mode degrades to a
|
|
11
11
|
logged warning and a ``False`` return — a notification that can't be sent must
|
|
12
|
-
never take down the worker it's reporting on.
|
|
12
|
+
never take down the worker it's reporting on.
|
|
13
|
+
|
|
14
|
+
`suspended()` is the one-time push for the auto-suspend event; `push_outcome()`
|
|
15
|
+
(§ visibility fix) is the general-purpose push for every ordinary FAILED/RAN
|
|
16
|
+
worker completion, wired in from worker.py's `_run_body` at the same points it
|
|
17
|
+
already records the ledger event."""
|
|
13
18
|
from __future__ import annotations
|
|
14
19
|
|
|
15
20
|
import json
|
|
@@ -19,6 +24,8 @@ import urllib.request
|
|
|
19
24
|
from collections.abc import Callable
|
|
20
25
|
from pathlib import Path
|
|
21
26
|
|
|
27
|
+
from cc_session_tools.lib.scheduler.digest import Outcome
|
|
28
|
+
|
|
22
29
|
logger = logging.getLogger(__name__)
|
|
23
30
|
|
|
24
31
|
_CREDS_PATH_ENV = "CCCS_CREDS_PATH"
|
|
@@ -95,3 +102,30 @@ def suspended(job_id: str, consecutive_failures: int, *, post: Poster = _default
|
|
|
95
102
|
f"`ccsched enable {job_id}` after fixing"
|
|
96
103
|
)
|
|
97
104
|
return send_telegram(message, post=post)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _push_message(job_id: str, outcome: Outcome, detail: str | None) -> str:
|
|
108
|
+
"""The single place that decides what a push message for outcome X looks
|
|
109
|
+
like. Deliberately simpler than digest.py's in-session line (no
|
|
110
|
+
overdue/deferred/expired context, no warning-vs-clean wording split) — a
|
|
111
|
+
detached worker has neither that context nor a need to match the digest
|
|
112
|
+
byte-for-byte, just to say what happened."""
|
|
113
|
+
if outcome is Outcome.FAILED:
|
|
114
|
+
suffix = f": {detail}" if detail else ""
|
|
115
|
+
return f"[cc-scheduler] {job_id} failed{suffix}"
|
|
116
|
+
suffix = f":\n{detail}" if detail else ""
|
|
117
|
+
return f"[cc-scheduler] {job_id} ran{suffix}"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def push_outcome(
|
|
121
|
+
job_id: str, outcome: Outcome, detail: str | None = None, *, post: Poster = _default_post,
|
|
122
|
+
) -> bool:
|
|
123
|
+
"""Best-effort push for one worker-completion outcome (§ visibility fix).
|
|
124
|
+
Fires unconditionally for every FAILED and RAN outcome the worker
|
|
125
|
+
records — `JobSpec.surface` no longer gates run-completion visibility
|
|
126
|
+
here, matching digest.py's in-session line. Never called for
|
|
127
|
+
LAUNCHED/SUSPENDED/SUMMARY: LAUNCHED is not informative enough to push,
|
|
128
|
+
SUSPENDED already pushes via `suspended()` above, and SUMMARY is a
|
|
129
|
+
digest-only backlog-fold concept that never arises at the worker level.
|
|
130
|
+
"""
|
|
131
|
+
return send_telegram(_push_message(job_id, outcome, detail), post=post)
|