cctally 1.93.0 → 1.94.0

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.
@@ -2938,7 +2938,12 @@ def _build_db_parser(subparsers, name, *, help_text, xref=None):
2938
2938
  db_recover.add_argument(
2939
2939
  "--yes",
2940
2940
  action="store_true",
2941
- help="Required for --db stats (non-re-derivable; may need a re-record)",
2941
+ help=(
2942
+ "No longer required: --db stats is retired because stats.db is a "
2943
+ "disposable index rebuilt from the journal (use `db rebuild --db "
2944
+ "stats`); a pre-cutover install whose stats.db may be the only "
2945
+ "copy of its history is repaired with `db repair --db stats --yes`"
2946
+ ),
2942
2947
  )
2943
2948
  db_recover.set_defaults(func=c.cmd_db_recover)
2944
2949
  db_rebuild = db_sub.add_parser(
@@ -3046,6 +3051,50 @@ def _build_db_parser(subparsers, name, *, help_text, xref=None):
3046
3051
  help=argparse.SUPPRESS,
3047
3052
  )
3048
3053
  db_backup.set_defaults(func=c.cmd_db_backup)
3054
+ db_prune = db_sub.add_parser(
3055
+ "prune",
3056
+ help="Reclaim retained corruption evidence the policy no longer keeps",
3057
+ formatter_class=CLIHelpFormatter,
3058
+ description=textwrap.dedent(
3059
+ """\
3060
+ Reclaim the quarantine incidents, forensics bundles,
3061
+ rebuild records and machine-written backups that the
3062
+ configured retention policy no longer keeps.
3063
+
3064
+ Preview by default: nothing is deleted until you pass
3065
+ --yes. Evidence cctally cannot classify is NEVER deleted,
3066
+ in either mode, and the last surviving example of each
3067
+ distinct damage shape is always kept, for as many shapes
3068
+ as max_shape_examples allows.
3069
+
3070
+ The policy lives at storage.artifact_retention in
3071
+ config.json. If it is present but malformed, this command
3072
+ exits 2 without deleting anything.
3073
+
3074
+ Examples:
3075
+ cctally db prune
3076
+ cctally db prune --yes
3077
+ cctally db prune --json
3078
+ cctally db prune --yes --include-backups
3079
+ """
3080
+ ),
3081
+ )
3082
+ db_prune.add_argument(
3083
+ "--yes",
3084
+ action="store_true",
3085
+ help="Apply the plan (default: preview only)",
3086
+ )
3087
+ db_prune.add_argument(
3088
+ "--include-backups",
3089
+ action="store_true",
3090
+ help="Also reclaim user-requested `cctally db backup` families",
3091
+ )
3092
+ db_prune.add_argument(
3093
+ "--json",
3094
+ action="store_true",
3095
+ help="Emit JSON to stdout",
3096
+ )
3097
+ db_prune.set_defaults(func=c.cmd_db_prune)
3049
3098
  db_checkpoint = db_sub.add_parser(
3050
3099
  "checkpoint",
3051
3100
  help="Drain the WAL (TRUNCATE checkpoint) — fast, non-destructive",
@@ -3401,6 +3450,26 @@ def _build_stats_corruption_heal_parser(subparsers, name, *, help_text, xref=Non
3401
3450
  )
3402
3451
  worker.set_defaults(func=c.cmd_stats_corruption_heal_internal)
3403
3452
 
3453
+ def _build_artifact_retention_parser(subparsers, name, *, help_text, xref=None):
3454
+ """Build the #496 S6 detached retained-artifact reclamation worker parser."""
3455
+ c = _cctally()
3456
+ worker = subparsers.add_parser(
3457
+ name,
3458
+ help=help_text,
3459
+ formatter_class=CLIHelpFormatter,
3460
+ description=textwrap.dedent(
3461
+ """\
3462
+ Internal subcommand: reclaim retained corruption evidence
3463
+ that the configured retention policy no longer keeps.
3464
+ Scheduled at most once a day by an ordinary successful
3465
+ command, and immediately whenever an interrupted sweep left
3466
+ a plan to finish. Always returns 0; failures are logged and
3467
+ remain retryable.
3468
+ """
3469
+ ),
3470
+ )
3471
+ worker.set_defaults(func=c.cmd_artifact_retention_internal)
3472
+
3404
3473
  def _build_codex_replay_drain_parser(subparsers, name, *, help_text, xref=None):
3405
3474
  """Build the `_codex-replay-drain` parser (public #5 §4)."""
3406
3475
  c = _cctally()
@@ -3504,6 +3573,7 @@ _REGISTRATION = (
3504
3573
  _Reg('_codex-quota-verify', _build_codex_quota_verify_parser, argparse.SUPPRESS, None, None),
3505
3574
  _Reg('_stats-epoch-rebuild', _build_stats_epoch_rebuild_parser, argparse.SUPPRESS, None, None),
3506
3575
  _Reg('_stats-corruption-heal', _build_stats_corruption_heal_parser, argparse.SUPPRESS, None, None),
3576
+ _Reg('_artifact-retention', _build_artifact_retention_parser, argparse.SUPPRESS, None, None),
3507
3577
  _Reg('_codex-replay-drain', _build_codex_replay_drain_parser, argparse.SUPPRESS, None, None),
3508
3578
  _Reg('repair-symlinks', _build_repair_symlinks_parser, argparse.SUPPRESS, None, None),
3509
3579
  )
@@ -788,8 +788,6 @@ def cmd_project(args: argparse.Namespace) -> int:
788
788
  period_end=until_dt,
789
789
  display_tz=display_tz_str,
790
790
  version=c._share_resolve_version(),
791
- theme=args.theme,
792
- reveal_projects=args.reveal_projects,
793
791
  )
