cc-session-tools 2.8.2__tar.gz → 2.10.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.
Files changed (260) hide show
  1. {cc_session_tools-2.8.2/src/cc_session_tools.egg-info → cc_session_tools-2.10.0}/PKG-INFO +1 -1
  2. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/pyproject.toml +1 -1
  3. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/cli/ccsched.py +14 -14
  4. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/cli/ccst.py +101 -2
  5. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/pdata/init_service.py +57 -0
  6. cc_session_tools-2.10.0/src/cc_session_tools/lib/pdata/rename_group.py +161 -0
  7. cc_session_tools-2.10.0/src/cc_session_tools/lib/scheduler/digest.py +130 -0
  8. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/scheduler/ledger.py +18 -0
  9. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/scheduler/notify.py +35 -1
  10. cc_session_tools-2.10.0/src/cc_session_tools/lib/scheduler/surface.py +255 -0
  11. cc_session_tools-2.10.0/src/cc_session_tools/lib/scheduler/worker.py +235 -0
  12. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/analyse-cc-usage/SKILL.md +17 -1
  13. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0/src/cc_session_tools.egg-info}/PKG-INFO +1 -1
  14. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools.egg-info/SOURCES.txt +2 -0
  15. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/catchup.py +27 -2
  16. cc_session_tools-2.10.0/tests/test_ccst_pdata_rename_group_cli.py +117 -0
  17. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_pdata_verify_cli.py +41 -1
  18. cc_session_tools-2.8.2/src/cc_session_tools/lib/scheduler/digest.py +0 -93
  19. cc_session_tools-2.8.2/src/cc_session_tools/lib/scheduler/surface.py +0 -177
  20. cc_session_tools-2.8.2/src/cc_session_tools/lib/scheduler/worker.py +0 -133
  21. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/LICENSE +0 -0
  22. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/README.md +0 -0
  23. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/setup.cfg +0 -0
  24. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/__init__.py +0 -0
  25. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/cli/__init__.py +0 -0
  26. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/cli/ccd.py +0 -0
  27. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/cli/ccmsg.py +0 -0
  28. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/cli/ccr.py +0 -0
  29. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/cli/ccs.py +0 -0
  30. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/cli/migrate_ccmsg.py +0 -0
  31. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/cli/migrate_ccsched.py +0 -0
  32. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/cli/migrate_sessions_db.py +0 -0
  33. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/cli/migrate_telemetry.py +0 -0
  34. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/config/hooks-bundle.json +0 -0
  35. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/config/session-tag-hook.json +0 -0
  36. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/hooks_install.py +0 -0
  37. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/__init__.py +0 -0
  38. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/claude_flags.py +0 -0
  39. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/claude_md_install.py +0 -0
  40. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/context_overrides.py +0 -0
  41. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/db.py +0 -0
  42. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/debug.py +0 -0
  43. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/doctor.py +0 -0
  44. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/doctor_mutes.py +0 -0
  45. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/hook_registry.py +0 -0
  46. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/install_sync.py +0 -0
  47. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/levenshtein.py +0 -0
  48. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/messaging/__init__.py +0 -0
  49. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/messaging/addressing.py +0 -0
  50. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/messaging/cursor.py +0 -0
  51. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/messaging/lock.py +0 -0
  52. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/messaging/message.py +0 -0
  53. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/messaging/move_safety.py +0 -0
  54. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/messaging/repository.py +0 -0
  55. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/messaging/retention.py +0 -0
  56. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/messaging/service.py +0 -0
  57. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/messaging/store.py +0 -0
  58. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/paths.py +0 -0
  59. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/pdata/__init__.py +0 -0
  60. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/pdata/backup.py +0 -0
  61. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/pdata/classify.py +0 -0
  62. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/pdata/cutover.py +0 -0
  63. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/pdata/formatting.py +0 -0
  64. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/pdata/importers.py +0 -0
  65. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/pdata/init_paths.py +0 -0
  66. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/pdata/manifest.py +0 -0
  67. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/pdata/naming.py +0 -0
  68. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/pdata/reorganize.py +0 -0
  69. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/pdata/repository.py +0 -0
  70. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/pdata/service.py +0 -0
  71. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/pdata/session_output.py +0 -0
  72. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/pdata/store.py +0 -0
  73. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/pdata/verify.py +0 -0
  74. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/pdata/write_log.py +0 -0
  75. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/picker.py +0 -0
  76. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/proc_lock.py +0 -0
  77. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/prompts.py +0 -0
  78. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/roots.py +0 -0
  79. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/rules.py +0 -0
  80. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/scheduler/__init__.py +0 -0
  81. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/scheduler/bundled_jobs.py +0 -0
  82. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/scheduler/cadence.py +0 -0
  83. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/scheduler/cursor.py +0 -0
  84. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/scheduler/due.py +0 -0
  85. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/scheduler/duration.py +0 -0
  86. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/scheduler/jobspec.py +0 -0
  87. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/scheduler/lock.py +0 -0
  88. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/scheduler/reconcile.py +0 -0
  89. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/scheduler/registry.py +0 -0
  90. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/scheduler/runner.py +0 -0
  91. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/scheduler/state.py +0 -0
  92. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/scheduler/store.py +0 -0
  93. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/scheduler/throttle.py +0 -0
  94. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/session_gc.py +0 -0
  95. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/sessions.py +0 -0
  96. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/sessions_db.py +0 -0
  97. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/sessions_repair.py +0 -0
  98. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/shell_install.py +0 -0
  99. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/tasklist.py +0 -0
  100. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/lib/telemetry_store.py +0 -0
  101. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/prompts/pdata-migration-claude-md-update.md +0 -0
  102. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/prompts/pdata-migration-skills-update.md +0 -0
  103. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/context-override/SKILL.md +0 -0
  104. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/delete-sessions/SKILL.md +0 -0
  105. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/delete-sessions/scripts/delete_sessions.py +0 -0
  106. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/do-executor-critic-assessor-loop/SKILL.md +0 -0
  107. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/find-claude-code-session/SKILL.md +0 -0
  108. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/generate-8digit-code/SKILL.md +0 -0
  109. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/generate-8digit-code/scripts/generate_8digit_code.py +0 -0
  110. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/list-empty-sessions/SKILL.md +0 -0
  111. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/list-empty-sessions/scripts/list_empty_sessions.py +0 -0
  112. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/manage-recurring-cc-jobs-using-ccsched/SKILL.md +0 -0
  113. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/move-session/SKILL.md +0 -0
  114. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/move-session/hooks/sessionstart-pending-rename.sh +0 -0
  115. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/move-session/scripts/cc_session_rules.py +0 -0
  116. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/move-session/scripts/move_session.py +0 -0
  117. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/pm-pdata-conflict-resolution/SKILL.md +0 -0
  118. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/pm-pdata-schema-design/SKILL.md +0 -0
  119. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/pm-project-init/SKILL.md +0 -0
  120. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/pm-project-layout-reference/SKILL.md +0 -0
  121. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/pm-update-central-files/SKILL.md +0 -0
  122. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/reduce-persistent-context/CALIBRATION.md +0 -0
  123. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/reduce-persistent-context/SKILL.md +0 -0
  124. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/reduce-persistent-context/scripts/analyze_context.py +0 -0
  125. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/reduce-persistent-context/scripts/duplicates.py +0 -0
  126. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/reduce-persistent-context/scripts/measure.py +0 -0
  127. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/reduce-persistent-context/scripts/report.py +0 -0
  128. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/reduce-persistent-context/scripts/tokens.py +0 -0
  129. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/reduce-persistent-context/scripts/usage.py +0 -0
  130. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/select-agent-model/SKILL.md +0 -0
  131. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/send-session-message/SKILL.md +0 -0
  132. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/update-command-cache/SKILL.md +0 -0
  133. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools/skills/update-command-cache/scripts/update_command_cache.py +0 -0
  134. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools.egg-info/dependency_links.txt +0 -0
  135. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools.egg-info/entry_points.txt +0 -0
  136. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools.egg-info/requires.txt +0 -0
  137. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cc_session_tools.egg-info/top_level.txt +0 -0
  138. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/__init__.py +0 -0
  139. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/after_response.py +0 -0
  140. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/bash_hard_deny.py +0 -0
  141. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/bash_security_review.py +0 -0
  142. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/cache.py +0 -0
  143. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/confirm_8digit.py +0 -0
  144. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/context_window_warning.py +0 -0
  145. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/last_screenshot.py +0 -0
  146. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/marker_allow.py +0 -0
  147. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/markers.py +0 -0
  148. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/messaging_deliver.py +0 -0
  149. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/model_info.py +0 -0
  150. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/normalise.py +0 -0
  151. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/pending_migration.py +0 -0
  152. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/pending_rename.py +0 -0
  153. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/session_tag.py +0 -0
  154. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/stats.py +0 -0
  155. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/telemetry.py +0 -0
  156. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/telemetry_query.py +0 -0
  157. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/telemetry_trim.py +0 -0
  158. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/transcript.py +0 -0
  159. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/cccs_hooks/worklog_guard.py +0 -0
  160. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/claude_code_usage/__init__.py +0 -0
  161. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/claude_code_usage/attribution.py +0 -0
  162. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/claude_code_usage/cache.py +0 -0
  163. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/claude_code_usage/ccusage_wrapper.py +0 -0
  164. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/claude_code_usage/cli.py +0 -0
  165. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/claude_code_usage/data/pricing.json +0 -0
  166. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/claude_code_usage/parent_inference.py +0 -0
  167. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/claude_code_usage/parser.py +0 -0
  168. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/claude_code_usage/pricing.py +0 -0
  169. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/claude_code_usage/query.py +0 -0
  170. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/claude_code_usage/report.py +0 -0
  171. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/claude_code_usage/schema.py +0 -0
  172. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/src/claude_code_usage/session_names.py +0 -0
  173. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_after_response.py +0 -0
  174. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_attribution.py +0 -0
  175. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_bash_hard_deny.py +0 -0
  176. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_bash_security_review.py +0 -0
  177. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_cache.py +0 -0
  178. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_cache_sqlite.py +0 -0
  179. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccr_orphans.py +0 -0
  180. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccs_emptiness.py +0 -0
  181. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccs_eta.py +0 -0
  182. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccs_list_mode.py +0 -0
  183. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccs_scope_flags.py +0 -0
  184. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccs_sentinel_sort.py +0 -0
  185. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccs_session_counts.py +0 -0
  186. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_bundle_discovery.py +0 -0
  187. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_ccsched_jobs_cli.py +0 -0
  188. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_cli.py +0 -0
  189. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_context_override_cli.py +0 -0
  190. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_doctor.py +0 -0
  191. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_gc_report.py +0 -0
  192. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_hook_dispatcher.py +0 -0
  193. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_hooks_install_prune.py +0 -0
  194. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_hooks_install_zero_arg.py +0 -0
  195. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_hooks_uninstall.py +0 -0
  196. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_install_everything.py +0 -0
  197. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_main_gate.py +0 -0
  198. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_pdata_cli.py +0 -0
  199. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_pdata_init_cli.py +0 -0
  200. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_pdata_reconcile_cli.py +0 -0
  201. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_pdata_reorganize_cli.py +0 -0
  202. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_repair_sessions_cli.py +0 -0
  203. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_sessions_cli.py +0 -0
  204. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_shell_install.py +0 -0
  205. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_skills_install.py +0 -0
  206. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_skills_uninstall.py +0 -0
  207. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_telemetry_query.py +0 -0
  208. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccst_telemetry_trim.py +0 -0
  209. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_ccusage_wrapper.py +0 -0
  210. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_claude_flags.py +0 -0
  211. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_claude_md_install.py +0 -0
  212. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_cli.py +0 -0
  213. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_cli_ccd.py +0 -0
  214. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_cli_ccd_dry_run.py +0 -0
  215. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_cli_ccr.py +0 -0
  216. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_cli_ccs.py +0 -0
  217. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_cli_version.py +0 -0
  218. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_confirm_8digit.py +0 -0
  219. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_context_overrides.py +0 -0
  220. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_context_window_warning.py +0 -0
  221. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_db.py +0 -0
  222. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_debug.py +0 -0
  223. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_doctor_mutes.py +0 -0
  224. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_empty_session.py +0 -0
  225. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_hook_cache.py +0 -0
  226. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_hooks_install.py +0 -0
  227. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_install_sync.py +0 -0
  228. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_last_screenshot.py +0 -0
  229. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_levenshtein.py +0 -0
  230. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_marker_allow.py +0 -0
  231. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_migrate_ccmsg.py +0 -0
  232. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_migrate_ccsched.py +0 -0
  233. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_migrate_sessions_db.py +0 -0
  234. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_migrate_telemetry.py +0 -0
  235. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_model_info.py +0 -0
  236. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_normalise.py +0 -0
  237. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_parent_inference.py +0 -0
  238. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_parser.py +0 -0
  239. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_paths.py +0 -0
  240. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_pending_migration_hook.py +0 -0
  241. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_picker.py +0 -0
  242. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_pricing.py +0 -0
  243. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_proc_lock.py +0 -0
  244. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_prompts.py +0 -0
  245. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_query.py +0 -0
  246. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_report.py +0 -0
  247. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_roots.py +0 -0
  248. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_rules.py +0 -0
  249. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_scheduler_bundled_jobs.py +0 -0
  250. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_schema.py +0 -0
  251. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_session_names.py +0 -0
  252. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_session_tag.py +0 -0
  253. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_sessions.py +0 -0
  254. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_sessions_db.py +0 -0
  255. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_sessions_repair.py +0 -0
  256. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_tasklist.py +0 -0
  257. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_telemetry.py +0 -0
  258. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_telemetry_store.py +0 -0
  259. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/tests/test_transcript.py +0 -0
  260. {cc_session_tools-2.8.2 → cc_session_tools-2.10.0}/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.8.2
