cc-session-tools 2.12.2__tar.gz → 2.12.3__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 (275) hide show
  1. {cc_session_tools-2.12.2/src/cc_session_tools.egg-info → cc_session_tools-2.12.3}/PKG-INFO +1 -1
  2. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/pyproject.toml +1 -1
  3. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/cli/ccst.py +10 -5
  4. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/doctor.py +10 -6
  5. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/scheduler/bundled_jobs.py +56 -13
  6. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/scheduler/digest.py +16 -1
  7. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/scheduler/surface.py +84 -14
  8. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3/src/cc_session_tools.egg-info}/PKG-INFO +1 -1
  9. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_ccst_ccsched_jobs_cli.py +8 -1
  10. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_ccst_doctor.py +28 -0
  11. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_scheduler_bundled_jobs.py +60 -1
  12. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/LICENSE +0 -0
  13. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/README.md +0 -0
  14. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/setup.cfg +0 -0
  15. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/__init__.py +0 -0
  16. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/cli/__init__.py +0 -0
  17. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/cli/ccd.py +0 -0
  18. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/cli/ccmsg.py +0 -0
  19. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/cli/ccr.py +0 -0
  20. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/cli/ccs.py +0 -0
  21. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/cli/ccsched.py +0 -0
  22. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/cli/migrate_ccmsg.py +0 -0
  23. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/cli/migrate_ccsched.py +0 -0
  24. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/cli/migrate_sessions_db.py +0 -0
  25. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/cli/migrate_telemetry.py +0 -0
  26. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/config/hooks-bundle.json +0 -0
  27. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/config/session-tag-hook.json +0 -0
  28. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/hooks_install.py +0 -0
  29. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/__init__.py +0 -0
  30. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/claude_flags.py +0 -0
  31. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/claude_md_install.py +0 -0
  32. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/context_overrides.py +0 -0
  33. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/db.py +0 -0
  34. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/debug.py +0 -0
  35. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/doctor_mutes.py +0 -0
  36. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/hook_registry.py +0 -0
  37. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/install_sync.py +0 -0
  38. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/levenshtein.py +0 -0
  39. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/machine_identity.py +0 -0
  40. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/messaging/__init__.py +0 -0
  41. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/messaging/addressing.py +0 -0
  42. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/messaging/cursor.py +0 -0
  43. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/messaging/lock.py +0 -0
  44. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/messaging/message.py +0 -0
  45. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/messaging/move_safety.py +0 -0
  46. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/messaging/repository.py +0 -0
  47. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/messaging/retention.py +0 -0
  48. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/messaging/service.py +0 -0
  49. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/messaging/store.py +0 -0
  50. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/occupancy.py +0 -0
  51. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/paths.py +0 -0
  52. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/pdata/__init__.py +0 -0
  53. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/pdata/backup.py +0 -0
  54. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/pdata/classify.py +0 -0
  55. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/pdata/cutover.py +0 -0
  56. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/pdata/dump.py +0 -0
  57. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/pdata/formatting.py +0 -0
  58. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/pdata/importers.py +0 -0
  59. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/pdata/init_paths.py +0 -0
  60. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/pdata/init_service.py +0 -0
  61. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/pdata/manifest.py +0 -0
  62. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/pdata/naming.py +0 -0
  63. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/pdata/rehydrate.py +0 -0
  64. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/pdata/rename_group.py +0 -0
  65. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/pdata/reorganize.py +0 -0
  66. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/pdata/repository.py +0 -0
  67. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/pdata/resolve.py +0 -0
  68. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/pdata/service.py +0 -0
  69. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/pdata/session_output.py +0 -0
  70. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/pdata/store.py +0 -0
  71. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/pdata/sync_check.py +0 -0
  72. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/pdata/sync_lock.py +0 -0
  73. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/pdata/sync_notify.py +0 -0
  74. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/pdata/vector_clock.py +0 -0
  75. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/pdata/vector_clock_store.py +0 -0
  76. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/pdata/verify.py +0 -0
  77. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/pdata/write_log.py +0 -0
  78. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/picker.py +0 -0
  79. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/proc_lock.py +0 -0
  80. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/prompts.py +0 -0
  81. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/roots.py +0 -0
  82. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/rules.py +0 -0
  83. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/scheduler/__init__.py +0 -0
  84. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/scheduler/cadence.py +0 -0
  85. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/scheduler/cursor.py +0 -0
  86. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/scheduler/due.py +0 -0
  87. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/scheduler/duration.py +0 -0
  88. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/scheduler/jobspec.py +0 -0
  89. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/scheduler/ledger.py +0 -0
  90. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/scheduler/lock.py +0 -0
  91. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/scheduler/notify.py +0 -0
  92. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/scheduler/reconcile.py +0 -0
  93. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/scheduler/registry.py +0 -0
  94. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/scheduler/runner.py +0 -0
  95. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/scheduler/state.py +0 -0
  96. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/scheduler/store.py +0 -0
  97. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/scheduler/throttle.py +0 -0
  98. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/scheduler/worker.py +0 -0
  99. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/session_gc.py +0 -0
  100. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/sessions.py +0 -0
  101. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/sessions_db.py +0 -0
  102. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/sessions_repair.py +0 -0
  103. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/shell_install.py +0 -0
  104. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/tasklist.py +0 -0
  105. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/lib/telemetry_store.py +0 -0
  106. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/prompts/pdata-migration-claude-md-update.md +0 -0
  107. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/prompts/pdata-migration-skills-update.md +0 -0
  108. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/skills/analyse-cc-usage/SKILL.md +0 -0
  109. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/skills/clean-hook-sessions/SKILL.md +0 -0
  110. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/skills/clean-hook-sessions/scripts/clean-hook-sessions.py +0 -0
  111. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/skills/context-override/SKILL.md +0 -0
  112. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/skills/delete-sessions/SKILL.md +0 -0
  113. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/skills/delete-sessions/scripts/delete_sessions.py +0 -0
  114. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/skills/do-executor-critic-assessor-loop/SKILL.md +0 -0
  115. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/skills/find-claude-code-session/SKILL.md +0 -0
  116. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/skills/generate-8digit-code/SKILL.md +0 -0
  117. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/skills/generate-8digit-code/scripts/generate_8digit_code.py +0 -0
  118. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/skills/list-empty-sessions/SKILL.md +0 -0
  119. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/skills/list-empty-sessions/scripts/list_empty_sessions.py +0 -0
  120. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/skills/manage-recurring-cc-jobs-using-ccsched/SKILL.md +0 -0
  121. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/skills/move-session/SKILL.md +0 -0
  122. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/skills/move-session/hooks/sessionstart-pending-rename.sh +0 -0
  123. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/skills/move-session/scripts/cc_session_rules.py +0 -0
  124. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/skills/move-session/scripts/move_session.py +0 -0
  125. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/skills/pm-pdata-conflict-resolution/SKILL.md +0 -0
  126. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/skills/pm-pdata-schema-design/SKILL.md +0 -0
  127. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/skills/pm-project-init/SKILL.md +0 -0
  128. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/skills/pm-project-layout-reference/SKILL.md +0 -0
  129. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/skills/pm-update-central-files/SKILL.md +0 -0
  130. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/skills/reduce-persistent-context/CALIBRATION.md +0 -0
  131. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/skills/reduce-persistent-context/SKILL.md +0 -0
  132. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/skills/reduce-persistent-context/scripts/analyze_context.py +0 -0
  133. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/skills/reduce-persistent-context/scripts/duplicates.py +0 -0
  134. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/skills/reduce-persistent-context/scripts/measure.py +0 -0
  135. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/skills/reduce-persistent-context/scripts/report.py +0 -0
  136. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/skills/reduce-persistent-context/scripts/tokens.py +0 -0
  137. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/skills/reduce-persistent-context/scripts/usage.py +0 -0
  138. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/skills/select-agent-model/SKILL.md +0 -0
  139. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/skills/send-session-message/SKILL.md +0 -0
  140. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/skills/update-command-cache/SKILL.md +0 -0
  141. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools/skills/update-command-cache/scripts/update_command_cache.py +0 -0
  142. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools.egg-info/SOURCES.txt +0 -0
  143. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools.egg-info/dependency_links.txt +0 -0
  144. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools.egg-info/entry_points.txt +0 -0
  145. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools.egg-info/requires.txt +0 -0
  146. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cc_session_tools.egg-info/top_level.txt +0 -0
  147. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cccs_hooks/__init__.py +0 -0
  148. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cccs_hooks/after_response.py +0 -0
  149. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cccs_hooks/bash_hard_deny.py +0 -0
  150. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cccs_hooks/bash_security_review.py +0 -0
  151. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cccs_hooks/cache.py +0 -0
  152. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cccs_hooks/catchup.py +0 -0
  153. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cccs_hooks/confirm_8digit.py +0 -0
  154. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cccs_hooks/context_window_warning.py +0 -0
  155. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cccs_hooks/last_screenshot.py +0 -0
  156. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cccs_hooks/marker_allow.py +0 -0
  157. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cccs_hooks/markers.py +0 -0
  158. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cccs_hooks/messaging_deliver.py +0 -0
  159. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cccs_hooks/model_info.py +0 -0
  160. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cccs_hooks/normalise.py +0 -0
  161. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cccs_hooks/pdata_sync.py +0 -0
  162. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cccs_hooks/pending_migration.py +0 -0
  163. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cccs_hooks/pending_rename.py +0 -0
  164. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cccs_hooks/session_tag.py +0 -0
  165. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cccs_hooks/stats.py +0 -0
  166. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cccs_hooks/telemetry.py +0 -0
  167. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cccs_hooks/telemetry_query.py +0 -0
  168. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cccs_hooks/telemetry_trim.py +0 -0
  169. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cccs_hooks/transcript.py +0 -0
  170. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/cccs_hooks/worklog_guard.py +0 -0
  171. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/claude_code_usage/__init__.py +0 -0
  172. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/claude_code_usage/attribution.py +0 -0
  173. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/claude_code_usage/cache.py +0 -0
  174. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/claude_code_usage/ccusage_wrapper.py +0 -0
  175. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/claude_code_usage/cli.py +0 -0
  176. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/claude_code_usage/data/pricing.json +0 -0
  177. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/claude_code_usage/parent_inference.py +0 -0
  178. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/claude_code_usage/parser.py +0 -0
  179. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/claude_code_usage/pricing.py +0 -0
  180. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/claude_code_usage/query.py +0 -0
  181. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/claude_code_usage/report.py +0 -0
  182. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/claude_code_usage/schema.py +0 -0
  183. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/src/claude_code_usage/session_names.py +0 -0
  184. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_after_response.py +0 -0
  185. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_attribution.py +0 -0
  186. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_bash_hard_deny.py +0 -0
  187. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_bash_security_review.py +0 -0
  188. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_cache.py +0 -0
  189. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_cache_sqlite.py +0 -0
  190. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_ccr_orphans.py +0 -0
  191. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_ccs_emptiness.py +0 -0
  192. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_ccs_eta.py +0 -0
  193. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_ccs_list_mode.py +0 -0
  194. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_ccs_scope_flags.py +0 -0
  195. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_ccs_sentinel_sort.py +0 -0
  196. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_ccs_session_counts.py +0 -0
  197. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_ccst_bundle_discovery.py +0 -0
  198. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_ccst_cli.py +0 -0
  199. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_ccst_context_override_cli.py +0 -0
  200. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_ccst_gc_prune.py +0 -0
  201. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_ccst_gc_report.py +0 -0
  202. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_ccst_hook_dispatcher.py +0 -0
  203. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_ccst_hooks_install_prune.py +0 -0
  204. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_ccst_hooks_install_zero_arg.py +0 -0
  205. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_ccst_hooks_uninstall.py +0 -0
  206. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_ccst_install_everything.py +0 -0
  207. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_ccst_machine_identity_cli.py +0 -0
  208. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_ccst_main_gate.py +0 -0
  209. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_ccst_pdata_cli.py +0 -0
  210. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_ccst_pdata_init_cli.py +0 -0
  211. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_ccst_pdata_reconcile_cli.py +0 -0
  212. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_ccst_pdata_rename_group_cli.py +0 -0
  213. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_ccst_pdata_reorganize_cli.py +0 -0
  214. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_ccst_pdata_sync_cli.py +0 -0
  215. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_ccst_pdata_verify_cli.py +0 -0
  216. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_ccst_repair_sessions_cli.py +0 -0
  217. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_ccst_sessions_cli.py +0 -0
  218. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_ccst_shell_install.py +0 -0
  219. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_ccst_skills_install.py +0 -0
  220. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_ccst_skills_uninstall.py +0 -0
  221. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_ccst_telemetry_query.py +0 -0
  222. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_ccst_telemetry_trim.py +0 -0
  223. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_ccusage_wrapper.py +0 -0
  224. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_claude_flags.py +0 -0
  225. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_claude_md_install.py +0 -0
  226. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_cli.py +0 -0
  227. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_cli_ccd.py +0 -0
  228. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_cli_ccd_dry_run.py +0 -0
  229. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_cli_ccr.py +0 -0
  230. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_cli_ccs.py +0 -0
  231. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_cli_version.py +0 -0
  232. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_confirm_8digit.py +0 -0
  233. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_context_overrides.py +0 -0
  234. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_context_window_warning.py +0 -0
  235. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_db.py +0 -0
  236. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_debug.py +0 -0
  237. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_doctor_mutes.py +0 -0
  238. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_empty_session.py +0 -0
  239. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_hook_cache.py +0 -0
  240. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_hooks_install.py +0 -0
  241. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_install_sync.py +0 -0
  242. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_last_screenshot.py +0 -0
  243. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_levenshtein.py +0 -0
  244. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_machine_identity.py +0 -0
  245. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_marker_allow.py +0 -0
  246. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_migrate_ccmsg.py +0 -0
  247. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_migrate_ccsched.py +0 -0
  248. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_migrate_sessions_db.py +0 -0
  249. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_migrate_telemetry.py +0 -0
  250. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_model_info.py +0 -0
  251. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_normalise.py +0 -0
  252. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_occupancy.py +0 -0
  253. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_parent_inference.py +0 -0
  254. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_parser.py +0 -0
  255. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_paths.py +0 -0
  256. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_pending_migration_hook.py +0 -0
  257. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_picker.py +0 -0
  258. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_pricing.py +0 -0
  259. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_proc_lock.py +0 -0
  260. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_prompts.py +0 -0
  261. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_query.py +0 -0
  262. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_report.py +0 -0
  263. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_roots.py +0 -0
  264. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_rules.py +0 -0
  265. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_schema.py +0 -0
  266. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_session_names.py +0 -0
  267. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_session_tag.py +0 -0
  268. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_sessions.py +0 -0
  269. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_sessions_db.py +0 -0
  270. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_sessions_repair.py +0 -0
  271. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_tasklist.py +0 -0
  272. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_telemetry.py +0 -0
  273. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_telemetry_store.py +0 -0
  274. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/tests/test_transcript.py +0 -0
  275. {cc_session_tools-2.12.2 → cc_session_tools-2.12.3}/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.12.2
