zeno-cli 0.3.4__py3-none-any.whl
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.
- zeno_adapters/__init__.py +17 -0
- zeno_adapters/_common.py +38 -0
- zeno_adapters/anthropic.py +68 -0
- zeno_adapters/claude_code.py +101 -0
- zeno_adapters/crewai.py +92 -0
- zeno_adapters/langgraph.py +49 -0
- zeno_adapters/openai.py +108 -0
- zeno_cli/__init__.py +1 -0
- zeno_cli/_hooks/cc_bridge.py +1016 -0
- zeno_cli/doctor.py +535 -0
- zeno_cli/hook_install.py +269 -0
- zeno_cli/hud/__init__.py +1 -0
- zeno_cli/hud/hud_install.py +652 -0
- zeno_cli/hud/zeno_attention.py +288 -0
- zeno_cli/hud/zeno_cognition.py +457 -0
- zeno_cli/hud/zeno_hud.py +496 -0
- zeno_cli/interview_invites.py +342 -0
- zeno_cli/login.py +241 -0
- zeno_cli/main.py +2534 -0
- zeno_cli/onboard.py +206 -0
- zeno_cli/outreach.py +456 -0
- zeno_cli/version.py +67 -0
- zeno_cli-0.3.4.dist-info/METADATA +161 -0
- zeno_cli-0.3.4.dist-info/RECORD +69 -0
- zeno_cli-0.3.4.dist-info/WHEEL +4 -0
- zeno_cli-0.3.4.dist-info/entry_points.txt +4 -0
- zeno_core/__init__.py +67 -0
- zeno_core/analytics.py +193 -0
- zeno_core/rtlx_s.py +460 -0
- zeno_core/streak.py +178 -0
- zeno_core/tlx_s.py +192 -0
- zeno_sdk/__init__.py +6 -0
- zeno_sdk/_generated/__init__.py +6 -0
- zeno_sdk/_generated/client.py +819 -0
- zeno_sdk/_migrations/alembic/env.py +33 -0
- zeno_sdk/_migrations/alembic/script.py.mako +18 -0
- zeno_sdk/_migrations/alembic/versions/0001_initial.py +79 -0
- zeno_sdk/_migrations/alembic/versions/0002_cognition_samples.py +53 -0
- zeno_sdk/_migrations/alembic/versions/0003_cognition_drivers.py +41 -0
- zeno_sdk/_migrations/alembic/versions/0004_transcript_intelligence.py +248 -0
- zeno_sdk/_migrations/alembic.ini +35 -0
- zeno_sdk/_runtime.py +12 -0
- zeno_sdk/adapters/__init__.py +15 -0
- zeno_sdk/adapters/anthropic.py +5 -0
- zeno_sdk/adapters/claude_code.py +5 -0
- zeno_sdk/adapters/crewai.py +5 -0
- zeno_sdk/adapters/langgraph.py +5 -0
- zeno_sdk/adapters/openai.py +5 -0
- zeno_sdk/auth.py +25 -0
- zeno_sdk/client.py +87 -0
- zeno_sdk/config.py +61 -0
- zeno_sdk/daemon.py +72 -0
- zeno_sdk/privacy.py +46 -0
- zeno_sdk/session.py +179 -0
- zeno_sdk/storage.py +487 -0
- zeno_sdk/types/__init__.py +121 -0
- zeno_session_intel/__init__.py +19 -0
- zeno_session_intel/analytics.py +588 -0
- zeno_session_intel/compression.py +123 -0
- zeno_session_intel/ingest.py +376 -0
- zeno_session_intel/model.py +129 -0
- zeno_session_intel/parsers/__init__.py +31 -0
- zeno_session_intel/parsers/claude_code.py +169 -0
- zeno_session_intel/parsers/codex.py +265 -0
- zeno_session_intel/parsers/cursor.py +198 -0
- zeno_session_intel/prices.py +281 -0
- zeno_session_intel/schema.py +277 -0
- zeno_session_intel/signals.py +319 -0
- zeno_session_intel/taxonomy.py +71 -0
zeno_cli/hud/zeno_hud.py
ADDED
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""zeno-hud - a Claude Code statusline that shows, every render (~300ms):
|
|
3
|
+
|
|
4
|
+
zeno · Opus 4.8 · zeno
|
|
5
|
+
ctx ███████░░░ 68% 5h ██░░░░░░░░ 22% wk ████░░░░░░ 41%
|
|
6
|
+
att ███████▇░░ 74 ▲ sharp · keep going eff ████████ 60 · drv ██████░░ 45
|
|
7
|
+
|
|
8
|
+
The first three meters (context %, 5h usage %, weekly usage %) come straight from
|
|
9
|
+
the JSON Claude Code pipes on stdin - no API call. The bottom line is the cognition
|
|
10
|
+
read - three bars sharing one model (zeno_cognition):
|
|
11
|
+
|
|
12
|
+
att the composite attention score, with its label/nudge ("sharp · keep going")
|
|
13
|
+
and, when notable, the most-deviant OFF-bar driver as a tag (e.g. ↑fatigue).
|
|
14
|
+
eff how hard you are driving this turn - the effort driver.
|
|
15
|
+
drv how hands-on you are vs delegating - the inverse of the autonomy (AI-led)
|
|
16
|
+
driver, i.e. your share of the wheel.
|
|
17
|
+
|
|
18
|
+
Capped at three bars on purpose. The session count + RTLX-S SCED progress moved to
|
|
19
|
+
the tailnet dashboard - the bar stays a pure, glanceable cognitive read.
|
|
20
|
+
|
|
21
|
+
Design: stdlib-only, Python 3.9+ safe, never raises (a broken statusline shows
|
|
22
|
+
nothing), targets <60ms. The attention model is a documented, tunable v1 heuristic
|
|
23
|
+
(see README.md); env vars ZENO_HUD_* override the weights/widths.
|
|
24
|
+
|
|
25
|
+
Pairs with the zeno-cc-bridge hook (the WRITE/capture surface); this is the READ
|
|
26
|
+
surface. Wire via ~/.claude/settings.json statusLine -> see README.md.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
import json
|
|
30
|
+
import os
|
|
31
|
+
import re
|
|
32
|
+
import sys
|
|
33
|
+
|
|
34
|
+
# zeno_attention is the single source of truth for the attention formula: the
|
|
35
|
+
# cc-bridge hook persists the score and the bar reads it back, so HUD and dashboard
|
|
36
|
+
# never diverge (see docs/COGNITION_UNIFIED_PLAN.md Phase 2). zeno_cognition is the v2
|
|
37
|
+
# per-turn model the cc-bridge hook writes and the HUD reads back through
|
|
38
|
+
# COG.present_sample (falling back to the v1 transcript compute when no row exists).
|
|
39
|
+
# Both are siblings in this package (zeno_cli.hud), so the imports are plain relative.
|
|
40
|
+
from . import zeno_attention as A
|
|
41
|
+
from . import zeno_cognition as COG
|
|
42
|
+
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
# config (env-overridable)
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
NO_COLOR = bool(os.environ.get("NO_COLOR"))
|
|
47
|
+
BAR_W = int(os.environ.get("ZENO_HUD_BAR_WIDTH", "10"))
|
|
48
|
+
ATT_BAR_W = int(os.environ.get("ZENO_HUD_ATT_WIDTH", "10"))
|
|
49
|
+
COG_BAR_W = int(os.environ.get("ZENO_HUD_COG_WIDTH", "8")) # the eff/drv sub-bars
|
|
50
|
+
ZENO_DB = os.environ.get("ZENO_DB_PATH") or os.path.join(
|
|
51
|
+
os.environ.get("ZENO_HOME", os.path.join(os.path.expanduser("~"), ".zeno")), "zeno.db"
|
|
52
|
+
)
|
|
53
|
+
# attention model knobs live in the shared module; re-exported so existing
|
|
54
|
+
# importers/tests that read H.WINDOW / H.TAIL_BYTES keep working unchanged.
|
|
55
|
+
W_EFFORT, W_DELIB, W_TREND = A.W_EFFORT, A.W_DELIB, A.W_TREND
|
|
56
|
+
WINDOW = A.WINDOW
|
|
57
|
+
TAIL_BYTES = A.TAIL_BYTES
|
|
58
|
+
|
|
59
|
+
# ANSI codes (GREEN/YELLOW/RED/CYAN match zeno_attention's color bands)
|
|
60
|
+
GREEN, YELLOW, RED, CYAN, DIM, BOLD = "92", "93", "91", "96", "90", "1"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
# rendering primitives
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
def c(s, code):
|
|
67
|
+
"""Wrap s in an ANSI color unless NO_COLOR."""
|
|
68
|
+
if NO_COLOR or code is None:
|
|
69
|
+
return s
|
|
70
|
+
return f"\x1b[{code}m{s}\x1b[0m"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def bar(frac, width=BAR_W):
|
|
74
|
+
"""A unicode block bar with eighth-cell partials. Returns exactly `width` chars."""
|
|
75
|
+
frac = 0.0 if frac is None else max(0.0, min(1.0, frac))
|
|
76
|
+
eighths = int(round(frac * width * 8))
|
|
77
|
+
full, rem = divmod(eighths, 8)
|
|
78
|
+
full = min(full, width)
|
|
79
|
+
out = "█" * full
|
|
80
|
+
if full < width:
|
|
81
|
+
out += " ▏▎▍▌▋▊▉"[rem] if rem else "░"
|
|
82
|
+
out += "░" * (width - full - 1)
|
|
83
|
+
return out
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def pct_color(pct, lo=70, hi=85):
|
|
87
|
+
"""Context/usage coloring: green under lo, yellow lo..hi, red above hi."""
|
|
88
|
+
if pct is None:
|
|
89
|
+
return DIM
|
|
90
|
+
if pct < lo:
|
|
91
|
+
return GREEN
|
|
92
|
+
if pct < hi:
|
|
93
|
+
return YELLOW
|
|
94
|
+
return RED
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def meter(label, pct, width=BAR_W, lo=70, hi=85):
|
|
98
|
+
if pct is None:
|
|
99
|
+
return "{} {} {}".format(c(label, DIM), c("░" * width, DIM), c("--", DIM))
|
|
100
|
+
col = pct_color(pct, lo, hi)
|
|
101
|
+
return "{} {} {}".format(c(label, DIM), c(bar(pct / 100.0, width), col), f"{pct:>3.0f}%")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ---------------------------------------------------------------------------
|
|
105
|
+
# stdin (the native, accurate data)
|
|
106
|
+
# ---------------------------------------------------------------------------
|
|
107
|
+
def read_stdin():
|
|
108
|
+
try:
|
|
109
|
+
raw = sys.stdin.read()
|
|
110
|
+
except Exception:
|
|
111
|
+
return {}
|
|
112
|
+
raw = (raw or "").strip()
|
|
113
|
+
if not raw:
|
|
114
|
+
return {}
|
|
115
|
+
try:
|
|
116
|
+
return json.loads(raw)
|
|
117
|
+
except Exception:
|
|
118
|
+
return {}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _dig(d, *path):
|
|
122
|
+
cur = d
|
|
123
|
+
for k in path:
|
|
124
|
+
if not isinstance(cur, dict):
|
|
125
|
+
return None
|
|
126
|
+
cur = cur.get(k)
|
|
127
|
+
return cur
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def context_pct(stdin):
|
|
131
|
+
p = _dig(stdin, "context_window", "used_percentage")
|
|
132
|
+
if isinstance(p, (int, float)):
|
|
133
|
+
return float(p)
|
|
134
|
+
# fall back to current_usage / size
|
|
135
|
+
used = _dig(stdin, "context_window", "current_usage", "input_tokens")
|
|
136
|
+
size = _dig(stdin, "context_window", "context_window_size")
|
|
137
|
+
if isinstance(used, (int, float)) and isinstance(size, (int, float)) and size:
|
|
138
|
+
return 100.0 * used / size
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def rate_pct(stdin, window):
|
|
143
|
+
p = _dig(stdin, "rate_limits", window, "used_percentage")
|
|
144
|
+
return float(p) if isinstance(p, (int, float)) else None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def session_id(stdin):
|
|
148
|
+
"""Map the Claude Code session id to the same zeno session UUID the
|
|
149
|
+
cc-bridge uses (uuid5 over 'cc:<id>'), so HUD samples and bridge
|
|
150
|
+
sessions/runs join on session_id. Returns None if no session id."""
|
|
151
|
+
cc = stdin.get("session_id")
|
|
152
|
+
if not isinstance(cc, str) or not cc:
|
|
153
|
+
return None
|
|
154
|
+
try:
|
|
155
|
+
import uuid
|
|
156
|
+
|
|
157
|
+
return str(uuid.uuid5(uuid.NAMESPACE_URL, "cc:" + cc))
|
|
158
|
+
except Exception:
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def effort_level(stdin):
|
|
163
|
+
e = stdin.get("effort")
|
|
164
|
+
if isinstance(e, dict):
|
|
165
|
+
return e.get("level")
|
|
166
|
+
if isinstance(e, str):
|
|
167
|
+
return e
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def model_name(stdin):
|
|
172
|
+
name = _dig(stdin, "model", "display_name") or ""
|
|
173
|
+
name = re.sub(r"\s*\([^)]*context[^)]*\)", "", name).strip() # strip "(1M context)"
|
|
174
|
+
return name or "claude"
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def project_name(stdin):
|
|
178
|
+
d = _dig(stdin, "workspace", "current_dir") or stdin.get("cwd") or ""
|
|
179
|
+
return os.path.basename(d.rstrip("/")) if d else ""
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# ---------------------------------------------------------------------------
|
|
183
|
+
# cognitive-attention engine - the formula lives in zeno_attention (single
|
|
184
|
+
# source of truth). These thin aliases preserve the public names the HUD and
|
|
185
|
+
# its tests call so the writer + bar + dashboard all share one model.
|
|
186
|
+
# ---------------------------------------------------------------------------
|
|
187
|
+
_clean_prompt_text = A.clean_prompt_text
|
|
188
|
+
_prompt_effort = A.prompt_effort
|
|
189
|
+
_delib_score = A.delib_score
|
|
190
|
+
_parse_ts = A.parse_ts
|
|
191
|
+
parse_transcript = A.parse_transcript
|
|
192
|
+
attention = A.attention
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def attention_meter(stdin, a=None, width=None):
|
|
196
|
+
width = ATT_BAR_W if width is None else width
|
|
197
|
+
if a is None:
|
|
198
|
+
prompts, asst = parse_transcript(stdin.get("transcript_path"))
|
|
199
|
+
a = attention(prompts, asst)
|
|
200
|
+
if not a.get("ok"):
|
|
201
|
+
return "{} {} {}".format(c("att", DIM), c("░" * width, DIM), c("--", DIM))
|
|
202
|
+
# v2: append the most-deviant driver ("one big thing") as a small arrow tag.
|
|
203
|
+
# effort + autonomy already have their own bars on this line (eff / drv), so the
|
|
204
|
+
# tag only surfaces an OFF-bar driver (verification / fatigue / flow) - the thing
|
|
205
|
+
# you would otherwise miss. Absent on v1 rows / live fallback.
|
|
206
|
+
tag = ""
|
|
207
|
+
top = a.get("top_driver")
|
|
208
|
+
drivers = a.get("drivers")
|
|
209
|
+
if top and top not in ("effort", "autonomy") and isinstance(drivers, dict) and top in drivers:
|
|
210
|
+
v = drivers[top]
|
|
211
|
+
arrow = "↑" if v >= 55 else ("↓" if v <= 45 else "·")
|
|
212
|
+
tag = " " + c(arrow + top, DIM)
|
|
213
|
+
return "{} {} {} {} {}{}".format(
|
|
214
|
+
c("att", DIM),
|
|
215
|
+
c(bar(a["score"] / 100.0, width), a["color"]),
|
|
216
|
+
"{:>2d}".format(a["score"]),
|
|
217
|
+
c(a["glyph"] + " " + a["label"], a["color"]),
|
|
218
|
+
c("· " + a["nudge"], DIM),
|
|
219
|
+
tag,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
# ---------------------------------------------------------------------------
|
|
224
|
+
# cognition sub-bars (eff / drv) - the two component drivers shown beside the
|
|
225
|
+
# composite attention bar. They read the same per-turn drivers the cc-bridge hook
|
|
226
|
+
# writes; no DB query of their own.
|
|
227
|
+
# ---------------------------------------------------------------------------
|
|
228
|
+
def _driver_pct(a, name):
|
|
229
|
+
"""0..100 for a named v2 driver from the presented attention dict, else None.
|
|
230
|
+
|
|
231
|
+
The cc-bridge hook writes the five standardized drivers and the HUD reads them
|
|
232
|
+
back. Absent on a v1 row / fresh session, where the caller falls back."""
|
|
233
|
+
drivers = a.get("drivers")
|
|
234
|
+
if isinstance(drivers, dict):
|
|
235
|
+
v = drivers.get(name)
|
|
236
|
+
if isinstance(v, (int, float)):
|
|
237
|
+
return float(v)
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def effort_pct(a):
|
|
242
|
+
"""The 'eff' bar value (0..100): how hard the human is driving this turn.
|
|
243
|
+
|
|
244
|
+
Prefers the v2 effort driver (standardized to your own baseline; 50 == your
|
|
245
|
+
median). Falls back to the v1 raw 0..1 effort scaled to 0..100 when no driver
|
|
246
|
+
row exists yet (no hook / fresh session)."""
|
|
247
|
+
p = _driver_pct(a, "effort")
|
|
248
|
+
if p is not None:
|
|
249
|
+
return p
|
|
250
|
+
v1 = a.get("effort")
|
|
251
|
+
return float(v1) * 100.0 if isinstance(v1, (int, float)) else None
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def drive_pct(a):
|
|
255
|
+
"""The 'drv' bar value (0..100): how hands-on you are vs delegating - your share
|
|
256
|
+
of the wheel. It is the inverse of the autonomy driver (the AI-led ratio: tool
|
|
257
|
+
activity + thin prompts + long autonomous runs). v2 only - None on a v1 row."""
|
|
258
|
+
auto = _driver_pct(a, "autonomy")
|
|
259
|
+
return (100.0 - auto) if auto is not None else None
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def cog_meter(label, pct, width=COG_BAR_W):
|
|
263
|
+
"""A neutral cognition sub-bar: label + bar + 0..100 value.
|
|
264
|
+
|
|
265
|
+
Unlike the usage meters these carry NO good/bad coloring - high effort or low
|
|
266
|
+
drive are not 'bad' (a clean autonomous run is healthy delegation), so the fill
|
|
267
|
+
is a flat accent (CYAN) and the read stays descriptive, not a target. Renders a
|
|
268
|
+
dim '--' when the value is absent (v1 row / fresh session)."""
|
|
269
|
+
if pct is None:
|
|
270
|
+
return "{} {} {}".format(c(label, DIM), c("░" * width, DIM), c("--", DIM))
|
|
271
|
+
return "{} {} {}".format(c(label, DIM), c(bar(pct / 100.0, width), CYAN), f"{pct:>2.0f}")
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
# ---------------------------------------------------------------------------
|
|
275
|
+
# cognition_samples reader (the READ half). The WRITE half lives in the
|
|
276
|
+
# zeno-cc-bridge hook (the event-time writer of the rich v2 rows); the bar is
|
|
277
|
+
# read-only, so the statusline render never writes the store.
|
|
278
|
+
# ---------------------------------------------------------------------------
|
|
279
|
+
def latest_cognition_sample(sid):
|
|
280
|
+
"""Read the most recent persisted cognition_samples row for this session.
|
|
281
|
+
|
|
282
|
+
Returns a dict of column->value (the attention_* + token + ts fields) or
|
|
283
|
+
None when there is no DB, no table, no row for the session, or any error.
|
|
284
|
+
Read-only and best-effort: it must never slow or break the render. This is
|
|
285
|
+
the READ half that lets the bar show the exact row the dashboard reads, so
|
|
286
|
+
the two surfaces can never disagree."""
|
|
287
|
+
if sid is None:
|
|
288
|
+
return None
|
|
289
|
+
try:
|
|
290
|
+
import sqlite3
|
|
291
|
+
except Exception:
|
|
292
|
+
return None
|
|
293
|
+
if not os.path.exists(ZENO_DB):
|
|
294
|
+
return None
|
|
295
|
+
try:
|
|
296
|
+
con = sqlite3.connect(f"file:{ZENO_DB}?mode=ro", uri=True, timeout=0.3)
|
|
297
|
+
try:
|
|
298
|
+
con.row_factory = sqlite3.Row
|
|
299
|
+
# SELECT * so we pick up the v2 driver columns (attention_autonomy/
|
|
300
|
+
# verification/fatigue/flow) when present, and still work on a v1 DB
|
|
301
|
+
# that lacks them (present_sample treats absent drivers as missing).
|
|
302
|
+
row = con.execute(
|
|
303
|
+
"""
|
|
304
|
+
SELECT * FROM cognition_samples
|
|
305
|
+
WHERE session_id = ?
|
|
306
|
+
ORDER BY ts DESC
|
|
307
|
+
LIMIT 1
|
|
308
|
+
""",
|
|
309
|
+
(sid,),
|
|
310
|
+
).fetchone()
|
|
311
|
+
finally:
|
|
312
|
+
con.close()
|
|
313
|
+
return dict(row) if row is not None else None
|
|
314
|
+
except Exception:
|
|
315
|
+
return None
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def attention_for_render(stdin, live_att):
|
|
319
|
+
"""The attention dict the bar should show: prefer the persisted sample so the
|
|
320
|
+
HUD matches the dashboard exactly, fall back to the live compute when no row
|
|
321
|
+
exists yet (fresh session, capture disabled, or read failure).
|
|
322
|
+
|
|
323
|
+
`live_att` is the freshly computed attention dict (the fallback). When a stored row
|
|
324
|
+
carries a score, we re-present it through the shared classifier so the rendered bar
|
|
325
|
+
equals the row a dashboard would draw - identical by construction."""
|
|
326
|
+
sid = session_id(stdin)
|
|
327
|
+
row = latest_cognition_sample(sid)
|
|
328
|
+
if row is not None:
|
|
329
|
+
# carry the live long-session framing so the nudge band matches; the row
|
|
330
|
+
# itself does not persist session length.
|
|
331
|
+
if isinstance(live_att, dict) and live_att.get("nudge") in ("wrap soon", "rest soon"):
|
|
332
|
+
row = dict(row)
|
|
333
|
+
row["long_session"] = True
|
|
334
|
+
# Pick the presenter by what the row actually carries. A v2 row (any of the
|
|
335
|
+
# four v2 driver columns present) goes through COG.present_sample (drivers +
|
|
336
|
+
# off-bar tag). A v1-only row (no v2 drivers) goes through A.present_sample,
|
|
337
|
+
# which scales the legacy 0..1 attention_effort correctly - so eff is right,
|
|
338
|
+
# not a 0..100 misread of a 0..1 value. Each falls back to the other if the
|
|
339
|
+
# chosen presenter cannot render the row, then to the live compute.
|
|
340
|
+
has_v2 = any(
|
|
341
|
+
isinstance(row.get("attention_" + d), (int, float))
|
|
342
|
+
for d in ("autonomy", "verification", "fatigue", "flow")
|
|
343
|
+
)
|
|
344
|
+
presented = COG.present_sample(row) if has_v2 else A.present_sample(row)
|
|
345
|
+
if not presented.get("ok"):
|
|
346
|
+
presented = A.present_sample(row) if has_v2 else COG.present_sample(row)
|
|
347
|
+
if presented.get("ok"):
|
|
348
|
+
return presented
|
|
349
|
+
return live_att
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
# ---------------------------------------------------------------------------
|
|
353
|
+
# the cognition bar (the one differentiated line)
|
|
354
|
+
# ---------------------------------------------------------------------------
|
|
355
|
+
def _bar_widths():
|
|
356
|
+
"""Pick (att_bar_width, cog_bar_width) for the current terminal.
|
|
357
|
+
|
|
358
|
+
The ZENO_HUD_*_WIDTH env knobs are the wide-terminal default (att 10, eff/drv 8),
|
|
359
|
+
and that default is also exactly the COLUMNS-unset behavior - so the bar's output
|
|
360
|
+
is byte-for-byte the historical `line3` whenever COLUMNS is unset or comfortably
|
|
361
|
+
wide (the golden regression + determinism hold). When COLUMNS IS set and narrow,
|
|
362
|
+
the three bars shrink together to a floor of 3 cells so the line stays single-line
|
|
363
|
+
on a narrow pane; on a wide pane the bars never grow past the knob default and
|
|
364
|
+
never pad, so there is no overflow or padding garbage. Width is presentation only -
|
|
365
|
+
the att/eff/drv numbers and labels (the cognition meaning) are unchanged."""
|
|
366
|
+
att_w, cog_w = ATT_BAR_W, COG_BAR_W
|
|
367
|
+
cols = os.environ.get("COLUMNS")
|
|
368
|
+
if not cols:
|
|
369
|
+
return att_w, cog_w
|
|
370
|
+
try:
|
|
371
|
+
cols = int(cols)
|
|
372
|
+
except (TypeError, ValueError):
|
|
373
|
+
return att_w, cog_w
|
|
374
|
+
# at >= 90 cols the default bars + their text fit comfortably; below that, scale
|
|
375
|
+
# the bars down linearly to a floor so the fill shrinks instead of forcing a wrap.
|
|
376
|
+
if cols >= 90:
|
|
377
|
+
return att_w, cog_w
|
|
378
|
+
scale = max(0.0, (cols - 10)) / 80.0 # 1.0 at 90 cols, ~0.375 at 40, 0 at <=10
|
|
379
|
+
return max(3, int(round(att_w * scale))), max(3, int(round(cog_w * scale)))
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def render_bar(stdin):
|
|
383
|
+
"""Render the single differentiated cognition line - the former HUD `line3`.
|
|
384
|
+
|
|
385
|
+
Output (one line): the composite attention bar (with its label/nudge and, when
|
|
386
|
+
notable, the most-deviant OFF-bar driver tag), then the two component sub-bars -
|
|
387
|
+
effort (how hard you drive) and drive (how hands-on vs delegating). Capped at
|
|
388
|
+
three bars by design; the session + SCED counters live on the dashboard now.
|
|
389
|
+
|
|
390
|
+
This is the renderer shared by the whole-HUD `zeno-hud` (its third line) and the
|
|
391
|
+
standalone `zeno-hud-bar` add-on, so the two can never diverge. It is READ-ONLY:
|
|
392
|
+
it reads the latest persisted cognition_samples row (written by the zeno-cc-bridge
|
|
393
|
+
capture hook) and re-presents it, falling back to a live transcript compute when no
|
|
394
|
+
row exists yet. The statusline render never writes the store.
|
|
395
|
+
"""
|
|
396
|
+
prompts, asst = parse_transcript(stdin.get("transcript_path"))
|
|
397
|
+
live_att = attention(prompts, asst)
|
|
398
|
+
# READ-only: render the bar from the latest persisted sample (falling back to the
|
|
399
|
+
# live compute) so the in-terminal HUD shows the exact number the dashboard reads
|
|
400
|
+
# from the same row - zero divergence by construction. The cc-bridge hook is the
|
|
401
|
+
# writer; the bar never writes (decision revised 2026-06-26: the bar is pure-read).
|
|
402
|
+
att = attention_for_render(stdin, live_att)
|
|
403
|
+
att_w, cog_w = _bar_widths()
|
|
404
|
+
return "{} {} · {}".format(
|
|
405
|
+
attention_meter(stdin, att, att_w),
|
|
406
|
+
cog_meter("eff", effort_pct(att), cog_w),
|
|
407
|
+
cog_meter("drv", drive_pct(att), cog_w),
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
# ---------------------------------------------------------------------------
|
|
412
|
+
# render
|
|
413
|
+
# ---------------------------------------------------------------------------
|
|
414
|
+
def render(stdin):
|
|
415
|
+
line1_parts = [c("zeno", BOLD)]
|
|
416
|
+
mn = model_name(stdin)
|
|
417
|
+
if mn:
|
|
418
|
+
line1_parts.append(c(mn, DIM))
|
|
419
|
+
pn = project_name(stdin)
|
|
420
|
+
if pn:
|
|
421
|
+
line1_parts.append(c(pn, DIM))
|
|
422
|
+
eff = effort_level(stdin)
|
|
423
|
+
if eff:
|
|
424
|
+
line1_parts.append(c("⚡" + eff, DIM))
|
|
425
|
+
line1 = c(" · ", DIM).join(line1_parts)
|
|
426
|
+
|
|
427
|
+
line2 = " ".join(
|
|
428
|
+
[
|
|
429
|
+
meter("ctx", context_pct(stdin), BAR_W, 70, 85),
|
|
430
|
+
meter("5h", rate_pct(stdin, "five_hour"), BAR_W, 60, 85),
|
|
431
|
+
meter("wk", rate_pct(stdin, "seven_day"), BAR_W, 60, 85),
|
|
432
|
+
]
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
# the cognition line is the shared bar renderer, so the whole HUD's third line
|
|
436
|
+
# and the standalone zeno-hud-bar are byte-for-byte identical (the golden
|
|
437
|
+
# regression). render_bar also owns the throttled cognition_samples write.
|
|
438
|
+
line3 = render_bar(stdin)
|
|
439
|
+
|
|
440
|
+
return "\n".join([line1, line2, line3])
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def _read_stdin_object():
|
|
444
|
+
"""Read stdin for the bar: return (ok, dict).
|
|
445
|
+
|
|
446
|
+
ok is False for the three crash-safe-empty cases - no/empty stdin, malformed
|
|
447
|
+
JSON, or valid JSON whose top level is not an object - so bar_main prints an
|
|
448
|
+
empty line. ok is True for any JSON object (even `{}`), where render_bar
|
|
449
|
+
degrades the missing fields to `--` rather than emitting nothing.
|
|
450
|
+
"""
|
|
451
|
+
try:
|
|
452
|
+
raw = sys.stdin.read()
|
|
453
|
+
except Exception:
|
|
454
|
+
return False, {}
|
|
455
|
+
raw = (raw or "").strip()
|
|
456
|
+
if not raw:
|
|
457
|
+
return False, {}
|
|
458
|
+
try:
|
|
459
|
+
data = json.loads(raw)
|
|
460
|
+
except Exception:
|
|
461
|
+
return False, {}
|
|
462
|
+
if not isinstance(data, dict):
|
|
463
|
+
return False, {}
|
|
464
|
+
return True, data
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def main():
|
|
468
|
+
try:
|
|
469
|
+
sys.stdout.write(render(read_stdin()))
|
|
470
|
+
except Exception:
|
|
471
|
+
# a statusline must never crash; degrade to the brand mark
|
|
472
|
+
sys.stdout.write(c("zeno", BOLD))
|
|
473
|
+
sys.stdout.write("\n")
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def bar_main():
|
|
477
|
+
"""Entry point for the `zeno-hud-bar` console_script and `zeno hud bar`.
|
|
478
|
+
|
|
479
|
+
Crash-safe contract (stricter than main): on ANY error - no/empty stdin,
|
|
480
|
+
malformed JSON, or an internal failure - print an empty line and exit 0. The
|
|
481
|
+
bar is an APPENDAGE stacked UNDER the host HUD (claude-hud / ccstatusline), so
|
|
482
|
+
its failure must never pollute the host's output. main() prints the `zeno`
|
|
483
|
+
brand mark because it owns the whole statusline; the bar owns only one row, so
|
|
484
|
+
its fallback is an empty row, not a brand mark.
|
|
485
|
+
"""
|
|
486
|
+
try:
|
|
487
|
+
ok, stdin = _read_stdin_object()
|
|
488
|
+
out = render_bar(stdin) if ok else ""
|
|
489
|
+
except Exception:
|
|
490
|
+
out = ""
|
|
491
|
+
sys.stdout.write(out)
|
|
492
|
+
sys.stdout.write("\n")
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
if __name__ == "__main__":
|
|
496
|
+
main()
|