3
+ Version: 2.10.0
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cc-session-tools"
7
- version = "2.8.2"
7
+ version = "2.10.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(args: argparse.Namespace) -> int:
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
- crashed = outcome.timed_out or outcome.exit_code not in spec.success_exit_codes
253
- if crashed:
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
- event=ledger.LedgerEvent.FAIL if crashed else ledger.LedgerEvent.RUN,
264
- owed=1, ran=0 if crashed else 1, exit_code=outcome.exit_code,
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
@@ -1274,7 +1332,16 @@ def _cmd_pdata_verify(args: argparse.Namespace) -> int:
1274
1332
  print("ccst pdata verify: no project databases found", file=sys.stderr)
1275
1333
  return 2
1276
1334
 
1335
+ # --all-projects without --verbose collapses the per-project listing to
1336
+ # one summary line - with N projects, a line each (even when every one
1337
+ # is clean) was the "very long" default output a daily unattended job
1338
+ # printed into every catch-up digest. --project already prints just one
1339
+ # project's worth, which isn't that problem, so the split only applies
1340
+ # to --all-projects; a single --project run always gets full detail.
1341
+ compact = args.all_projects and not args.verbose
1342
+
1277
1343
  worst = 0
1344
+ flagged: list[str] = []
1278
1345
  for project in projects:
