cc-session-tools 2.3.0__tar.gz → 2.4.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 (239) hide show
  1. {cc_session_tools-2.3.0/src/cc_session_tools.egg-info → cc_session_tools-2.4.0}/PKG-INFO +14 -3
  2. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/README.md +13 -2
  3. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/pyproject.toml +1 -1
  4. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/cli/ccst.py +88 -16
  5. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/doctor.py +80 -8
  6. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/doctor_mutes.py +12 -1
  7. cc_session_tools-2.4.0/src/cc_session_tools/lib/install_sync.py +103 -0
  8. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/sessions_db.py +6 -0
  9. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0/src/cc_session_tools.egg-info}/PKG-INFO +14 -3
  10. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools.egg-info/SOURCES.txt +3 -0
  11. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cccs_hooks/bash_security_review.py +43 -37
  12. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cccs_hooks/normalise.py +4 -0
  13. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_bash_security_review.py +222 -0
  14. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_ccst_doctor.py +154 -0
  15. cc_session_tools-2.4.0/tests/test_ccst_install_everything.py +293 -0
  16. cc_session_tools-2.4.0/tests/test_ccst_main_gate.py +197 -0
  17. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_ccst_repair_sessions_cli.py +39 -0
  18. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_doctor_mutes.py +13 -0
  19. cc_session_tools-2.4.0/tests/test_install_sync.py +155 -0
  20. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_normalise.py +34 -0
  21. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_sessions_db.py +2 -2
  22. cc_session_tools-2.3.0/tests/test_ccst_install_everything.py +0 -160
  23. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/LICENSE +0 -0
  24. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/setup.cfg +0 -0
  25. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/__init__.py +0 -0
  26. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/cli/__init__.py +0 -0
  27. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/cli/ccd.py +0 -0
  28. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/cli/ccmsg.py +0 -0
  29. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/cli/ccr.py +0 -0
  30. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/cli/ccs.py +0 -0
  31. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/cli/ccsched.py +0 -0
  32. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/cli/migrate_ccmsg.py +0 -0
  33. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/cli/migrate_ccsched.py +0 -0
  34. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/cli/migrate_sessions_db.py +0 -0
  35. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/cli/migrate_telemetry.py +0 -0
  36. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/config/hooks-bundle.json +0 -0
  37. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/config/session-tag-hook.json +0 -0
  38. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/hooks_install.py +0 -0
  39. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/__init__.py +0 -0
  40. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/claude_flags.py +0 -0
  41. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/claude_md_install.py +0 -0
  42. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/db.py +0 -0
  43. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/debug.py +0 -0
  44. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/hook_registry.py +0 -0
  45. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/levenshtein.py +0 -0
  46. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/messaging/__init__.py +0 -0
  47. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/messaging/addressing.py +0 -0
  48. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/messaging/cursor.py +0 -0
  49. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/messaging/lock.py +0 -0
  50. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/messaging/message.py +0 -0
  51. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/messaging/move_safety.py +0 -0
  52. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/messaging/repository.py +0 -0
  53. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/messaging/retention.py +0 -0
  54. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/messaging/service.py +0 -0
  55. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/messaging/store.py +0 -0
  56. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/paths.py +0 -0
  57. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/pdata/__init__.py +0 -0
  58. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/pdata/backup.py +0 -0
  59. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/pdata/classify.py +0 -0
  60. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/pdata/cutover.py +0 -0
  61. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/pdata/formatting.py +0 -0
  62. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/pdata/importers.py +0 -0
  63. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/pdata/init_paths.py +0 -0
  64. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/pdata/init_service.py +0 -0
  65. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/pdata/manifest.py +0 -0
  66. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/pdata/naming.py +0 -0
  67. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/pdata/repository.py +0 -0
  68. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/pdata/service.py +0 -0
  69. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/pdata/session_output.py +0 -0
  70. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/pdata/store.py +0 -0
  71. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/pdata/verify.py +0 -0
  72. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/picker.py +0 -0
  73. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/prompts.py +0 -0
  74. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/roots.py +0 -0
  75. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/rules.py +0 -0
  76. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/scheduler/__init__.py +0 -0
  77. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/scheduler/bundled_jobs.py +0 -0
  78. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/scheduler/cadence.py +0 -0
  79. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/scheduler/cursor.py +0 -0
  80. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/scheduler/digest.py +0 -0
  81. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/scheduler/due.py +0 -0
  82. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/scheduler/duration.py +0 -0
  83. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/scheduler/jobspec.py +0 -0
  84. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/scheduler/ledger.py +0 -0
  85. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/scheduler/lock.py +0 -0
  86. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/scheduler/notify.py +0 -0
  87. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/scheduler/reconcile.py +0 -0
  88. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/scheduler/registry.py +0 -0
  89. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/scheduler/runner.py +0 -0
  90. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/scheduler/state.py +0 -0
  91. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/scheduler/store.py +0 -0
  92. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/scheduler/surface.py +0 -0
  93. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/scheduler/throttle.py +0 -0
  94. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/scheduler/worker.py +0 -0
  95. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/session_gc.py +0 -0
  96. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/sessions.py +0 -0
  97. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/sessions_repair.py +0 -0
  98. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/shell_install.py +0 -0
  99. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/tasklist.py +0 -0
  100. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/lib/telemetry_store.py +0 -0
  101. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/skills/analyse-cc-usage/SKILL.md +0 -0
  102. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/skills/delete-sessions/SKILL.md +0 -0
  103. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/skills/delete-sessions/scripts/delete_sessions.py +0 -0
  104. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/skills/do-executor-critic-assessor-loop/SKILL.md +0 -0
  105. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/skills/find-claude-code-session/SKILL.md +0 -0
  106. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/skills/generate-8digit-code/SKILL.md +0 -0
  107. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/skills/generate-8digit-code/scripts/generate_8digit_code.py +0 -0
  108. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/skills/list-empty-sessions/SKILL.md +0 -0
  109. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/skills/list-empty-sessions/scripts/list_empty_sessions.py +0 -0
  110. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/skills/manage-recurring-cc-jobs-using-ccsched/SKILL.md +0 -0
  111. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/skills/move-session/SKILL.md +0 -0
  112. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/skills/move-session/hooks/sessionstart-pending-rename.sh +0 -0
  113. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/skills/move-session/scripts/cc_session_rules.py +0 -0
  114. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/skills/move-session/scripts/move_session.py +0 -0
  115. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/skills/pm-pdata-conflict-resolution/SKILL.md +0 -0
  116. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/skills/pm-pdata-schema-design/SKILL.md +0 -0
  117. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/skills/pm-project-init/SKILL.md +0 -0
  118. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/skills/pm-update-central-files/SKILL.md +0 -0
  119. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/skills/reduce-persistent-context/CALIBRATION.md +0 -0
  120. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/skills/reduce-persistent-context/SKILL.md +0 -0
  121. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/skills/reduce-persistent-context/scripts/analyze_context.py +0 -0
  122. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/skills/reduce-persistent-context/scripts/duplicates.py +0 -0
  123. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/skills/reduce-persistent-context/scripts/measure.py +0 -0
  124. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/skills/reduce-persistent-context/scripts/report.py +0 -0
  125. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/skills/reduce-persistent-context/scripts/tokens.py +0 -0
  126. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/skills/reduce-persistent-context/scripts/usage.py +0 -0
  127. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/skills/select-agent-model/SKILL.md +0 -0
  128. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/skills/send-session-message/SKILL.md +0 -0
  129. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/skills/update-command-cache/SKILL.md +0 -0
  130. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools/skills/update-command-cache/scripts/update_command_cache.py +0 -0
  131. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools.egg-info/dependency_links.txt +0 -0
  132. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools.egg-info/entry_points.txt +0 -0
  133. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools.egg-info/requires.txt +0 -0
  134. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cc_session_tools.egg-info/top_level.txt +0 -0
  135. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cccs_hooks/__init__.py +0 -0
  136. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cccs_hooks/after_response.py +0 -0
  137. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cccs_hooks/bash_hard_deny.py +0 -0
  138. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cccs_hooks/cache.py +0 -0
  139. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cccs_hooks/catchup.py +0 -0
  140. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cccs_hooks/confirm_8digit.py +0 -0
  141. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cccs_hooks/last_screenshot.py +0 -0
  142. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cccs_hooks/marker_allow.py +0 -0
  143. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cccs_hooks/markers.py +0 -0
  144. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cccs_hooks/messaging_deliver.py +0 -0
  145. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cccs_hooks/pending_migration.py +0 -0
  146. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cccs_hooks/session_tag.py +0 -0
  147. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cccs_hooks/stats.py +0 -0
  148. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cccs_hooks/telemetry.py +0 -0
  149. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cccs_hooks/telemetry_query.py +0 -0
  150. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cccs_hooks/telemetry_trim.py +0 -0
  151. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cccs_hooks/transcript.py +0 -0
  152. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/cccs_hooks/worklog_guard.py +0 -0
  153. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/claude_code_usage/__init__.py +0 -0
  154. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/claude_code_usage/attribution.py +0 -0
  155. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/claude_code_usage/cache.py +0 -0
  156. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/claude_code_usage/ccusage_wrapper.py +0 -0
  157. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/claude_code_usage/cli.py +0 -0
  158. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/claude_code_usage/data/pricing.json +0 -0
  159. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/claude_code_usage/parent_inference.py +0 -0
  160. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/claude_code_usage/parser.py +0 -0
  161. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/claude_code_usage/pricing.py +0 -0
  162. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/claude_code_usage/query.py +0 -0
  163. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/claude_code_usage/report.py +0 -0
  164. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/claude_code_usage/schema.py +0 -0
  165. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/src/claude_code_usage/session_names.py +0 -0
  166. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_after_response.py +0 -0
  167. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_attribution.py +0 -0
  168. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_bash_hard_deny.py +0 -0
  169. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_cache.py +0 -0
  170. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_cache_sqlite.py +0 -0
  171. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_ccr_orphans.py +0 -0
  172. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_ccs_emptiness.py +0 -0
  173. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_ccs_eta.py +0 -0
  174. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_ccs_list_mode.py +0 -0
  175. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_ccs_scope_flags.py +0 -0
  176. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_ccs_sentinel_sort.py +0 -0
  177. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_ccs_session_counts.py +0 -0
  178. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_ccst_bundle_discovery.py +0 -0
  179. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_ccst_ccsched_jobs_cli.py +0 -0
  180. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_ccst_cli.py +0 -0
  181. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_ccst_gc_report.py +0 -0
  182. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_ccst_hook_dispatcher.py +0 -0
  183. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_ccst_hooks_install_prune.py +0 -0
  184. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_ccst_hooks_install_zero_arg.py +0 -0
  185. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_ccst_hooks_uninstall.py +0 -0
  186. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_ccst_pdata_cli.py +0 -0
  187. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_ccst_pdata_init_cli.py +0 -0
  188. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_ccst_pdata_reconcile_cli.py +0 -0
  189. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_ccst_pdata_verify_cli.py +0 -0
  190. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_ccst_sessions_cli.py +0 -0
  191. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_ccst_shell_install.py +0 -0
  192. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_ccst_skills_install.py +0 -0
  193. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_ccst_skills_uninstall.py +0 -0
  194. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_ccst_telemetry_query.py +0 -0
  195. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_ccst_telemetry_trim.py +0 -0
  196. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_ccusage_wrapper.py +0 -0
  197. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_claude_flags.py +0 -0
  198. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_claude_md_install.py +0 -0
  199. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_cli.py +0 -0
  200. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_cli_ccd.py +0 -0
  201. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_cli_ccd_dry_run.py +0 -0
  202. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_cli_ccr.py +0 -0
  203. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_cli_ccs.py +0 -0
  204. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_cli_version.py +0 -0
  205. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_confirm_8digit.py +0 -0
  206. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_db.py +0 -0
  207. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_debug.py +0 -0
  208. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_empty_session.py +0 -0
  209. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_hook_cache.py +0 -0
  210. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_hooks_install.py +0 -0
  211. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_last_screenshot.py +0 -0
  212. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_levenshtein.py +0 -0
  213. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_marker_allow.py +0 -0
  214. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_migrate_ccmsg.py +0 -0
  215. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_migrate_ccsched.py +0 -0
  216. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_migrate_sessions_db.py +0 -0
  217. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_migrate_telemetry.py +0 -0
  218. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_parent_inference.py +0 -0
  219. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_parser.py +0 -0
  220. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_paths.py +0 -0
  221. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_pending_migration_hook.py +0 -0
  222. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_picker.py +0 -0
  223. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_pricing.py +0 -0
  224. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_prompts.py +0 -0
  225. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_query.py +0 -0
  226. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_report.py +0 -0
  227. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_roots.py +0 -0
  228. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_rules.py +0 -0
  229. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_scheduler_bundled_jobs.py +0 -0
  230. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_schema.py +0 -0
  231. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_session_names.py +0 -0
  232. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_session_tag.py +0 -0
  233. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_sessions.py +0 -0
  234. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_sessions_repair.py +0 -0
  235. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_tasklist.py +0 -0
  236. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_telemetry.py +0 -0
  237. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_telemetry_store.py +0 -0
  238. {cc_session_tools-2.3.0 → cc_session_tools-2.4.0}/tests/test_transcript.py +0 -0
  239. {cc_session_tools-2.3.0 → cc_session_tools-2.4.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.3.0
3
+ Version: 2.4.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
@@ -144,6 +144,12 @@ It runs `ccst doctor` at the end so you can see the health check immediately. Re
144
144
 
145
145
  > **Note:** `install-everything.sh` handles steps 1–5 of setup. Step 6 — adding broader CCST guidance to your global `~/.claude/CLAUDE.md` (session management, 8-digit gate, etc.) — is interactive and must be run separately afterwards. See [Configure your global CLAUDE.md](#configure-your-global-claudemd) below.
146
146
 
147
+ > **After upgrading** (`uv tool install --reinstall`/`--upgrade`, or `pip`/`pipx` equivalents):
148
+ > re-run `ccst install-everything --apply` so skills/hooks/shell functions/scheduled jobs stay in
149
+ > sync with the new version. If you forget, `ccst` itself will remind you — any interactive
150
+ > command run from a real terminal on a version that hasn't been synced prints a nudge and exits
151
+ > until you do (automated callers like Claude Code's own hooks are never affected).
152
+
147
153
  **Manual path — step by step:**
148
154
 
149
155
  ```sh
@@ -884,14 +890,19 @@ ccst claude-md uninstall --apply
884
890
 
885
891
  Run a full health check: PATH for all six CLIs, env vars (`REPO_ROOT`/`PROJ_ROOT`),
886
892
  `~/.claude/settings.json` JSON validity, expected hook registrations present,
887
- skill symlinks correct and pointing at the installed source, version drift
888
- between installed `ccst` and the latest release on PyPI.
893
+ skill symlinks correct and pointing at the installed source, whether
894
+ `install-everything --apply` has been run for the currently-installed version,
895
+ and version drift between installed `ccst` and the latest release on PyPI.
889
896
 
890
897
  ```sh
891
898
  ccst doctor # checks everything, including PyPI version check
892
899
  ccst doctor --no-pypi # skip the network version check (useful in CI)
900
+ ccst doctor --all # also print OK checks (default: only WARN/FAIL)
893
901
  ```
894
902
 
903
+ By default only WARN/FAIL results are printed — a clean machine is otherwise dozens of `[OK]`
904
+ lines to scroll past. Use `--all` to see the full check list, including everything that passed.
905
+
895
906
  Exit `0` if all checks are OK. Exit `1` if any check is WARN or FAIL.
896
907
 
897
908
  ### `ccst telemetry trim`
@@ -87,6 +87,12 @@ It runs `ccst doctor` at the end so you can see the health check immediately. Re
87
87
 
88
88
  > **Note:** `install-everything.sh` handles steps 1–5 of setup. Step 6 — adding broader CCST guidance to your global `~/.claude/CLAUDE.md` (session management, 8-digit gate, etc.) — is interactive and must be run separately afterwards. See [Configure your global CLAUDE.md](#configure-your-global-claudemd) below.
89
89
 
90
+ > **After upgrading** (`uv tool install --reinstall`/`--upgrade`, or `pip`/`pipx` equivalents):
91
+ > re-run `ccst install-everything --apply` so skills/hooks/shell functions/scheduled jobs stay in
92
+ > sync with the new version. If you forget, `ccst` itself will remind you — any interactive
93
+ > command run from a real terminal on a version that hasn't been synced prints a nudge and exits
94
+ > until you do (automated callers like Claude Code's own hooks are never affected).
95
+
90
96
  **Manual path — step by step:**
91
97
 
92
98
  ```sh
@@ -827,14 +833,19 @@ ccst claude-md uninstall --apply
827
833
 
828
834
  Run a full health check: PATH for all six CLIs, env vars (`REPO_ROOT`/`PROJ_ROOT`),
829
835
  `~/.claude/settings.json` JSON validity, expected hook registrations present,
830
- skill symlinks correct and pointing at the installed source, version drift
831
- between installed `ccst` and the latest release on PyPI.
836
+ skill symlinks correct and pointing at the installed source, whether
837
+ `install-everything --apply` has been run for the currently-installed version,
838
+ and version drift between installed `ccst` and the latest release on PyPI.
832
839
 
833
840
  ```sh
834
841
  ccst doctor # checks everything, including PyPI version check
835
842
  ccst doctor --no-pypi # skip the network version check (useful in CI)
843
+ ccst doctor --all # also print OK checks (default: only WARN/FAIL)
836
844
  ```
837
845
 
846
+ By default only WARN/FAIL results are printed — a clean machine is otherwise dozens of `[OK]`
847
+ lines to scroll past. Use `--all` to see the full check list, including everything that passed.
848
+
838
849
  Exit `0` if all checks are OK. Exit `1` if any check is WARN or FAIL.
839
850
 
840
851
  ### `ccst telemetry trim`
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cc-session-tools"
7
- version = "2.3.0"
7
+ version = "2.4.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" }
@@ -653,6 +653,7 @@ def _cmd_doctor(args: argparse.Namespace) -> int:
653
653
 
654
654
  from cc_session_tools.lib.pdata import verify as _pdata_verify
655
655
  from cc_session_tools.lib.pdata.init_paths import default_projects_root
656
+ from cc_session_tools.lib import install_sync
656
657
 
657
658
  results = run_all_checks(
658
659
  installed_version=__version__,
@@ -667,6 +668,7 @@ def _cmd_doctor(args: argparse.Namespace) -> int:
667
668
  projects_root=default_projects_root(),
668
669
  pdata_verify_projects=_pdata_verify.discover_projects(),
669
670
  sessions_db_path=store_paths["sessions"],
671
+ synced_version=install_sync.get_synced_version(),
670
672
  )
671
673
 
672
674
  if args.drift or getattr(args, "mode", None) == "drift":
@@ -677,7 +679,7 @@ def _cmd_doctor(args: argparse.Namespace) -> int:
677
679
  print(report)
678
680
  return 1 if unmuted else 0
679
681
 
680
- print(format_results(results))
682
+ print(format_results(results, show_all=getattr(args, "all", False)))
681
683
 
682
684
  any_issue = any(r.status in (Status.WARN, Status.FAIL) for r in results)
683
685
  return 1 if any_issue else 0
@@ -1239,6 +1241,8 @@ def _cmd_sessions_list(args: argparse.Namespace) -> int:
1239
1241
 
1240
1242
 
1241
1243
  def _cmd_repair_sessions(args: argparse.Namespace) -> int:
1244
+ import sqlite3
1245
+
1242
1246
  from cc_session_tools.lib import db as db_lib
1243
1247
  from cc_session_tools.lib import sessions_db, sessions_repair
1244
1248
  from cc_session_tools.lib.roots import RootsConfigError, load_session_roots
@@ -1250,21 +1254,32 @@ def _cmd_repair_sessions(args: argparse.Namespace) -> int:
1250
1254
  print(str(e), file=sys.stderr)
1251
1255
  return 1
1252
1256
 
1253
- if args.execute:
1254
- if not db_path.exists():
1255
- # sqlite3.connect() auto-creates an empty file, so without this check
1256
- # db_lib.backup_to() below would silently back up (and repair() would open)
1257
- # a brand-new, empty sessions.db instead of failing loudly on the mistake.
1258
- print(f"No sessions.db found at {db_path} — nothing to back up or repair.", file=sys.stderr)
1259
- return 1
1260
- backup_dir = db_path.parent / "repair-backups"
1261
- backup_dir.mkdir(parents=True, exist_ok=True)
1262
- stamp = datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%dT%H%M%SZ")
1263
- backup_path = backup_dir / f"sessions-{stamp}.db"
1264
- db_lib.backup_to(db_path, backup_path)
1265
- print(f"Backed up sessions.db to {backup_path}")
1266
-
1267
- report = sessions_repair.repair(roots, path=db_path, dry_run=not args.execute)
1257
+ # sqlite3.connect() (and the online backup API below) open lazily and
1258
+ # only fail once a statement actually touches the file, so a corrupt-
1259
+ # but-existing sessions.db reaches this try, not the exists() check
1260
+ # above. This is the tool users are told to run to fix store corruption
1261
+ # - both the backup step and the repair itself must fail loudly with a
1262
+ # clear message here, not a raw traceback.
1263
+ try:
1264
+ if args.execute:
1265
+ if not db_path.exists():
1266
+ # sqlite3.connect() auto-creates an empty file, so without this
1267
+ # check db_lib.backup_to() below would silently back up (and
1268
+ # repair() would open) a brand-new, empty sessions.db instead
1269
+ # of failing loudly on the mistake.
1270
+ print(f"No sessions.db found at {db_path} — nothing to back up or repair.", file=sys.stderr)
1271
+ return 1
1272
+ backup_dir = db_path.parent / "repair-backups"
1273
+ backup_dir.mkdir(parents=True, exist_ok=True)
1274
+ stamp = datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%dT%H%M%SZ")
1275
+ backup_path = backup_dir / f"sessions-{stamp}.db"
1276
+ db_lib.backup_to(db_path, backup_path)
1277
+ print(f"Backed up sessions.db to {backup_path}")
1278
+
1279
+ report = sessions_repair.repair(roots, path=db_path, dry_run=not args.execute)
1280
+ except sqlite3.DatabaseError as exc:
1281
+ print(f"{db_path} exists but failed to open: {exc}", file=sys.stderr)
1282
+ return 1
1268
1283
  if not any((report.repaired, report.unresolved, report.ambiguous, report.conflicts)):
1269
1284
  print("No non-absolute project_dir rows found in sessions.db.")
1270
1285
  return 0
@@ -1486,6 +1501,27 @@ def _cmd_install_everything(args: argparse.Namespace) -> int:
1486
1501
  if rc != 0:
1487
1502
  overall_rc = rc
1488
1503
 
1504
+ if apply and overall_rc == 0:
1505
+ import sqlite3
1506
+
1507
+ from cc_session_tools.lib import install_sync
1508
+ try:
1509
+ install_sync.record_synced(__version__)
1510
+ except sqlite3.DatabaseError as exc:
1511
+ # A corrupt sessions.db must not erase the five install steps'
1512
+ # already-successful work (and their printed summary) that ran
1513
+ # just above - print a clear warning and continue to the health
1514
+ # check, which surfaces store corruption of its own accord.
1515
+ # Letting this propagate would also be self-defeating: this is
1516
+ # the exact command the interactive gate (main()) tells a user
1517
+ # to run to fix an out-of-sync install.
1518
+ print(
1519
+ f" warning: could not record the install-everything sync marker "
1520
+ f"({exc}) - sessions.db may be corrupt; run `ccst repair sessions` "
1521
+ "to investigate",
1522
+ file=sys.stderr,
1523
+ )
1524
+
1489
1525
  print(f"\n=== {total_steps}/{total_steps} Health check ===")
1490
1526
  _cmd_doctor(
1491
1527
  argparse.Namespace(
@@ -1674,6 +1710,11 @@ def _build_parser() -> argparse.ArgumentParser:
1674
1710
  action="store_true",
1675
1711
  help="Skip the PyPI version-drift check",
1676
1712
  )
1713
+ doctor_parser.add_argument(
1714
+ "--all",
1715
+ action="store_true",
1716
+ help="Show every check result, including OK (default: only non-OK)",
1717
+ )
1677
1718
  doctor_parser.add_argument(
1678
1719
  "--drift",
1679
1720
  action="store_true",
@@ -2181,6 +2222,37 @@ def main() -> None:
2181
2222
  parser.print_help(sys.stderr)
2182
2223
  sys.exit(1)
2183
2224
 
2225
+ from cc_session_tools.lib import install_sync
2226
+
2227
+ is_interactive = sys.stderr.isatty()
2228
+ # Read the marker only when interactive: `ccst hooks run <verb>` fires on
2229
+ # every single tool call in every open Claude Code session, and ccsched
2230
+ # jobs run on a schedule - neither has a TTY on stderr, and neither
2231
+ # should pay a SQLite open/close on every invocation just to compute a
2232
+ # value should_block_for_unsynced_install would immediately discard
2233
+ # anyway (its own is_interactive=False check returns False before ever
2234
+ # looking at synced_version).
2235
+ synced_version = install_sync.get_synced_version() if is_interactive else None
2236
+ if install_sync.should_block_for_unsynced_install(
2237
+ noun=args.noun,
2238
+ verb=getattr(args, "verb", None),
2239
+ installed_version=__version__,
2240
+ synced_version=synced_version,
2241
+ is_interactive=is_interactive,
2242
+ ):
2243
+ if synced_version is None:
2244
+ state = "install-everything has never been run for this installation"
2245
+ else:
2246
+ state = f"install-everything was last synced at {synced_version}"
2247
+ print(
2248
+ f"ccst is installed at {__version__}, but {state}.\n"
2249
+ "Skills, hooks, shell functions, scheduled jobs, and CLAUDE.md config may be "
2250
+ "out of sync with this version.\n\n"
2251
+ "Run: ccst install-everything --apply\n",
2252
+ file=sys.stderr,
2253
+ )
2254
+ sys.exit(1)
2255
+
2184
2256
  if args.noun == "hooks":
2185
2257
  if args.verb == "install":
2186
2258
  sys.exit(_cmd_hooks_install(args))
@@ -293,6 +293,36 @@ def check_pypi_version(installed_version: str, timeout: float = 3.0) -> CheckRes
293
293
  )
294
294
 
295
295
 
296
+ def check_install_everything_synced(
297
+ installed_version: str, synced_version: str | None
298
+ ) -> CheckResult:
299
+ """WARN (not FAIL - always self-recoverable with one command, same
300
+ severity rationale as check_ccsched_job_registered) if the running ccst
301
+ version doesn't match the version 'install-everything --apply' last
302
+ succeeded for."""
303
+ name = "install:synced"
304
+ if synced_version == installed_version:
305
+ return CheckResult(
306
+ name=name, status=Status.OK,
307
+ reason=f"install-everything last synced at {installed_version}",
308
+ )
309
+ if synced_version is None:
310
+ return CheckResult(
311
+ name=name, status=Status.WARN,
312
+ reason=(
313
+ f"installed {installed_version}, install-everything has never been run — "
314
+ "run `ccst install-everything --apply`"
315
+ ),
316
+ )
317
+ return CheckResult(
318
+ name=name, status=Status.WARN,
319
+ reason=(
320
+ f"installed {installed_version}, install-everything last synced at "
321
+ f"{synced_version} — run `ccst install-everything --apply`"
322
+ ),
323
+ )
324
+
325
+
296
326
  def _version_tuple(v: str) -> tuple[int, ...]:
297
327
  """Parse a simple a.b.c version string into a comparable tuple."""
298
328
  parts: list[int] = []
@@ -454,7 +484,13 @@ def _count_new_store_rows(db_path: Path, tables: tuple[str, ...]) -> int:
454
484
  for table in tables:
455
485
  try:
456
486
  total += int(conn.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0])
457
- except sqlite3.OperationalError:
487
+ except sqlite3.DatabaseError:
488
+ # sqlite3.OperationalError ("no such table") is the common
489
+ # case; a corrupt file also reaches here, not the connect()
490
+ # guard above, because sqlite3.connect() opens lazily and
491
+ # only fails once a statement actually touches the file —
492
+ # both count as "can't be opened" per this function's
493
+ # contract, so both return 0 rather than raising.
458
494
  continue
459
495
  return total
460
496
  finally:
@@ -683,10 +719,24 @@ def check_sessions_project_dir_absolute(sessions_db_path: Path) -> list[CheckRes
683
719
  project_dir.parent against a resolved, absolute root — a relative project_dir can
684
720
  never match, so the row becomes permanently invisible to --global listings without
685
721
  ever raising an error. Not FAIL: it degrades --global listings but does not block
686
- core functionality, and `ccst repair sessions` fixes it non-destructively."""
722
+ core functionality, and `ccst repair sessions` fixes it non-destructively.
723
+
724
+ FAILs instead if sessions.db exists but isn't a valid SQLite file — same
725
+ "exists but failed to open" treatment check_data_stores already gives
726
+ this exact condition for other stores; sqlite3.connect() opens lazily
727
+ and only fails once find_non_absolute_rows actually queries the file, so
728
+ this must be caught here rather than assumed impossible."""
729
+ import sqlite3
730
+
687
731
  from cc_session_tools.lib import sessions_repair
688
732
 
689
- bad = sessions_repair.find_non_absolute_rows(path=sessions_db_path)
733
+ try:
734
+ bad = sessions_repair.find_non_absolute_rows(path=sessions_db_path)
735
+ except sqlite3.DatabaseError as exc:
736
+ return [CheckResult(
737
+ name="sessions:project-dir-absolute", status=Status.FAIL,
738
+ reason=f"{sessions_db_path} exists but failed to open: {exc}",
739
+ )]
690
740
  if not bad:
691
741
  return [CheckResult(
692
742
  name="sessions:project-dir-absolute", status=Status.OK,
@@ -719,6 +769,7 @@ def run_all_checks(
719
769
  projects_root: Path | None = None,
720
770
  pdata_verify_projects: list[str] | None = None,
721
771
  sessions_db_path: Path | None = None,
772
+ synced_version: str | None = None,
722
773
  ) -> list[CheckResult]:
723
774
  """Run the full doctor suite and return results.
724
775
 
@@ -755,6 +806,9 @@ def run_all_checks(
755
806
  sessions_db_path:
756
807
  Path to sessions.db; when None, the project_dir-absolute check is
757
808
  skipped.
809
+ synced_version:
810
+ The version ``install-everything --apply`` last succeeded for, or
811
+ None if it has never been recorded.
758
812
  """
759
813
  results: list[CheckResult] = []
760
814
 
@@ -831,6 +885,11 @@ def run_all_checks(
831
885
  if not skip_pypi:
832
886
  results.append(check_pypi_version(installed_version))
833
887
 
888
+ # Install-everything sync state
889
+ results.append(
890
+ check_install_everything_synced(installed_version, synced_version)
891
+ )
892
+
834
893
  return results
835
894
 
836
895
 
@@ -860,17 +919,30 @@ def _extract_bundle_hook_names(bundle_path: Path) -> list[str]:
860
919
  return names
861
920
 
862
921
 
863
- def format_results(results: list[CheckResult]) -> str:
864
- """Return a human-readable table of check results."""
922
+ def format_results(results: list[CheckResult], *, show_all: bool = False) -> str:
923
+ """Return a human-readable table of check results.
924
+
925
+ By default (show_all=False) only WARN/FAIL results are printed, with a
926
+ hint on how to see the rest — a clean machine's `ccst doctor` output is
927
+ otherwise dozens of [OK] lines a user has to scroll past to find the one
928
+ thing that needs attention. show_all=True reproduces the full table this
929
+ function always printed before this parameter existed.
930
+ """
865
931
  if not results:
866
932
  return "(no checks ran)"
867
- name_w = max(len(r.name) for r in results)
933
+ shown = results if show_all else [r for r in results if r.status is not Status.OK]
868
934
  lines = []
869
- for r in results:
870
- lines.append(f"[{r.status.value:<4}] {r.name:<{name_w}} {r.reason}")
935
+ if shown:
936
+ name_w = max(len(r.name) for r in shown)
937
+ for r in shown:
938
+ lines.append(f"[{r.status.value:<4}] {r.name:<{name_w}} {r.reason}")
939
+ elif not show_all:
940
+ lines.append(f"All {len(results)} checks OK.")
871
941
  has_issues = any(r.status in (Status.WARN, Status.FAIL) for r in results)
872
942
  if has_issues:
873
943
  lines.append("\nTip: run `ccst install-everything --apply` to sync skills, hooks, shell, and CLAUDE.md")
944
+ if not show_all:
945
+ lines.append("\nTo see full doctor output use --all argument")
874
946
  return "\n".join(lines)
875
947
 
876
948
 
@@ -25,7 +25,16 @@ def default_mutes_path() -> Path:
25
25
 
26
26
  def load_mutes(path: Path) -> dict[str, str]:
27
27
  """Return the mute map (check name -> ISO date). Empty if the store has
28
- never been written to."""
28
+ never been written to, or is corrupt.
29
+
30
+ connect(readonly=True) skips DDL by design, so a sessions.db that
31
+ predates the doctor_mutes table reaches the SELECT and must be caught
32
+ there too, not just around connect() itself - and connect() opens
33
+ lazily, so a corrupt (not-a-valid-SQLite-file) db only fails on the
34
+ SELECT as sqlite3.DatabaseError, not on connect() as OperationalError.
35
+ `ccst doctor --list-mutes`/`--drift` must never crash on this - doctor
36
+ is exempt from the install-sync gate specifically so it always works.
37
+ """
29
38
  try:
30
39
  conn = sessions_db.connect(path=path, readonly=True)
31
40
  except sqlite3.OperationalError:
@@ -33,6 +42,8 @@ def load_mutes(path: Path) -> dict[str, str]:
33
42
  try:
34
43
  rows = conn.execute("SELECT name, muted_at FROM doctor_mutes").fetchall()
35
44
  return {r["name"]: r["muted_at"] for r in rows}
45
+ except sqlite3.DatabaseError:
46
+ return {}
36
47
  finally:
37
48
  conn.close()
38
49
 
@@ -0,0 +1,103 @@
1
+ """Tracks the last ccst version for which `ccst install-everything --apply`
2
+ succeeded — lets `main()` nudge an interactive user to re-run it after an
3
+ upgrade, and lets `ccst doctor` report the same fact as a check result.
4
+
5
+ Backed by the install_sync table in sessions.db, following the same
6
+ established pattern as doctor_mutes.py (see its module docstring and
7
+ sessions_db.py's): small persistent CLI state belongs in the shared
8
+ sessions.db file, not a bespoke JSON/db file per subsystem.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import sqlite3
13
+ from pathlib import Path
14
+
15
+ from cc_session_tools.lib import sessions_db
16
+
17
+ _SYNCED_VERSION_KEY = "synced_version"
18
+
19
+
20
+ def get_synced_version(*, path: Path | None = None) -> str | None:
21
+ """Return the version `install-everything --apply` last succeeded for,
22
+ or None if it has never been recorded.
23
+
24
+ Covers three distinct states, all returning None: no sessions.db file at
25
+ all (fresh machine); a sessions.db that predates this table (every
26
+ existing installation upgrading to the version that ships this feature —
27
+ session_tags/sessions/doctor_mutes already exist, but install_sync
28
+ doesn't yet — connect(readonly=True) skips DDL by design, since it must
29
+ not create/migrate a store it's only meant to read, so this case reaches
30
+ the SELECT and must be caught there too, not just around connect()
31
+ itself); and a sessions.db that is corrupt/not a valid SQLite file at
32
+ all, which callers of this function (main()'s interactive gate among
33
+ them) must be able to survive without crashing - a corrupt db is
34
+ reported the same as "never synced" rather than propagating an
35
+ exception. sqlite3.DatabaseError is the shared parent of
36
+ OperationalError ("no such table") and the corrupt-file case, so
37
+ catching it alone covers both without a second except clause.
38
+ """
39
+ try:
40
+ conn = sessions_db.connect(path=path, readonly=True)
41
+ except sqlite3.OperationalError:
42
+ return None
43
+ try:
44
+ row = conn.execute(
45
+ "SELECT value FROM install_sync WHERE key = ?", (_SYNCED_VERSION_KEY,)
46
+ ).fetchone()
47
+ return row["value"] if row is not None else None
48
+ except sqlite3.DatabaseError:
49
+ return None # "no such table: install_sync" (pre-upgrade db) or a corrupt file
50
+ finally:
51
+ conn.close()
52
+
53
+
54
+ _EXEMPT_NOUNS = frozenset({"install-everything", "doctor", "repair", "migrate"})
55
+
56
+
57
+ def should_block_for_unsynced_install(
58
+ *,
59
+ noun: str | None,
60
+ verb: str | None,
61
+ installed_version: str,
62
+ synced_version: str | None,
63
+ is_interactive: bool,
64
+ ) -> bool:
65
+ """True if main() should abort this invocation with an install-everything
66
+ nudge instead of dispatching it.
67
+
68
+ is_interactive must be False for every automated caller (a Claude Code
69
+ hook via `ccst hooks run`, a ccsched job, any future scheduled/scripted
70
+ caller) - this is the primary safety property, checked first. Exempt
71
+ nouns are never blocked regardless of interactivity, so the user always
72
+ has a way to see or fix the state this function is protecting against:
73
+ install-everything (the fix), doctor (the diagnostic tool), repair
74
+ ("Repair known sessions.db/store corruption" - the exact tool needed
75
+ when the sync marker's own store is what's broken), and migrate (this
76
+ repo's own doctor output tells users to run `ccst migrate all` "from a
77
+ plain terminal" when a legacy-data migration is pending - blocking that
78
+ instruction would be self-defeating). `hooks run` is additionally exempt
79
+ by name, belt-and-braces alongside is_interactive always being False for
80
+ it in practice - the one path this function must never block under any
81
+ circumstance.
82
+ """
83
+ if not is_interactive:
84
+ return False
85
+ if noun in _EXEMPT_NOUNS:
86
+ return False
87
+ if noun == "hooks" and verb == "run":
88
+ return False
89
+ return synced_version != installed_version
90
+
91
+
92
+ def record_synced(version: str, *, path: Path | None = None) -> None:
93
+ """Record that `install-everything --apply` just succeeded for `version`."""
94
+ conn = sessions_db.connect(path=path)
95
+ try:
96
+ conn.execute(
97
+ "INSERT INTO install_sync (key, value, updated_at) VALUES (?, ?, ?) "
98
+ "ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at",
99
+ (_SYNCED_VERSION_KEY, version, sessions_db._now_iso()),
100
+ )
101
+ conn.commit()
102
+ finally:
103
+ conn.close()
@@ -57,6 +57,12 @@ CREATE TABLE IF NOT EXISTS doctor_mutes (
57
57
  name TEXT PRIMARY KEY,
58
58
  muted_at TEXT NOT NULL
59
59
  );
60
+
61
+ CREATE TABLE IF NOT EXISTS install_sync (
62
+ key TEXT PRIMARY KEY,
63
+ value TEXT NOT NULL,
64
+ updated_at TEXT NOT NULL
65
+ );
60
66
  """
61
67
 
62
68
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cc-session-tools
3
- Version: 2.3.0
3
+ Version: 2.4.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
@@ -144,6 +144,12 @@ It runs `ccst doctor` at the end so you can see the health check immediately. Re
144
144
 
145
145
  > **Note:** `install-everything.sh` handles steps 1–5 of setup. Step 6 — adding broader CCST guidance to your global `~/.claude/CLAUDE.md` (session management, 8-digit gate, etc.) — is interactive and must be run separately afterwards. See [Configure your global CLAUDE.md](#configure-your-global-claudemd) below.
146
146
 
147
+ > **After upgrading** (`uv tool install --reinstall`/`--upgrade`, or `pip`/`pipx` equivalents):
148
+ > re-run `ccst install-everything --apply` so skills/hooks/shell functions/scheduled jobs stay in
149
+ > sync with the new version. If you forget, `ccst` itself will remind you — any interactive
150
+ > command run from a real terminal on a version that hasn't been synced prints a nudge and exits
151
+ > until you do (automated callers like Claude Code's own hooks are never affected).
152
+
147
153
  **Manual path — step by step:**
148
154
 
149
155
  ```sh
@@ -884,14 +890,19 @@ ccst claude-md uninstall --apply
884
890
 
885
891
  Run a full health check: PATH for all six CLIs, env vars (`REPO_ROOT`/`PROJ_ROOT`),
886
892
  `~/.claude/settings.json` JSON validity, expected hook registrations present,
887
- skill symlinks correct and pointing at the installed source, version drift
888
- between installed `ccst` and the latest release on PyPI.
893
+ skill symlinks correct and pointing at the installed source, whether
894
+ `install-everything --apply` has been run for the currently-installed version,
895
+ and version drift between installed `ccst` and the latest release on PyPI.
889
896
 
890
897
  ```sh
891
898
  ccst doctor # checks everything, including PyPI version check
892
899
  ccst doctor --no-pypi # skip the network version check (useful in CI)
900
+ ccst doctor --all # also print OK checks (default: only WARN/FAIL)
893
901
  ```
894
902
 
903
+ By default only WARN/FAIL results are printed — a clean machine is otherwise dozens of `[OK]`
904
+ lines to scroll past. Use `--all` to see the full check list, including everything that passed.
905
+
895
906
  Exit `0` if all checks are OK. Exit `1` if any check is WARN or FAIL.
896
907
 
897
908
  ### `ccst telemetry trim`
@@ -30,6 +30,7 @@ src/cc_session_tools/lib/debug.py
30
30
  src/cc_session_tools/lib/doctor.py
31
31
  src/cc_session_tools/lib/doctor_mutes.py
32
32
  src/cc_session_tools/lib/hook_registry.py
33
+ src/cc_session_tools/lib/install_sync.py
33
34
  src/cc_session_tools/lib/levenshtein.py
34
35
  src/cc_session_tools/lib/paths.py
35
36
  src/cc_session_tools/lib/picker.py
@@ -173,6 +174,7 @@ tests/test_ccst_hooks_install_prune.py
173
174
  tests/test_ccst_hooks_install_zero_arg.py
174
175
  tests/test_ccst_hooks_uninstall.py
175
176
  tests/test_ccst_install_everything.py
177
+ tests/test_ccst_main_gate.py
176
178
  tests/test_ccst_pdata_cli.py
177
179
  tests/test_ccst_pdata_init_cli.py
178
180
  tests/test_ccst_pdata_reconcile_cli.py
@@ -200,6 +202,7 @@ tests/test_doctor_mutes.py
200
202
  tests/test_empty_session.py
201
203
  tests/test_hook_cache.py
202
204
  tests/test_hooks_install.py
205
+ tests/test_install_sync.py
203
206
  tests/test_last_screenshot.py
204
207
  tests/test_levenshtein.py
205
208
  tests/test_marker_allow.py