@arbiterforge/ca-pi 0.6.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/LICENSE +661 -0
- package/README.md +558 -0
- package/package.json +35 -0
- package/plugins/ca-pi/CHANGELOG.md +1030 -0
- package/plugins/ca-pi/COMMANDS.md +90 -0
- package/plugins/ca-pi/ORCHESTRATOR.md +159 -0
- package/plugins/ca-pi/SKILLS.md +47 -0
- package/plugins/ca-pi/SPRINT.md +142 -0
- package/plugins/ca-pi/agents/INDEX.md +31 -0
- package/plugins/ca-pi/agents/architecture-drift-reviewer.md +86 -0
- package/plugins/ca-pi/agents/auth-crypto-reviewer.md +60 -0
- package/plugins/ca-pi/agents/backend-author.md +60 -0
- package/plugins/ca-pi/agents/checkpoint-aggregator.md +111 -0
- package/plugins/ca-pi/agents/coverage-auditor.md +71 -0
- package/plugins/ca-pi/agents/decision-challenger.md +116 -0
- package/plugins/ca-pi/agents/dependency-reviewer.md +79 -0
- package/plugins/ca-pi/agents/design-quality-reviewer.md +80 -0
- package/plugins/ca-pi/agents/finding-triage.md +86 -0
- package/plugins/ca-pi/agents/frontend-author.md +64 -0
- package/plugins/ca-pi/agents/grader.md +173 -0
- package/plugins/ca-pi/agents/infra-author.md +64 -0
- package/plugins/ca-pi/agents/map-deps.md +35 -0
- package/plugins/ca-pi/agents/map-structure.md +37 -0
- package/plugins/ca-pi/agents/migration-reviewer.md +65 -0
- package/plugins/ca-pi/agents/scout.md +127 -0
- package/plugins/ca-pi/agents/security-reviewer.md +72 -0
- package/plugins/ca-pi/agents/tribunal-lens-reviewer.md +65 -0
- package/plugins/ca-pi/extensions/codearbiter-child.js +1885 -0
- package/plugins/ca-pi/extensions/codearbiter.js +9802 -0
- package/plugins/ca-pi/generated/command-catalog.json +197 -0
- package/plugins/ca-pi/generated/roles.json +213 -0
- package/plugins/ca-pi/helpers/windows-supervisor.js +205 -0
- package/plugins/ca-pi/hooks/_activationlib.py +196 -0
- package/plugins/ca-pi/hooks/_arbiterstatelib.py +208 -0
- package/plugins/ca-pi/hooks/_babysitlib.py +76 -0
- package/plugins/ca-pi/hooks/_bashguardlib.py +1667 -0
- package/plugins/ca-pi/hooks/_boxlib.py +131 -0
- package/plugins/ca-pi/hooks/_colorlib.py +304 -0
- package/plugins/ca-pi/hooks/_durabilitylib.py +186 -0
- package/plugins/ca-pi/hooks/_entrylib.py +41 -0
- package/plugins/ca-pi/hooks/_fmtlib.py +161 -0
- package/plugins/ca-pi/hooks/_gitexec.py +45 -0
- package/plugins/ca-pi/hooks/_githooks.py +920 -0
- package/plugins/ca-pi/hooks/_gitlib.py +110 -0
- package/plugins/ca-pi/hooks/_hooklib.py +595 -0
- package/plugins/ca-pi/hooks/_host.py +115 -0
- package/plugins/ca-pi/hooks/_intentlib.py +242 -0
- package/plugins/ca-pi/hooks/_ledgerlib.py +1035 -0
- package/plugins/ca-pi/hooks/_metricslib.py +709 -0
- package/plugins/ca-pi/hooks/_pathnorm.py +74 -0
- package/plugins/ca-pi/hooks/_planfilelib.py +664 -0
- package/plugins/ca-pi/hooks/_previewlib.py +193 -0
- package/plugins/ca-pi/hooks/_protectedlib.py +312 -0
- package/plugins/ca-pi/hooks/_protectedstatelib.py +411 -0
- package/plugins/ca-pi/hooks/_provenancelib.py +971 -0
- package/plugins/ca-pi/hooks/_prunelib.py +1398 -0
- package/plugins/ca-pi/hooks/_prunepolicy.py +235 -0
- package/plugins/ca-pi/hooks/_readinjectlib.py +1080 -0
- package/plugins/ca-pi/hooks/_releaselib.py +2657 -0
- package/plugins/ca-pi/hooks/_scopelib.py +262 -0
- package/plugins/ca-pi/hooks/_segmentslib.py +278 -0
- package/plugins/ca-pi/hooks/_sensitivelib.py +270 -0
- package/plugins/ca-pi/hooks/_sessionlib.py +78 -0
- package/plugins/ca-pi/hooks/_sloplib.py +244 -0
- package/plugins/ca-pi/hooks/_standuplib.py +214 -0
- package/plugins/ca-pi/hooks/_subagentslib.py +219 -0
- package/plugins/ca-pi/hooks/_taskboardlib.py +1088 -0
- package/plugins/ca-pi/hooks/_updatelib.py +278 -0
- package/plugins/ca-pi/hooks/babysit.py +47 -0
- package/plugins/ca-pi/hooks/boardsync.py +129 -0
- package/plugins/ca-pi/hooks/doctor.py +420 -0
- package/plugins/ca-pi/hooks/git-enforce.py +325 -0
- package/plugins/ca-pi/hooks/hostapi.py +460 -0
- package/plugins/ca-pi/hooks/init-codearbiter.py +225 -0
- package/plugins/ca-pi/hooks/metrics.py +62 -0
- package/plugins/ca-pi/hooks/migration-pass.py +129 -0
- package/plugins/ca-pi/hooks/pi-bridge.py +543 -0
- package/plugins/ca-pi/hooks/post-write-edit.py +231 -0
- package/plugins/ca-pi/hooks/pre-bash.py +90 -0
- package/plugins/ca-pi/hooks/pre-edit.py +284 -0
- package/plugins/ca-pi/hooks/pre-read.py +81 -0
- package/plugins/ca-pi/hooks/pre-write.py +217 -0
- package/plugins/ca-pi/hooks/preview.py +69 -0
- package/plugins/ca-pi/hooks/prune-transcript.py +232 -0
- package/plugins/ca-pi/hooks/releasehash.py +216 -0
- package/plugins/ca-pi/hooks/security-pass.py +139 -0
- package/plugins/ca-pi/hooks/session-start.py +1218 -0
- package/plugins/ca-pi/hooks/statusline.py +736 -0
- package/plugins/ca-pi/hooks/taskwrite.py +351 -0
- package/plugins/ca-pi/hooks/update-refresh.py +51 -0
- package/plugins/ca-pi/hooks/wire-statusline.py +435 -0
- package/plugins/ca-pi/includes/anti-slop-design/INDEX.md +55 -0
- package/plugins/ca-pi/includes/anti-slop-design/color.md +43 -0
- package/plugins/ca-pi/includes/anti-slop-design/core.md +244 -0
- package/plugins/ca-pi/includes/anti-slop-design/images.md +32 -0
- package/plugins/ca-pi/includes/anti-slop-design/layout.md +45 -0
- package/plugins/ca-pi/includes/anti-slop-design/medium-cli.md +39 -0
- package/plugins/ca-pi/includes/anti-slop-design/medium-dataviz.md +43 -0
- package/plugins/ca-pi/includes/anti-slop-design/medium-diagram.md +35 -0
- package/plugins/ca-pi/includes/anti-slop-design/medium-documents.md +70 -0
- package/plugins/ca-pi/includes/anti-slop-design/medium-slides.md +30 -0
- package/plugins/ca-pi/includes/anti-slop-design/medium-web.md +39 -0
- package/plugins/ca-pi/includes/anti-slop-design/typography.md +51 -0
- package/plugins/ca-pi/includes/author-tdd-workflow.md +14 -0
- package/plugins/ca-pi/includes/compaction-charter.md +16 -0
- package/plugins/ca-pi/includes/cut-docs.md +16 -0
- package/plugins/ca-pi/includes/dev-mode.md +30 -0
- package/plugins/ca-pi/includes/farm.md +237 -0
- package/plugins/ca-pi/includes/fresh-verification.md +14 -0
- package/plugins/ca-pi/includes/harvest.md +69 -0
- package/plugins/ca-pi/includes/maturity-coverage.md +102 -0
- package/plugins/ca-pi/includes/pi-host-notes.md +69 -0
- package/plugins/ca-pi/includes/redirect.md +69 -0
- package/plugins/ca-pi/includes/reference-map.md +22 -0
- package/plugins/ca-pi/includes/review-matrix.md +14 -0
- package/plugins/ca-pi/includes/reviewer-contract.md +53 -0
- package/plugins/ca-pi/includes/routing-table.md +47 -0
- package/plugins/ca-pi/includes/security-gate-record.md +22 -0
- package/plugins/ca-pi/includes/smarts/core.md +90 -0
- package/plugins/ca-pi/includes/smarts/decision-log-format.md +56 -0
- package/plugins/ca-pi/routines/INDEX.md +32 -0
- package/plugins/ca-pi/routines/brainstorming/SKILL.md +122 -0
- package/plugins/ca-pi/routines/commit-gate/SKILL.md +151 -0
- package/plugins/ca-pi/routines/context-check/SKILL.md +85 -0
- package/plugins/ca-pi/routines/context-creation/SKILL.md +171 -0
- package/plugins/ca-pi/routines/crypto-compliance/SKILL.md +41 -0
- package/plugins/ca-pi/routines/debug/SKILL.md +99 -0
- package/plugins/ca-pi/routines/decision-lifecycle/SKILL.md +104 -0
- package/plugins/ca-pi/routines/decision-lifecycle/references/adr-template.md +74 -0
- package/plugins/ca-pi/routines/decision-variance/SKILL.md +147 -0
- package/plugins/ca-pi/routines/decompose/SKILL.md +168 -0
- package/plugins/ca-pi/routines/dispatching-parallel-agents/SKILL.md +76 -0
- package/plugins/ca-pi/routines/executing-plans/SKILL.md +83 -0
- package/plugins/ca-pi/routines/finishing-a-development-branch/SKILL.md +91 -0
- package/plugins/ca-pi/routines/post-merge-cleanup/SKILL.md +233 -0
- package/plugins/ca-pi/routines/refactor/SKILL.md +91 -0
- package/plugins/ca-pi/routines/release/SKILL.md +315 -0
- package/plugins/ca-pi/routines/secret-handling/SKILL.md +67 -0
- package/plugins/ca-pi/routines/security-architecture/SKILL.md +63 -0
- package/plugins/ca-pi/routines/skill-author/SKILL.md +108 -0
- package/plugins/ca-pi/routines/skill-author/references/skill-template.md +58 -0
- package/plugins/ca-pi/routines/subagent-driven-development/SKILL.md +149 -0
- package/plugins/ca-pi/routines/subagent-driven-development/references/farm-dispatch.md +145 -0
- package/plugins/ca-pi/routines/tdd/SKILL.md +139 -0
- package/plugins/ca-pi/routines/tribunal/SKILL.md +109 -0
- package/plugins/ca-pi/routines/tribunal/references/ai-markers.md +29 -0
- package/plugins/ca-pi/routines/tribunal/references/cost-and-models.md +64 -0
- package/plugins/ca-pi/routines/tribunal/references/finding-record.md +27 -0
- package/plugins/ca-pi/routines/tribunal/references/issue-filing.md +47 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/appsec.md +22 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/architecture.md +23 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/coverage.md +20 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/infra.md +24 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/migration.md +22 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/observability.md +21 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/performance.md +22 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/reliability.md +23 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/secrets-supply.md +22 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/test-fidelity.md +24 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/typesafety.md +21 -0
- package/plugins/ca-pi/routines/tribunal/references/report.md +19 -0
- package/plugins/ca-pi/routines/tribunal/references/schemas.md +58 -0
- package/plugins/ca-pi/routines/tribunal/references/telemetry.md +28 -0
- package/plugins/ca-pi/routines/tribunal/references/triage.md +53 -0
- package/plugins/ca-pi/routines/using-git-worktrees/SKILL.md +85 -0
- package/plugins/ca-pi/routines/writing-plans/SKILL.md +129 -0
- package/plugins/ca-pi/routines/writing-plans/references/farm-plan.md +50 -0
- package/plugins/ca-pi/skills/ca-add-dep/SKILL.md +88 -0
- package/plugins/ca-pi/skills/ca-adr/SKILL.md +30 -0
- package/plugins/ca-pi/skills/ca-adr-status/SKILL.md +30 -0
- package/plugins/ca-pi/skills/ca-arbiter/SKILL.md +36 -0
- package/plugins/ca-pi/skills/ca-audit/SKILL.md +51 -0
- package/plugins/ca-pi/skills/ca-btw/SKILL.md +23 -0
- package/plugins/ca-pi/skills/ca-checkpoint/SKILL.md +50 -0
- package/plugins/ca-pi/skills/ca-chore/SKILL.md +58 -0
- package/plugins/ca-pi/skills/ca-cleanup/SKILL.md +55 -0
- package/plugins/ca-pi/skills/ca-commands/SKILL.md +21 -0
- package/plugins/ca-pi/skills/ca-commit/SKILL.md +27 -0
- package/plugins/ca-pi/skills/ca-conflict/SKILL.md +61 -0
- package/plugins/ca-pi/skills/ca-context-check/SKILL.md +32 -0
- package/plugins/ca-pi/skills/ca-create-context/SKILL.md +32 -0
- package/plugins/ca-pi/skills/ca-debug/SKILL.md +42 -0
- package/plugins/ca-pi/skills/ca-decompose/SKILL.md +30 -0
- package/plugins/ca-pi/skills/ca-dev/SKILL.md +42 -0
- package/plugins/ca-pi/skills/ca-doctor/SKILL.md +44 -0
- package/plugins/ca-pi/skills/ca-feature/SKILL.md +105 -0
- package/plugins/ca-pi/skills/ca-fix/SKILL.md +42 -0
- package/plugins/ca-pi/skills/ca-init/SKILL.md +56 -0
- package/plugins/ca-pi/skills/ca-metrics/SKILL.md +80 -0
- package/plugins/ca-pi/skills/ca-new-skill/SKILL.md +34 -0
- package/plugins/ca-pi/skills/ca-override/SKILL.md +72 -0
- package/plugins/ca-pi/skills/ca-pr/SKILL.md +61 -0
- package/plugins/ca-pi/skills/ca-preview/SKILL.md +86 -0
- package/plugins/ca-pi/skills/ca-prune/SKILL.md +100 -0
- package/plugins/ca-pi/skills/ca-reconcile/SKILL.md +43 -0
- package/plugins/ca-pi/skills/ca-refactor/SKILL.md +43 -0
- package/plugins/ca-pi/skills/ca-release/SKILL.md +57 -0
- package/plugins/ca-pi/skills/ca-review/SKILL.md +63 -0
- package/plugins/ca-pi/skills/ca-spike/SKILL.md +41 -0
- package/plugins/ca-pi/skills/ca-sprint/SKILL.md +44 -0
- package/plugins/ca-pi/skills/ca-standup/SKILL.md +112 -0
- package/plugins/ca-pi/skills/ca-status/SKILL.md +56 -0
- package/plugins/ca-pi/skills/ca-task/SKILL.md +61 -0
- package/plugins/ca-pi/skills/ca-threat-model/SKILL.md +45 -0
- package/plugins/ca-pi/skills/ca-tribunal/SKILL.md +44 -0
- package/plugins/ca-pi/skills/ca-watch/SKILL.md +78 -0
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# codeArbiter statusline — dependency-free (Python stdlib only, no Nerd Font).
|
|
3
|
+
#
|
|
4
|
+
# Renders a sectioned, full-width box. The usage segments (folder, git, model,
|
|
5
|
+
# rate limits, context, cumulative tokens, API-equivalent cost, per-call burn)
|
|
6
|
+
# render in every repo. The arbiter segments (stage / tasks / open-questions /
|
|
7
|
+
# overrides-since-checkpoint) render only when the repo's .codearbiter/CONTEXT.md
|
|
8
|
+
# frontmatter is `arbiter: enabled`.
|
|
9
|
+
#
|
|
10
|
+
# Design:
|
|
11
|
+
# - single neon-violet palette (a subtle dark->bright sheen, no rainbow);
|
|
12
|
+
# green/amber/red are reserved strictly for threshold status (ctx, rates).
|
|
13
|
+
# - native-font glyphs only: box-drawing, block elements, arrows, ASCII labels.
|
|
14
|
+
# - top line = active folder; second line = git project (owner/name + branch),
|
|
15
|
+
# with a no-git fallback.
|
|
16
|
+
# - token COUNTS come from the session TRANSCRIPT (deduped per requestId; the
|
|
17
|
+
# host sends no cumulative counts). The COST is the host's authoritative
|
|
18
|
+
# cost.total_cost_usd — it already prices every call (incl. subagents, which
|
|
19
|
+
# live in separate transcripts) the way your bill does; a token*price table
|
|
20
|
+
# would miss subagents and drift, so it is used only as a fallback.
|
|
21
|
+
# - context trusts context_window.used_percentage + context_window_size
|
|
22
|
+
# (1M for million-token models, 200K otherwise) — never exceeds 100%.
|
|
23
|
+
#
|
|
24
|
+
# A small user-level ledger (~/.codearbiter/ledger.json) tails each session's
|
|
25
|
+
# transcript from a stored byte offset (append-only -> O(new lines) per render),
|
|
26
|
+
# accumulating true cumulative tokens + API-equivalent cost per session, so the
|
|
27
|
+
# box shows this session, today's totals across sessions, and a sparkline of the
|
|
28
|
+
# real per-message token burn.
|
|
29
|
+
#
|
|
30
|
+
# Robustness contract: this script must NEVER print a traceback. The host pipes
|
|
31
|
+
# arbitrary JSON on stdin and the output lands in the user's terminal. Every
|
|
32
|
+
# segment is wrapped (safe()), the whole render is guarded, all host values pass
|
|
33
|
+
# through num() coercion, and every row is width-clamped so a bad value or a
|
|
34
|
+
# narrow terminal degrades a segment — it never breaks the box.
|
|
35
|
+
#
|
|
36
|
+
# This file is a THIN ENTRY POINT (architecture-004): it parses stdin, resolves
|
|
37
|
+
# the box width, and wires together the concern-group libraries below into the
|
|
38
|
+
# final render. Each concern lives in its own `_<name>lib.py` sibling, imported
|
|
39
|
+
# guarded the same defensive way as _taskboardlib/_hooklib below — a missing lib
|
|
40
|
+
# degrades its segment(s), it never breaks the box:
|
|
41
|
+
# _colorlib palette, glyphs, ANSI regex, width-aware clip/pad/gradient
|
|
42
|
+
# _fmtlib token/USD/duration/path/sparkline formatting
|
|
43
|
+
# _gitlib project root, branch, dirty-check
|
|
44
|
+
# _arbiterstatelib .codearbiter/ frontmatter + override/question counting + cache
|
|
45
|
+
# _sessionlib true wall-clock session-start resolution
|
|
46
|
+
# _subagentslib subagent directory scan + per-subagent labeling
|
|
47
|
+
# _boxlib the box shell (top/row/sep/bottom) + column layout helpers
|
|
48
|
+
# _segmentslib the individual content segments (ctx bar, rate cells, pill, …)
|
|
49
|
+
# _ledgerlib token/cost ledger (extracted earlier, T-12)
|
|
50
|
+
# `seg_update` / `plugin_root_for_render` (the update-notifier surface) and
|
|
51
|
+
# `dev_active`'s render-time callers stay wired here because a test patches
|
|
52
|
+
# `statusline.plugin_root_for_render` directly — that seam must resolve through
|
|
53
|
+
# this module's own globals, not a lib's.
|
|
54
|
+
#
|
|
55
|
+
# Env:
|
|
56
|
+
# CODEARBITER_STATUSLINE=off disable entirely
|
|
57
|
+
# CODEARBITER_COMPACT=1 drop the subagent rows (lean mode)
|
|
58
|
+
# CODEARBITER_WIDTH / COLUMNS box width (clamped 70..160)
|
|
59
|
+
# CODEARBITER_COMPACT_AT=NN context %% treated as the compaction threshold
|
|
60
|
+
# CODEARBITER_LEDGER=path override ledger location
|
|
61
|
+
|
|
62
|
+
import json
|
|
63
|
+
import os
|
|
64
|
+
import re
|
|
65
|
+
import shutil
|
|
66
|
+
import sys
|
|
67
|
+
import threading
|
|
68
|
+
import time
|
|
69
|
+
|
|
70
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
71
|
+
import hostapi # noqa: E402 — host seam (ADR-0011)
|
|
72
|
+
import _hooklib # noqa: E402 — set_host DI seam (#257)
|
|
73
|
+
import _entrylib # noqa: E402 — shared run() dispatch (jscpd dedup)
|
|
74
|
+
|
|
75
|
+
# Shared task-board logic so the "tasks" segment counts in-flight items the same
|
|
76
|
+
# way the SessionStart hook does (excludes done). Guarded: a failed import must
|
|
77
|
+
# degrade the segment, never break the box (robustness contract above).
|
|
78
|
+
try:
|
|
79
|
+
from _taskboardlib import count_in_flight as _count_in_flight, read_board as _read_board
|
|
80
|
+
except Exception: # pragma: no cover — never let an import break the statusline
|
|
81
|
+
_count_in_flight = None
|
|
82
|
+
_read_board = None
|
|
83
|
+
|
|
84
|
+
# Reuse the enforcement-hook activation contract so the box reports "arbiter
|
|
85
|
+
# enabled" exactly the way the hooks gate on it (one frontmatter parser, not two).
|
|
86
|
+
# Guarded the same defensive way as _taskboardlib: a missing lib degrades the
|
|
87
|
+
# arbiter segment to a local fallback, it never breaks the box.
|
|
88
|
+
try:
|
|
89
|
+
from _hooklib import frontmatter_enabled as _frontmatter_enabled
|
|
90
|
+
except Exception: # pragma: no cover — never let an import break the statusline
|
|
91
|
+
_frontmatter_enabled = None
|
|
92
|
+
|
|
93
|
+
# Update-available notifier (spec: update-available-notifier.md) — the statusline
|
|
94
|
+
# renders the SAME cache SessionStart reads (RENDER ONLY: no fetch, no spawn, no
|
|
95
|
+
# network here at all). Guarded the same defensive way as the imports above.
|
|
96
|
+
try:
|
|
97
|
+
import _updatelib
|
|
98
|
+
except Exception: # pragma: no cover — never let an import break the statusline
|
|
99
|
+
_updatelib = None
|
|
100
|
+
|
|
101
|
+
# The cost/token ledger subsystem lives in _ledgerlib (extracted T-12) with zero
|
|
102
|
+
# import-time side effects. Re-bind its functions into this module so the cost
|
|
103
|
+
# segment — and the existing test suite that reaches them via statusline — keep
|
|
104
|
+
# working. Guarded: a missing lib leaves the names None and the cost segment
|
|
105
|
+
# safe()-degrades, never breaking the box.
|
|
106
|
+
try:
|
|
107
|
+
import _ledgerlib
|
|
108
|
+
ledger_update = _ledgerlib.ledger_update
|
|
109
|
+
_tx_accumulate = _ledgerlib._tx_accumulate
|
|
110
|
+
persist_sess_start = _ledgerlib.persist_sess_start
|
|
111
|
+
except Exception: # pragma: no cover — never let an import break the statusline
|
|
112
|
+
_ledgerlib = None
|
|
113
|
+
|
|
114
|
+
def ledger_update(data, sid): # safe blanks: keeps render()'s 3-tuple contract
|
|
115
|
+
blank = {"in": 0.0, "out": 0.0, "cost": 0.0}
|
|
116
|
+
return {}, dict(blank), dict(blank)
|
|
117
|
+
|
|
118
|
+
def persist_sess_start(sid, value): # no ledger lib -> nothing to persist into
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
# --------------------------------------------------------------------------- color / width primitives (_colorlib)
|
|
122
|
+
try:
|
|
123
|
+
import _colorlib
|
|
124
|
+
fg, bg = _colorlib.fg, _colorlib.bg
|
|
125
|
+
RESET, BOLD, DIM, ITAL = _colorlib.RESET, _colorlib.BOLD, _colorlib.DIM, _colorlib.ITAL
|
|
126
|
+
V0, V1, V2, V3 = _colorlib.V0, _colorlib.V1, _colorlib.V2, _colorlib.V3
|
|
127
|
+
GREY, WHITE, OK, WARN, DANGER, PILL_FG = (_colorlib.GREY, _colorlib.WHITE, _colorlib.OK,
|
|
128
|
+
_colorlib.WARN, _colorlib.DANGER, _colorlib.PILL_FG)
|
|
129
|
+
TL, TR, BL, BR = _colorlib.TL, _colorlib.TR, _colorlib.BL, _colorlib.BR
|
|
130
|
+
H, V, LT, RT = _colorlib.H, _colorlib.V, _colorlib.LT, _colorlib.RT
|
|
131
|
+
TD, TU = _colorlib.TD, _colorlib.TU
|
|
132
|
+
DOTH, DOT, SUBDOT, ELL = _colorlib.DOTH, _colorlib.DOT, _colorlib.SUBDOT, _colorlib.ELL
|
|
133
|
+
BFULL, BEMPTY = _colorlib.BFULL, _colorlib.BEMPTY
|
|
134
|
+
DN, UP, ARR, BOLT, SPARK = _colorlib.DN, _colorlib.UP, _colorlib.ARR, _colorlib.BOLT, _colorlib.SPARK
|
|
135
|
+
ANSI = _colorlib.ANSI
|
|
136
|
+
_cw, vlen, clip, pad, gradient_h = (_colorlib._cw, _colorlib.vlen, _colorlib.clip,
|
|
137
|
+
_colorlib.pad, _colorlib.gradient_h)
|
|
138
|
+
strip_control = _colorlib.strip_control
|
|
139
|
+
except Exception: # pragma: no cover — never let an import break the statusline
|
|
140
|
+
_colorlib = None
|
|
141
|
+
ANSI = re.compile(r"\033\[[0-9;]*m")
|
|
142
|
+
fg = bg = lambda *a: ""
|
|
143
|
+
RESET = BOLD = DIM = ITAL = ""
|
|
144
|
+
V0 = V1 = V2 = V3 = GREY = WHITE = OK = WARN = DANGER = PILL_FG = ""
|
|
145
|
+
TL = TR = BL = BR = H = V = LT = RT = TD = TU = DOTH = DOT = SUBDOT = ELL = BFULL = BEMPTY = ""
|
|
146
|
+
DN = UP = ARR = BOLT = ""
|
|
147
|
+
SPARK = " "
|
|
148
|
+
|
|
149
|
+
def vlen(s):
|
|
150
|
+
return len(ANSI.sub("", s))
|
|
151
|
+
|
|
152
|
+
def clip(s, w):
|
|
153
|
+
return ANSI.sub("", s)[:max(0, w)]
|
|
154
|
+
|
|
155
|
+
def pad(s, w):
|
|
156
|
+
v = vlen(s)
|
|
157
|
+
return s if v >= w else s + " " * (w - v)
|
|
158
|
+
|
|
159
|
+
def gradient_h(text, width, c_from=None, c_to=None):
|
|
160
|
+
return text
|
|
161
|
+
|
|
162
|
+
def strip_control(s):
|
|
163
|
+
return s if not isinstance(s, str) else re.sub(r"[\000-\037]", "", s)
|
|
164
|
+
|
|
165
|
+
# --------------------------------------------------------------------------- coercion
|
|
166
|
+
def num(x, default=0.0):
|
|
167
|
+
"""Coerce any host value to float; tolerate strings, None, and containers."""
|
|
168
|
+
try:
|
|
169
|
+
return float(x)
|
|
170
|
+
except (TypeError, ValueError):
|
|
171
|
+
return default
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def safe(fn, *a, **k):
|
|
175
|
+
"""Run a segment builder; swallow any failure so one bad segment can't break
|
|
176
|
+
the whole statusline. Returns None on error (caller omits the segment)."""
|
|
177
|
+
try:
|
|
178
|
+
return fn(*a, **k)
|
|
179
|
+
except Exception:
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def get(d, *path, default=None):
|
|
184
|
+
cur = d
|
|
185
|
+
for k in path:
|
|
186
|
+
if not isinstance(cur, dict) or k not in cur or cur[k] is None:
|
|
187
|
+
return default
|
|
188
|
+
cur = cur[k]
|
|
189
|
+
return cur
|
|
190
|
+
|
|
191
|
+
# --------------------------------------------------------------------------- format (_fmtlib)
|
|
192
|
+
try:
|
|
193
|
+
import _fmtlib
|
|
194
|
+
fmt_tok, usd, usd_fine, short_path = (_fmtlib.fmt_tok, _fmtlib.usd,
|
|
195
|
+
_fmtlib.usd_fine, _fmtlib.short_path)
|
|
196
|
+
human_dur, parse_iso, to_epoch, sparkline = (_fmtlib.human_dur, _fmtlib.parse_iso,
|
|
197
|
+
_fmtlib.to_epoch, _fmtlib.sparkline)
|
|
198
|
+
except Exception: # pragma: no cover — never let an import break the statusline
|
|
199
|
+
_fmtlib = None
|
|
200
|
+
|
|
201
|
+
def fmt_tok(n):
|
|
202
|
+
return str(int(num(n)))
|
|
203
|
+
|
|
204
|
+
def usd(c):
|
|
205
|
+
return f"${num(c):.2f}"
|
|
206
|
+
|
|
207
|
+
def usd_fine(c):
|
|
208
|
+
return f"${num(c):.2f}"
|
|
209
|
+
|
|
210
|
+
def short_path(p):
|
|
211
|
+
return str(p) if p else "?"
|
|
212
|
+
|
|
213
|
+
def human_dur(secs):
|
|
214
|
+
return f"{int(max(0, secs))}s"
|
|
215
|
+
|
|
216
|
+
def parse_iso(s):
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
def to_epoch(v):
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
def sparkline(vals, grad=True):
|
|
223
|
+
return ""
|
|
224
|
+
|
|
225
|
+
# --------------------------------------------------------------------------- git (_gitlib)
|
|
226
|
+
try:
|
|
227
|
+
import _gitlib
|
|
228
|
+
project_root, head_branch, git_dirty = (_gitlib.project_root, _gitlib.head_branch,
|
|
229
|
+
_gitlib.git_dirty)
|
|
230
|
+
except Exception: # pragma: no cover — never let an import break the statusline
|
|
231
|
+
_gitlib = None
|
|
232
|
+
|
|
233
|
+
def project_root(data):
|
|
234
|
+
return get(data, "workspace", "project_dir") or get(data, "cwd") or os.getcwd()
|
|
235
|
+
|
|
236
|
+
def head_branch(root):
|
|
237
|
+
return None
|
|
238
|
+
|
|
239
|
+
def git_dirty(root):
|
|
240
|
+
return False
|
|
241
|
+
|
|
242
|
+
# --------------------------------------------------------------------------- arbiter (_arbiterstatelib)
|
|
243
|
+
# arbiter_state's task-in-flight count and enabled-gate dependencies
|
|
244
|
+
# (_count_in_flight / _read_board / _frontmatter_enabled) are guarded-imported
|
|
245
|
+
# ABOVE, in this module — arbiter_state() below passes them through EACH CALL
|
|
246
|
+
# (not at import time) so a test that monkeypatches this module's fallback (e.g.
|
|
247
|
+
# `mod._count_in_flight = None`) is observed on the next call, exactly as before
|
|
248
|
+
# the extraction.
|
|
249
|
+
try:
|
|
250
|
+
import _arbiterstatelib
|
|
251
|
+
frontmatter, count_matches = _arbiterstatelib.frontmatter, _arbiterstatelib.count_matches
|
|
252
|
+
_ARBITER_CACHE, _ARBITER_FILES = _arbiterstatelib._ARBITER_CACHE, _arbiterstatelib._ARBITER_FILES
|
|
253
|
+
_arbiter_mtime_key = _arbiterstatelib._arbiter_mtime_key
|
|
254
|
+
dev_active = _arbiterstatelib.dev_active
|
|
255
|
+
|
|
256
|
+
def _arbiter_enabled(ctx_path):
|
|
257
|
+
return _arbiterstatelib._arbiter_enabled(ctx_path, _frontmatter_enabled)
|
|
258
|
+
|
|
259
|
+
def arbiter_state(root, ctx_text=None, ot_text=None, oq_text=None):
|
|
260
|
+
# performance-003 (#194): ctx_text/ot_text/oq_text let a caller (e.g.
|
|
261
|
+
# session-start.py's governance_line) that already read CONTEXT.md/
|
|
262
|
+
# open-tasks.md/open-questions.md thread that content through instead of
|
|
263
|
+
# a second disk read. Every existing caller passes nothing and is
|
|
264
|
+
# unaffected.
|
|
265
|
+
return _arbiterstatelib.arbiter_state(
|
|
266
|
+
root, _count_in_flight, _read_board, _frontmatter_enabled,
|
|
267
|
+
ctx_text=ctx_text, ot_text=ot_text, oq_text=oq_text)
|
|
268
|
+
except Exception: # pragma: no cover — never let an import break the statusline
|
|
269
|
+
_arbiterstatelib = None
|
|
270
|
+
_ARBITER_CACHE = {}
|
|
271
|
+
_ARBITER_FILES = ()
|
|
272
|
+
|
|
273
|
+
def frontmatter(path):
|
|
274
|
+
return {}
|
|
275
|
+
|
|
276
|
+
def count_matches(path, pattern):
|
|
277
|
+
return 0
|
|
278
|
+
|
|
279
|
+
def _arbiter_mtime_key(cad):
|
|
280
|
+
return -1.0
|
|
281
|
+
|
|
282
|
+
def _arbiter_enabled(ctx_path):
|
|
283
|
+
return False
|
|
284
|
+
|
|
285
|
+
def arbiter_state(root, ctx_text=None, ot_text=None, oq_text=None):
|
|
286
|
+
return None
|
|
287
|
+
|
|
288
|
+
def dev_active(root):
|
|
289
|
+
return False
|
|
290
|
+
|
|
291
|
+
# --------------------------------------------------------------------------- subagents (_subagentslib)
|
|
292
|
+
try:
|
|
293
|
+
import _subagentslib
|
|
294
|
+
subagent_dir, read_subagents, sub_label = (_subagentslib.subagent_dir,
|
|
295
|
+
_subagentslib.read_subagents,
|
|
296
|
+
_subagentslib.sub_label)
|
|
297
|
+
ACTIVE_WINDOW, SHOW_WINDOW = _subagentslib.ACTIVE_WINDOW, _subagentslib.SHOW_WINDOW
|
|
298
|
+
MAX_SUB_ROWS, MAX_SUB_FILES, MAX_SUB_LINES = (_subagentslib.MAX_SUB_ROWS,
|
|
299
|
+
_subagentslib.MAX_SUB_FILES,
|
|
300
|
+
_subagentslib.MAX_SUB_LINES)
|
|
301
|
+
except Exception: # pragma: no cover — never let an import break the statusline
|
|
302
|
+
_subagentslib = None
|
|
303
|
+
ACTIVE_WINDOW = SHOW_WINDOW = MAX_SUB_ROWS = MAX_SUB_FILES = MAX_SUB_LINES = 0
|
|
304
|
+
|
|
305
|
+
def subagent_dir(data, root, sid):
|
|
306
|
+
return None
|
|
307
|
+
|
|
308
|
+
def read_subagents(sdir):
|
|
309
|
+
return 0, 0, [], (0, 0)
|
|
310
|
+
|
|
311
|
+
def sub_label(content):
|
|
312
|
+
return ""
|
|
313
|
+
|
|
314
|
+
# --------------------------------------------------------------------------- box (_boxlib)
|
|
315
|
+
try:
|
|
316
|
+
import _boxlib
|
|
317
|
+
Box, lr, cols = _boxlib.Box, _boxlib.lr, _boxlib.cols
|
|
318
|
+
except Exception: # pragma: no cover — never let an import break the statusline
|
|
319
|
+
_boxlib = None
|
|
320
|
+
|
|
321
|
+
class Box: # minimal degrade: plain lines, no box drawing
|
|
322
|
+
def __init__(self, width):
|
|
323
|
+
self.W = width
|
|
324
|
+
self.inner = max(1, width - 4)
|
|
325
|
+
self.lines = []
|
|
326
|
+
|
|
327
|
+
def top(self, title, badge=""):
|
|
328
|
+
self.lines.append(pad(title, self.inner))
|
|
329
|
+
|
|
330
|
+
def row(self, content):
|
|
331
|
+
self.lines.append(pad(content, self.inner))
|
|
332
|
+
|
|
333
|
+
def sep(self, tees=None):
|
|
334
|
+
self.lines.append(H * self.inner if H else "-" * self.inner)
|
|
335
|
+
|
|
336
|
+
def bottom(self, tees=None):
|
|
337
|
+
self.lines.append("")
|
|
338
|
+
|
|
339
|
+
def render(self):
|
|
340
|
+
return "\n".join(self.lines)
|
|
341
|
+
|
|
342
|
+
def lr(left, right, inner):
|
|
343
|
+
gap = max(1, inner - vlen(left) - vlen(right))
|
|
344
|
+
return left + " " * gap + right
|
|
345
|
+
|
|
346
|
+
def cols(cells, inner):
|
|
347
|
+
return pad(" ".join(cells), inner), []
|
|
348
|
+
|
|
349
|
+
# --------------------------------------------------------------------------- segments (_segmentslib)
|
|
350
|
+
try:
|
|
351
|
+
import _segmentslib
|
|
352
|
+
seg_ctx_lines = _segmentslib.seg_ctx_lines
|
|
353
|
+
seg_window_cells, seg_window_inline = _segmentslib.seg_window_cells, _segmentslib.seg_window_inline
|
|
354
|
+
EFF_DISP, model_pill, usage_row = _segmentslib.EFF_DISP, _segmentslib.model_pill, _segmentslib.usage_row
|
|
355
|
+
seg_lines, seg_pr, seg_prune = _segmentslib.seg_lines, _segmentslib.seg_pr, _segmentslib.seg_prune
|
|
356
|
+
redshift = _segmentslib.redshift
|
|
357
|
+
except Exception: # pragma: no cover — never let an import break the statusline
|
|
358
|
+
_segmentslib = None
|
|
359
|
+
EFF_DISP = {}
|
|
360
|
+
|
|
361
|
+
def seg_ctx_lines(data, w):
|
|
362
|
+
return [f"{GREY}ctx --{RESET}", ""]
|
|
363
|
+
|
|
364
|
+
def seg_window_cells(data):
|
|
365
|
+
return []
|
|
366
|
+
|
|
367
|
+
def seg_window_inline(data):
|
|
368
|
+
return ""
|
|
369
|
+
|
|
370
|
+
def model_pill(model, effort=""):
|
|
371
|
+
return f"{V2}{model}{RESET}"
|
|
372
|
+
|
|
373
|
+
def usage_row(label, tin, tout, cost, trail=""):
|
|
374
|
+
return f"{label} {fmt_tok(tin)}/{fmt_tok(tout)} {usd_fine(cost)}"
|
|
375
|
+
|
|
376
|
+
def seg_lines(data):
|
|
377
|
+
return None
|
|
378
|
+
|
|
379
|
+
def seg_pr(data):
|
|
380
|
+
return None
|
|
381
|
+
|
|
382
|
+
def seg_prune(data, sid):
|
|
383
|
+
return None
|
|
384
|
+
|
|
385
|
+
def redshift(s):
|
|
386
|
+
return s
|
|
387
|
+
|
|
388
|
+
# --------------------------------------------------------------------------- session start (_sessionlib)
|
|
389
|
+
try:
|
|
390
|
+
import _sessionlib
|
|
391
|
+
session_start = _sessionlib.session_start
|
|
392
|
+
except Exception: # pragma: no cover — never let an import break the statusline
|
|
393
|
+
_sessionlib = None
|
|
394
|
+
|
|
395
|
+
def session_start(sid, rec=None):
|
|
396
|
+
return None
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
# --------------------------------------------------------------------------- ledger render-bridge
|
|
400
|
+
# burn_spark stays here (not in _ledgerlib): it is the render bridge that turns
|
|
401
|
+
# the lib's numeric samples into a colored sparkline, so it keeps the ANSI
|
|
402
|
+
# dependency out of the lib.
|
|
403
|
+
def burn_spark(rec):
|
|
404
|
+
"""Sparkline of recent per-message token burn — real per-API-call totals
|
|
405
|
+
accumulated from the transcript (via _ledgerlib.burn_samples), not a
|
|
406
|
+
time-extrapolated estimate."""
|
|
407
|
+
samples = _ledgerlib.burn_samples(rec) if _ledgerlib is not None else []
|
|
408
|
+
return sparkline(samples) if len(samples) >= 2 else ""
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
# --------------------------------------------------------------------------- update-available marker
|
|
412
|
+
def plugin_root_for_render():
|
|
413
|
+
"""The plugin root to resolve the installed version against, for seg_update.
|
|
414
|
+
Delegates to _updatelib.plugin_root() (CLAUDE_PLUGIN_ROOT, else derived from
|
|
415
|
+
this install's own file location) — a thin seam so tests can monkeypatch it
|
|
416
|
+
without touching env vars. None if _updatelib failed to import."""
|
|
417
|
+
if _updatelib is None:
|
|
418
|
+
return None
|
|
419
|
+
return _updatelib.plugin_root()
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def seg_update(plugin=None):
|
|
423
|
+
"""Update-available marker (spec: update-available-notifier.md, AC-1/AC-2):
|
|
424
|
+
reads the SAME user-global cache SessionStart reads and compares against the
|
|
425
|
+
installed plugin.json version. RENDER ONLY — no fetch, no spawn, no network
|
|
426
|
+
call of any kind; a missing/corrupt cache or an up-to-date install both
|
|
427
|
+
degrade to None (no segment), and any error is swallowed (never breaks the
|
|
428
|
+
box, mirrors seg_prune)."""
|
|
429
|
+
if _updatelib is None:
|
|
430
|
+
return None
|
|
431
|
+
try:
|
|
432
|
+
plugin = plugin if plugin is not None else plugin_root_for_render()
|
|
433
|
+
state = _updatelib.read_state(_updatelib.state_path())
|
|
434
|
+
latest = state.get("latest") if isinstance(state, dict) else None
|
|
435
|
+
installed = _updatelib.installed_version(plugin)
|
|
436
|
+
if not _updatelib.update_available(installed, latest):
|
|
437
|
+
return None
|
|
438
|
+
return f"{V2}{UP}{RESET} {WHITE}{latest}{RESET}"
|
|
439
|
+
except Exception: # noqa: BLE001
|
|
440
|
+
return None
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
# --------------------------------------------------------------------------- main
|
|
444
|
+
WIDTH_MARGIN = 6 # columns kept clear of the terminal's right edge
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def detect_box_width():
|
|
448
|
+
"""Fit the box to the terminal, leaving a margin so the right border never
|
|
449
|
+
lands in the final column — writing there auto-wraps and corrupts a
|
|
450
|
+
multi-line statusline. The host runs this command with stdout as a pipe (no
|
|
451
|
+
tty), so COLUMNS (which the host sets) is the primary source; terminal-size
|
|
452
|
+
probes are a fallback. Explicit CODEARBITER_WIDTH wins and is used verbatim."""
|
|
453
|
+
env = os.environ.get("CODEARBITER_WIDTH")
|
|
454
|
+
if env:
|
|
455
|
+
try:
|
|
456
|
+
return max(40, min(200, int(env)))
|
|
457
|
+
except ValueError:
|
|
458
|
+
pass
|
|
459
|
+
raw = 0
|
|
460
|
+
try:
|
|
461
|
+
raw = int(os.environ.get("COLUMNS") or 0)
|
|
462
|
+
except ValueError:
|
|
463
|
+
raw = 0
|
|
464
|
+
if raw <= 0:
|
|
465
|
+
raw = shutil.get_terminal_size(fallback=(0, 0)).columns
|
|
466
|
+
if raw <= 0:
|
|
467
|
+
for fd in (1, 2, 0):
|
|
468
|
+
try:
|
|
469
|
+
raw = os.get_terminal_size(fd).columns
|
|
470
|
+
if raw > 0:
|
|
471
|
+
break
|
|
472
|
+
except OSError:
|
|
473
|
+
pass
|
|
474
|
+
if raw <= 0:
|
|
475
|
+
raw = 140 # wide default when nothing reports a width
|
|
476
|
+
return max(60, min(160, raw - WIDTH_MARGIN))
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
_PALETTE_RENDER_LOCK = threading.RLock()
|
|
480
|
+
_COLORLIB_PALETTE_EXPORTS = (
|
|
481
|
+
"ACTIVE_THEME_NAME", "ACTIVE_PALETTE", "V0", "V1", "V2", "V3",
|
|
482
|
+
"GREY", "WHITE", "OK", "WARN", "DANGER", "PILL_FG",
|
|
483
|
+
)
|
|
484
|
+
_STATUSLINE_PALETTE_EXPORTS = (
|
|
485
|
+
"V0", "V1", "V2", "V3", "GREY", "WHITE", "OK", "WARN", "DANGER", "PILL_FG",
|
|
486
|
+
)
|
|
487
|
+
_CONSUMER_PALETTE_EXPORTS = (
|
|
488
|
+
("_fmtlib", ("_RESET", "_V2", "_gradient_h")),
|
|
489
|
+
("_boxlib", ("_RESET", "_V0", "_gradient_h", "_box_gradient_h")),
|
|
490
|
+
("_segmentslib", ("RESET", "BOLD", "DIM", "V0", "V2", "V3", "GREY",
|
|
491
|
+
"WHITE", "OK", "WARN", "DANGER", "PILL_FG")),
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _palette_snapshot():
|
|
496
|
+
snapshot = [(globals(), {name: globals()[name] for name in _STATUSLINE_PALETTE_EXPORTS})]
|
|
497
|
+
if _colorlib is not None:
|
|
498
|
+
snapshot.append((_colorlib, {name: getattr(_colorlib, name)
|
|
499
|
+
for name in _COLORLIB_PALETTE_EXPORTS}))
|
|
500
|
+
for module_name, names in _CONSUMER_PALETTE_EXPORTS:
|
|
501
|
+
module = globals().get(module_name)
|
|
502
|
+
if module is not None:
|
|
503
|
+
snapshot.append((module, {name: getattr(module, name) for name in names}))
|
|
504
|
+
return snapshot
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _restore_palette(snapshot):
|
|
508
|
+
for target, values in snapshot:
|
|
509
|
+
if isinstance(target, dict):
|
|
510
|
+
target.update(values)
|
|
511
|
+
else:
|
|
512
|
+
for name, value in values.items():
|
|
513
|
+
setattr(target, name, value)
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def render(raw):
|
|
517
|
+
# Palette exports are process-global compatibility state. Keep activation,
|
|
518
|
+
# dependent-module rebinding, and consumption in one re-entrant critical
|
|
519
|
+
# section so concurrent renders cannot observe a torn theme.
|
|
520
|
+
with _PALETTE_RENDER_LOCK:
|
|
521
|
+
snapshot = _palette_snapshot()
|
|
522
|
+
try:
|
|
523
|
+
if _colorlib is not None:
|
|
524
|
+
_colorlib.activate_palette()
|
|
525
|
+
globals().update({name: getattr(_colorlib, name) for name in
|
|
526
|
+
("V0", "V1", "V2", "V3", "GREY", "WHITE", "OK",
|
|
527
|
+
"WARN", "DANGER", "PILL_FG")})
|
|
528
|
+
for lib in (_fmtlib, _boxlib, _segmentslib):
|
|
529
|
+
if lib is not None:
|
|
530
|
+
lib.sync_palette()
|
|
531
|
+
except Exception:
|
|
532
|
+
# Preserve the statusline's fail-soft contract: retain the last
|
|
533
|
+
# coherent exports if activation or a dependent rebind fails.
|
|
534
|
+
_restore_palette(snapshot)
|
|
535
|
+
return _render_active_palette(raw)
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def _render_active_palette(raw):
|
|
539
|
+
try:
|
|
540
|
+
data = json.loads(raw) if raw.strip() else {}
|
|
541
|
+
except (ValueError, TypeError):
|
|
542
|
+
data = {}
|
|
543
|
+
if not isinstance(data, dict):
|
|
544
|
+
data = {}
|
|
545
|
+
|
|
546
|
+
W = detect_box_width()
|
|
547
|
+
compact = os.environ.get("CODEARBITER_COMPACT", "").lower() in ("1", "true", "on", "yes")
|
|
548
|
+
|
|
549
|
+
root = safe(project_root, data) or os.getcwd()
|
|
550
|
+
sid = data.get("session_id") or data.get("sessionId")
|
|
551
|
+
arb = safe(arbiter_state, root)
|
|
552
|
+
effort = (get(data, "effort", "level") or "").lower()
|
|
553
|
+
sprint = bool(arb and arb.get("sprint"))
|
|
554
|
+
# /dev takes precedence over sprint: a textual [DEV] tell rides alongside the
|
|
555
|
+
# full-bar redshift so dev mode reads even where color is stripped or unseen.
|
|
556
|
+
if safe(dev_active, root):
|
|
557
|
+
badge = f"{BOLD}[DEV]{RESET}"
|
|
558
|
+
elif sprint:
|
|
559
|
+
badge = f"{V3}{BOLD}[SPRINT]{RESET}" # effort now shows by the model pill
|
|
560
|
+
else:
|
|
561
|
+
badge = ""
|
|
562
|
+
|
|
563
|
+
led = safe(ledger_update, data, sid)
|
|
564
|
+
if not (isinstance(led, tuple) and len(led) == 3):
|
|
565
|
+
led = ({}, {"in": 0.0, "out": 0.0, "cost": 0.0}, {"in": 0.0, "out": 0.0, "cost": 0.0})
|
|
566
|
+
rec, sess, day = led
|
|
567
|
+
spark = safe(burn_spark, rec) or ""
|
|
568
|
+
# True session age from Claude Code's session metadata (the wall clock /usage
|
|
569
|
+
# shows, incl. idle gaps); fall back to the current transcript's first message.
|
|
570
|
+
had_cached_start = isinstance(rec, dict) and bool(num(rec.get("sess_start"), None))
|
|
571
|
+
s0 = safe(session_start, sid, rec)
|
|
572
|
+
# Freshly resolved (not from cache) -> persist into the ledger so the next render
|
|
573
|
+
# skips the ~/.claude/sessions scan. ledger_update already ran this render, so a
|
|
574
|
+
# targeted write seeds the cache now rather than waiting for the next dirty write.
|
|
575
|
+
if s0 is not None and not had_cached_start and sid:
|
|
576
|
+
safe(persist_sess_start, sid, s0)
|
|
577
|
+
if s0 is None:
|
|
578
|
+
s0 = num(rec.get("t0"), num(rec.get("first_ts"), time.time()))
|
|
579
|
+
age_str = f"{GREY}age{RESET} {WHITE}{human_dur(time.time() - s0)}{RESET}"
|
|
580
|
+
s_trail = spark # age moves under the ctx bar (next to compact %), not here
|
|
581
|
+
|
|
582
|
+
box = Box(W)
|
|
583
|
+
inner = box.inner
|
|
584
|
+
|
|
585
|
+
folder = safe(short_path, get(data, "workspace", "current_dir") or get(data, "cwd")) or "?"
|
|
586
|
+
box.top(folder, badge)
|
|
587
|
+
|
|
588
|
+
# git project + rates + model
|
|
589
|
+
owner = get(data, "workspace", "repo", "owner")
|
|
590
|
+
name = get(data, "workspace", "repo", "name")
|
|
591
|
+
branch = safe(head_branch, root)
|
|
592
|
+
if name:
|
|
593
|
+
gp = f"{GREY}git{RESET} {WHITE}{(str(owner) + '/' if owner else '') + str(name)}{RESET}"
|
|
594
|
+
elif branch:
|
|
595
|
+
gp = f"{GREY}git{RESET} {WHITE}{os.path.basename(root)}{RESET}"
|
|
596
|
+
else:
|
|
597
|
+
gp = f"{GREY}no git{RESET}"
|
|
598
|
+
if branch:
|
|
599
|
+
dirty = "*" if safe(git_dirty, root) else ""
|
|
600
|
+
gp += f" {V0}{V}{RESET} {(WARN if dirty else OK)}{branch}{dirty}{RESET}"
|
|
601
|
+
model = strip_control(get(data, "model", "display_name") or get(data, "model", "id") or "?")
|
|
602
|
+
pill = safe(model_pill, model, effort) or f"{V2}{model}{RESET}"
|
|
603
|
+
rates = safe(seg_window_inline, data) or ""
|
|
604
|
+
prseg = safe(seg_pr, data) or ""
|
|
605
|
+
updseg = safe(seg_update) or ""
|
|
606
|
+
head_bits = [b for b in (rates, prseg, updseg) if b]
|
|
607
|
+
right = (" ".join(head_bits) + " " if head_bits else "") + pill
|
|
608
|
+
# churn is the lowest-priority left segment: append it only if the whole
|
|
609
|
+
# "+N/-M" fits beside the right cluster — never let lr() clip it to "lines …".
|
|
610
|
+
ln = safe(seg_lines, data)
|
|
611
|
+
if ln and vlen(gp) + 2 + vlen(ln) + 3 + vlen(right) <= inner:
|
|
612
|
+
gp += f" {ln}"
|
|
613
|
+
box.row(lr(gp, right, inner))
|
|
614
|
+
|
|
615
|
+
box.sep()
|
|
616
|
+
|
|
617
|
+
# arbiter line (gated)
|
|
618
|
+
if arb:
|
|
619
|
+
s = f" {V0}·{RESET} "
|
|
620
|
+
qcol = DANGER if arb["q"] > 0 else GREY
|
|
621
|
+
ocol = DANGER if arb["over"] > 0 else GREY
|
|
622
|
+
tcol = WHITE if arb["tasks"] > 0 else GREY
|
|
623
|
+
arbline = (
|
|
624
|
+
f"{OK}{DOT}{RESET} {WHITE}stage:{arb['stage']}{RESET}{s}{tcol}tasks:{arb['tasks']}{RESET}"
|
|
625
|
+
f"{s}{qcol}q:{arb['q']}{RESET}{s}{ocol}over:{arb['over']}{RESET}"
|
|
626
|
+
)
|
|
627
|
+
prune_bit = safe(seg_prune, data, sid)
|
|
628
|
+
if prune_bit:
|
|
629
|
+
arbline += f"{s}{prune_bit}"
|
|
630
|
+
box.row(arbline)
|
|
631
|
+
box.sep()
|
|
632
|
+
|
|
633
|
+
# usage block — Session over Total (label │ ↓ in ↑ out │ $cost), a
|
|
634
|
+
# double-height left table; the right column carries the context-window
|
|
635
|
+
# detail. Rate limits live on the header line now, not their own row.
|
|
636
|
+
LW = max(34, min(50, inner // 2)) # proportional: don't starve the ctx panel at narrow widths
|
|
637
|
+
srow = safe(usage_row, "Session", sess["in"], sess["out"], sess["cost"], s_trail) or ""
|
|
638
|
+
trow = safe(usage_row, "Today", day["in"], day["out"], day["cost"]) or ""
|
|
639
|
+
ctxl = safe(seg_ctx_lines, data, max(8, inner - LW - 3)) or [f"{GREY}ctx --{RESET}", ""]
|
|
640
|
+
div = f"{V0}{V}{RESET}"
|
|
641
|
+
box.row(f"{pad(srow, LW)} {div} {ctxl[0]}")
|
|
642
|
+
cl2 = ctxl[1] if len(ctxl) > 1 else ""
|
|
643
|
+
cl2 = f"{cl2} {age_str}" if cl2 else age_str # session age sits under the bar, by the compact %
|
|
644
|
+
box.row(f"{pad(trow, LW)} {div} {cl2}")
|
|
645
|
+
tail_tees = []
|
|
646
|
+
|
|
647
|
+
# subagent rows (gated on presence; lean mode drops)
|
|
648
|
+
if not compact:
|
|
649
|
+
sdir = safe(subagent_dir, data, root, sid)
|
|
650
|
+
if sdir:
|
|
651
|
+
res = safe(read_subagents, sdir)
|
|
652
|
+
if res:
|
|
653
|
+
active, recent, shown, (tin, tout) = res
|
|
654
|
+
if shown:
|
|
655
|
+
box.sep()
|
|
656
|
+
hdr = f"{V3}subagents{RESET} {WHITE}{active}{RESET} {GREY}active{RESET}"
|
|
657
|
+
if recent > active: # reconcile the header with the rows shown
|
|
658
|
+
hdr += f" {V0}·{RESET} {GREY}{recent} recent{RESET}"
|
|
659
|
+
hdr += (f" {V2}{DN}{RESET} {WHITE}{fmt_tok(tin)}{RESET}"
|
|
660
|
+
f" {V2}{UP}{RESET} {WHITE}{fmt_tok(tout)}{RESET}")
|
|
661
|
+
box.row(hdr)
|
|
662
|
+
for sub in shown:
|
|
663
|
+
live = sub.get("active")
|
|
664
|
+
glyph = f"{OK}{DOT}{RESET}" if live else f"{GREY}✓{RESET}"
|
|
665
|
+
lcol = WHITE if live else GREY
|
|
666
|
+
# Keep model + metrics as the stable tail. At narrow widths the
|
|
667
|
+
# task label yields all of its space before this information.
|
|
668
|
+
model_tag = f"{V3}{sub.get('model', 'model:?')}{RESET}"
|
|
669
|
+
tail = (f" {model_tag}"
|
|
670
|
+
f" {V2}{DN}{RESET} {GREY}{fmt_tok(sub['inp'])}{RESET}"
|
|
671
|
+
f" {V2}{UP}{RESET} {GREY}{fmt_tok(sub['out'])}{RESET}"
|
|
672
|
+
f" {DIM}{human_dur(sub['age'])}{RESET}")
|
|
673
|
+
# grow the label to its natural width up to the row limit, then the
|
|
674
|
+
# tail sits right after it (was a fixed 22, wasting wide terminals);
|
|
675
|
+
# clip (not pad) keeps the metrics adjacent and leaves trailing space
|
|
676
|
+
lw = max(0, inner - 4 - vlen(tail))
|
|
677
|
+
label = f"{lcol}{clip(sub['label'], lw)}{RESET}" if lw else ""
|
|
678
|
+
box.row(f" {glyph} {label}{tail}")
|
|
679
|
+
tail_tees = []
|
|
680
|
+
|
|
681
|
+
box.bottom(tees=tail_tees)
|
|
682
|
+
out = box.render()
|
|
683
|
+
out = redshift(out) if safe(dev_active, root) else out
|
|
684
|
+
# Honor the NO_COLOR convention by stripping SGR from the final render. Do NOT gate on
|
|
685
|
+
# isatty: a Claude Code statusline is intentionally piped, so an isatty test would drop
|
|
686
|
+
# color in normal use. Width math already ignores ANSI, so stripping keeps alignment.
|
|
687
|
+
if "NO_COLOR" in os.environ:
|
|
688
|
+
out = ANSI.sub("", out)
|
|
689
|
+
return out
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def main():
|
|
693
|
+
if os.environ.get("CODEARBITER_STATUSLINE", "").lower() == "off":
|
|
694
|
+
return
|
|
695
|
+
for s in (sys.stdout, sys.stdin):
|
|
696
|
+
try:
|
|
697
|
+
s.reconfigure(encoding="utf-8", errors="replace")
|
|
698
|
+
except (AttributeError, ValueError):
|
|
699
|
+
pass
|
|
700
|
+
raw = ""
|
|
701
|
+
try:
|
|
702
|
+
raw = sys.stdin.read() if not sys.stdin.isatty() else ""
|
|
703
|
+
except Exception:
|
|
704
|
+
raw = ""
|
|
705
|
+
try:
|
|
706
|
+
out = render(raw)
|
|
707
|
+
b = out.encode("utf-8", "replace")
|
|
708
|
+
buf = getattr(sys.stdout, "buffer", None)
|
|
709
|
+
if buf is not None:
|
|
710
|
+
buf.write(b)
|
|
711
|
+
buf.flush()
|
|
712
|
+
else:
|
|
713
|
+
sys.stdout.write(out)
|
|
714
|
+
except Exception:
|
|
715
|
+
# A statusline must never emit a traceback. Degrade to no output.
|
|
716
|
+
return
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def run(host, argv=None):
|
|
720
|
+
"""Host-seam entry point (ADR-0011): the __main__ guard calls this with the
|
|
721
|
+
plugin's loaded Host. Wraps main() unchanged — main() still communicates
|
|
722
|
+
via sys.exit/stdout/stderr, and its return value stays discarded exactly
|
|
723
|
+
as the old bare `main()` guard discarded it (so the process still exits 0
|
|
724
|
+
on a normal fall-through).
|
|
725
|
+
|
|
726
|
+
Wires `host` live (#257): primes `_hooklib`'s process-cached Host via
|
|
727
|
+
`set_host()` BEFORE main() runs, so any `get_host()` call downstream
|
|
728
|
+
resolves to the SAME instance the caller passed here — no second
|
|
729
|
+
`hostapi.load_host()`, and `run(fake_host)` genuinely exercises
|
|
730
|
+
`fake_host`."""
|
|
731
|
+
return _entrylib.dispatch(host, argv, main, _hooklib.set_host,
|
|
732
|
+
pass_argv=False, propagate_result=False)
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
if __name__ == "__main__":
|
|
736
|
+
sys.exit(run(hostapi.load_host()) or 0)
|