794
792
  c._share_render_and_emit(snap, args)
795
793
  return 0
@@ -5185,6 +5185,24 @@ def cmd_hook_tick(args: argparse.Namespace) -> int:
5185
5185
  _hook_tick_log_line(line)
5186
5186
  _hook_tick_log_rotate_if_needed()
5187
5187
 
5188
+ # #496 S6 §5.2, the hook-tick branch. It is admitted HERE rather than from
5189
+ # `main`'s post-command hook because the parent returns at the fork and
5190
+ # never reaches that hook, and because the INLINE FALLBACK — `os.fork()`
5191
+ # failed, so this body is running on Claude Code's blocking path — must not
5192
+ # gain a spawn. `forked` is exactly that discriminator.
5193
+ try:
5194
+ _cctally()._load_sibling(
5195
+ "_cctally_retention"
5196
+ ).maybe_defer_artifact_retention(
5197
+ command="hook-tick",
5198
+ exit_code=0,
5199
+ hook_forked=forked,
5200
+ hook_explain=explain,
5201
+ hook_foreground=foreground,
5202
+ )
5203
+ except Exception:
5204
+ pass
5205
+
5188
5206
  # --- Step 9: exit code ---
5189
5207
  if not explain:
5190
5208
  # Forked child: skip Python's atexit / argparse / cleanup paths
@@ -139,7 +139,6 @@ def cmd_daily(args: argparse.Namespace) -> int:
139
139
  snap = c._build_daily_snapshot(
140
140
  view, period_start=range_start, period_end=range_end,
141
141
  display_tz=display_tz_str, version=c._share_resolve_version(),
142
- theme=args.theme, reveal_projects=args.reveal_projects,
143
142
  )
144
143
  if args.order == "desc":
145
144
  snap = dataclasses.replace(snap, rows=tuple(reversed(snap.rows)))
@@ -249,8 +248,6 @@ def cmd_daily(args: argparse.Namespace) -> int:
249
248
  period_end=range_end,
250
249
  display_tz=display_tz_str,
251
250
  version=c._share_resolve_version(),
252
- theme=args.theme,
253
- reveal_projects=args.reveal_projects,
254
251
  )
255
252
  if args.order == "desc":
256
253
  snap = dataclasses.replace(snap, rows=tuple(reversed(snap.rows)))
@@ -325,8 +322,6 @@ def cmd_monthly(args: argparse.Namespace) -> int:
325
322
  period_end=range_end,
326
323
  display_tz=display_tz_str,
327
324
  version=c._share_resolve_version(),
328
- theme=args.theme,
329
- reveal_projects=args.reveal_projects,
330
325
  )
331
326
  if args.order == "desc":
332
327
  snap = dataclasses.replace(snap, rows=tuple(reversed(snap.rows)))
@@ -460,8 +455,6 @@ def cmd_weekly(args: argparse.Namespace) -> int:
460
455
  period_end=range_end,
461
456
  display_tz=display_tz_str,
462
457
  version=c._share_resolve_version(),
463
- theme=args.theme,
464
- reveal_projects=args.reveal_projects,
465
458
  breakdown_model=bool(getattr(args, "breakdown", False)),
466
459
  )
467
460
  if args.order == "desc":
@@ -618,8 +611,6 @@ def cmd_session(args: argparse.Namespace) -> int:
618
611
  period_end=range_end,
619
612
  display_tz=display_tz_str,
620
613
  version=c._share_resolve_version(),
621
- theme=args.theme,
622
- reveal_projects=args.reveal_projects,
623
614
  top_n=top_n,
624
615
  tz=tz,
625
616
  )