1279
1346
  try:
1280
1347
  summary = verify.run_verify(project=project, full=args.full)
@@ -1282,11 +1349,24 @@ def _cmd_pdata_verify(args: argparse.Namespace) -> int:
1282
1349
  print(f"ccst pdata verify: {project}: {exc}", file=sys.stderr)
1283
1350
  worst = max(worst, 2)
1284
1351
  continue
1352
+ if summary.status != "OK":
1353
+ worst = max(worst, 1)
1354
+ flagged.append(project)
1355
+ if compact:
1356
+ continue
1285
1357
  print(f"{project}: {summary.status} ({len(summary.issues)} issue(s))")
1286
1358
  for issue in summary.issues:
1287
1359
  print(f" [{issue.severity}] {issue.check}: {issue.message}")
1288
- if summary.status != "OK":
1289
- worst = max(worst, 1)
1360
+
1361
+ if compact:
1362
+ if flagged:
1363
+ print(
1364
+ f"ccst pdata verify --all-projects: ISSUES in {len(flagged)} of "
1365
+ f"{len(projects)} project(s) - run 'ccst pdata verify --all-projects "
1366
+ f"--verbose' for details"
1367
+ )
1368
+ else:
1369
+ print(f"ccst pdata verify --all-projects: OK ({len(projects)} project(s), 0 issue(s))")
1290
1370
  return worst
