cc-session-tools 3.7.0__tar.gz → 3.8.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 (295) hide show
  1. {cc_session_tools-3.7.0/src/cc_session_tools.egg-info → cc_session_tools-3.8.0}/PKG-INFO +20 -1
  2. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/README.md +19 -0
  3. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/pyproject.toml +1 -1
  4. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/cli/ccst.py +75 -17
  5. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/doctor.py +11 -6
  6. cc_session_tools-3.8.0/src/cc_session_tools/lib/pdata/readiness.py +319 -0
  7. cc_session_tools-3.8.0/src/cc_session_tools/lib/text_wrap.py +35 -0
  8. cc_session_tools-3.8.0/src/cc_session_tools/skills/pm-pdata-do-audit-and-prepare-to-migrate/SKILL.md +162 -0
  9. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0/src/cc_session_tools.egg-info}/PKG-INFO +20 -1
  10. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools.egg-info/SOURCES.txt +6 -0
  11. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_doctor.py +23 -0
  12. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_hooks_install_zero_arg.py +47 -11
  13. cc_session_tools-3.8.0/tests/test_ccst_pdata_readiness_cli.py +95 -0
  14. cc_session_tools-3.8.0/tests/test_hooks_install_records.py +104 -0
  15. cc_session_tools-3.8.0/tests/test_pdata_audit_skill.py +89 -0
  16. cc_session_tools-3.8.0/tests/test_text_wrap.py +70 -0
  17. cc_session_tools-3.7.0/src/cc_session_tools/skills/pm-pdata-do-audit-and-prepare-to-migrate/SKILL.md +0 -199
  18. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/LICENSE +0 -0
  19. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/setup.cfg +0 -0
  20. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/__init__.py +0 -0
  21. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/cli/__init__.py +0 -0
  22. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/cli/ccd.py +0 -0
  23. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/cli/ccmsg.py +0 -0
  24. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/cli/ccr.py +0 -0
  25. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/cli/ccs.py +0 -0
  26. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/cli/ccsched.py +0 -0
  27. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/cli/migrate_ccmsg.py +0 -0
  28. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/cli/migrate_ccsched.py +0 -0
  29. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/cli/migrate_sessions_db.py +0 -0
  30. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/cli/migrate_telemetry.py +0 -0
  31. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/config/hooks-bundle.json +0 -0
  32. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/config/session-tag-hook.json +0 -0
  33. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/hooks_install.py +0 -0
  34. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/__init__.py +0 -0
  35. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/bun_tools.py +0 -0
  36. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/claude_flags.py +0 -0
  37. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/claude_md_install.py +0 -0
  38. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/context_overrides.py +0 -0
  39. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/db.py +0 -0
  40. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/debug.py +0 -0
  41. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/doctor_mutes.py +0 -0
  42. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/hook_registry.py +0 -0
  43. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/install_sync.py +0 -0
  44. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/levenshtein.py +0 -0
  45. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/machine_identity.py +0 -0
  46. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/messaging/__init__.py +0 -0
  47. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/messaging/addressing.py +0 -0
  48. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/messaging/cursor.py +0 -0
  49. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/messaging/lock.py +0 -0
  50. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/messaging/message.py +0 -0
  51. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/messaging/move_safety.py +0 -0
  52. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/messaging/repository.py +0 -0
  53. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/messaging/retention.py +0 -0
  54. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/messaging/service.py +0 -0
  55. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/messaging/store.py +0 -0
  56. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/occupancy.py +0 -0
  57. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/paths.py +0 -0
  58. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/__init__.py +0 -0
  59. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/backup.py +0 -0
  60. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/classify.py +0 -0
  61. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/cutover.py +0 -0
  62. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/dump.py +0 -0
  63. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/formatting.py +0 -0
  64. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/importers.py +0 -0
  65. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/init_paths.py +0 -0
  66. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/init_service.py +0 -0
  67. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/manifest.py +0 -0
  68. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/naming.py +0 -0
  69. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/rehydrate.py +0 -0
  70. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/rename_group.py +0 -0
  71. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/reorganize.py +0 -0
  72. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/repository.py +0 -0
  73. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/resolve.py +0 -0
  74. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/service.py +0 -0
  75. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/session_output.py +0 -0
  76. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/store.py +0 -0
  77. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/sync_check.py +0 -0
  78. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/sync_lock.py +0 -0
  79. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/sync_notify.py +0 -0
  80. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/vector_clock.py +0 -0
  81. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/vector_clock_store.py +0 -0
  82. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/verify.py +0 -0
  83. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/write_log.py +0 -0
  84. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/picker.py +0 -0
  85. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/proc_lock.py +0 -0
  86. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/prompts.py +0 -0
  87. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/roots.py +0 -0
  88. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/rules.py +0 -0
  89. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/scheduler/__init__.py +0 -0
  90. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/scheduler/bundled_jobs.py +0 -0
  91. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/scheduler/cadence.py +0 -0
  92. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/scheduler/cursor.py +0 -0
  93. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/scheduler/digest.py +0 -0
  94. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/scheduler/due.py +0 -0
  95. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/scheduler/duration.py +0 -0
  96. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/scheduler/jobspec.py +0 -0
  97. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/scheduler/ledger.py +0 -0
  98. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/scheduler/lock.py +0 -0
  99. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/scheduler/notify.py +0 -0
  100. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/scheduler/reconcile.py +0 -0
  101. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/scheduler/registry.py +0 -0
  102. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/scheduler/runner.py +0 -0
  103. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/scheduler/state.py +0 -0
  104. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/scheduler/store.py +0 -0
  105. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/scheduler/surface.py +0 -0
  106. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/scheduler/throttle.py +0 -0
  107. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/scheduler/worker.py +0 -0
  108. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/session_gc.py +0 -0
  109. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/sessions.py +0 -0
  110. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/sessions_db.py +0 -0
  111. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/sessions_repair.py +0 -0
  112. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/shell_install.py +0 -0
  113. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/tasklist.py +0 -0
  114. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/telemetry_store.py +0 -0
  115. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/prompts/pdata-migration-claude-md-update.md +0 -0
  116. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/prompts/pdata-migration-skills-update.md +0 -0
  117. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/analyse-cc-usage/SKILL.md +0 -0
  118. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/clean-hook-sessions/SKILL.md +0 -0
  119. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/clean-hook-sessions/scripts/clean-hook-sessions.py +0 -0
  120. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/compact-smart/SKILL.md +0 -0
  121. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/context-override/SKILL.md +0 -0
  122. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/delete-sessions/SKILL.md +0 -0
  123. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/delete-sessions/scripts/delete_sessions.py +0 -0
  124. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/do-executor-critic-assessor-loop/SKILL.md +0 -0
  125. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/extract-pptx/SKILL.md +0 -0
  126. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/extract-pptx/scripts/extract_pptx.py +0 -0
  127. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/extract-to-text/SKILL.md +0 -0
  128. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/extract-to-text/scripts/extract_to_text.py +0 -0
  129. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/find-claude-code-session/SKILL.md +0 -0
  130. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/generate-8digit-code/SKILL.md +0 -0
  131. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/generate-8digit-code/scripts/generate_8digit_code.py +0 -0
  132. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/list-empty-sessions/SKILL.md +0 -0
  133. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/list-empty-sessions/scripts/list_empty_sessions.py +0 -0
  134. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/manage-recurring-cc-jobs-using-ccsched/SKILL.md +0 -0
  135. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/move-session/SKILL.md +0 -0
  136. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/move-session/hooks/sessionstart-pending-rename.sh +0 -0
  137. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/move-session/scripts/cc_session_rules.py +0 -0
  138. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/move-session/scripts/move_session.py +0 -0
  139. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/pm-pdata-design-schema/SKILL.md +0 -0
  140. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/pm-pdata-do-init/SKILL.md +0 -0
  141. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/pm-pdata-do-manual-migration/SKILL.md +0 -0
  142. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/pm-pdata-resolve-conflicts/SKILL.md +0 -0
  143. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/pm-project-layout-reference/SKILL.md +0 -0
  144. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/pm-update-central-files/SKILL.md +0 -0
  145. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/reduce-persistent-context/CALIBRATION.md +0 -0
  146. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/reduce-persistent-context/SKILL.md +0 -0
  147. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/reduce-persistent-context/scripts/analyze_context.py +0 -0
  148. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/reduce-persistent-context/scripts/duplicates.py +0 -0
  149. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/reduce-persistent-context/scripts/measure.py +0 -0
  150. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/reduce-persistent-context/scripts/report.py +0 -0
  151. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/reduce-persistent-context/scripts/tokens.py +0 -0
  152. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/reduce-persistent-context/scripts/usage.py +0 -0
  153. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/select-agent-model/SKILL.md +0 -0
  154. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/send-session-message/SKILL.md +0 -0
  155. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/update-command-cache/SKILL.md +0 -0
  156. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/update-command-cache/scripts/update_command_cache.py +0 -0
  157. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools.egg-info/dependency_links.txt +0 -0
  158. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools.egg-info/entry_points.txt +0 -0
  159. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools.egg-info/requires.txt +0 -0
  160. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools.egg-info/top_level.txt +0 -0
  161. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/claude_code_usage/__init__.py +0 -0
  162. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/claude_code_usage/attribution.py +0 -0
  163. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/claude_code_usage/cache.py +0 -0
  164. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/claude_code_usage/ccusage_wrapper.py +0 -0
  165. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/claude_code_usage/cli.py +0 -0
  166. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/claude_code_usage/data/pricing.json +0 -0
  167. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/claude_code_usage/parent_inference.py +0 -0
  168. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/claude_code_usage/parser.py +0 -0
  169. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/claude_code_usage/pricing.py +0 -0
  170. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/claude_code_usage/query.py +0 -0
  171. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/claude_code_usage/report.py +0 -0
  172. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/claude_code_usage/schema.py +0 -0
  173. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/claude_code_usage/session_names.py +0 -0
  174. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/__init__.py +0 -0
  175. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/after_response.py +0 -0
  176. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/bash_hard_deny.py +0 -0
  177. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/bash_security_review.py +0 -0
  178. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/cache.py +0 -0
  179. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/catchup.py +0 -0
  180. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/confirm_8digit.py +0 -0
  181. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/context_window_warning.py +0 -0
  182. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/last_screenshot.py +0 -0
  183. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/marker_allow.py +0 -0
  184. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/markers.py +0 -0
  185. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/messaging_deliver.py +0 -0
  186. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/model_info.py +0 -0
  187. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/normalise.py +0 -0
  188. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/pdata_sync.py +0 -0
  189. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/pending_migration.py +0 -0
  190. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/pending_rename.py +0 -0
  191. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/script_allowlist.py +0 -0
  192. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/session_tag.py +0 -0
  193. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/stats.py +0 -0
  194. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/telemetry.py +0 -0
  195. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/telemetry_query.py +0 -0
  196. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/telemetry_trim.py +0 -0
  197. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/transcript.py +0 -0
  198. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/worklog_guard.py +0 -0
  199. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_after_response.py +0 -0
  200. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_attribution.py +0 -0
  201. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_bash_hard_deny.py +0 -0
  202. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_bash_security_review.py +0 -0
  203. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_bash_security_review_allowlist.py +0 -0
  204. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_bun_tools.py +0 -0
  205. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_cache.py +0 -0
  206. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_cache_sqlite.py +0 -0
  207. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccr_orphans.py +0 -0
  208. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccs_emptiness.py +0 -0
  209. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccs_eta.py +0 -0
  210. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccs_list_mode.py +0 -0
  211. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccs_scope_flags.py +0 -0
  212. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccs_sentinel_sort.py +0 -0
  213. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccs_session_counts.py +0 -0
  214. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_bundle_discovery.py +0 -0
  215. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_ccsched_jobs_cli.py +0 -0
  216. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_cli.py +0 -0
  217. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_context_override_cli.py +0 -0
  218. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_gc_prune.py +0 -0
  219. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_gc_report.py +0 -0
  220. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_hook_dispatcher.py +0 -0
  221. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_hooks_allowlist.py +0 -0
  222. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_hooks_install_prune.py +0 -0
  223. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_hooks_uninstall.py +0 -0
  224. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_install_everything.py +0 -0
  225. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_machine_identity_cli.py +0 -0
  226. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_main_gate.py +0 -0
  227. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_pdata_cli.py +0 -0
  228. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_pdata_init_cli.py +0 -0
  229. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_pdata_reconcile_cli.py +0 -0
  230. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_pdata_rename_group_cli.py +0 -0
  231. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_pdata_reorganize_cli.py +0 -0
  232. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_pdata_sync_cli.py +0 -0
  233. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_pdata_verify_cli.py +0 -0
  234. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_repair_sessions_cli.py +0 -0
  235. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_sessions_cli.py +0 -0
  236. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_shell_install.py +0 -0
  237. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_skills_install.py +0 -0
  238. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_skills_uninstall.py +0 -0
  239. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_telemetry_query.py +0 -0
  240. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_telemetry_trim.py +0 -0
  241. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccusage_wrapper.py +0 -0
  242. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_claude_flags.py +0 -0
  243. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_claude_md_install.py +0 -0
  244. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_cli.py +0 -0
  245. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_cli_ccd.py +0 -0
  246. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_cli_ccd_dry_run.py +0 -0
  247. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_cli_ccr.py +0 -0
  248. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_cli_ccs.py +0 -0
  249. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_cli_version.py +0 -0
  250. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_confirm_8digit.py +0 -0
  251. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_context_overrides.py +0 -0
  252. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_context_window_warning.py +0 -0
  253. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_db.py +0 -0
  254. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_debug.py +0 -0
  255. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_doctor_mutes.py +0 -0
  256. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_empty_session.py +0 -0
  257. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_hook_cache.py +0 -0
  258. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_hooks_install.py +0 -0
  259. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_install_sync.py +0 -0
  260. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_last_screenshot.py +0 -0
  261. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_levenshtein.py +0 -0
  262. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_machine_identity.py +0 -0
  263. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_marker_allow.py +0 -0
  264. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_migrate_ccmsg.py +0 -0
  265. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_migrate_ccsched.py +0 -0
  266. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_migrate_sessions_db.py +0 -0
  267. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_migrate_telemetry.py +0 -0
  268. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_model_info.py +0 -0
  269. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_normalise.py +0 -0
  270. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_occupancy.py +0 -0
  271. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_parent_inference.py +0 -0
  272. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_parser.py +0 -0
  273. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_paths.py +0 -0
  274. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_pending_migration_hook.py +0 -0
  275. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_picker.py +0 -0
  276. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_pricing.py +0 -0
  277. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_proc_lock.py +0 -0
  278. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_prompts.py +0 -0
  279. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_query.py +0 -0
  280. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_report.py +0 -0
  281. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_roots.py +0 -0
  282. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_rules.py +0 -0
  283. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_scheduler_bundled_jobs.py +0 -0
  284. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_schema.py +0 -0
  285. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_script_allowlist.py +0 -0
  286. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_session_names.py +0 -0
  287. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_session_tag.py +0 -0
  288. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_sessions.py +0 -0
  289. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_sessions_db.py +0 -0
  290. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_sessions_repair.py +0 -0
  291. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_tasklist.py +0 -0
  292. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_telemetry.py +0 -0
  293. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_telemetry_store.py +0 -0
  294. {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_transcript.py +0 -0
  295. {cc_session_tools-3.7.0 → cc_session_tools-3.8.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: 3.7.0
3
+ Version: 3.8.0
4
4
  Summary: Claude Code session management CLI tools (ccd, ccr, ccs, claude-code-usage, ccst) and hook library (hooks package)
5
5
  Author: raffishquartan
6
6
  License: MIT License
@@ -928,6 +928,25 @@ ccst context-override off # re-enable the warnings
928
928
  ccst context-override status # report the current state
929
929
  ```
930
930
 
931
+ ### `ccst pdata readiness-scan`
932
+
933
+ A read-only scan of a project's CSV files for the mechanical blockers to a `ccst pdata init`
934
+ migration, used by the `pm-pdata-do-audit-and-prepare-to-migrate` skill and useful on its own:
935
+
936
+ ```sh
937
+ ccst pdata readiness-scan --project myproject # compact Markdown report
938
+ ccst pdata readiness-scan --project myproject --format json # machine-readable
939
+ ccst pdata readiness-scan --project myproject --path notes/ --findings-only # one folder's findings
940
+ ```
941
+
942
+ It reports ragged rows, empty/duplicate headers, mixed date formats, mixed `|`/`;` separators,
943
+ machine-specific paths, null-like placeholder strings, duplicate rows, comment rows, repeated
944
+ header rows, byte-order marks, CRLF line endings and unreadable files, plus a per-file inventory
945
+ and the columns that are unique on every row. Findings carry counts and row numbers, never cell
946
+ text, and a unique column is a fact rather than a recommended key. It exits 0 whenever the scan
947
+ ran (findings are not failures), and 2 when the project directory does not exist. It scans exactly
948
+ the CSVs `ccst pdata init` would classify.
949
+
931
950
  ### `ccst skills install`
932
951
 
933
952
  > You normally don't need this: `ccst install-everything --apply` runs it, and `ccst` runs that
@@ -871,6 +871,25 @@ ccst context-override off # re-enable the warnings
871
871
  ccst context-override status # report the current state
872
872
  ```
873
873
 
874
+ ### `ccst pdata readiness-scan`
875
+
876
+ A read-only scan of a project's CSV files for the mechanical blockers to a `ccst pdata init`
877
+ migration, used by the `pm-pdata-do-audit-and-prepare-to-migrate` skill and useful on its own:
878
+
879
+ ```sh
880
+ ccst pdata readiness-scan --project myproject # compact Markdown report
881
+ ccst pdata readiness-scan --project myproject --format json # machine-readable
882
+ ccst pdata readiness-scan --project myproject --path notes/ --findings-only # one folder's findings
883
+ ```
884
+
885
+ It reports ragged rows, empty/duplicate headers, mixed date formats, mixed `|`/`;` separators,
886
+ machine-specific paths, null-like placeholder strings, duplicate rows, comment rows, repeated
887
+ header rows, byte-order marks, CRLF line endings and unreadable files, plus a per-file inventory
888
+ and the columns that are unique on every row. Findings carry counts and row numbers, never cell
889
+ text, and a unique column is a fact rather than a recommended key. It exits 0 whenever the scan
890
+ ran (findings are not failures), and 2 when the project directory does not exist. It scans exactly
891
+ the CSVs `ccst pdata init` would classify.
892
+
874
893
  ### `ccst skills install`
875
894
 
876
895
  > You normally don't need this: `ccst install-everything --apply` runs it, and `ccst` runs that
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cc-session-tools"
7
- version = "3.7.0"
7
+ version = "3.8.0"
8
8
  description = "Claude Code session management CLI tools (ccd, ccr, ccs, claude-code-usage, ccst) and hook library (hooks package)"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -112,6 +112,7 @@ from cc_session_tools.hooks_install import (
112
112
  )
113
113
  from cc_session_tools.lib import machine_identity
114
114
  from cc_session_tools.lib.hook_registry import HOOK_DESCRIPTIONS, HOOK_VERBS
115
+ from cc_session_tools.lib.text_wrap import wrap_block
115
116
 
116
117
  if TYPE_CHECKING:
117
118
  # Type-only, so the handler's own lazy `from ... import sync_check` (this file's convention:
@@ -538,30 +539,45 @@ def _bundle_inventory(bundle: dict[str, Any]) -> list[tuple[str, str, str | None
538
539
  return out
539
540
 
540
541
 
541
- def _print_hooks_install_table(
542
+ def _format_hooks_install_records(
542
543
  inventory: list[tuple[str, str, str | None, str]],
543
544
  added_keys: set[tuple[str, str | None, str]],
544
- ) -> None:
545
- """Print a Hook | Status | Event | Description table to stdout."""
546
- headers = ("Hook", "Status", "Event", "Description")
547
- if not inventory:
548
- return
545
+ ) -> str:
546
+ """One record per hook: a `name status` header, an indented `Events:` line, then the
547
+ indented, terminal-width-wrapped description. Records are separated by a blank line.
549
548
 
550
- rows: list[tuple[str, str, str, str]] = []
549
+ A hook registered for several events is one record; when only some of its registrations
550
+ would be added, those events carry a `(new)` marker. Order follows the inventory (sorted by
551
+ hook name).
552
+ """
553
+ by_hook: dict[str, list[tuple[bool, str]]] = {}
551
554
  for name, event, matcher, cmd in inventory:
552
- status = "install" if (event, matcher, cmd) in added_keys else "already-installed"
553
555
  event_label = f"{event}[{matcher}]" if matcher else event
556
+ by_hook.setdefault(name, []).append(((event, matcher, cmd) in added_keys, event_label))
557
+
558
+ records: list[str] = []
559
+ for name, registrations in by_hook.items():
560
+ any_new = any(is_new for is_new, _ in registrations)
561
+ mixed = any_new and not all(is_new for is_new, _ in registrations)
562
+ status = "install" if any_new else "already-installed"
563
+ events = ", ".join(
564
+ f"{label} (new)" if mixed and is_new else label for is_new, label in registrations
565
+ )
566
+ lines = [f"{name} {status}", wrap_block(f"Events: {events}")]
554
567
  description = HOOK_DESCRIPTIONS.get(name, "")
555
- rows.append((name, status, event_label, description))
568
+ if description:
569
+ lines.append(wrap_block(description))
570
+ records.append("\n".join(lines))
571
+ return "\n\n".join(records)
556
572
 
557
- widths = [
558
- max([len(headers[i])] + [len(r[i]) for r in rows]) for i in range(4)
559
- ]
560
- fmt = f"{{:<{widths[0]}}} {{:<{widths[1]}}} {{:<{widths[2]}}} {{:<{widths[3]}}}"
561
- print(fmt.format(*headers))
562
- print(fmt.format(*("-" * w for w in widths)))
563
- for row in rows:
564
- print(fmt.format(*row))
573
+
574
+ def _print_hooks_install_table(
575
+ inventory: list[tuple[str, str, str | None, str]],
576
+ added_keys: set[tuple[str, str | None, str]],
577
+ ) -> None:
578
+ """Print the per-hook install records to stdout."""
579
+ if inventory:
580
+ print(_format_hooks_install_records(inventory, added_keys))
565
581
 
566
582
 
567
583
  def _list_bundle_hook_names(bundle: dict[str, Any]) -> list[str]:
@@ -1842,6 +1858,28 @@ def _cmd_pdata_resolve(args: argparse.Namespace) -> int:
1842
1858
  return 1 if outstanding else 0
1843
1859
 
1844
1860
 
1861
+ def _cmd_pdata_readiness_scan(args: argparse.Namespace) -> int:
1862
+ from cc_session_tools.lib.pdata import readiness, store
1863
+
1864
+ try:
1865
+ root = store.project_root(args.project)
1866
+ except ValueError as exc:
1867
+ print(f"ccst pdata readiness-scan: {exc}", file=sys.stderr)
1868
+ return 2
1869
+ if not root.is_dir():
1870
+ print(
1871
+ f"ccst pdata readiness-scan: project directory does not exist: {root}",
1872
+ file=sys.stderr,
1873
+ )
1874
+ return 2
1875
+ report = readiness.scan_project(root, path_prefix=args.path)
1876
+ if args.format == "json":
1877
+ print(readiness.format_json(report))
1878
+ else:
1879
+ print(readiness.format_markdown(report, findings_only=args.findings_only))
1880
+ return 0
1881
+
1882
+
1845
1883
  def _cmd_pdata_sync_check(args: argparse.Namespace) -> int:
1846
1884
  """The spec's "Hourly `ccsched` job" trigger: per project, rehydrate-check and - only if no
1847
1885
  rehydrate happened - dump-check. The whole decision lives in `pdata.sync_check` (shared with
@@ -3240,6 +3278,24 @@ def _build_parser() -> argparse.ArgumentParser:
3240
3278
  "the default one-line summary (--project always prints full detail)",
3241
3279
  )
3242
3280
 
3281
+ pdata_readiness_parser = pdata_sub.add_parser(
3282
+ "readiness-scan",
3283
+ help="Scan a project's CSVs for pdata-migration blockers (read-only)",
3284
+ )
3285
+ pdata_readiness_parser.add_argument("--project", metavar="NAME", required=True)
3286
+ pdata_readiness_parser.add_argument(
3287
+ "--format", choices=("markdown", "json"), default="markdown",
3288
+ help="Output format (default: markdown)",
3289
+ )
3290
+ pdata_readiness_parser.add_argument(
3291
+ "--path", metavar="PREFIX", default=None,
3292
+ help="Only scan CSVs whose project-relative path starts with PREFIX",
3293
+ )
3294
+ pdata_readiness_parser.add_argument(
3295
+ "--findings-only", action="store_true",
3296
+ help="Omit the per-file inventory from the Markdown report",
3297
+ )
3298
+
3243
3299
  pdata_dump_parser = pdata_sub.add_parser(
3244
3300
  "dump", help="Publish the current local DB state to .pdata-db-dump/latest.sql"
3245
3301
  )
@@ -3643,6 +3699,8 @@ def main() -> None:
3643
3699
  sys.exit(_cmd_pdata_reconcile_session_output(args))
3644
3700
  if args.verb == "verify":
3645
3701
  sys.exit(_cmd_pdata_verify(args))
3702
+ if args.verb == "readiness-scan":
3703
+ sys.exit(_cmd_pdata_readiness_scan(args))
3646
3704
  if args.verb == "dump":
3647
3705
  sys.exit(_cmd_pdata_dump(args))
3648
3706
  if args.verb == "rehydrate":
@@ -14,7 +14,6 @@ import os
14
14
  import shutil
15
15
  import sqlite3
16
16
  import subprocess
17
- import textwrap
18
17
  import time
19
18
  from dataclasses import dataclass
20
19
  from enum import Enum
@@ -29,6 +28,7 @@ from cc_session_tools.lib.pdata.init_paths import (
29
28
  MIGRATED_ARCHIVE_DIRNAME,
30
29
  MIGRATED_MANIFEST_FILENAME,
31
30
  )
31
+ from cc_session_tools.lib.text_wrap import wrap_block
32
32
 
33
33
  if TYPE_CHECKING:
34
34
  from cc_session_tools.lib.install_sync import FailedAttempt
@@ -1224,12 +1224,11 @@ def _wrap_reason(reason: str, *, indent: str = " ") -> str:
1224
1224
  embedded paths and remediation commands); printed on a single line they
1225
1225
  hard-wrap at the terminal edge with no continuation indent, making a
1226
1226
  wrapped reason indistinguishable from a new, unrelated result. Wrapping
1227
- to the real terminal width — falling back to 80 columns for non-tty
1227
+ to the real terminal width - falling back to 80 columns for non-tty
1228
1228
  output (piped, redirected, or the scheduled drift-monitor job whose
1229
- stdout is forwarded to Telegram) — and indenting every line fixes that.
1229
+ stdout is forwarded to Telegram) - and indenting every line fixes that.
1230
1230
  """
1231
- width = shutil.get_terminal_size(fallback=(80, 24)).columns
1232
- return textwrap.fill(reason, width=width, initial_indent=indent, subsequent_indent=indent)
1231
+ return wrap_block(reason, indent=indent)
1233
1232
 
1234
1233
 
1235
1234
  def format_results(results: list[CheckResult], *, show_all: bool = False) -> str:
@@ -1265,7 +1264,13 @@ def format_results(results: list[CheckResult], *, show_all: bool = False) -> str
1265
1264
  lines.append(f"All {len(results)} checks OK.")
1266
1265
  has_issues = any(r.status in (Status.WARN, Status.FAIL) for r in results)
1267
1266
  if has_issues:
1268
- lines.append("\nTip: run `ccst install-everything --apply` to sync skills, hooks, shell, and CLAUDE.md")
1267
+ lines.append("")
1268
+ lines.append(
1269
+ wrap_block(
1270
+ "Tip: run `ccst install-everything --apply` to sync skills, hooks, shell, and CLAUDE.md",
1271
+ indent="",
1272
+ )
1273
+ )
1269
1274
  if not show_all:
1270
1275
  lines.append("\nTo see full doctor output use --all argument")
1271
1276
  return "\n".join(lines)
@@ -0,0 +1,319 @@
1
+ """Deterministic pdata-readiness scan of a project's CSV files (`ccst pdata readiness-scan`).
2
+
3
+ Reports the mechanical blockers a migration audit would otherwise find by reading: ragged rows,
4
+ mixed date formats, mixed separators, machine-specific paths, fake nulls, duplicates, BOM/CRLF and
5
+ unreadable files, plus a per-file inventory and unique-column facts. Read-only: the scan never
6
+ writes. Findings carry counts and 1-based data-record numbers (header excluded), never cell text;
7
+ `unreadable` reasons are a fixed vocabulary, so file content cannot reach a report.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import csv
12
+ import io
13
+ import json
14
+ import os
15
+ import re
16
+ from dataclasses import dataclass
17
+ from datetime import datetime
18
+ from pathlib import Path
19
+
20
+ from cc_session_tools.lib.pdata.init_paths import EXCLUDED_DIR_NAMES
21
+
22
+ FINDING_KINDS: tuple[str, ...] = (
23
+ "ragged-rows", "machine-paths", "null-strings", "duplicate-rows", "comment-rows",
24
+ "repeated-header", "mixed-date-formats", "mixed-separators", "bad-header",
25
+ "no-unique-column", "unreadable", "bom", "crlf",
26
+ )
27
+
28
+ MAX_EXAMPLE_ROWS = 3
29
+ FREE_TEXT_MEAN_LENGTH = 64
30
+ NULL_STRINGS = frozenset({"NO DATA", "N/A", "NA", "NULL", "NONE", "TBD", "UNKNOWN", "-", "--", "?"})
31
+ _MACHINE_PATH = re.compile(r"/Users/[^/\s]+|/home/[^/\s]+|/mnt/[a-z]/|[A-Za-z]:\\Users\\")
32
+ _DATE_FORMATS: tuple[tuple[str, re.Pattern[str]], ...] = (
33
+ ("iso-datetime", re.compile(r"^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}")),
34
+ ("iso-date", re.compile(r"^\d{4}-\d{2}-\d{2}$")),
35
+ ("slash", re.compile(r"^\d{1,2}/\d{1,2}/\d{2,4}$")),
36
+ ("d-month-yyyy", re.compile(r"^\d{1,2} [A-Za-z]{3,9} \d{4}$")),
37
+ ("month-d-yyyy", re.compile(r"^[A-Za-z]{3,9} \d{1,2},? \d{4}$")),
38
+ )
39
+ _BOM = b"\xef\xbb\xbf"
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class Finding:
44
+ kind: str
45
+ file: str
46
+ column: str | None = None
47
+ count: int | None = None
48
+ rows: tuple[int, ...] = ()
49
+ detail: tuple[tuple[str, int | str], ...] = ()
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class UniqueColumn:
54
+ name: str
55
+ labels: tuple[str, ...]
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class FileReport:
60
+ path: str
61
+ rows: int
62
+ columns: int
63
+ header: tuple[str, ...]
64
+ unique_columns: tuple[UniqueColumn, ...]
65
+
66
+
67
+ @dataclass(frozen=True)
68
+ class ReadinessReport:
69
+ files: tuple[FileReport, ...]
70
+ findings: tuple[Finding, ...]
71
+ not_scanned: dict[str, int]
72
+
73
+
74
+ class _Unreadable(Exception):
75
+ def __init__(self, reason: str) -> None:
76
+ super().__init__(reason)
77
+ self.reason = reason
78
+
79
+
80
+ def _is_real_yyyymmdd(value: str) -> bool:
81
+ if not re.fullmatch(r"\d{8}", value):
82
+ return False
83
+ try:
84
+ parsed = datetime.strptime(value, "%Y%m%d")
85
+ except ValueError:
86
+ return False
87
+ return 1900 <= parsed.year <= 2100
88
+
89
+
90
+ def _date_format(value: str) -> str | None:
91
+ for name, pattern in _DATE_FORMATS:
92
+ if pattern.match(value):
93
+ return name
94
+ return "yyyymmdd" if _is_real_yyyymmdd(value) else None
95
+
96
+
97
+ def _walk(root: Path) -> list[Path]:
98
+ """Every file under root outside EXCLUDED_DIR_NAMES, without following directory symlinks -
99
+ the same exclusion set `ccst pdata init`'s classifier uses, so coverage matches migration."""
100
+ found: list[Path] = []
101
+ for dirpath, dirnames, filenames in os.walk(root, followlinks=False):
102
+ dirnames[:] = sorted(d for d in dirnames if d not in EXCLUDED_DIR_NAMES)
103
+ found.extend(Path(dirpath) / name for name in sorted(filenames))
104
+ return found
105
+
106
+
107
+ def _parse(raw: bytes) -> tuple[list[list[str]], bool, bool]:
108
+ if not raw:
109
+ raise _Unreadable("empty-file")
110
+ try:
111
+ text = raw.decode("utf-8-sig")
112
+ except UnicodeDecodeError:
113
+ raise _Unreadable("not-utf8") from None
114
+ if not text.strip():
115
+ raise _Unreadable("empty-file")
116
+ try:
117
+ records = list(csv.reader(io.StringIO(text, newline=""), strict=True))
118
+ except csv.Error as exc:
119
+ raise _Unreadable(
120
+ "field-too-large" if "field larger" in str(exc) else "csv-parse-error"
121
+ ) from None
122
+ return records, raw.startswith(_BOM), b"\r\n" in raw
123
+
124
+
125
+ def _scan_file(rel: str, raw: bytes) -> tuple[FileReport | None, list[Finding]]:
126
+ try:
127
+ records, has_bom, has_crlf = _parse(raw)
128
+ except _Unreadable as exc:
129
+ return None, [Finding("unreadable", rel, detail=(("reason", exc.reason),))]
130
+
131
+ findings: list[Finding] = []
132
+ if has_bom:
133
+ findings.append(Finding("bom", rel))
134
+ if has_crlf:
135
+ findings.append(Finding("crlf", rel))
136
+
137
+ header = records[0]
138
+ ncols = len(header)
139
+ if any(not h.strip() for h in header) or len(set(header)) != ncols:
140
+ findings.append(Finding("bad-header", rel))
141
+
142
+ data = [r for r in records[1:] if r]
143
+ nonempty = [0] * ncols
144
+ lengths = [0] * ncols
145
+ distinct: list[set[str]] = [set() for _ in range(ncols)]
146
+ all_int = [True] * ncols
147
+ dates: list[dict[str, int]] = [{} for _ in range(ncols)]
148
+ seps: list[dict[str, int]] = [{} for _ in range(ncols)]
149
+ nulls: list[dict[str, int]] = [{} for _ in range(ncols)]
150
+ null_rows: list[list[int]] = [[] for _ in range(ncols)]
151
+ path_rows: list[list[int]] = [[] for _ in range(ncols)]
152
+ ragged: list[int] = []
153
+ dup_rows: list[int] = []
154
+ comment_rows: list[int] = []
155
+ header_rows: list[int] = []
156
+ seen_rows: set[tuple[str, ...]] = set()
157
+
158
+ for number, row in enumerate(data, start=1):
159
+ if row == header:
160
+ header_rows.append(number)
161
+ if row[0].startswith("#"):
162
+ comment_rows.append(number)
163
+ if len(row) != ncols:
164
+ ragged.append(number)
165
+ key = tuple(row)
166
+ if key in seen_rows:
167
+ dup_rows.append(number)
168
+ seen_rows.add(key)
169
+ for i, value in enumerate(row[:ncols]):
170
+ if not value.strip():
171
+ continue
172
+ nonempty[i] += 1
173
+ lengths[i] += len(value)
174
+ distinct[i].add(value)
175
+ if not re.fullmatch(r"-?\d+", value.strip()):
176
+ all_int[i] = False
177
+ fmt = _date_format(value.strip())
178
+ if fmt:
179
+ dates[i][fmt] = dates[i].get(fmt, 0) + 1
180
+ for sep in ("|", ";"):
181
+ if sep in value:
182
+ seps[i][sep] = seps[i].get(sep, 0) + 1
183
+ token = value.strip().upper()
184
+ if token in NULL_STRINGS:
185
+ nulls[i][token] = nulls[i].get(token, 0) + 1
186
+ null_rows[i].append(number)
187
+ if _MACHINE_PATH.search(value):
188
+ path_rows[i].append(number)
189
+
190
+ def row_finding(kind: str, rows: list[int], column: str | None = None,
191
+ detail: tuple[tuple[str, int | str], ...] = ()) -> Finding:
192
+ return Finding(kind, rel, column, len(rows), tuple(rows[:MAX_EXAMPLE_ROWS]), detail)
193
+
194
+ if ragged:
195
+ findings.append(row_finding("ragged-rows", ragged))
196
+ if dup_rows:
197
+ findings.append(row_finding("duplicate-rows", dup_rows))
198
+ if comment_rows:
199
+ findings.append(row_finding("comment-rows", comment_rows))
200
+ if header_rows:
201
+ findings.append(row_finding("repeated-header", header_rows))
202
+ for i, name in enumerate(header):
203
+ if len(dates[i]) >= 2:
204
+ findings.append(Finding("mixed-date-formats", rel, name,
205
+ detail=tuple(sorted(dates[i].items()))))
206
+ if len(seps[i]) == 2:
207
+ findings.append(Finding("mixed-separators", rel, name,
208
+ detail=tuple(sorted(seps[i].items()))))
209
+ if path_rows[i]:
210
+ findings.append(row_finding("machine-paths", path_rows[i], name))
211
+ if null_rows[i]:
212
+ findings.append(row_finding("null-strings", null_rows[i], name,
213
+ tuple(sorted(nulls[i].items()))))
214
+
215
+ unique: list[UniqueColumn] = []
216
+ if len(data) >= 2:
217
+ for i, name in enumerate(header):
218
+ if nonempty[i] == len(data) and len(distinct[i]) == len(data):
219
+ labels: list[str] = []
220
+ if all_int[i]:
221
+ labels.append("integer-like")
222
+ if lengths[i] / len(data) > FREE_TEXT_MEAN_LENGTH:
223
+ labels.append("free-text")
224
+ unique.append(UniqueColumn(name, tuple(labels)))
225
+ if not unique:
226
+ findings.append(Finding("no-unique-column", rel))
227
+
228
+ return FileReport(rel, len(data), ncols, tuple(header), tuple(unique)), findings
229
+
230
+
231
+ def scan_project(root: Path, *, path_prefix: str | None = None) -> ReadinessReport:
232
+ files: list[FileReport] = []
233
+ findings: list[Finding] = []
234
+ not_scanned = {"json": 0, "markdown": 0, "other": 0}
235
+ for abs_path in _walk(root):
236
+ rel = abs_path.relative_to(root).as_posix()
237
+ if path_prefix and not rel.startswith(path_prefix):
238
+ continue
239
+ suffix = abs_path.suffix.lower()
240
+ if suffix != ".csv":
241
+ bucket = {".json": "json", ".md": "markdown", ".markdown": "markdown"}.get(suffix, "other")
242
+ not_scanned[bucket] += 1
243
+ continue
244
+ report, file_findings = _scan_file(rel, abs_path.read_bytes())
245
+ if report is not None:
246
+ files.append(report)
247
+ findings.extend(file_findings)
248
+ findings.sort(key=lambda f: (f.file, FINDING_KINDS.index(f.kind), f.column or ""))
249
+ return ReadinessReport(tuple(files), tuple(findings), not_scanned)
250
+
251
+
252
+ def _finding_line(f: Finding) -> str:
253
+ parts = [f"- {f.kind}: {f.file}"]
254
+ if f.column is not None:
255
+ parts.append(f"column {f.column!r}")
256
+ if f.count is not None:
257
+ parts.append(f"count={f.count}")
258
+ if f.rows:
259
+ parts.append("rows=" + ",".join(str(r) for r in f.rows))
260
+ if f.detail:
261
+ parts.append(", ".join(f"{k}={v}" for k, v in f.detail))
262
+ return " ".join(parts)
263
+
264
+
265
+ def format_markdown(report: ReadinessReport, *, findings_only: bool = False) -> str:
266
+ ns = report.not_scanned
267
+ lines = [
268
+ "# pdata readiness scan",
269
+ "",
270
+ "Row numbers are 1-based data records (header excluded), not line numbers: a quoted "
271
+ "multiline cell makes them differ.",
272
+ f"Scanned {len(report.files)} CSV file(s); not scanned: {ns['json']} JSON, "
273
+ f"{ns['markdown']} Markdown, {ns['other']} other.",
274
+ ]
275
+ if not report.files and not any(f.kind == "unreadable" for f in report.findings):
276
+ lines += ["", "No CSV files found."]
277
+ lines += ["", "## Findings"]
278
+ lines += [_finding_line(f) for f in report.findings] or ["No findings."]
279
+ if not findings_only and report.files:
280
+ lines += [
281
+ "",
282
+ "## Inventory",
283
+ "A unique column is a fact, not a recommended key: it is not a stable key until a "
284
+ "human has confirmed its values are not re-numbered per session or machine.",
285
+ ]
286
+ for f in report.files:
287
+ unique = ", ".join(
288
+ u.name + (f" ({'/'.join(u.labels)})" if u.labels else "") for u in f.unique_columns
289
+ )
290
+ lines.append(
291
+ f"- {f.path}: {f.rows} rows, {f.columns} columns"
292
+ + (f"; unique: {unique}" if unique else "")
293
+ )
294
+ return "\n".join(lines)
295
+
296
+
297
+ def format_json(report: ReadinessReport) -> str:
298
+ return json.dumps(
299
+ {
300
+ "files": [
301
+ {
302
+ "path": f.path, "rows": f.rows, "columns": f.columns,
303
+ "header": list(f.header),
304
+ "unique_columns": [{"name": u.name, "labels": list(u.labels)}
305
+ for u in f.unique_columns],
306
+ }
307
+ for f in report.files
308
+ ],
309
+ "findings": [
310
+ {
311
+ "kind": f.kind, "file": f.file, "column": f.column, "count": f.count,
312
+ "rows": list(f.rows), "detail": dict(f.detail),
313
+ }
314
+ for f in report.findings
315
+ ],
316
+ "not_scanned": report.not_scanned,
317
+ },
318
+ indent=2,
319
+ )
@@ -0,0 +1,35 @@
1
+ """Terminal-width word wrapping shared by `ccst doctor` and the hooks install output."""
2
+ from __future__ import annotations
3
+
4
+ import shutil
5
+ import textwrap
6
+
7
+ FALLBACK_WIDTH = 80
8
+ MIN_WIDTH = 40
9
+
10
+
11
+ def effective_width() -> int:
12
+ """Width to wrap to: the detected terminal width, never below MIN_WIDTH.
13
+
14
+ `shutil.get_terminal_size` reads `COLUMNS`, then the real terminal via the process's stdout
15
+ file descriptor (so an in-process `redirect_stdout` capture still sees the real terminal),
16
+ and falls back to FALLBACK_WIDTH when neither is available. It is called through the module
17
+ attribute so tests can patch it.
18
+ """
19
+ return max(shutil.get_terminal_size(fallback=(FALLBACK_WIDTH, 24)).columns, MIN_WIDTH)
20
+
21
+
22
+ def wrap_block(text: str, *, indent: str = " ") -> str:
23
+ """Word-wrap `text` to the effective width, indenting every line.
24
+
25
+ Words are never split: a path, hyphenated name or backticked command that has no whitespace
26
+ stays on one line, and a single token longer than the width overflows rather than breaking.
27
+ """
28
+ return textwrap.fill(
29
+ text,
30
+ width=effective_width(),
31
+ initial_indent=indent,
32
+ subsequent_indent=indent,
33
+ break_long_words=False,
34
+ break_on_hyphens=False,
35
+ )