@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.
Files changed (206) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +558 -0
  3. package/package.json +35 -0
  4. package/plugins/ca-pi/CHANGELOG.md +1030 -0
  5. package/plugins/ca-pi/COMMANDS.md +90 -0
  6. package/plugins/ca-pi/ORCHESTRATOR.md +159 -0
  7. package/plugins/ca-pi/SKILLS.md +47 -0
  8. package/plugins/ca-pi/SPRINT.md +142 -0
  9. package/plugins/ca-pi/agents/INDEX.md +31 -0
  10. package/plugins/ca-pi/agents/architecture-drift-reviewer.md +86 -0
  11. package/plugins/ca-pi/agents/auth-crypto-reviewer.md +60 -0
  12. package/plugins/ca-pi/agents/backend-author.md +60 -0
  13. package/plugins/ca-pi/agents/checkpoint-aggregator.md +111 -0
  14. package/plugins/ca-pi/agents/coverage-auditor.md +71 -0
  15. package/plugins/ca-pi/agents/decision-challenger.md +116 -0
  16. package/plugins/ca-pi/agents/dependency-reviewer.md +79 -0
  17. package/plugins/ca-pi/agents/design-quality-reviewer.md +80 -0
  18. package/plugins/ca-pi/agents/finding-triage.md +86 -0
  19. package/plugins/ca-pi/agents/frontend-author.md +64 -0
  20. package/plugins/ca-pi/agents/grader.md +173 -0
  21. package/plugins/ca-pi/agents/infra-author.md +64 -0
  22. package/plugins/ca-pi/agents/map-deps.md +35 -0
  23. package/plugins/ca-pi/agents/map-structure.md +37 -0
  24. package/plugins/ca-pi/agents/migration-reviewer.md +65 -0
  25. package/plugins/ca-pi/agents/scout.md +127 -0
  26. package/plugins/ca-pi/agents/security-reviewer.md +72 -0
  27. package/plugins/ca-pi/agents/tribunal-lens-reviewer.md +65 -0
  28. package/plugins/ca-pi/extensions/codearbiter-child.js +1885 -0
  29. package/plugins/ca-pi/extensions/codearbiter.js +9802 -0
  30. package/plugins/ca-pi/generated/command-catalog.json +197 -0
  31. package/plugins/ca-pi/generated/roles.json +213 -0
  32. package/plugins/ca-pi/helpers/windows-supervisor.js +205 -0
  33. package/plugins/ca-pi/hooks/_activationlib.py +196 -0
  34. package/plugins/ca-pi/hooks/_arbiterstatelib.py +208 -0
  35. package/plugins/ca-pi/hooks/_babysitlib.py +76 -0
  36. package/plugins/ca-pi/hooks/_bashguardlib.py +1667 -0
  37. package/plugins/ca-pi/hooks/_boxlib.py +131 -0
  38. package/plugins/ca-pi/hooks/_colorlib.py +304 -0
  39. package/plugins/ca-pi/hooks/_durabilitylib.py +186 -0
  40. package/plugins/ca-pi/hooks/_entrylib.py +41 -0
  41. package/plugins/ca-pi/hooks/_fmtlib.py +161 -0
  42. package/plugins/ca-pi/hooks/_gitexec.py +45 -0
  43. package/plugins/ca-pi/hooks/_githooks.py +920 -0
  44. package/plugins/ca-pi/hooks/_gitlib.py +110 -0
  45. package/plugins/ca-pi/hooks/_hooklib.py +595 -0
  46. package/plugins/ca-pi/hooks/_host.py +115 -0
  47. package/plugins/ca-pi/hooks/_intentlib.py +242 -0
  48. package/plugins/ca-pi/hooks/_ledgerlib.py +1035 -0
  49. package/plugins/ca-pi/hooks/_metricslib.py +709 -0
  50. package/plugins/ca-pi/hooks/_pathnorm.py +74 -0
  51. package/plugins/ca-pi/hooks/_planfilelib.py +664 -0
  52. package/plugins/ca-pi/hooks/_previewlib.py +193 -0
  53. package/plugins/ca-pi/hooks/_protectedlib.py +312 -0
  54. package/plugins/ca-pi/hooks/_protectedstatelib.py +411 -0
  55. package/plugins/ca-pi/hooks/_provenancelib.py +971 -0
  56. package/plugins/ca-pi/hooks/_prunelib.py +1398 -0
  57. package/plugins/ca-pi/hooks/_prunepolicy.py +235 -0
  58. package/plugins/ca-pi/hooks/_readinjectlib.py +1080 -0
  59. package/plugins/ca-pi/hooks/_releaselib.py +2657 -0
  60. package/plugins/ca-pi/hooks/_scopelib.py +262 -0
  61. package/plugins/ca-pi/hooks/_segmentslib.py +278 -0
  62. package/plugins/ca-pi/hooks/_sensitivelib.py +270 -0
  63. package/plugins/ca-pi/hooks/_sessionlib.py +78 -0
  64. package/plugins/ca-pi/hooks/_sloplib.py +244 -0
  65. package/plugins/ca-pi/hooks/_standuplib.py +214 -0
  66. package/plugins/ca-pi/hooks/_subagentslib.py +219 -0
  67. package/plugins/ca-pi/hooks/_taskboardlib.py +1088 -0
  68. package/plugins/ca-pi/hooks/_updatelib.py +278 -0
  69. package/plugins/ca-pi/hooks/babysit.py +47 -0
  70. package/plugins/ca-pi/hooks/boardsync.py +129 -0
  71. package/plugins/ca-pi/hooks/doctor.py +420 -0
  72. package/plugins/ca-pi/hooks/git-enforce.py +325 -0
  73. package/plugins/ca-pi/hooks/hostapi.py +460 -0
  74. package/plugins/ca-pi/hooks/init-codearbiter.py +225 -0
  75. package/plugins/ca-pi/hooks/metrics.py +62 -0
  76. package/plugins/ca-pi/hooks/migration-pass.py +129 -0
  77. package/plugins/ca-pi/hooks/pi-bridge.py +543 -0
  78. package/plugins/ca-pi/hooks/post-write-edit.py +231 -0
  79. package/plugins/ca-pi/hooks/pre-bash.py +90 -0
  80. package/plugins/ca-pi/hooks/pre-edit.py +284 -0
  81. package/plugins/ca-pi/hooks/pre-read.py +81 -0
  82. package/plugins/ca-pi/hooks/pre-write.py +217 -0
  83. package/plugins/ca-pi/hooks/preview.py +69 -0
  84. package/plugins/ca-pi/hooks/prune-transcript.py +232 -0
  85. package/plugins/ca-pi/hooks/releasehash.py +216 -0
  86. package/plugins/ca-pi/hooks/security-pass.py +139 -0
  87. package/plugins/ca-pi/hooks/session-start.py +1218 -0
  88. package/plugins/ca-pi/hooks/statusline.py +736 -0
  89. package/plugins/ca-pi/hooks/taskwrite.py +351 -0
  90. package/plugins/ca-pi/hooks/update-refresh.py +51 -0
  91. package/plugins/ca-pi/hooks/wire-statusline.py +435 -0
  92. package/plugins/ca-pi/includes/anti-slop-design/INDEX.md +55 -0
  93. package/plugins/ca-pi/includes/anti-slop-design/color.md +43 -0
  94. package/plugins/ca-pi/includes/anti-slop-design/core.md +244 -0
  95. package/plugins/ca-pi/includes/anti-slop-design/images.md +32 -0
  96. package/plugins/ca-pi/includes/anti-slop-design/layout.md +45 -0
  97. package/plugins/ca-pi/includes/anti-slop-design/medium-cli.md +39 -0
  98. package/plugins/ca-pi/includes/anti-slop-design/medium-dataviz.md +43 -0
  99. package/plugins/ca-pi/includes/anti-slop-design/medium-diagram.md +35 -0
  100. package/plugins/ca-pi/includes/anti-slop-design/medium-documents.md +70 -0
  101. package/plugins/ca-pi/includes/anti-slop-design/medium-slides.md +30 -0
  102. package/plugins/ca-pi/includes/anti-slop-design/medium-web.md +39 -0
  103. package/plugins/ca-pi/includes/anti-slop-design/typography.md +51 -0
  104. package/plugins/ca-pi/includes/author-tdd-workflow.md +14 -0
  105. package/plugins/ca-pi/includes/compaction-charter.md +16 -0
  106. package/plugins/ca-pi/includes/cut-docs.md +16 -0
  107. package/plugins/ca-pi/includes/dev-mode.md +30 -0
  108. package/plugins/ca-pi/includes/farm.md +237 -0
  109. package/plugins/ca-pi/includes/fresh-verification.md +14 -0
  110. package/plugins/ca-pi/includes/harvest.md +69 -0
  111. package/plugins/ca-pi/includes/maturity-coverage.md +102 -0
  112. package/plugins/ca-pi/includes/pi-host-notes.md +69 -0
  113. package/plugins/ca-pi/includes/redirect.md +69 -0
  114. package/plugins/ca-pi/includes/reference-map.md +22 -0
  115. package/plugins/ca-pi/includes/review-matrix.md +14 -0
  116. package/plugins/ca-pi/includes/reviewer-contract.md +53 -0
  117. package/plugins/ca-pi/includes/routing-table.md +47 -0
  118. package/plugins/ca-pi/includes/security-gate-record.md +22 -0
  119. package/plugins/ca-pi/includes/smarts/core.md +90 -0
  120. package/plugins/ca-pi/includes/smarts/decision-log-format.md +56 -0
  121. package/plugins/ca-pi/routines/INDEX.md +32 -0
  122. package/plugins/ca-pi/routines/brainstorming/SKILL.md +122 -0
  123. package/plugins/ca-pi/routines/commit-gate/SKILL.md +151 -0
  124. package/plugins/ca-pi/routines/context-check/SKILL.md +85 -0
  125. package/plugins/ca-pi/routines/context-creation/SKILL.md +171 -0
  126. package/plugins/ca-pi/routines/crypto-compliance/SKILL.md +41 -0
  127. package/plugins/ca-pi/routines/debug/SKILL.md +99 -0
  128. package/plugins/ca-pi/routines/decision-lifecycle/SKILL.md +104 -0
  129. package/plugins/ca-pi/routines/decision-lifecycle/references/adr-template.md +74 -0
  130. package/plugins/ca-pi/routines/decision-variance/SKILL.md +147 -0
  131. package/plugins/ca-pi/routines/decompose/SKILL.md +168 -0
  132. package/plugins/ca-pi/routines/dispatching-parallel-agents/SKILL.md +76 -0
  133. package/plugins/ca-pi/routines/executing-plans/SKILL.md +83 -0
  134. package/plugins/ca-pi/routines/finishing-a-development-branch/SKILL.md +91 -0
  135. package/plugins/ca-pi/routines/post-merge-cleanup/SKILL.md +233 -0
  136. package/plugins/ca-pi/routines/refactor/SKILL.md +91 -0
  137. package/plugins/ca-pi/routines/release/SKILL.md +315 -0
  138. package/plugins/ca-pi/routines/secret-handling/SKILL.md +67 -0
  139. package/plugins/ca-pi/routines/security-architecture/SKILL.md +63 -0
  140. package/plugins/ca-pi/routines/skill-author/SKILL.md +108 -0
  141. package/plugins/ca-pi/routines/skill-author/references/skill-template.md +58 -0
  142. package/plugins/ca-pi/routines/subagent-driven-development/SKILL.md +149 -0
  143. package/plugins/ca-pi/routines/subagent-driven-development/references/farm-dispatch.md +145 -0
  144. package/plugins/ca-pi/routines/tdd/SKILL.md +139 -0
  145. package/plugins/ca-pi/routines/tribunal/SKILL.md +109 -0
  146. package/plugins/ca-pi/routines/tribunal/references/ai-markers.md +29 -0
  147. package/plugins/ca-pi/routines/tribunal/references/cost-and-models.md +64 -0
  148. package/plugins/ca-pi/routines/tribunal/references/finding-record.md +27 -0
  149. package/plugins/ca-pi/routines/tribunal/references/issue-filing.md +47 -0
  150. package/plugins/ca-pi/routines/tribunal/references/lenses/appsec.md +22 -0
  151. package/plugins/ca-pi/routines/tribunal/references/lenses/architecture.md +23 -0
  152. package/plugins/ca-pi/routines/tribunal/references/lenses/coverage.md +20 -0
  153. package/plugins/ca-pi/routines/tribunal/references/lenses/infra.md +24 -0
  154. package/plugins/ca-pi/routines/tribunal/references/lenses/migration.md +22 -0
  155. package/plugins/ca-pi/routines/tribunal/references/lenses/observability.md +21 -0
  156. package/plugins/ca-pi/routines/tribunal/references/lenses/performance.md +22 -0
  157. package/plugins/ca-pi/routines/tribunal/references/lenses/reliability.md +23 -0
  158. package/plugins/ca-pi/routines/tribunal/references/lenses/secrets-supply.md +22 -0
  159. package/plugins/ca-pi/routines/tribunal/references/lenses/test-fidelity.md +24 -0
  160. package/plugins/ca-pi/routines/tribunal/references/lenses/typesafety.md +21 -0
  161. package/plugins/ca-pi/routines/tribunal/references/report.md +19 -0
  162. package/plugins/ca-pi/routines/tribunal/references/schemas.md +58 -0
  163. package/plugins/ca-pi/routines/tribunal/references/telemetry.md +28 -0
  164. package/plugins/ca-pi/routines/tribunal/references/triage.md +53 -0
  165. package/plugins/ca-pi/routines/using-git-worktrees/SKILL.md +85 -0
  166. package/plugins/ca-pi/routines/writing-plans/SKILL.md +129 -0
  167. package/plugins/ca-pi/routines/writing-plans/references/farm-plan.md +50 -0
  168. package/plugins/ca-pi/skills/ca-add-dep/SKILL.md +88 -0
  169. package/plugins/ca-pi/skills/ca-adr/SKILL.md +30 -0
  170. package/plugins/ca-pi/skills/ca-adr-status/SKILL.md +30 -0
  171. package/plugins/ca-pi/skills/ca-arbiter/SKILL.md +36 -0
  172. package/plugins/ca-pi/skills/ca-audit/SKILL.md +51 -0
  173. package/plugins/ca-pi/skills/ca-btw/SKILL.md +23 -0
  174. package/plugins/ca-pi/skills/ca-checkpoint/SKILL.md +50 -0
  175. package/plugins/ca-pi/skills/ca-chore/SKILL.md +58 -0
  176. package/plugins/ca-pi/skills/ca-cleanup/SKILL.md +55 -0
  177. package/plugins/ca-pi/skills/ca-commands/SKILL.md +21 -0
  178. package/plugins/ca-pi/skills/ca-commit/SKILL.md +27 -0
  179. package/plugins/ca-pi/skills/ca-conflict/SKILL.md +61 -0
  180. package/plugins/ca-pi/skills/ca-context-check/SKILL.md +32 -0
  181. package/plugins/ca-pi/skills/ca-create-context/SKILL.md +32 -0
  182. package/plugins/ca-pi/skills/ca-debug/SKILL.md +42 -0
  183. package/plugins/ca-pi/skills/ca-decompose/SKILL.md +30 -0
  184. package/plugins/ca-pi/skills/ca-dev/SKILL.md +42 -0
  185. package/plugins/ca-pi/skills/ca-doctor/SKILL.md +44 -0
  186. package/plugins/ca-pi/skills/ca-feature/SKILL.md +105 -0
  187. package/plugins/ca-pi/skills/ca-fix/SKILL.md +42 -0
  188. package/plugins/ca-pi/skills/ca-init/SKILL.md +56 -0
  189. package/plugins/ca-pi/skills/ca-metrics/SKILL.md +80 -0
  190. package/plugins/ca-pi/skills/ca-new-skill/SKILL.md +34 -0
  191. package/plugins/ca-pi/skills/ca-override/SKILL.md +72 -0
  192. package/plugins/ca-pi/skills/ca-pr/SKILL.md +61 -0
  193. package/plugins/ca-pi/skills/ca-preview/SKILL.md +86 -0
  194. package/plugins/ca-pi/skills/ca-prune/SKILL.md +100 -0
  195. package/plugins/ca-pi/skills/ca-reconcile/SKILL.md +43 -0
  196. package/plugins/ca-pi/skills/ca-refactor/SKILL.md +43 -0
  197. package/plugins/ca-pi/skills/ca-release/SKILL.md +57 -0
  198. package/plugins/ca-pi/skills/ca-review/SKILL.md +63 -0
  199. package/plugins/ca-pi/skills/ca-spike/SKILL.md +41 -0
  200. package/plugins/ca-pi/skills/ca-sprint/SKILL.md +44 -0
  201. package/plugins/ca-pi/skills/ca-standup/SKILL.md +112 -0
  202. package/plugins/ca-pi/skills/ca-status/SKILL.md +56 -0
  203. package/plugins/ca-pi/skills/ca-task/SKILL.md +61 -0
  204. package/plugins/ca-pi/skills/ca-threat-model/SKILL.md +45 -0
  205. package/plugins/ca-pi/skills/ca-tribunal/SKILL.md +44 -0
  206. 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)