1291
1371
 
1292
1372
 
@@ -2247,6 +2327,18 @@ def _build_parser() -> argparse.ArgumentParser:
2247
2327
  help="Perform the move and update matching pdata records (default: dry-run only)",
2248
2328
  )
2249
2329
 
2330
+ pdata_rename_group_parser = pdata_sub.add_parser(
2331
+ "rename-group", help="Rename a record_group in place (records, record_group_fields, "
2332
+ "its ext table, and any matching manifest entries)"
2333
+ )
2334
+ pdata_rename_group_parser.add_argument("--project", required=True, metavar="NAME")
2335
+ pdata_rename_group_parser.add_argument("--from", dest="from_", required=True, metavar="OLD")
2336
+ pdata_rename_group_parser.add_argument("--to", required=True, metavar="NEW")
2337
+ pdata_rename_group_parser.add_argument(
2338
+ "--write", action="store_true",
2339
+ help="Perform the rename and update .ccst-pdata-proposal.json (default: dry-run only)",
2340
+ )
2341
+
2250
2342
  pdata_reconcile_parser = pdata_sub.add_parser(
2251
2343
  "reconcile-session-output",
2252
2344
  help="Backfill the session-output index from cc-sessions/*/out/ on disk (idempotent)",
@@ -2277,6 +2369,11 @@ def _build_parser() -> argparse.ArgumentParser:
2277
2369
  "--full", action="store_true",
2278
2370
  help="Rescan every row instead of only rows changed since the last run",
2279
2371
  )