3
+ Version: 2.12.3
4
4
  Summary: Claude Code session management CLI tools (ccd, ccr, ccs, claude-code-usage, ccst) and hook library (cccs_hooks)
5
5
  Author: raffishquartan
6
6
  License: MIT License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cc-session-tools"
7
- version = "2.12.2"
7
+ version = "2.12.3"
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" }
@@ -2132,12 +2132,17 @@ def _cmd_ccsched_jobs_install(args: argparse.Namespace) -> int:
2132
2132
  for job in bundled_jobs.BUNDLED_CCSCHED_JOBS:
2133
2133
  spec = existing.get(job.job_id)
2134
2134
  if spec is not None:
2135
- changed = bundled_jobs.diff_from_bundled(spec, job)
2136
- if changed:
2135
+ changed_detail = bundled_jobs.diff_from_bundled_detail(spec, job)
2136
+ if changed_detail:
2137
+ field_word = "field differs" if len(changed_detail) == 1 else "fields differ"
2137
2138
  print(
2138
- f" changed (not touched): {job.job_id} - {', '.join(changed)} "
2139
- f"differ from the bundled definition; run 'ccsched edit' to realign, or "
2140
- f"leave as your intentional customization"
2139
+ f" changed (not touched): {job.job_id} - {len(changed_detail)} "
2140
+ f"{field_word} from the bundled definition:"
2141
+ )
2142
+ print(bundled_jobs.render_field_diffs(changed_detail))
2143
+ print(
2144
+ f" run 'ccsched edit {job.job_id}' to realign, or leave as your "
2145
+ f"intentional customization"
2141
2146
  )
