cctally 1.77.0 → 1.79.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.
- package/CHANGELOG.md +28 -0
- package/README.md +2 -0
- package/bin/_cctally_cache.py +140 -2
- package/bin/_cctally_config.py +50 -0
- package/bin/_cctally_core.py +7 -4
- package/bin/_cctally_dashboard.py +352 -58
- package/bin/_cctally_dashboard_conversation.py +4 -3
- package/bin/_cctally_dashboard_envelope.py +12 -0
- package/bin/_cctally_dashboard_sources.py +25 -3
- package/bin/_cctally_doctor.py +35 -4
- package/bin/_cctally_milestone_history.py +845 -0
- package/bin/_cctally_statusline.py +128 -3
- package/bin/_cctally_tui.py +23 -0
- package/bin/_cctally_update.py +338 -15
- package/bin/_cctally_weekrefs.py +6 -2
- package/bin/_lib_codex_conversation.py +1342 -18
- package/bin/_lib_codex_conversation_export.py +21 -1
- package/bin/_lib_codex_conversation_query.py +530 -67
- package/bin/_lib_conversation_dispatch.py +2 -2
- package/bin/_lib_dashboard_json.py +43 -0
- package/bin/_lib_doctor.py +38 -0
- package/bin/_lib_milestone_history.py +158 -0
- package/bin/_lib_source_analytics.py +20 -6
- package/bin/cctally +36 -0
- package/dashboard/static/assets/index-BwvAcS_k.js +92 -0
- package/dashboard/static/assets/index-CrIlpAlQ.css +1 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +4 -1
- package/dashboard/static/assets/index-CvcCeA4L.css +0 -1
- package/dashboard/static/assets/index-DLNXAevv.js +0 -87
|
@@ -787,13 +787,13 @@ def neutral_payload(
|
|
|
787
787
|
|
|
788
788
|
Provider-specific selectors: Claude keeps its existing contract (``tool_use_id``
|
|
789
789
|
+ ``which ∈ {input, result}``), Codex uses ``block_key`` + ``which ∈ {call,
|
|
790
|
-
output}``. Statuses: ``ok`` | ``not_found`` (404) | ``gone`` (410 — the physical
|
|
790
|
+
output, event}``. Statuses: ``ok`` | ``not_found`` (404) | ``gone`` (410 — the physical
|
|
791
791
|
record moved/mutated)."""
|
|
792
792
|
cref = resolve_conversation_ref(ref)
|
|
793
793
|
if cref is None:
|
|
794
794
|
return {"status": "not_found", "conversation_key": ref}
|
|
795
795
|
if cref.source == "codex":
|
|
796
|
-
if not block_key or which not in ("call", "output"):
|
|
796
|
+
if not block_key or which not in ("call", "output", "event"):
|
|
797
797
|
return {"status": "not_found", "block_key": block_key, "which": which}
|
|
798
798
|
return q.read_codex_payload(conn, cref.conversation_key, block_key, which)
|
|
799
799
|
# Claude: tool_use_id + which={input,result}, contract unchanged.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Strict outbound JSON contract shared by dashboard HTTP/SSE and Doctor.
|
|
2
|
+
|
|
3
|
+
Python's ``json.dumps`` defaults to emitting JavaScript-only ``NaN`` and
|
|
4
|
+
``Infinity`` tokens. Browsers reject those tokens in ``JSON.parse`` and
|
|
5
|
+
``Response.json``. Normalize supported JSON containers recursively, then keep
|
|
6
|
+
``allow_nan=False`` as the final fail-loud guard. Unsupported objects and
|
|
7
|
+
non-finite mapping keys are deliberately not coerced.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import math
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def normalize_dashboard_json(value: Any) -> Any:
|
|
17
|
+
"""Return a non-mutating JSON value with non-finite floats mapped to null."""
|
|
18
|
+
if isinstance(value, float):
|
|
19
|
+
return value if math.isfinite(value) else None
|
|
20
|
+
if isinstance(value, dict):
|
|
21
|
+
return {
|
|
22
|
+
key: normalize_dashboard_json(item)
|
|
23
|
+
for key, item in value.items()
|
|
24
|
+
}
|
|
25
|
+
if isinstance(value, list):
|
|
26
|
+
return [normalize_dashboard_json(item) for item in value]
|
|
27
|
+
if isinstance(value, tuple):
|
|
28
|
+
return tuple(normalize_dashboard_json(item) for item in value)
|
|
29
|
+
return value
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def encode_dashboard_json(value: Any, **kwargs: Any) -> str:
|
|
33
|
+
"""Serialize one outbound payload with browser-strict JSON semantics."""
|
|
34
|
+
return json.dumps(
|
|
35
|
+
normalize_dashboard_json(value),
|
|
36
|
+
allow_nan=False,
|
|
37
|
+
**kwargs,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def encode_dashboard_json_bytes(value: Any, **kwargs: Any) -> bytes:
|
|
42
|
+
"""UTF-8 bytes companion for HTTP response bodies."""
|
|
43
|
+
return encode_dashboard_json(value, **kwargs).encode("utf-8")
|
package/bin/_lib_doctor.py
CHANGED
|
@@ -234,6 +234,13 @@ class DoctorState:
|
|
|
234
234
|
# retained corpus from unreadable metadata.
|
|
235
235
|
codex_project_metadata_health: Optional[dict] = None
|
|
236
236
|
codex_project_metadata_error: Optional[str] = None
|
|
237
|
+
# Beta-channel (spec 2026-07-21 §3): the configured RELEASE channel
|
|
238
|
+
# (stable|beta) that `cctally update` tracks — DISTINCT from the preview
|
|
239
|
+
# `channel` (prod|preview) above. Populated by doctor_gather_state from a
|
|
240
|
+
# raw config read (fail-soft to "stable"). Drives the `install.update_channel`
|
|
241
|
+
# check (WARN on beta+brew, else OK). Defaulted (placed last) so existing
|
|
242
|
+
# constructors stay valid and default to the stable posture.
|
|
243
|
+
update_channel: str = "stable"
|
|
237
244
|
|
|
238
245
|
|
|
239
246
|
@dataclasses.dataclass(frozen=True)
|
|
@@ -445,6 +452,36 @@ def _check_install_dev_mode(s: DoctorState) -> CheckResult:
|
|
|
445
452
|
)
|
|
446
453
|
|
|
447
454
|
|
|
455
|
+
def _check_install_update_channel(s: DoctorState) -> CheckResult:
|
|
456
|
+
"""Report the configured update (release) channel alongside the install
|
|
457
|
+
method (beta-channel, spec 2026-07-21 §3).
|
|
458
|
+
|
|
459
|
+
DISTINCT from the preview `channel` in install.mode: this reports the
|
|
460
|
+
`update.channel` config leaf (stable|beta) that `cctally update` tracks.
|
|
461
|
+
WARN on the brew+beta mismatch — Homebrew IS the stable channel (Q2), so
|
|
462
|
+
a beta opt-in on brew silently resolves stable; the WARN makes that
|
|
463
|
+
explicit and actionable. Otherwise always OK (a diagnostic surface, never
|
|
464
|
+
a hard failure — doctor exits 2 only on FAIL)."""
|
|
465
|
+
channel = s.update_channel or "stable"
|
|
466
|
+
if channel == "beta" and s.install_is_brew:
|
|
467
|
+
return CheckResult(
|
|
468
|
+
id="install.update_channel", title="Update channel",
|
|
469
|
+
severity="warn",
|
|
470
|
+
summary="beta (unavailable on Homebrew — installs track stable)",
|
|
471
|
+
remediation=(
|
|
472
|
+
"Homebrew tracks stable only. Install via npm or source to "
|
|
473
|
+
"receive beta releases, or run "
|
|
474
|
+
"`cctally config set update.channel stable`."
|
|
475
|
+
),
|
|
476
|
+
details={"channel": channel, "method": "brew"},
|
|
477
|
+
)
|
|
478
|
+
return CheckResult(
|
|
479
|
+
id="install.update_channel", title="Update channel",
|
|
480
|
+
severity="ok", summary=channel, remediation=None,
|
|
481
|
+
details={"channel": channel},
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
|
|
448
485
|
_REQUIRED_HOOK_EVENTS = ("PostToolBatch", "Stop", "SubagentStop")
|
|
449
486
|
|
|
450
487
|
|
|
@@ -1830,6 +1867,7 @@ def _check_db_conversations_reclaimable(s: DoctorState) -> CheckResult:
|
|
|
1830
1867
|
_CATEGORY_DEFINITIONS: tuple[tuple[str, str, tuple[tuple[str, str], ...]], ...] = (
|
|
1831
1868
|
("install", "Install", (
|
|
1832
1869
|
("install.mode", "_check_install_dev_mode"),
|
|
1870
|
+
("install.update_channel", "_check_install_update_channel"),
|
|
1833
1871
|
("install.symlinks", "_check_install_symlinks"),
|
|
1834
1872
|
("install.path", "_check_install_path"),
|
|
1835
1873
|
("install.legacy_snippet", "_check_install_legacy_snippet"),
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Pure kernel for the hero-modal milestone-history feature (stdlib only).
|
|
2
|
+
|
|
3
|
+
Small, dependency-free helpers shared by the Claude + Codex history glue
|
|
4
|
+
in ``_cctally_milestone_history.py``:
|
|
5
|
+
|
|
6
|
+
* ``compute_detail_stamp`` — content digest used both on the SSE index
|
|
7
|
+
entry and the detail response so the client can cache by
|
|
8
|
+
``(source, key, detail_stamp)`` and revalidate when a week's underlying
|
|
9
|
+
rows move (spec §2 caching/invalidation).
|
|
10
|
+
* ``intersects`` — half-open interval intersection on epoch seconds
|
|
11
|
+
(a block appears in every week it intersects, spec §1b dual membership).
|
|
12
|
+
* ``coalesce_week_refs`` — collapse same-``key`` ``WeekRef`` segments
|
|
13
|
+
produced by reset correction for an in-place-credited week into ONE
|
|
14
|
+
navigation entry spanning the outer cycle boundary (spec §1a); the
|
|
15
|
+
per-segment structure lives in the detail payload, never as duplicate
|
|
16
|
+
chips.
|
|
17
|
+
|
|
18
|
+
No imports of the cctally namespace or ``_cctally_core`` — these are pure
|
|
19
|
+
functions. ``coalesce_week_refs`` is generic over any frozen dataclass
|
|
20
|
+
carrying ``key`` / ``week_start_at`` / ``week_end_at`` string fields
|
|
21
|
+
(the ``WeekRef`` shape) via ``dataclasses.replace``.
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import datetime as dt
|
|
26
|
+
import hashlib
|
|
27
|
+
from dataclasses import replace
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# Codex ``quota_window_blocks`` rows carry second-level capture jitter in their
|
|
31
|
+
# ``resets_at``: one physical reset (weekly *or* 5h) surfaces as several rows
|
|
32
|
+
# whose resets differ by a few seconds. This is the Codex-scoped analogue of
|
|
33
|
+
# the Claude 5h jitter floor (``_FIVE_HOUR_JITTER_FLOOR_SECONDS``) — same
|
|
34
|
+
# concept, different provider. Deliberately NOT ``_canonical_5h_window_key``,
|
|
35
|
+
# which is Claude-scoped per spec §1c. 600s (10 min) sits far above the
|
|
36
|
+
# observed jitter yet far below any genuine early re-anchor (hours apart) or a
|
|
37
|
+
# real weekly reset (~7 days apart), so only jitter is collapsed.
|
|
38
|
+
CODEX_CYCLE_JITTER_FLOOR_SECONDS = 600
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def cluster_by_reset_jitter(items, *, reset_key, floor_seconds=CODEX_CYCLE_JITTER_FLOOR_SECONDS):
|
|
42
|
+
"""Group items that share one physical reset, collapsing capture jitter.
|
|
43
|
+
|
|
44
|
+
``items`` is any iterable; ``reset_key(item)`` returns that item's reset as
|
|
45
|
+
an epoch ``int``/``float``. Items are sorted by reset ascending, and each
|
|
46
|
+
run of consecutive items whose neighbour-to-neighbour reset gap is
|
|
47
|
+
``<= floor_seconds`` forms one cluster. A gap greater than the floor starts
|
|
48
|
+
a new cluster, so genuine early re-anchors (hours apart) and real weekly
|
|
49
|
+
resets (~7 days apart) stay in distinct clusters — only sub-floor jitter is
|
|
50
|
+
merged.
|
|
51
|
+
|
|
52
|
+
Returns a list of clusters (each a non-empty list of the original items),
|
|
53
|
+
ordered by ascending cluster reset; within a cluster items follow the reset
|
|
54
|
+
sort. An empty ``items`` yields ``[]``.
|
|
55
|
+
"""
|
|
56
|
+
ordered = sorted(items, key=reset_key)
|
|
57
|
+
clusters: list = []
|
|
58
|
+
current: list = []
|
|
59
|
+
prev = None
|
|
60
|
+
for item in ordered:
|
|
61
|
+
r = reset_key(item)
|
|
62
|
+
if prev is None or (r - prev) <= floor_seconds:
|
|
63
|
+
current.append(item)
|
|
64
|
+
else:
|
|
65
|
+
clusters.append(current)
|
|
66
|
+
current = [item]
|
|
67
|
+
prev = r
|
|
68
|
+
if current:
|
|
69
|
+
clusters.append(current)
|
|
70
|
+
return clusters
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def compute_detail_stamp(*parts: object) -> str:
|
|
74
|
+
"""Return a 16-hex-char sha256 digest over ``"|"``-joined ``str(p)``.
|
|
75
|
+
|
|
76
|
+
``None`` parts serialize as the empty string so a nullable count/timestamp
|
|
77
|
+
contributes deterministically. Truncating to 16 chars keeps the wire small
|
|
78
|
+
while staying collision-safe for the small per-week input space.
|
|
79
|
+
"""
|
|
80
|
+
joined = "|".join("" if p is None else str(p) for p in parts)
|
|
81
|
+
return hashlib.sha256(joined.encode("utf-8")).hexdigest()[:16]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def intersects(start_a: int, end_a: int, start_b: int, end_b: int) -> bool:
|
|
85
|
+
"""Half-open interval intersection on epoch seconds.
|
|
86
|
+
|
|
87
|
+
``[start_a, end_a)`` overlaps ``[start_b, end_b)`` iff each interval
|
|
88
|
+
starts before the other ends. Used for block/week dual membership so a
|
|
89
|
+
block straddling a week boundary is reported in every week it intersects.
|
|
90
|
+
"""
|
|
91
|
+
return start_a < end_b and start_b < end_a
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _parse_epoch(value: "str | None") -> "int | None":
|
|
95
|
+
"""Parse a canonical ISO-8601 timestamp to an epoch int, or ``None``.
|
|
96
|
+
|
|
97
|
+
Accepts a trailing ``Z`` or an explicit offset; a naive value is treated
|
|
98
|
+
as UTC. Returns ``None`` on empty / unparseable input so callers can skip
|
|
99
|
+
boundary-less refs.
|
|
100
|
+
"""
|
|
101
|
+
if not value:
|
|
102
|
+
return None
|
|
103
|
+
try:
|
|
104
|
+
s = value.replace("Z", "+00:00")
|
|
105
|
+
parsed = dt.datetime.fromisoformat(s)
|
|
106
|
+
except ValueError:
|
|
107
|
+
return None
|
|
108
|
+
if parsed.tzinfo is None:
|
|
109
|
+
parsed = parsed.replace(tzinfo=dt.timezone.utc)
|
|
110
|
+
return int(parsed.timestamp())
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def coalesce_week_refs(refs: list) -> list:
|
|
114
|
+
"""Collapse same-``key`` refs into one ref spanning the outer boundary.
|
|
115
|
+
|
|
116
|
+
Input is the ``get_recent_weeks`` ``WeekRef`` list (newest-first). An
|
|
117
|
+
in-place-credited week is represented there as TWO same-``key`` refs
|
|
118
|
+
(a pre-credit segment ``[orig_start, effective)`` and a post-credit
|
|
119
|
+
segment ``[effective, orig_end)``); every other week is a single ref.
|
|
120
|
+
|
|
121
|
+
The result preserves the newest-first order of each key's FIRST
|
|
122
|
+
occurrence, and for a multi-ref key emits one ref whose
|
|
123
|
+
``week_start_at`` is the earliest segment start and ``week_end_at`` the
|
|
124
|
+
latest segment end (the outer cycle boundary). The original ISO strings
|
|
125
|
+
are carried through unchanged (no reformatting drift); comparison is on
|
|
126
|
+
parsed epochs so mixed ``Z``/offset forms order correctly.
|
|
127
|
+
"""
|
|
128
|
+
order: list = [] # keys in first-occurrence order
|
|
129
|
+
groups: dict = {} # key -> list of refs
|
|
130
|
+
for ref in refs:
|
|
131
|
+
k = ref.key
|
|
132
|
+
if k not in groups:
|
|
133
|
+
groups[k] = []
|
|
134
|
+
order.append(k)
|
|
135
|
+
groups[k].append(ref)
|
|
136
|
+
|
|
137
|
+
out: list = []
|
|
138
|
+
for k in order:
|
|
139
|
+
members = groups[k]
|
|
140
|
+
if len(members) == 1:
|
|
141
|
+
out.append(members[0])
|
|
142
|
+
continue
|
|
143
|
+
# Pick earliest start / latest end across the same-key segments.
|
|
144
|
+
earliest = members[0]
|
|
145
|
+
earliest_e = _parse_epoch(members[0].week_start_at)
|
|
146
|
+
latest_end_iso = members[0].week_end_at
|
|
147
|
+
latest_e = _parse_epoch(members[0].week_end_at)
|
|
148
|
+
for m in members[1:]:
|
|
149
|
+
se = _parse_epoch(m.week_start_at)
|
|
150
|
+
if se is not None and (earliest_e is None or se < earliest_e):
|
|
151
|
+
earliest, earliest_e = m, se
|
|
152
|
+
ee = _parse_epoch(m.week_end_at)
|
|
153
|
+
if ee is not None and (latest_e is None or ee > latest_e):
|
|
154
|
+
latest_e, latest_end_iso = ee, m.week_end_at
|
|
155
|
+
# Base on the earliest-start ref (its start_at/start date are the
|
|
156
|
+
# outer start), widen its end to the latest segment end.
|
|
157
|
+
out.append(replace(earliest, week_end_at=latest_end_iso))
|
|
158
|
+
return out
|
|
@@ -83,9 +83,26 @@ def assign_collision_safe_project_labels(
|
|
|
83
83
|
selection the exact string that terminal, JSON, and share emit.
|
|
84
84
|
"""
|
|
85
85
|
values = tuple(entries)
|
|
86
|
+
assigned = collision_safe_project_label_map(
|
|
87
|
+
(entry.project_key, entry.project_label) for entry in values
|
|
88
|
+
)
|
|
89
|
+
return tuple(
|
|
90
|
+
replace(entry, display_label=assigned.get(entry.project_key, entry.project_label))
|
|
91
|
+
for entry in values
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def collision_safe_project_label_map(
|
|
96
|
+
identities_and_labels: Iterable[tuple[str, str]],
|
|
97
|
+
) -> dict[str, str]:
|
|
98
|
+
"""Allocate the shared deterministic privacy-safe label contract.
|
|
99
|
+
|
|
100
|
+
Callers supply opaque internal identities. Only the returned labels are
|
|
101
|
+
presentation data; the identity keys must remain inside the adapter.
|
|
102
|
+
"""
|
|
86
103
|
keys_by_label: dict[str, set[str]] = defaultdict(set)
|
|
87
|
-
for
|
|
88
|
-
keys_by_label[
|
|
104
|
+
for identity, label in identities_and_labels:
|
|
105
|
+
keys_by_label[label].add(identity)
|
|
89
106
|
# Never let an allocator-owned ``label (N)`` shadow a literal project
|
|
90
107
|
# label. Reserving every raw label also makes a later presentation pass
|
|
91
108
|
# unnecessary, which avoids producing ``label (N) (N)``.
|
|
@@ -107,10 +124,7 @@ def assign_collision_safe_project_labels(
|
|
|
107
124
|
assigned[key] = candidate
|
|
108
125
|
used_labels.add(candidate)
|
|
109
126
|
ordinal += 1
|
|
110
|
-
return
|
|
111
|
-
replace(entry, display_label=assigned.get(entry.project_key, entry.project_label))
|
|
112
|
-
for entry in values
|
|
113
|
-
)
|
|
127
|
+
return assigned
|
|
114
128
|
|
|
115
129
|
|
|
116
130
|
def opaque_project_key(source: str, root_key: str, resolved_key: str) -> str:
|
package/bin/cctally
CHANGED
|
@@ -558,6 +558,10 @@ _lib_log = _load_sibling("_lib_log")
|
|
|
558
558
|
_lib_json_envelope = _load_sibling("_lib_json_envelope")
|
|
559
559
|
stamp_schema_version = _lib_json_envelope.stamp_schema_version
|
|
560
560
|
|
|
561
|
+
# Strict outbound dashboard/Doctor JSON: recursive non-finite normalization
|
|
562
|
+
# followed by allow_nan=False serialization.
|
|
563
|
+
_lib_dashboard_json = _load_sibling("_lib_dashboard_json")
|
|
564
|
+
|
|
561
565
|
_lib_changelog = _load_sibling("_lib_changelog")
|
|
562
566
|
# Backward-compat re-export: callers and tests reach the helper through
|
|
563
567
|
# ``cctally._release_read_latest_release_version`` (incl. monkeypatch
|
|
@@ -1127,6 +1131,10 @@ _UPDATE_CHECK_TTL_HOURS_MIN = _cctally_update._UPDATE_CHECK_TTL_HOURS_MIN
|
|
|
1127
1131
|
_UPDATE_CHECK_TTL_HOURS_MAX = _cctally_update._UPDATE_CHECK_TTL_HOURS_MAX
|
|
1128
1132
|
_normalize_update_check_enabled_value = _cctally_update._normalize_update_check_enabled_value
|
|
1129
1133
|
_validate_update_check_ttl_hours_value = _cctally_update._validate_update_check_ttl_hours_value
|
|
1134
|
+
UPDATE_CHANNELS = _cctally_update.UPDATE_CHANNELS
|
|
1135
|
+
UPDATE_CHANNEL_DEFAULT = _cctally_update.UPDATE_CHANNEL_DEFAULT
|
|
1136
|
+
_validate_update_channel_value = _cctally_update._validate_update_channel_value
|
|
1137
|
+
resolve_update_channel = _cctally_update.resolve_update_channel
|
|
1130
1138
|
_UPDATE_STATE_SCHEMA_MAX = _cctally_update._UPDATE_STATE_SCHEMA_MAX
|
|
1131
1139
|
_UPDATE_SUPPRESS_SCHEMA_MAX = _cctally_update._UPDATE_SUPPRESS_SCHEMA_MAX
|
|
1132
1140
|
_load_update_state = _cctally_update._load_update_state
|
|
@@ -1149,6 +1157,9 @@ _BREW_VERSION_RE_LIST = _cctally_update._BREW_VERSION_RE_LIST
|
|
|
1149
1157
|
_update_user_agent = _cctally_update._update_user_agent
|
|
1150
1158
|
_fetch_url = _cctally_update._fetch_url
|
|
1151
1159
|
_check_npm_latest_version = _cctally_update._check_npm_latest_version
|
|
1160
|
+
_fetch_npm_dist_tag_version = _cctally_update._fetch_npm_dist_tag_version
|
|
1161
|
+
_resolve_npm_channel_target = _cctally_update._resolve_npm_channel_target
|
|
1162
|
+
_semver_or_parse_error = _cctally_update._semver_or_parse_error
|
|
1152
1163
|
_check_brew_latest_version = _cctally_update._check_brew_latest_version
|
|
1153
1164
|
_is_update_check_due = _cctally_update._is_update_check_due
|
|
1154
1165
|
_do_update_check = _cctally_update._do_update_check
|
|
@@ -1158,6 +1169,9 @@ _spawn_background_telemetry_beat = _cctally_update._spawn_background_telemetry_b
|
|
|
1158
1169
|
cmd_telemetry_beat_internal = _cctally_update.cmd_telemetry_beat_internal
|
|
1159
1170
|
SKIP_USE_STATE_LATEST = _cctally_update.SKIP_USE_STATE_LATEST
|
|
1160
1171
|
_format_update_command = _cctally_update._format_update_command
|
|
1172
|
+
BREW_BETA_ADVISORY = _cctally_update.BREW_BETA_ADVISORY
|
|
1173
|
+
_resolved_install_target = _cctally_update._resolved_install_target
|
|
1174
|
+
_resolved_update_command = _cctally_update._resolved_update_command
|
|
1161
1175
|
_prerelease_note = _cctally_update._prerelease_note
|
|
1162
1176
|
_format_update_check_json = _cctally_update._format_update_check_json
|
|
1163
1177
|
_UPDATE_METHOD_HUMAN_LABEL = _cctally_update._UPDATE_METHOD_HUMAN_LABEL
|
|
@@ -1336,6 +1350,7 @@ _build_statusline_injections = _cctally_statusline._build_statusline_injections
|
|
|
1336
1350
|
# `cctally._statusline_persist` (test surface) + the `_cctally()` accessor
|
|
1337
1351
|
# resolve them.
|
|
1338
1352
|
_statusline_persist = _cctally_statusline._statusline_persist
|
|
1353
|
+
_statusline_oauth_tick = _cctally_statusline._statusline_oauth_tick
|
|
1339
1354
|
_fork_persist = _cctally_statusline._fork_persist
|
|
1340
1355
|
_record_args = _cctally_statusline._record_args
|
|
1341
1356
|
_try_acquire_persist_lock = _cctally_statusline._try_acquire_persist_lock
|
|
@@ -1626,6 +1641,14 @@ UPDATE_NPM_REGISTRY_URL = os.environ.get(
|
|
|
1626
1641
|
"CCTALLY_TEST_UPDATE_NPM_URL",
|
|
1627
1642
|
"https://registry.npmjs.org/cctally/latest",
|
|
1628
1643
|
)
|
|
1644
|
+
# Beta dist-tag endpoint (beta-channel, spec 2026-07-21 §3). The client
|
|
1645
|
+
# resolves the beta target as SemVer-max(dist-tags.beta, dist-tags.latest);
|
|
1646
|
+
# an exact 404 here means the `beta` dist-tag is not yet published
|
|
1647
|
+
# (transition window → fall back to latest). Env-overridable for fixtures.
|
|
1648
|
+
UPDATE_NPM_BETA_REGISTRY_URL = os.environ.get(
|
|
1649
|
+
"CCTALLY_TEST_UPDATE_NPM_BETA_URL",
|
|
1650
|
+
"https://registry.npmjs.org/cctally/beta",
|
|
1651
|
+
)
|
|
1629
1652
|
UPDATE_BREW_FORMULA_URL = os.environ.get(
|
|
1630
1653
|
"CCTALLY_TEST_UPDATE_BREW_URL",
|
|
1631
1654
|
"https://raw.githubusercontent.com/omrikais/homebrew-cctally/main/Formula/cctally.rb",
|
|
@@ -2931,6 +2954,18 @@ _RESET_ZERO_MIN_DROP_PCT = _cctally_weekrefs._RESET_ZERO_MIN_DROP_PCT
|
|
|
2931
2954
|
_is_reset_drop = _cctally_weekrefs._is_reset_drop
|
|
2932
2955
|
|
|
2933
2956
|
|
|
2957
|
+
# Eager re-export of bin/_cctally_milestone_history.py — the hero-modal
|
|
2958
|
+
# historical milestone navigation glue (week/cycle index + week/cycle detail).
|
|
2959
|
+
# Reached via `c.` from the envelope build, the source build, and the
|
|
2960
|
+
# /api/milestones route handler. Refs get their impure cctally symbols via the
|
|
2961
|
+
# module's own call-time _cctally() accessor, so load order is irrelevant.
|
|
2962
|
+
_cctally_milestone_history = _load_sibling("_cctally_milestone_history")
|
|
2963
|
+
build_claude_week_index = _cctally_milestone_history.build_claude_week_index
|
|
2964
|
+
build_claude_week_detail = _cctally_milestone_history.build_claude_week_detail
|
|
2965
|
+
build_codex_cycle_index = _cctally_milestone_history.build_codex_cycle_index
|
|
2966
|
+
build_codex_cycle_detail = _cctally_milestone_history.build_codex_cycle_detail
|
|
2967
|
+
|
|
2968
|
+
|
|
2934
2969
|
# Eager re-export of bin/_cctally_percent_breakdown.py — the percent-breakdown
|
|
2935
2970
|
# handler (C8, #125 Batch B). Parser dispatch reaches c.cmd_percent_breakdown.
|
|
2936
2971
|
_cctally_percent_breakdown = _load_sibling("_cctally_percent_breakdown")
|
|
@@ -3075,6 +3110,7 @@ _tui_sparkline_big = _cctally_tui._tui_sparkline_big
|
|
|
3075
3110
|
_tui_width_bucket = _cctally_tui._tui_width_bucket
|
|
3076
3111
|
# Snapshot builders (per-panel + orchestrator + empty)
|
|
3077
3112
|
_tui_build_percent_milestones = _cctally_tui._tui_build_percent_milestones
|
|
3113
|
+
_tui_build_five_hour_milestones = _cctally_tui._tui_build_five_hour_milestones # milestone-history week detail c.
|
|
3078
3114
|
_tui_build_current_week = _cctally_tui._tui_build_current_week
|
|
3079
3115
|
_tui_build_forecast = _cctally_tui._tui_build_forecast
|
|
3080
3116
|
_tui_build_trend = _cctally_tui._tui_build_trend
|