2372
+ pdata_verify_parser.add_argument(
2373
+ "--verbose", action="store_true",
2374
+ help="With --all-projects, print the full per-project listing instead of "
2375
+ "the default one-line summary (--project always prints full detail)",
2376
+ )
2280
2377
 
2281
2378
  # ---- sessions ----
2282
2379
  sessions_parser = sub.add_parser("sessions", help="sessions.db management commands")
@@ -2553,6 +2650,8 @@ def main() -> None:
2553
2650
  sys.exit(_cmd_pdata_init(args))
2554
2651
  if args.verb == "reorganize":
2555
2652
  sys.exit(_cmd_pdata_reorganize(args))
2653
+ if args.verb == "rename-group":
2654
+ sys.exit(_cmd_pdata_rename_group(args))
2556
2655
  if args.verb == "reconcile-session-output":
2557
2656
  sys.exit(_cmd_pdata_reconcile_session_output(args))
2558
2657
  if args.verb == "verify":
@@ -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)
@@ -0,0 +1,130 @@
1
+ """Pure formatting of the SessionStart catch-up digest (§11). Takes structured
2
+ sweep results, returns a string. No I/O."""
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from enum import Enum
7
+
8
+
9
+ class Outcome(str, Enum):
10
+ RAN = "ran"
11
+ FAILED = "failed"
12
+ LAUNCHED = "launched"
13
+ SUSPENDED = "suspended"
14
+ SUMMARY = "summary"
15
+
16
+
17
+ @dataclass(frozen=True, slots=True)
18
+ class JobReport:
19
+ job_id: str
20
+ outcome: Outcome
21
+ surface: bool
22
+ overdue: str
23
+ ran: int
24
+ deferred: int
25
+ expired: int
26
+ consecutive_failures: int
27
+ # Relative-age suffix (e.g. "12d ago"). Always set by the surfacing layer
28
+ # for FAILED/SUSPENDED reports, and for SUMMARY reports (oldest folded
29
+ # entry); unused otherwise.
30
+ age: str | None = None
31
+ # SUMMARY only: number of routine entries folded into this one line.
32
+ count: int = 0
33
+ # RAN only: a successful-but-nonzero-exit run's captured stdout (e.g. a
34
+ # drift monitor's "found something" report). Always surfaced with a
35
+ # warning wording, never folded into SUMMARY - see surface.py.
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
43
+
44
+
45
+ def _ordinal(n: int) -> str:
46
+ if 10 <= n % 100 <= 20:
47
+ suffix = "th"
48
+ else:
49
+ suffix = {1: "st", 2: "nd", 3: "rd"}.get(n % 10, "th")
50
+ return f"{n}{suffix}"
51
+
52
+
53
+ def _line(report: JobReport) -> str | None:
54
+ if report.outcome is Outcome.SUMMARY:
55
+ return (
56
+ f"⏱ {report.count} routine job runs since your last session, "
57
+ f"oldest {report.age}"
58
+ )
59
+ if report.outcome is Outcome.SUSPENDED:
60
+ age_suffix = f", {report.age}" if report.age else ""
61
+ return (
62
+ f"⛔ {report.job_id} auto-suspended after "
63
+ f"{report.consecutive_failures} consecutive failures{age_suffix} — see "
64
+ f"`ccsched status {report.job_id}` / run `ccsched enable {report.job_id}` after fixing"
65
+ )
66
+ if report.outcome is Outcome.FAILED:
67
+ age_suffix = f", {report.age}" if report.age else ""
68
+ return (
69
+ f"✗ {report.job_id} failed "
70
+ f"({_ordinal(report.consecutive_failures)} consecutive{age_suffix}) — see "
71
+ f"`ccsched status {report.job_id}`"
72
+ )
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.
76
+ overdue = f" ({report.overdue} overdue)" if report.overdue else ""
77
+ # Every RAN variant below carries this same age suffix (when set) so a
78
+ # result read back from an earlier run is never mistaken for something
79
+ # that just happened - e.g. sitting next to a fresh LAUNCHED line for
80
+ # the same job from this same sweep.
81
+ age_suffix = f", {report.age}" if report.age else ""
82
+ if report.findings:
83
+ # Bypasses the surface gate deliberately, same as FAILED/SUSPENDED
84
+ # above: findings are signal, not routine noise a job can silence.
85
+ header = f"⚠ {report.job_id} ran with findings{overdue}{age_suffix}:"
86
+ body = "\n".join(f" {line}" for line in report.findings.splitlines())
87
+ return f"{header}\n{body}"
88
+ if report.output:
89
+ # A clean (0-exit) run's captured stdout - neutral wording, kept
90
+ # distinct from the findings/warning case above so a passing check
91
+ # is never mistaken for one that found something. One line-group:
92
+ # never falls through to the bare "✓ ran" case below.
93
+ header = f"✓ ran {report.job_id}{overdue}{age_suffix}:"
94
+ body = "\n".join(f" {line}" for line in report.output.splitlines())
95
+ return f"{header}\n{body}"
96
+ if report.count > 1:
97
+ # Several unremarkable runs of the same job coalesced within one
98
+ # sweep (§ coalesce fix) - one line with a count, not N
99
+ # near-identical bare ticks. Only bare (no output/findings) runs
100
+ # are ever coalesced - see surface.py - so this never competes
101
+ # with the findings/output branches above.
102
+ recency = f" (most recently {report.age})" if report.age else ""
103
+ return f"✓ ran {report.job_id} ×{report.count}{recency}"
104
+ base = f"✓ ran {report.job_id}{overdue}{age_suffix}"
105
+ if report.deferred:
106
+ base += f"\n⏳ {report.job_id}: {report.deferred} backfills deferred"
107
+ if report.expired:
108
+ base += f"\n ({report.expired} missed run(s) dropped as expired)"
109
+ return base
110
+ # Only Outcome.LAUNCHED can reach here - SUMMARY/SUSPENDED/FAILED/RAN are
111
+ # all fully handled above and always return. A "started" notice is not
112
+ # "it ran", so it is the one outcome that still respects `surface`.
113
+ if not report.surface:
114
+ return None
115
+ if report.count > 1:
116
+ # Several launches of the same job coalesced within one sweep (§
117
+ # coalesce fix) - see the matching RAN branch above.
118
+ recency = f" (most recently {report.age})" if report.age else ""
119
+ return f"▶ launched {report.job_id} ×{report.count}{recency}"
120
+ age_suffix = f", {report.age}" if report.age else ""
121
+ return f"▶ launched {report.job_id} (running in background){age_suffix}"
122
+
123
+
124
+ def format_digest(reports: list[JobReport], *, parse_error: str | None = None) -> str:
125
+ if parse_error is not None:
126
+ return f"[cc-scheduler] job registry failed to load — no jobs ran: {parse_error}"
127
+ lines = [line for line in (_line(r) for r in reports) if line is not None]
128
+ if not lines:
129
+ return ""
130
+ return "\n".join(["[cc-scheduler] scheduled-task catch-up:", *lines])
@@ -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