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.
Files changed (258) hide show
  1. {cc_session_tools-2.8.2/src/cc_session_tools.egg-info → cc_session_tools-2.9.0}/PKG-INFO +1 -1
  2. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/pyproject.toml +1 -1
  3. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/cli/ccsched.py +14 -14
  4. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/cli/ccst.py +72 -0
  5. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/pdata/init_service.py +57 -0
  6. cc_session_tools-2.9.0/src/cc_session_tools/lib/pdata/rename_group.py +161 -0
  7. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/scheduler/digest.py +35 -17
  8. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/scheduler/ledger.py +18 -0
  9. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/scheduler/notify.py +35 -1
  10. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/scheduler/surface.py +57 -23
  11. cc_session_tools-2.9.0/src/cc_session_tools/lib/scheduler/worker.py +235 -0
  12. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/skills/analyse-cc-usage/SKILL.md +17 -1
  13. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0/src/cc_session_tools.egg-info}/PKG-INFO +1 -1
  14. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools.egg-info/SOURCES.txt +2 -0
  15. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/catchup.py +17 -2
  16. cc_session_tools-2.9.0/tests/test_ccst_pdata_rename_group_cli.py +117 -0
  17. cc_session_tools-2.8.2/src/cc_session_tools/lib/scheduler/worker.py +0 -133
  18. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/LICENSE +0 -0
  19. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/README.md +0 -0
  20. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/setup.cfg +0 -0
  21. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/__init__.py +0 -0
  22. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/cli/__init__.py +0 -0
  23. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/cli/ccd.py +0 -0
  24. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/cli/ccmsg.py +0 -0
  25. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/cli/ccr.py +0 -0
  26. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/cli/ccs.py +0 -0
  27. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/cli/migrate_ccmsg.py +0 -0
  28. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/cli/migrate_ccsched.py +0 -0
  29. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/cli/migrate_sessions_db.py +0 -0
  30. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/cli/migrate_telemetry.py +0 -0
  31. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/config/hooks-bundle.json +0 -0
  32. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/config/session-tag-hook.json +0 -0
  33. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/hooks_install.py +0 -0
  34. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/__init__.py +0 -0
  35. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/claude_flags.py +0 -0
  36. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/claude_md_install.py +0 -0
  37. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/context_overrides.py +0 -0
  38. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/db.py +0 -0
  39. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/debug.py +0 -0
  40. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/doctor.py +0 -0
  41. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/doctor_mutes.py +0 -0
  42. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/hook_registry.py +0 -0
  43. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/install_sync.py +0 -0
  44. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/levenshtein.py +0 -0
  45. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/messaging/__init__.py +0 -0
  46. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/messaging/addressing.py +0 -0
  47. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/messaging/cursor.py +0 -0
  48. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/messaging/lock.py +0 -0
  49. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/messaging/message.py +0 -0
  50. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/messaging/move_safety.py +0 -0
  51. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/messaging/repository.py +0 -0
  52. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/messaging/retention.py +0 -0
  53. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/messaging/service.py +0 -0
  54. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/messaging/store.py +0 -0
  55. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/paths.py +0 -0
  56. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/pdata/__init__.py +0 -0
  57. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/pdata/backup.py +0 -0
  58. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/pdata/classify.py +0 -0
  59. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/pdata/cutover.py +0 -0
  60. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/pdata/formatting.py +0 -0
  61. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/pdata/importers.py +0 -0
  62. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/pdata/init_paths.py +0 -0
  63. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/pdata/manifest.py +0 -0
  64. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/pdata/naming.py +0 -0
  65. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/pdata/reorganize.py +0 -0
  66. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/pdata/repository.py +0 -0
  67. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/pdata/service.py +0 -0
  68. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/pdata/session_output.py +0 -0
  69. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/pdata/store.py +0 -0
  70. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/pdata/verify.py +0 -0
  71. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/pdata/write_log.py +0 -0
  72. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/picker.py +0 -0
  73. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/proc_lock.py +0 -0
  74. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/prompts.py +0 -0
  75. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/roots.py +0 -0
  76. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/rules.py +0 -0
  77. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/scheduler/__init__.py +0 -0
  78. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/scheduler/bundled_jobs.py +0 -0
  79. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/scheduler/cadence.py +0 -0
  80. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/scheduler/cursor.py +0 -0
  81. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/scheduler/due.py +0 -0
  82. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/scheduler/duration.py +0 -0
  83. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/scheduler/jobspec.py +0 -0
  84. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/scheduler/lock.py +0 -0
  85. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/scheduler/reconcile.py +0 -0
  86. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/scheduler/registry.py +0 -0
  87. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/scheduler/runner.py +0 -0
  88. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/scheduler/state.py +0 -0
  89. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/scheduler/store.py +0 -0
  90. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/scheduler/throttle.py +0 -0
  91. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/session_gc.py +0 -0
  92. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/sessions.py +0 -0
  93. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/sessions_db.py +0 -0
  94. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/sessions_repair.py +0 -0
  95. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/shell_install.py +0 -0
  96. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/tasklist.py +0 -0
  97. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/lib/telemetry_store.py +0 -0
  98. {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
  99. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/prompts/pdata-migration-skills-update.md +0 -0
  100. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/skills/context-override/SKILL.md +0 -0
  101. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/skills/delete-sessions/SKILL.md +0 -0
  102. {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
  103. {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
  104. {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
  105. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/skills/generate-8digit-code/SKILL.md +0 -0
  106. {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
  107. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/skills/list-empty-sessions/SKILL.md +0 -0
  108. {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
  109. {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
  110. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/skills/move-session/SKILL.md +0 -0
  111. {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
  112. {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
  113. {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
  114. {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
  115. {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
  116. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/skills/pm-project-init/SKILL.md +0 -0
  117. {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
  118. {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
  119. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/skills/reduce-persistent-context/CALIBRATION.md +0 -0
  120. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/skills/reduce-persistent-context/SKILL.md +0 -0
  121. {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
  122. {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
  123. {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
  124. {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
  125. {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
  126. {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
  127. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/skills/select-agent-model/SKILL.md +0 -0
  128. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/skills/send-session-message/SKILL.md +0 -0
  129. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools/skills/update-command-cache/SKILL.md +0 -0
  130. {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
  131. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools.egg-info/dependency_links.txt +0 -0
  132. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools.egg-info/entry_points.txt +0 -0
  133. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools.egg-info/requires.txt +0 -0
  134. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cc_session_tools.egg-info/top_level.txt +0 -0
  135. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/__init__.py +0 -0
  136. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/after_response.py +0 -0
  137. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/bash_hard_deny.py +0 -0
  138. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/bash_security_review.py +0 -0
  139. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/cache.py +0 -0
  140. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/confirm_8digit.py +0 -0
  141. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/context_window_warning.py +0 -0
  142. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/last_screenshot.py +0 -0
  143. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/marker_allow.py +0 -0
  144. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/markers.py +0 -0
  145. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/messaging_deliver.py +0 -0
  146. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/model_info.py +0 -0
  147. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/normalise.py +0 -0
  148. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/pending_migration.py +0 -0
  149. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/pending_rename.py +0 -0
  150. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/session_tag.py +0 -0
  151. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/stats.py +0 -0
  152. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/telemetry.py +0 -0
  153. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/telemetry_query.py +0 -0
  154. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/telemetry_trim.py +0 -0
  155. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/transcript.py +0 -0
  156. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/cccs_hooks/worklog_guard.py +0 -0
  157. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/claude_code_usage/__init__.py +0 -0
  158. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/claude_code_usage/attribution.py +0 -0
  159. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/claude_code_usage/cache.py +0 -0
  160. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/claude_code_usage/ccusage_wrapper.py +0 -0
  161. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/claude_code_usage/cli.py +0 -0
  162. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/claude_code_usage/data/pricing.json +0 -0
  163. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/claude_code_usage/parent_inference.py +0 -0
  164. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/claude_code_usage/parser.py +0 -0
  165. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/claude_code_usage/pricing.py +0 -0
  166. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/claude_code_usage/query.py +0 -0
  167. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/claude_code_usage/report.py +0 -0
  168. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/claude_code_usage/schema.py +0 -0
  169. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/src/claude_code_usage/session_names.py +0 -0
  170. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_after_response.py +0 -0
  171. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_attribution.py +0 -0
  172. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_bash_hard_deny.py +0 -0
  173. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_bash_security_review.py +0 -0
  174. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_cache.py +0 -0
  175. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_cache_sqlite.py +0 -0
  176. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccr_orphans.py +0 -0
  177. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccs_emptiness.py +0 -0
  178. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccs_eta.py +0 -0
  179. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccs_list_mode.py +0 -0
  180. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccs_scope_flags.py +0 -0
  181. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccs_sentinel_sort.py +0 -0
  182. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccs_session_counts.py +0 -0
  183. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_bundle_discovery.py +0 -0
  184. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_ccsched_jobs_cli.py +0 -0
  185. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_cli.py +0 -0
  186. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_context_override_cli.py +0 -0
  187. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_doctor.py +0 -0
  188. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_gc_report.py +0 -0
  189. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_hook_dispatcher.py +0 -0
  190. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_hooks_install_prune.py +0 -0
  191. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_hooks_install_zero_arg.py +0 -0
  192. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_hooks_uninstall.py +0 -0
  193. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_install_everything.py +0 -0
  194. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_main_gate.py +0 -0
  195. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_pdata_cli.py +0 -0
  196. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_pdata_init_cli.py +0 -0
  197. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_pdata_reconcile_cli.py +0 -0
  198. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_pdata_reorganize_cli.py +0 -0
  199. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_pdata_verify_cli.py +0 -0
  200. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_repair_sessions_cli.py +0 -0
  201. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_sessions_cli.py +0 -0
  202. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_shell_install.py +0 -0
  203. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_skills_install.py +0 -0
  204. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_skills_uninstall.py +0 -0
  205. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_telemetry_query.py +0 -0
  206. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccst_telemetry_trim.py +0 -0
  207. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_ccusage_wrapper.py +0 -0
  208. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_claude_flags.py +0 -0
  209. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_claude_md_install.py +0 -0
  210. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_cli.py +0 -0
  211. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_cli_ccd.py +0 -0
  212. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_cli_ccd_dry_run.py +0 -0
  213. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_cli_ccr.py +0 -0
  214. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_cli_ccs.py +0 -0
  215. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_cli_version.py +0 -0
  216. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_confirm_8digit.py +0 -0
  217. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_context_overrides.py +0 -0
  218. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_context_window_warning.py +0 -0
  219. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_db.py +0 -0
  220. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_debug.py +0 -0
  221. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_doctor_mutes.py +0 -0
  222. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_empty_session.py +0 -0
  223. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_hook_cache.py +0 -0
  224. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_hooks_install.py +0 -0
  225. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_install_sync.py +0 -0
  226. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_last_screenshot.py +0 -0
  227. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_levenshtein.py +0 -0
  228. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_marker_allow.py +0 -0
  229. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_migrate_ccmsg.py +0 -0
  230. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_migrate_ccsched.py +0 -0
  231. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_migrate_sessions_db.py +0 -0
  232. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_migrate_telemetry.py +0 -0
  233. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_model_info.py +0 -0
  234. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_normalise.py +0 -0
  235. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_parent_inference.py +0 -0
  236. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_parser.py +0 -0
  237. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_paths.py +0 -0
  238. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_pending_migration_hook.py +0 -0
  239. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_picker.py +0 -0
  240. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_pricing.py +0 -0
  241. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_proc_lock.py +0 -0
  242. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_prompts.py +0 -0
  243. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_query.py +0 -0
  244. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_report.py +0 -0
  245. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_roots.py +0 -0
  246. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_rules.py +0 -0
  247. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_scheduler_bundled_jobs.py +0 -0
  248. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_schema.py +0 -0
  249. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_session_names.py +0 -0
  250. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_session_tag.py +0 -0
  251. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_sessions.py +0 -0
  252. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_sessions_db.py +0 -0
  253. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_sessions_repair.py +0 -0
  254. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_tasklist.py +0 -0
  255. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_telemetry.py +0 -0
  256. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_telemetry_store.py +0 -0
  257. {cc_session_tools-2.8.2 → cc_session_tools-2.9.0}/tests/test_transcript.py +0 -0
  258. {cc_session_tools-2.8.2 → cc_session_tools-2.9.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.9.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.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(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
@@ -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":
@@ -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)
@@ -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, never folded into SUMMARY -
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 and report.findings:
68
- # Bypasses the surface gate deliberately, same as FAILED/SUSPENDED
69
- # above: findings are signal, not routine noise a job can silence.
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
- header = f"⚠ {report.job_id} ran with findings{overdue}:"
72
- body = "\n".join(f" {line}" for line in report.findings.splitlines())
73
- return f"{header}\n{body}"
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
- if report.outcome is Outcome.LAUNCHED:
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:
@@ -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
@@ -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)