2142
2147
  elif not spec.enabled:
2143
2148
  print(f" disabled (not touched): {job.job_id} - run 'ccsched enable {job.job_id}' to re-enable")
@@ -256,16 +256,20 @@ def check_ccsched_job_registered(
256
256
  for spec in specs:
257
257
  if spec.job_id == job_id:
258
258
  if expected is not None:
259
- from cc_session_tools.lib.scheduler.bundled_jobs import diff_from_bundled
259
+ from cc_session_tools.lib.scheduler.bundled_jobs import (
260
+ diff_from_bundled_detail, render_field_diffs,
261
+ )
260
262
 
261
- changed = diff_from_bundled(spec, expected)
262
- if changed:
263
+ changed_detail = diff_from_bundled_detail(spec, expected)
264
+ if changed_detail:
265
+ field_word = "field differs" if len(changed_detail) == 1 else "fields differ"
263
266
  return CheckResult(
264
267
  name=name, status=Status.WARN,
265
268
  reason=(
266
- f"registered but {', '.join(changed)} differ from the bundled "
267
- f"definition — run 'ccst ccsched-jobs install --apply' to see the "
268
- f"diff, or 'ccsched edit {job_id}' if this was intentional"
269
+ f"registered but {len(changed_detail)} {field_word} from "
270
+ f"the bundled definition:\n{render_field_diffs(changed_detail)}\n"
271
+ f"run 'ccsched edit {job_id}' to realign, or leave as your "
272
+ f"intentional customization"
269
273
  ),
270
274
  )
271
275
  if spec.enabled:
@@ -10,6 +10,7 @@ deliberately excludes `enabled`: that is per-machine operational state a human t
10
10
  `_cmd_ccsched_jobs_install` and `doctor.check_ccsched_job_registered`, the two callers."""
11
11
  from __future__ import annotations
12
12
 
13
+ import shlex
13
14
  from dataclasses import dataclass
14
15
  from pathlib import Path
15
16
  from typing import TYPE_CHECKING
@@ -183,23 +184,65 @@ BUNDLED_CCSCHED_JOBS: tuple[BundledJob, ...] = (
183
184
  )
184
185
 
185
186
 
186
- def diff_from_bundled(spec: "JobSpec", job: BundledJob) -> tuple[str, ...]:
187
- """Names of the fields where a registered JobSpec no longer matches its bundled
188
- definition, in declaration order; empty if it matches exactly. Deliberately ignores
189
- `enabled` — see module docstring."""
190
- differing: list[str] = []
187
+ @dataclass(frozen=True, slots=True)
188
+ class FieldDiff:
189
+ """One field where a registered JobSpec no longer matches its bundled definition,
190
+ carrying both the bundled and the current (live) value, rendered as display-ready
191
+ strings so a caller never has to know a field's underlying type - see
192
+ `diff_from_bundled_detail`."""
193
+ field: str
194
+ bundled: str
195
+ current: str
196
+
197
+
198
+ def diff_from_bundled_detail(spec: "JobSpec", job: BundledJob) -> tuple[FieldDiff, ...]:
199
+ """Same comparison as `diff_from_bundled`, but carries the before/after values for
200
+ display, in declaration order; empty if the spec matches its bundled definition
201
+ exactly. Deliberately ignores `enabled` — see module docstring. This is the one place
202
+ that decides both *whether* a field changed and *how* its value is rendered, so
203
+ `_cmd_ccsched_jobs_install` and `check_ccsched_job_registered` — the two callers —
204
+ can never disagree on either question."""
205
+ diffs: list[FieldDiff] = []
191
206
  if spec.cadence != job.cadence:
192
- differing.append("cadence")
207
+ diffs.append(FieldDiff("cadence", job.cadence, spec.cadence))
193
208
  if spec.coalesce.value != job.coalesce:
194
- differing.append("coalesce")
209
+ diffs.append(FieldDiff("coalesce", job.coalesce, spec.coalesce.value))
195
210
  if spec.catchup_window != job.catchup_window:
196
- differing.append("catchup_window")
211
+ diffs.append(FieldDiff("catchup_window", job.catchup_window, spec.catchup_window))
197
212
  if spec.timeout != job.timeout:
198
- differing.append("timeout")
213
+ diffs.append(FieldDiff("timeout", job.timeout, spec.timeout))
199
214
  if spec.surface != job.surface:
200
- differing.append("surface")
215
+ diffs.append(FieldDiff("surface", str(job.surface), str(spec.surface)))
201
216
  if spec.command != job.command:
202
- differing.append("command")
217
+ # shlex.join renders as an actual shell line, not a Python tuple repr - readable
218
+ # for a multi-arg command.
219
+ diffs.append(FieldDiff("command", shlex.join(job.command), shlex.join(spec.command)))
203
220
  if spec.success_exit_codes != job.success_exit_codes:
204
- differing.append("success_exit_codes")
205
- return tuple(differing)
221
+ diffs.append(FieldDiff(
222
+ "success_exit_codes",
223
+ ", ".join(str(c) for c in job.success_exit_codes),
224
+ ", ".join(str(c) for c in spec.success_exit_codes),
225
+ ))
226
+ return tuple(diffs)
227
+
228
+
229
+ def diff_from_bundled(spec: "JobSpec", job: BundledJob) -> tuple[str, ...]:
230
+ """Names of the fields where a registered JobSpec no longer matches its bundled
231
+ definition, in declaration order; empty if it matches exactly. A thin wrapper over
232
+ `diff_from_bundled_detail` so both callers stay guaranteed-identical in what counts
233
+ as "changed" — see module docstring."""
234
+ return tuple(fd.field for fd in diff_from_bundled_detail(spec, job))
235
+
236
+
237
+ def render_field_diffs(diffs: tuple[FieldDiff, ...], *, indent: str = " ") -> str:
238
+ """Shared before/after rendering for a non-empty `diff_from_bundled_detail` result -
239
+ one `field:` sub-block per entry, in the same order, `-`/`+` prefixed so it reads as a
240
+ diff without pulling in `difflib`. Both `_cmd_ccsched_jobs_install` and
241
+ `check_ccsched_job_registered` call this so they render byte-identical bodies and only
242
+ differ in the header/hint text wrapped around it."""
243
+ lines: list[str] = []
244
+ for d in diffs:
245
+ lines.append(f"{indent}{d.field}:")
246
+ lines.append(f"{indent} - bundled: {d.bundled}")
247
+ lines.append(f"{indent} + current: {d.current}")
248
+ return "\n".join(lines)
@@ -40,6 +40,11 @@ class JobReport:
40
40
  # exclusive with `findings`: a given run is either a clean 0-exit or a
41
41
  # findings-bearing nonzero exit, never both.
42
42
  output: str | None = None
43
+ # RAN with output and count > 1 only: the span from the oldest coalesced
44
+ # run to now (e.g. "11h", "2d"), formatted without an "ago" suffix so it
45
+ # reads as a window ("... in last {window}:") rather than a point in time
46
+ # - see surface.py's per-job clean-output coalescing.
47
+ window: str | None = None
43
48
 
44
49
 
45
50
  def _ordinal(n: int) -> str:
@@ -90,7 +95,17 @@ def _line(report: JobReport) -> str | None:
90
95
  # distinct from the findings/warning case above so a passing check
91
96
  # is never mistaken for one that found something. One line-group:
92
97
  # never falls through to the bare "✓ ran" case below.
93
- header = f"✓ ran {report.job_id}{overdue}{age_suffix}:"
98
+ if report.count > 1:
99
+ # Several clean-output runs of the same job coalesced within
100
+ # one sweep (§ output-coalesce fix, mirrors the bare-run
101
+ # coalesce below) - one line carrying a count and the window
102
+ # they span, body is the most recent run's output only.
103
+ header = (
104
+ f"✓ ran {report.job_id}{age_suffix} "
105
+ f"({report.count} times in last {report.window}):"
106
+ )
107
+ else:
108
+ header = f"✓ ran {report.job_id}{overdue}{age_suffix}:"
94
109
  body = "\n".join(f" {line}" for line in report.output.splitlines())
95
110
  return f"{header}\n{body}"
96
111
  if report.count > 1:
@@ -17,6 +17,13 @@ unconditionally rather than the per-job flag, since digest.py never consults
17
17
  `surface` for Outcome.RAN — the per-job flag is only meaningful for LAUNCH,
18
18
  where it is still looked up via `_surface_flag`.
19
19
 
20
+ A clean (0-exit) run's captured stdout coalesces per job_id the same way bare
21
+ routine runs do: several such runs for the same job within one digest render
22
+ as one line carrying a count, the span they cover, and only the MOST RECENT
23
+ run's output (§ output-coalesce fix) — see `_output_reports`. Findings
24
+ (nonzero-exit captured stdout) are exempt, same as FAILED/SUSPENDED: they are
25
+ signal, never folded, always replayed individually with their own age suffix.
26
+
20
27
  `surface(..., lookback=...)` optionally widens the read below the session's
21
28
  own cursor to also pick up ledger activity within `lookback` of `now`, for the
22
29
  SessionStart 24h rolling lookback (§ widen fix) — see catchup.py, the only
@@ -81,6 +88,19 @@ def _format_age(sent: datetime | None, now: datetime) -> str:
81
88
  return f"{hours // 24}d ago"
82
89
 
83
90
 
91
+ def _format_span(oldest: datetime, now: datetime) -> str:
92
+ """Same buckets as `_format_age`, but as a window ("11h") rather than a
93
+ point in time ("11h ago") - for the "N times in last {span}" wording on
94
+ coalesced clean-output runs."""
95
+ minutes = max(1, int((now - oldest).total_seconds() // 60))
96
+ if minutes < 60:
97
+ return f"{minutes}m"
98
+ hours = minutes // 60
99
+ if hours < 24:
100
+ return f"{hours}h"
101
+ return f"{hours // 24}d"
102
+
103
+
84
104
  # Sort key for an entry whose ts is missing/unparseable - sorts first, same
85
105
  # convention as `_format_age`'s "unknown time ago" treating it as maximally old.
86
106
  _UNKNOWN_TS = datetime.min.replace(tzinfo=timezone.utc)
@@ -161,6 +181,51 @@ def _routine_reports(
161
181
  return out
162
182
 
163
183
 
184
+ def _output_reports(
185
+ entries: list[dict[str, object]], *, now: datetime,
186
+ ) -> list[tuple[datetime, JobReport]]:
187
+ """Clean-output (0-exit, captured stdout) RUN/BACKFILL entries, grouped by
188
+ job_id. A job_id with only one entry renders exactly as before (no count
189
+ suffix). A job_id with several entries within one digest coalesces to one
190
+ line carrying a count, the span the group covers, and only the MOST
191
+ RECENT entry's output - not N near-identical output blocks for the same
192
+ job (§ output-coalesce fix). Findings-bearing (nonzero-exit) entries never
193
+ reach this function - they are signal, not routine noise, and always
194
+ replay individually (see the caller)."""
195
+ groups: dict[str, list[dict[str, object]]] = {}
196
+ for e in entries:
197
+ groups.setdefault(str(e.get("job_id", "")), []).append(e)
198
+
199
+ out: list[tuple[datetime, JobReport]] = []
200
+ for job_id, group in groups.items():
201
+ timestamps = [_parse_ts(g.get("ts")) for g in group]
202
+ latest = max((t for t in timestamps if t is not None), default=None)
203
+ oldest = min((t for t in timestamps if t is not None), default=None)
204
+ sort_ts = latest if latest is not None else _UNKNOWN_TS
205
+ if len(group) == 1:
206
+ e = group[0]
207
+ out.append((sort_ts, JobReport(
208
+ job_id=job_id, outcome=Outcome.RAN, surface=True, overdue="",
209
+ ran=int(cast(int, e.get("ran", 0)) or 0), deferred=0, expired=0,
210
+ consecutive_failures=0, age=_format_age(latest, now),
211
+ output=str(e.get("error")),
212
+ )))
213
+ continue
214
+ # Most recent entry by ts (fall back to list order for unparseable ts).
215
+ latest_entry = max(
216
+ group, key=lambda g: (_parse_ts(g.get("ts")) or _UNKNOWN_TS),
217
+ )
218
+ window = _format_span(oldest, now) if oldest is not None else "unknown time"
219
+ out.append((sort_ts, JobReport(
220
+ job_id=job_id, outcome=Outcome.RAN, surface=True, overdue="",
221
+ ran=sum(int(cast(int, g.get("ran", 0)) or 0) for g in group),
222
+ deferred=0, expired=0, consecutive_failures=0,
223
+ age=_format_age(latest, now), count=len(group),
224
+ output=str(latest_entry.get("error")), window=window,
225
+ )))
226
+ return out
227
+
228
+
164
229
  def surface(
165
230
  *, session_uuid: str, now: datetime, lookback: timedelta | None = None,
166
231
  ) -> SurfaceResult:
@@ -185,6 +250,7 @@ def surface(
185
250
  # them.
186
251
  pairs: list[tuple[datetime, JobReport]] = []
187
252
  routine: list[dict[str, object]] = []
253
+ clean_output: list[dict[str, object]] = []
188
254
  for e in entries:
189
255
  event = str(e.get("event", ""))
190
256
  job_id = str(e.get("job_id", ""))
@@ -218,20 +284,23 @@ def surface(
218
284
  captured = e.get("error")
219
285
  if captured:
220
286
  is_warning = raw_exit not in (0, None)
221
- # surface=True unconditionally: a completed run is always
222
- # visible, so the per-job flag is not load-bearing here (see
223
- # module docstring) - matches the pre-existing findings
224
- # convention rather than the FAILED/SUSPENDED "write the real
225
- # flag, bypass it in digest.py" one, since digest.py does not
226
- # even look at `surface` for Outcome.RAN.
227
- pairs.append((sort_ts, JobReport(
228
- job_id=job_id, outcome=Outcome.RAN, surface=True, overdue="",
229
- ran=int(cast(int, e.get("ran", 0)) or 0), deferred=0, expired=0,
230
- consecutive_failures=0,
231
- findings=str(captured) if is_warning else None,
232
- output=str(captured) if not is_warning else None,
233
- age=_format_age(sent, now),
234
- )))
287
+ if is_warning:
288
+ # Findings are signal, not routine noise - never coalesced,
289
+ # always replayed individually (see module docstring).
290
+ # surface=True unconditionally: a completed run is always
291
+ # visible, so the per-job flag is not load-bearing here.
292
+ pairs.append((sort_ts, JobReport(
293
+ job_id=job_id, outcome=Outcome.RAN, surface=True, overdue="",
294
+ ran=int(cast(int, e.get("ran", 0)) or 0), deferred=0, expired=0,
295
+ consecutive_failures=0, findings=str(captured),
296
+ age=_format_age(sent, now),
297
+ )))
298
+ else:
299
+ # A clean (0-exit) run's captured stdout - eligible for
300
+ # per-job_id coalescing (§ output-coalesce fix), same
301
+ # spirit as the bare-run routine fold below but keeping
302
+ # the most recent output rather than dropping it.
303
+ clean_output.append(e)
235
304
  else:
236
305
  # A bare clean run with no captured stdout - still eligible
237
306
  # for the routine backlog fold (spam control, not the
@@ -249,6 +318,7 @@ def surface(
249
318
  # skip_expired and defer events are not surfaced as standalone lines.
250
319
 
251
320
  pairs.extend(_routine_reports(routine, now=now))
321
+ pairs.extend(_output_reports(clean_output, now=now))
252
322
  pairs.sort(key=lambda p: p[0])
253
323
 
254
324
  cursor.write_cursor(session_uuid, new_offset)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cc-session-tools
3
- Version: 2.12.2
3
+ Version: 2.12.3
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
@@ -93,7 +93,14 @@ def test_changed_job_is_reported_not_touched_and_not_overwritten(base_env):
93
93
 
94
94
  r = _run(base_env, "ccsched-jobs", "install", "--apply")
95
95
  assert r.returncode == 0, r.stderr
96
- assert "changed (not touched): telemetry-trim-weekly - timeout" in r.stdout
96
+ assert (
97
+ "changed (not touched): telemetry-trim-weekly - 1 field differs "
98
+ "from the bundled definition:" in r.stdout
99
+ )
100
+ assert "timeout:" in r.stdout
101
+ assert "- bundled: 60s" in r.stdout
102
+ assert "+ current: 9s" in r.stdout
103
+ assert "run 'ccsched edit telemetry-trim-weekly' to realign" in r.stdout
97
104
  assert "already registered: telemetry-trim-weekly" not in r.stdout
98
105
 
99
106
  show = subprocess.run(
@@ -1207,6 +1207,34 @@ def test_check_ccsched_job_registered_warns_on_drift_from_bundled_definition(
1207
1207
  assert "timeout" in result.reason
1208
1208
 
1209
1209
 
1210
+ def test_check_ccsched_job_registered_shows_before_and_after_values_not_just_field_name(
1211
+ monkeypatch, tmp_path
1212
+ ) -> None:
1213
+ """§ diff-detail fix: the reason must carry the actual bundled/current values, not just
1214
+ name the field that differs — no more 'run install --apply to see the diff' indirection."""
1215
+ monkeypatch.setenv("CCST_DATA_HOME", str(tmp_path))
1216
+ from cc_session_tools.lib.scheduler import bundled_jobs, registry
1217
+ from cc_session_tools.lib.scheduler.jobspec import validate_job_fields
1218
+
1219
+ job = next(
1220
+ j for j in bundled_jobs.BUNDLED_CCSCHED_JOBS if j.job_id == "telemetry-trim-weekly"
1221
+ )
1222
+ spec = validate_job_fields(
1223
+ job_id=job.job_id, cadence=job.cadence, coalesce=job.coalesce,
1224
+ command=list(job.command), surface=job.surface, enabled=True,
1225
+ catchup_window=job.catchup_window, timeout="9s",
1226
+ success_exit_codes=job.success_exit_codes,
1227
+ )
1228
+ registry.add_job(spec)
1229
+
1230
+ result = check_ccsched_job_registered(job.job_id, expected=job)
1231
+ assert "1 field differ" in result.reason
1232
+ assert "- bundled: 60s" in result.reason
1233
+ assert "+ current: 9s" in result.reason
1234
+ assert "run 'ccsched edit telemetry-trim-weekly' to realign" in result.reason
1235
+ assert "install --apply' to see the diff" not in result.reason
1236
+
1237
+
1210
1238
  def test_check_ccsched_job_registered_ok_when_matching_bundled_definition(
1211
1239
  monkeypatch, tmp_path
1212
1240
  ) -> None:
@@ -3,7 +3,9 @@ from __future__ import annotations
3
3
  from pathlib import Path
4
4
 
5
5
  from cc_session_tools.lib.scheduler import bundled_jobs
6
- from cc_session_tools.lib.scheduler.bundled_jobs import diff_from_bundled
6
+ from cc_session_tools.lib.scheduler.bundled_jobs import (
7
+ diff_from_bundled, diff_from_bundled_detail, render_field_diffs,
8
+ )
7
9
  from cc_session_tools.lib.scheduler.jobspec import validate_job_fields
8
10
 
9
11
 
@@ -209,3 +211,60 @@ def test_diff_from_bundled_detects_a_changed_command():
209
211
  job = _job("pdata-verify-all")
210
212
  spec = _spec_for(job, command=["ccst", "pdata", "verify", "--project", "x"])
211
213
  assert diff_from_bundled(spec, job) == ("command",)
214
+
215
+
216
+ def test_diff_from_bundled_detail_is_empty_when_nothing_differs():
217
+ job = _job("pdata-verify-all")
218
+ assert diff_from_bundled_detail(_spec_for(job), job) == ()
219
+
220
+
221
+ def test_diff_from_bundled_detail_carries_before_and_after_for_one_field():
222
+ job = _job("pdata-verify-all")
223
+ spec = _spec_for(job, timeout="30s")
224
+ diffs = diff_from_bundled_detail(spec, job)
225
+ assert len(diffs) == 1
226
+ assert diffs[0].field == "timeout"
227
+ assert diffs[0].bundled == job.timeout
228
+ assert diffs[0].current == "30s"
229
+
230
+
231
+ def test_diff_from_bundled_detail_carries_every_differing_field_in_order():
232
+ job = _job("pdata-verify-all")
233
+ spec = _spec_for(job, cadence="every:3d", timeout="30s")
234
+ diffs = diff_from_bundled_detail(spec, job)
235
+ assert [d.field for d in diffs] == ["cadence", "timeout"]
236
+ assert diffs[0].bundled == job.cadence
237
+ assert diffs[0].current == "every:3d"
238
+
239
+
240
+ def test_diff_from_bundled_detail_renders_command_as_a_shell_line_not_a_tuple_repr():
241
+ job = _job("pdata-verify-all")
242
+ spec = _spec_for(job, command=["ccst", "pdata", "verify", "--project", "x y"])
243
+ diffs = diff_from_bundled_detail(spec, job)
244
+ assert len(diffs) == 1
245
+ assert diffs[0].field == "command"
246
+ assert diffs[0].bundled == "ccst pdata verify --all-projects"
247
+ # shlex.join quotes the argument containing a space, unlike a bare join(" ")
248
+ assert diffs[0].current == "ccst pdata verify --project 'x y'"
249
+
250
+
251
+ def test_diff_from_bundled_detail_is_the_single_source_diff_from_bundled_wraps():
252
+ """diff_from_bundled must return exactly the field names diff_from_bundled_detail
253
+ reports, in the same order — the two can never disagree since one wraps the other."""
254
+ job = _job("pdata-verify-all")
255
+ spec = _spec_for(job, cadence="every:3d", timeout="30s")
256
+ assert diff_from_bundled(spec, job) == tuple(
257
+ d.field for d in diff_from_bundled_detail(spec, job)
258
+ )
259
+
260
+
261
+ def test_render_field_diffs_shows_dash_bundled_plus_current_per_field():
262
+ job = _job("pdata-verify-all")
263
+ spec = _spec_for(job, cadence="every:3d", timeout="30s")
264
+ out = render_field_diffs(diff_from_bundled_detail(spec, job))
265
+ assert "cadence:" in out
266
+ assert "- bundled: daily@03:00" in out
267
+ assert "+ current: every:3d" in out
268
+ assert "timeout:" in out
269
+ assert "- bundled: 300s" in out
270
+ assert "+ current: 30s" in out