anvil-dev-framework 0.1.6

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 (190) hide show
  1. package/README.md +719 -0
  2. package/VERSION +1 -0
  3. package/docs/ANVIL-REPO-IMPLEMENTATION-PLAN.md +441 -0
  4. package/docs/FIRST-SKILL-TUTORIAL.md +408 -0
  5. package/docs/INSTALLATION-RETRO-NOTES.md +458 -0
  6. package/docs/INSTALLATION.md +984 -0
  7. package/docs/anvil-hud.md +469 -0
  8. package/docs/anvil-init.md +255 -0
  9. package/docs/anvil-state.md +210 -0
  10. package/docs/boris-cherny-ralph-wiggum-insights.md +608 -0
  11. package/docs/command-reference.md +2022 -0
  12. package/docs/hooks-tts.md +368 -0
  13. package/docs/implementation-guide.md +810 -0
  14. package/docs/linear-github-integration.md +247 -0
  15. package/docs/local-issues.md +677 -0
  16. package/docs/patterns/README.md +419 -0
  17. package/docs/planning-responsibilities.md +139 -0
  18. package/docs/session-workflow.md +573 -0
  19. package/docs/simplification-plan-template.md +297 -0
  20. package/docs/simplification-principles.md +129 -0
  21. package/docs/specifications/CCS-RALPH-INTEGRATION-DESIGN.md +633 -0
  22. package/docs/specifications/CCS-RESEARCH-REPORT.md +169 -0
  23. package/docs/specifications/PLAN-ANV-verification-ralph-wiggum.md +403 -0
  24. package/docs/specifications/PLAN-parallel-tracks-anvil-memory-ccs.md +494 -0
  25. package/docs/specifications/SPEC-ANV-VRW/component-01-verify.md +208 -0
  26. package/docs/specifications/SPEC-ANV-VRW/component-02-stop-gate.md +226 -0
  27. package/docs/specifications/SPEC-ANV-VRW/component-03-posttooluse.md +209 -0
  28. package/docs/specifications/SPEC-ANV-VRW/component-04-ralph-wiggum.md +604 -0
  29. package/docs/specifications/SPEC-ANV-VRW/component-05-atomic-actions.md +311 -0
  30. package/docs/specifications/SPEC-ANV-VRW/component-06-verify-subagent.md +264 -0
  31. package/docs/specifications/SPEC-ANV-VRW/component-07-claude-md.md +363 -0
  32. package/docs/specifications/SPEC-ANV-VRW/index.md +182 -0
  33. package/docs/specifications/SPEC-ANV-anvil-memory.md +573 -0
  34. package/docs/specifications/SPEC-ANV-context-checkpoints.md +781 -0
  35. package/docs/specifications/SPEC-ANV-verification-ralph-wiggum.md +789 -0
  36. package/docs/sync.md +122 -0
  37. package/global/CLAUDE.md +140 -0
  38. package/global/agents/verify-app.md +164 -0
  39. package/global/commands/anvil-settings.md +527 -0
  40. package/global/commands/anvil-sync.md +121 -0
  41. package/global/commands/change.md +197 -0
  42. package/global/commands/clarify.md +252 -0
  43. package/global/commands/cleanup.md +292 -0
  44. package/global/commands/commit-push-pr.md +207 -0
  45. package/global/commands/decay-review.md +127 -0
  46. package/global/commands/discover.md +158 -0
  47. package/global/commands/doc-coverage.md +122 -0
  48. package/global/commands/evidence.md +307 -0
  49. package/global/commands/explore.md +121 -0
  50. package/global/commands/force-exit.md +135 -0
  51. package/global/commands/handoff.md +191 -0
  52. package/global/commands/healthcheck.md +302 -0
  53. package/global/commands/hud.md +84 -0
  54. package/global/commands/insights.md +319 -0
  55. package/global/commands/linear-setup.md +184 -0
  56. package/global/commands/lint-fix.md +198 -0
  57. package/global/commands/orient.md +510 -0
  58. package/global/commands/plan.md +228 -0
  59. package/global/commands/ralph.md +346 -0
  60. package/global/commands/ready.md +182 -0
  61. package/global/commands/release.md +305 -0
  62. package/global/commands/retro.md +96 -0
  63. package/global/commands/shard.md +166 -0
  64. package/global/commands/spec.md +227 -0
  65. package/global/commands/sprint.md +184 -0
  66. package/global/commands/tasks.md +228 -0
  67. package/global/commands/test-and-commit.md +151 -0
  68. package/global/commands/validate.md +132 -0
  69. package/global/commands/verify.md +251 -0
  70. package/global/commands/weekly-review.md +156 -0
  71. package/global/hooks/__pycache__/ralph_context_monitor.cpython-314.pyc +0 -0
  72. package/global/hooks/__pycache__/statusline_agent_sync.cpython-314.pyc +0 -0
  73. package/global/hooks/anvil_memory_observe.ts +322 -0
  74. package/global/hooks/anvil_memory_session.ts +166 -0
  75. package/global/hooks/anvil_memory_stop.ts +187 -0
  76. package/global/hooks/parse_transcript.py +116 -0
  77. package/global/hooks/post_merge_cleanup.sh +132 -0
  78. package/global/hooks/post_tool_format.sh +215 -0
  79. package/global/hooks/ralph_context_monitor.py +240 -0
  80. package/global/hooks/ralph_stop.sh +502 -0
  81. package/global/hooks/statusline.sh +1110 -0
  82. package/global/hooks/statusline_agent_sync.py +224 -0
  83. package/global/hooks/stop_gate.sh +250 -0
  84. package/global/lib/.claude/anvil-state.json +21 -0
  85. package/global/lib/__pycache__/agent_registry.cpython-314.pyc +0 -0
  86. package/global/lib/__pycache__/claim_service.cpython-314.pyc +0 -0
  87. package/global/lib/__pycache__/coderabbit_service.cpython-314.pyc +0 -0
  88. package/global/lib/__pycache__/config_service.cpython-314.pyc +0 -0
  89. package/global/lib/__pycache__/coordination_service.cpython-314.pyc +0 -0
  90. package/global/lib/__pycache__/doc_coverage_service.cpython-314.pyc +0 -0
  91. package/global/lib/__pycache__/gate_logger.cpython-314.pyc +0 -0
  92. package/global/lib/__pycache__/github_service.cpython-314.pyc +0 -0
  93. package/global/lib/__pycache__/hygiene_service.cpython-314.pyc +0 -0
  94. package/global/lib/__pycache__/issue_models.cpython-314.pyc +0 -0
  95. package/global/lib/__pycache__/issue_provider.cpython-314.pyc +0 -0
  96. package/global/lib/__pycache__/linear_data_service.cpython-314.pyc +0 -0
  97. package/global/lib/__pycache__/linear_provider.cpython-314.pyc +0 -0
  98. package/global/lib/__pycache__/local_provider.cpython-314.pyc +0 -0
  99. package/global/lib/__pycache__/quality_service.cpython-314.pyc +0 -0
  100. package/global/lib/__pycache__/ralph_state.cpython-314.pyc +0 -0
  101. package/global/lib/__pycache__/state_manager.cpython-314.pyc +0 -0
  102. package/global/lib/__pycache__/transcript_parser.cpython-314.pyc +0 -0
  103. package/global/lib/__pycache__/verification_runner.cpython-314.pyc +0 -0
  104. package/global/lib/__pycache__/verify_iteration.cpython-314.pyc +0 -0
  105. package/global/lib/__pycache__/verify_subagent.cpython-314.pyc +0 -0
  106. package/global/lib/agent_registry.py +995 -0
  107. package/global/lib/anvil-state.sh +435 -0
  108. package/global/lib/claim_service.py +515 -0
  109. package/global/lib/coderabbit_service.py +314 -0
  110. package/global/lib/config_service.py +423 -0
  111. package/global/lib/coordination_service.py +331 -0
  112. package/global/lib/doc_coverage_service.py +1305 -0
  113. package/global/lib/gate_logger.py +316 -0
  114. package/global/lib/github_service.py +310 -0
  115. package/global/lib/handoff_generator.py +775 -0
  116. package/global/lib/hygiene_service.py +712 -0
  117. package/global/lib/issue_models.py +257 -0
  118. package/global/lib/issue_provider.py +339 -0
  119. package/global/lib/linear_data_service.py +210 -0
  120. package/global/lib/linear_provider.py +987 -0
  121. package/global/lib/linear_provider.py.backup +671 -0
  122. package/global/lib/local_provider.py +486 -0
  123. package/global/lib/orient_fast.py +457 -0
  124. package/global/lib/quality_service.py +470 -0
  125. package/global/lib/ralph_prompt_generator.py +563 -0
  126. package/global/lib/ralph_state.py +1202 -0
  127. package/global/lib/state_manager.py +417 -0
  128. package/global/lib/transcript_parser.py +597 -0
  129. package/global/lib/verification_runner.py +557 -0
  130. package/global/lib/verify_iteration.py +490 -0
  131. package/global/lib/verify_subagent.py +250 -0
  132. package/global/skills/README.md +155 -0
  133. package/global/skills/quality-gates/SKILL.md +252 -0
  134. package/global/skills/skill-template/SKILL.md +109 -0
  135. package/global/skills/testing-strategies/SKILL.md +337 -0
  136. package/global/templates/CHANGE-template.md +105 -0
  137. package/global/templates/HANDOFF-template.md +63 -0
  138. package/global/templates/PLAN-template.md +111 -0
  139. package/global/templates/SPEC-template.md +93 -0
  140. package/global/templates/ralph/PROMPT.md.template +89 -0
  141. package/global/templates/ralph/fix_plan.md.template +31 -0
  142. package/global/templates/ralph/progress.txt.template +23 -0
  143. package/global/tests/__pycache__/test_doc_coverage.cpython-314.pyc +0 -0
  144. package/global/tests/test_doc_coverage.py +520 -0
  145. package/global/tests/test_issue_models.py +299 -0
  146. package/global/tests/test_local_provider.py +323 -0
  147. package/global/tools/README.md +178 -0
  148. package/global/tools/__pycache__/anvil-hud.cpython-314.pyc +0 -0
  149. package/global/tools/anvil-hud.py +3622 -0
  150. package/global/tools/anvil-hud.py.bak +3318 -0
  151. package/global/tools/anvil-issue.py +432 -0
  152. package/global/tools/anvil-memory/CLAUDE.md +49 -0
  153. package/global/tools/anvil-memory/README.md +42 -0
  154. package/global/tools/anvil-memory/bun.lock +25 -0
  155. package/global/tools/anvil-memory/bunfig.toml +9 -0
  156. package/global/tools/anvil-memory/package.json +23 -0
  157. package/global/tools/anvil-memory/src/__tests__/ccs/context-monitor.test.ts +535 -0
  158. package/global/tools/anvil-memory/src/__tests__/ccs/edge-cases.test.ts +645 -0
  159. package/global/tools/anvil-memory/src/__tests__/ccs/fixtures.ts +363 -0
  160. package/global/tools/anvil-memory/src/__tests__/ccs/index.ts +8 -0
  161. package/global/tools/anvil-memory/src/__tests__/ccs/integration.test.ts +417 -0
  162. package/global/tools/anvil-memory/src/__tests__/ccs/prompt-generator.test.ts +571 -0
  163. package/global/tools/anvil-memory/src/__tests__/ccs/ralph-stop.test.ts +440 -0
  164. package/global/tools/anvil-memory/src/__tests__/ccs/test-utils.ts +252 -0
  165. package/global/tools/anvil-memory/src/__tests__/commands.test.ts +657 -0
  166. package/global/tools/anvil-memory/src/__tests__/db.test.ts +641 -0
  167. package/global/tools/anvil-memory/src/__tests__/hooks.test.ts +272 -0
  168. package/global/tools/anvil-memory/src/__tests__/performance.test.ts +427 -0
  169. package/global/tools/anvil-memory/src/__tests__/test-utils.ts +113 -0
  170. package/global/tools/anvil-memory/src/commands/checkpoint.ts +197 -0
  171. package/global/tools/anvil-memory/src/commands/get.ts +115 -0
  172. package/global/tools/anvil-memory/src/commands/init.ts +94 -0
  173. package/global/tools/anvil-memory/src/commands/observe.ts +163 -0
  174. package/global/tools/anvil-memory/src/commands/search.ts +112 -0
  175. package/global/tools/anvil-memory/src/db.ts +638 -0
  176. package/global/tools/anvil-memory/src/index.ts +205 -0
  177. package/global/tools/anvil-memory/src/types.ts +122 -0
  178. package/global/tools/anvil-memory/tsconfig.json +29 -0
  179. package/global/tools/ralph-loop.sh +359 -0
  180. package/package.json +45 -0
  181. package/scripts/anvil +822 -0
  182. package/scripts/extract_patterns.py +222 -0
  183. package/scripts/init-project.sh +541 -0
  184. package/scripts/install.sh +229 -0
  185. package/scripts/postinstall.js +41 -0
  186. package/scripts/rollback.sh +188 -0
  187. package/scripts/sync.sh +623 -0
  188. package/scripts/test-statusline.sh +248 -0
  189. package/scripts/update_claude_md.py +224 -0
  190. package/scripts/verify.sh +255 -0
@@ -0,0 +1,1110 @@
1
+ #!/bin/bash
2
+ # statusline.sh - Anvil framework statusline for Claude Code
3
+ # Version: 1.0.0
4
+ #
5
+ # This script reads Claude Code JSON input from stdin and outputs a formatted
6
+ # statusline showing model, context usage, workflow phase, and other context.
7
+ #
8
+ # Usage:
9
+ # Configure in .claude/settings.local.json:
10
+ # {
11
+ # "hooks": {
12
+ # "StatusLine": [{"command": "path/to/statusline.sh"}]
13
+ # }
14
+ # }
15
+
16
+ # Performance: Target <100ms total execution time
17
+ # Strategy: Minimize subshell spawns, use jq efficiently, cache where possible
18
+
19
+ set -euo pipefail
20
+
21
+ # ============================================================================
22
+ # Configuration
23
+ # ============================================================================
24
+
25
+ # State file location (relative to current directory)
26
+ ANVIL_STATE_FILE="${ANVIL_STATE_FILE:-.claude/anvil-state.json}"
27
+ ANVIL_CONFIG_FILE="${ANVIL_CONFIG_FILE:-.claude/anvil.config.json}"
28
+ ANVIL_AGENTS_FILE="${ANVIL_AGENTS_FILE:-$HOME/.anvil/agents.json}"
29
+
30
+ # ANSI color codes
31
+ RESET='\033[0m'
32
+ BOLD='\033[1m'
33
+ DIM='\033[2m'
34
+ CYAN='\033[36m'
35
+ GREEN='\033[32m'
36
+ YELLOW='\033[33m'
37
+ BLUE='\033[34m'
38
+ MAGENTA='\033[35m'
39
+ RED='\033[31m'
40
+
41
+ # Context bar characters
42
+ BAR_FILLED='█'
43
+ BAR_EMPTY='░'
44
+ BAR_WIDTH=15
45
+
46
+ # Context thresholds
47
+ THRESHOLD_WARNING=70
48
+ THRESHOLD_CRITICAL=85
49
+ THRESHOLD_COMPACT=95
50
+
51
+ # Context level names for CCS (Context Checkpoint System)
52
+ LEVEL_L0="L0" # Normal (< 70%)
53
+ LEVEL_L1="L1" # Warning (70-84%)
54
+ LEVEL_L2="L2" # Critical (85-94%)
55
+ LEVEL_L3="L3" # Emergency (95%+)
56
+
57
+ # ============================================================================
58
+ # Context Level Functions (ANV-192: CCS Threshold Indicators)
59
+ # ============================================================================
60
+
61
+ # Get context level based on percentage
62
+ # Returns: L0, L1, L2, or L3
63
+ get_context_level() {
64
+ local pct=$1
65
+
66
+ if [[ $pct -ge $THRESHOLD_COMPACT ]]; then
67
+ echo "$LEVEL_L3"
68
+ elif [[ $pct -ge $THRESHOLD_CRITICAL ]]; then
69
+ echo "$LEVEL_L2"
70
+ elif [[ $pct -ge $THRESHOLD_WARNING ]]; then
71
+ echo "$LEVEL_L1"
72
+ else
73
+ echo "$LEVEL_L0"
74
+ fi
75
+ }
76
+
77
+ # Format level indicator with emoji and color
78
+ # L0: (none) L1: ⚠️ L1 L2: 🔶 L2 L3: 🔴 L3
79
+ format_level_indicator() {
80
+ local level=$1
81
+
82
+ case "$level" in
83
+ "$LEVEL_L3")
84
+ echo "${RED}🔴 L3${RESET}"
85
+ ;;
86
+ "$LEVEL_L2")
87
+ echo "${YELLOW}🔶 L2${RESET}"
88
+ ;;
89
+ "$LEVEL_L1")
90
+ echo "${YELLOW}⚠️ L1${RESET}"
91
+ ;;
92
+ *)
93
+ echo "" # L0 shows no indicator
94
+ ;;
95
+ esac
96
+ }
97
+
98
+ # Get checkpoint readiness status
99
+ # Ready when at L1+ (70%+), or when explicitly set in state
100
+ get_checkpoint_status() {
101
+ local level=$1
102
+ local state_file="$ANVIL_STATE_FILE"
103
+
104
+ # Check if checkpoint is already active in state
105
+ if [[ -f "$state_file" ]]; then
106
+ local ckpt_active
107
+ ckpt_active=$(jq -r '.session.checkpointActive // false' "$state_file" 2>/dev/null || echo "false")
108
+ if [[ "$ckpt_active" == "true" ]]; then
109
+ echo "Active"
110
+ return
111
+ fi
112
+ fi
113
+
114
+ # Ready at L1+, not shown at L0
115
+ case "$level" in
116
+ "$LEVEL_L3")
117
+ echo "URGENT"
118
+ ;;
119
+ "$LEVEL_L2")
120
+ echo "Ready"
121
+ ;;
122
+ "$LEVEL_L1")
123
+ echo "Ready"
124
+ ;;
125
+ *)
126
+ echo ""
127
+ ;;
128
+ esac
129
+ }
130
+
131
+ # Format checkpoint indicator: [CKPT: Ready] or [CKPT: Active]
132
+ format_checkpoint_indicator() {
133
+ local status=$1
134
+
135
+ if [[ -z "$status" ]]; then
136
+ echo ""
137
+ return
138
+ fi
139
+
140
+ case "$status" in
141
+ "URGENT")
142
+ echo "${RED}[CKPT: ${BOLD}URGENT${RESET}${RED}]${RESET}"
143
+ ;;
144
+ "Active")
145
+ echo "${MAGENTA}[CKPT: Active]${RESET}"
146
+ ;;
147
+ "Ready")
148
+ echo "${DIM}[CKPT: Ready]${RESET}"
149
+ ;;
150
+ *)
151
+ echo ""
152
+ ;;
153
+ esac
154
+ }
155
+
156
+ # Get context trend based on previous reading
157
+ # Returns: "rising", "stable", or "falling"
158
+ get_context_trend() {
159
+ local current_pct=$1
160
+ local state_file="$ANVIL_STATE_FILE"
161
+
162
+ if [[ ! -f "$state_file" ]]; then
163
+ echo "stable"
164
+ return
165
+ fi
166
+
167
+ # Get previous context percentage from state
168
+ local prev_pct
169
+ prev_pct=$(jq -r '.cache.context.lastPercent // 0' "$state_file" 2>/dev/null || echo "0")
170
+
171
+ # Calculate difference
172
+ local diff=$((current_pct - prev_pct))
173
+
174
+ # Threshold for trend detection (ignore small fluctuations)
175
+ if [[ $diff -ge 5 ]]; then
176
+ echo "rising"
177
+ elif [[ $diff -le -5 ]]; then
178
+ echo "falling"
179
+ else
180
+ echo "stable"
181
+ fi
182
+ }
183
+
184
+ # Format trend indicator with arrow
185
+ format_trend_indicator() {
186
+ local trend=$1
187
+ local level=$2
188
+
189
+ # Only show trend at L1+ (when it matters)
190
+ if [[ "$level" == "$LEVEL_L0" ]]; then
191
+ echo ""
192
+ return
193
+ fi
194
+
195
+ case "$trend" in
196
+ "rising")
197
+ echo "${RED}↑${RESET}"
198
+ ;;
199
+ "falling")
200
+ echo "${GREEN}↓${RESET}"
201
+ ;;
202
+ *)
203
+ echo "${DIM}→${RESET}"
204
+ ;;
205
+ esac
206
+ }
207
+
208
+ # Update context percentage in state for trend tracking
209
+ update_context_state() {
210
+ local current_pct=$1
211
+ local state_file="$ANVIL_STATE_FILE"
212
+
213
+ if [[ ! -f "$state_file" ]]; then
214
+ return
215
+ fi
216
+
217
+ # Update in background to not block statusline output
218
+ (
219
+ local tmp_file="${state_file}.tmp.$$"
220
+ trap "rm -f '$tmp_file'" EXIT
221
+ local timestamp
222
+ timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
223
+
224
+ jq --argjson pct "$current_pct" --arg ts "$timestamp" '
225
+ .cache.context = {
226
+ lastPercent: $pct,
227
+ updatedAt: $ts
228
+ } |
229
+ .meta.updatedAt = $ts
230
+ ' "$state_file" > "$tmp_file" 2>/dev/null && mv "$tmp_file" "$state_file" || rm -f "$tmp_file"
231
+ ) &
232
+ }
233
+
234
+ # ============================================================================
235
+ # Alert System (ANV-193: Alert message templates and display)
236
+ # ============================================================================
237
+
238
+ # Alert message templates for each threshold level
239
+ # These are displayed once when crossing into a new level
240
+ get_alert_message() {
241
+ local level=$1
242
+ local pct=$2
243
+
244
+ case "$level" in
245
+ "$LEVEL_L3")
246
+ echo "🔴 Context at ${pct}% - Emergency checkpoint!"
247
+ ;;
248
+ "$LEVEL_L2")
249
+ echo "🔶 Context at ${pct}% - Initiating checkpoint sequence"
250
+ ;;
251
+ "$LEVEL_L1")
252
+ echo "⚠️ Context at ${pct}% - Consider completing current task"
253
+ ;;
254
+ *)
255
+ echo ""
256
+ ;;
257
+ esac
258
+ }
259
+
260
+ # Check if alerts are enabled in config
261
+ # Default: true
262
+ get_alerts_enabled() {
263
+ local config_file="$ANVIL_CONFIG_FILE"
264
+
265
+ if [[ -f "$config_file" ]]; then
266
+ local enabled
267
+ enabled=$(jq -r 'if .statusline.alerts.enabled == false then "false" else "true" end' "$config_file" 2>/dev/null || echo "true")
268
+ echo "$enabled"
269
+ else
270
+ echo "true"
271
+ fi
272
+ }
273
+
274
+ # Get the last alerted level from state
275
+ # Returns: L0, L1, L2, L3, or empty
276
+ get_last_alert_level() {
277
+ local state_file="$ANVIL_STATE_FILE"
278
+
279
+ if [[ -f "$state_file" ]]; then
280
+ jq -r '.cache.context.lastAlertLevel // ""' "$state_file" 2>/dev/null || echo ""
281
+ else
282
+ echo ""
283
+ fi
284
+ }
285
+
286
+ # Check if we should show an alert (threshold crossing detection)
287
+ # Returns: "true" if alert should be shown, "false" otherwise
288
+ should_show_alert() {
289
+ local current_level=$1
290
+ local last_level=$2
291
+
292
+ # No alert for L0 (normal)
293
+ if [[ "$current_level" == "$LEVEL_L0" ]]; then
294
+ echo "false"
295
+ return
296
+ fi
297
+
298
+ # No last level means first check - alert if we're at L1+
299
+ if [[ -z "$last_level" ]]; then
300
+ echo "true"
301
+ return
302
+ fi
303
+
304
+ # Convert levels to numeric for comparison
305
+ local current_num last_num
306
+ case "$current_level" in
307
+ "$LEVEL_L3") current_num=3 ;;
308
+ "$LEVEL_L2") current_num=2 ;;
309
+ "$LEVEL_L1") current_num=1 ;;
310
+ *) current_num=0 ;;
311
+ esac
312
+ case "$last_level" in
313
+ "$LEVEL_L3") last_num=3 ;;
314
+ "$LEVEL_L2") last_num=2 ;;
315
+ "$LEVEL_L1") last_num=1 ;;
316
+ *) last_num=0 ;;
317
+ esac
318
+
319
+ # Only alert when crossing UP into a higher level
320
+ if [[ $current_num -gt $last_num ]]; then
321
+ echo "true"
322
+ else
323
+ echo "false"
324
+ fi
325
+ }
326
+
327
+ # Update the last alerted level in state (for suppression)
328
+ update_alert_state() {
329
+ local level=$1
330
+ local state_file="$ANVIL_STATE_FILE"
331
+
332
+ if [[ ! -f "$state_file" ]]; then
333
+ return
334
+ fi
335
+
336
+ # Update in background to not block statusline output
337
+ (
338
+ local tmp_file="${state_file}.tmp.alert.$$"
339
+ local timestamp
340
+ timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
341
+
342
+ jq --arg level "$level" --arg ts "$timestamp" '
343
+ .cache.context.lastAlertLevel = $level |
344
+ .cache.context.lastAlertAt = $ts |
345
+ .meta.updatedAt = $ts
346
+ ' "$state_file" > "$tmp_file" 2>/dev/null && mv "$tmp_file" "$state_file"
347
+ ) &
348
+ }
349
+
350
+ # Format alert for display (separate line above statusline)
351
+ format_alert_output() {
352
+ local message=$1
353
+
354
+ if [[ -z "$message" ]]; then
355
+ return
356
+ fi
357
+
358
+ # Output alert on separate line with box styling
359
+ echo -e "${BOLD}╔══ CCS ALERT ══╗${RESET}"
360
+ echo -e "${BOLD}║${RESET} ${message}"
361
+ echo -e "${BOLD}╚════════════════╝${RESET}"
362
+ }
363
+
364
+ # Main alert check function - call from main output
365
+ # Returns alert message if threshold crossed, empty otherwise
366
+ check_and_get_alert() {
367
+ local current_level=$1
368
+ local current_pct=$2
369
+
370
+ # Check if alerts are enabled
371
+ local alerts_enabled
372
+ alerts_enabled=$(get_alerts_enabled)
373
+ if [[ "$alerts_enabled" != "true" ]]; then
374
+ return
375
+ fi
376
+
377
+ # Get last alerted level for suppression
378
+ local last_level
379
+ last_level=$(get_last_alert_level)
380
+
381
+ # Check if we should show alert
382
+ local should_alert
383
+ should_alert=$(should_show_alert "$current_level" "$last_level")
384
+
385
+ if [[ "$should_alert" == "true" ]]; then
386
+ # Update state to suppress future alerts at this level
387
+ update_alert_state "$current_level"
388
+
389
+ # Return the alert message
390
+ get_alert_message "$current_level" "$current_pct"
391
+ fi
392
+ }
393
+
394
+ # ============================================================================
395
+ # Dependency Check
396
+ # ============================================================================
397
+
398
+ if ! command -v jq &> /dev/null; then
399
+ # Fallback output when jq not available
400
+ echo "[Claude] jq required"
401
+ exit 0
402
+ fi
403
+
404
+ # ============================================================================
405
+ # Context Bar Functions (Legacy - kept for fallback)
406
+ # ============================================================================
407
+
408
+ # Generate visual context bar (15 chars wide using █░)
409
+ generate_context_bar() {
410
+ local pct=$1
411
+ local width=${BAR_WIDTH:-15}
412
+
413
+ # Calculate filled blocks (cap at width)
414
+ local filled=$((pct * width / 100))
415
+ if [[ $filled -gt $width ]]; then
416
+ filled=$width
417
+ fi
418
+ local empty=$((width - filled))
419
+
420
+ local bar=""
421
+ for ((i=0; i<filled; i++)); do
422
+ bar+="$BAR_FILLED"
423
+ done
424
+ for ((i=0; i<empty; i++)); do
425
+ bar+="$BAR_EMPTY"
426
+ done
427
+
428
+ echo "$bar"
429
+ }
430
+
431
+ # Get color code based on context percentage
432
+ get_context_color() {
433
+ local pct=$1
434
+
435
+ if [[ $pct -ge $THRESHOLD_COMPACT ]]; then
436
+ echo "$RED"
437
+ elif [[ $pct -ge $THRESHOLD_CRITICAL ]]; then
438
+ echo "$RED"
439
+ elif [[ $pct -ge $THRESHOLD_WARNING ]]; then
440
+ echo "$YELLOW"
441
+ else
442
+ echo "$GREEN"
443
+ fi
444
+ }
445
+
446
+ # ============================================================================
447
+ # Turns Until Compaction Display (ANV-176)
448
+ # ============================================================================
449
+
450
+ # Thresholds for turns-based display
451
+ TURNS_THRESHOLD_LOW=5
452
+ TURNS_THRESHOLD_MEDIUM=20
453
+
454
+ # Get estimated turns until compaction from agent registry
455
+ get_estimated_turns() {
456
+ local agents_file="$ANVIL_AGENTS_FILE"
457
+ local state_file="$ANVIL_STATE_FILE"
458
+ local my_agent_id=""
459
+
460
+ if [[ -f "$state_file" ]]; then
461
+ my_agent_id=$(jq -r '.session.agentId // empty' "$state_file" 2>/dev/null || echo "")
462
+ fi
463
+
464
+ if [[ -n "$my_agent_id" && -f "$agents_file" ]]; then
465
+ local turns
466
+ turns=$(jq -r --arg id "$my_agent_id" '
467
+ .agents[$id].estimatedTurns // null
468
+ ' "$agents_file" 2>/dev/null)
469
+
470
+ if [[ "$turns" != "null" && -n "$turns" && "$turns" != "0" ]]; then
471
+ echo "$turns"
472
+ return
473
+ fi
474
+ fi
475
+ echo ""
476
+ }
477
+
478
+ # Get color for turns display based on urgency
479
+ get_turns_color() {
480
+ local turns=$1
481
+
482
+ if [[ -z "$turns" || "$turns" == "null" ]]; then
483
+ echo "$DIM"
484
+ elif [[ $turns -lt $TURNS_THRESHOLD_LOW ]]; then
485
+ echo "$RED"
486
+ elif [[ $turns -lt $TURNS_THRESHOLD_MEDIUM ]]; then
487
+ echo "$YELLOW"
488
+ else
489
+ echo "$GREEN"
490
+ fi
491
+ }
492
+
493
+ # Format turns for display: "~16 turns" with color
494
+ format_turns_display() {
495
+ local turns=$1
496
+
497
+ if [[ -z "$turns" || "$turns" == "null" ]]; then
498
+ echo ""
499
+ return
500
+ fi
501
+
502
+ local color
503
+ color=$(get_turns_color "$turns")
504
+ echo "${color}~${turns} turns${RESET}"
505
+ }
506
+
507
+ # Format token count for display (e.g., 180000 -> "180k")
508
+ format_tokens() {
509
+ local tokens=$1
510
+ if [[ $tokens -ge 1000 ]]; then
511
+ echo "$((tokens / 1000))k"
512
+ else
513
+ echo "$tokens"
514
+ fi
515
+ }
516
+
517
+ # ============================================================================
518
+ # Todo Functions
519
+ # ============================================================================
520
+
521
+ # Get current todo from transcript
522
+ get_current_todo() {
523
+ local transcript_path="$1"
524
+
525
+ if [[ -z "$transcript_path" || ! -f "$transcript_path" ]]; then
526
+ echo ""
527
+ return
528
+ fi
529
+
530
+ # Use the parse_transcript.py hook for todo extraction
531
+ local hook_path="${SCRIPT_DIR}/parse_transcript.py"
532
+ if [[ -f "$hook_path" ]]; then
533
+ local todo_json
534
+ todo_json=$(python3 "$hook_path" todo "$transcript_path" 2>/dev/null || echo "{}")
535
+
536
+ # Extract content and counts
537
+ local content completed total
538
+ content=$(echo "$todo_json" | jq -r '.content // empty' 2>/dev/null)
539
+ completed=$(echo "$todo_json" | jq -r '.completed // 0' 2>/dev/null)
540
+ total=$(echo "$todo_json" | jq -r '.total // 0' 2>/dev/null)
541
+
542
+ if [[ -n "$content" && "$total" -gt 0 ]]; then
543
+ # Return "content|completed|total" format
544
+ echo "${content}|${completed}|${total}"
545
+ fi
546
+ fi
547
+ }
548
+
549
+ # Format todo for display: "▸ Content (3/7)"
550
+ format_todo_progress() {
551
+ local todo_data="$1"
552
+
553
+ if [[ -z "$todo_data" ]]; then
554
+ echo ""
555
+ return
556
+ fi
557
+
558
+ # Parse "content|completed|total" format
559
+ local content completed total
560
+ IFS='|' read -r content completed total <<< "$todo_data"
561
+
562
+ # Truncate content if too long
563
+ if [[ ${#content} -gt 20 ]]; then
564
+ content="${content:0:17}..."
565
+ fi
566
+
567
+ echo "▸ ${content} (${completed}/${total})"
568
+ }
569
+
570
+ # ============================================================================
571
+ # Read Configuration
572
+ # ============================================================================
573
+
574
+ # Check if statusline is disabled via config
575
+ get_statusline_config() {
576
+ local config_file="$ANVIL_CONFIG_FILE"
577
+ local enabled="true"
578
+ local variant="full"
579
+ local cost_display="agent"
580
+ local show_project="never"
581
+
582
+ if [[ -f "$config_file" ]]; then
583
+ # Use explicit check for false since jq's // operator treats false as falsey
584
+ enabled=$(jq -r 'if .statusline.enabled == false then "false" else "true" end' "$config_file" 2>/dev/null || echo "true")
585
+ variant=$(jq -r '.statusline.variant // "full"' "$config_file" 2>/dev/null || echo "full")
586
+ # ANV-176: New config options for cost and project display
587
+ cost_display=$(jq -r '.statusline.costDisplay // "agent"' "$config_file" 2>/dev/null || echo "agent")
588
+ show_project=$(jq -r '.statusline.showProject // "never"' "$config_file" 2>/dev/null || echo "never")
589
+ fi
590
+
591
+ echo "$enabled $variant $cost_display $show_project"
592
+ }
593
+
594
+ # ============================================================================
595
+ # Read Agent Registry
596
+ # ============================================================================
597
+
598
+ get_agent_count() {
599
+ local agents_file="$ANVIL_AGENTS_FILE"
600
+ local count=0
601
+
602
+ if [[ -f "$agents_file" ]]; then
603
+ # Count active agents (not stale - active within 30 minutes)
604
+ count=$(jq -r '
605
+ .agents // {} | to_entries |
606
+ map(select(.value.status == "active")) |
607
+ length
608
+ ' "$agents_file" 2>/dev/null || echo "0")
609
+ fi
610
+
611
+ echo "$count"
612
+ }
613
+
614
+ check_show_agent_count() {
615
+ local config_file="$ANVIL_CONFIG_FILE"
616
+ local show="true"
617
+
618
+ if [[ -f "$config_file" ]]; then
619
+ # Check hud.showAgentCount setting (default: true)
620
+ show=$(jq -r 'if .hud.showAgentCount == false then "false" else "true" end' "$config_file" 2>/dev/null || echo "true")
621
+ fi
622
+
623
+ echo "$show"
624
+ }
625
+
626
+ # Stale agent timeout in seconds (ANV-176: reduced from 30 min to 5 min)
627
+ STALE_TIMEOUT_SECONDS=300
628
+
629
+ # Get codename, agent count, and costs in single registry read (ANV-176)
630
+ # Returns: "codename|count|agent_cost|total_cost" (e.g., "A1|5|77.20|117.50")
631
+ # Supports costDisplay config: agent, total, or both
632
+ get_registry_data() {
633
+ local agents_file="$ANVIL_AGENTS_FILE"
634
+ local state_file="$ANVIL_STATE_FILE"
635
+
636
+ if [[ ! -f "$agents_file" ]]; then
637
+ echo "|0|0.00|0.00"
638
+ return
639
+ fi
640
+
641
+ # Get my agent ID from local state
642
+ local my_agent_id=""
643
+ if [[ -f "$state_file" ]]; then
644
+ my_agent_id=$(jq -r '.session.agentId // empty' "$state_file" 2>/dev/null || echo "")
645
+ fi
646
+
647
+ # Get current timestamp for stale filtering
648
+ local now_epoch
649
+ now_epoch=$(date -u "+%s")
650
+
651
+ # Single jq call to get codename, count (excluding stale), per-agent cost, and total cost
652
+ # Stale = inactive for more than STALE_TIMEOUT_SECONDS (5 min)
653
+ jq -r --arg id "$my_agent_id" --argjson now "$now_epoch" --argjson timeout "$STALE_TIMEOUT_SECONDS" '
654
+ .agents // {} | to_entries |
655
+ # Filter to active agents that are not stale (active within timeout)
656
+ map(select(
657
+ .value.status == "active" and
658
+ ((.value.lastActivity // "1970-01-01T00:00:00Z") |
659
+ gsub("\\.[0-9]+"; "") |
660
+ gsub("\\+00:00$"; "Z") |
661
+ strptime("%Y-%m-%dT%H:%M:%SZ") |
662
+ mktime) > ($now - $timeout)
663
+ )) |
664
+ # Find my agent data and calculate totals
665
+ (map(select(.key == $id)) | .[0].value) as $me |
666
+ (map(.value.sessionCost // 0) | add // 0) as $total |
667
+ {
668
+ codename: ($me.codename // ""),
669
+ count: length,
670
+ agent_cost: ($me.sessionCost // 0),
671
+ total_cost: $total
672
+ } |
673
+ "\(.codename)|\(.count)|\(.agent_cost)|\(.total_cost)"
674
+ ' "$agents_file" 2>/dev/null || echo "|0|0.00|0.00"
675
+ }
676
+
677
+ # Set terminal window title for agent identification (ANV-135, ANV-176)
678
+ # Format: "A1: ANV-136" or "A2: explore" or "A3: main"
679
+ # Priority: codename + (issue > phase > branch)
680
+ set_terminal_title() {
681
+ local codename="$1"
682
+ local issue="$2"
683
+ local branch="$3"
684
+ local phase="$4"
685
+
686
+ local title=""
687
+
688
+ # Always start with agent codename if available
689
+ if [[ -n "$codename" ]]; then
690
+ title="$codename"
691
+ fi
692
+
693
+ # Priority: issue > phase > branch
694
+ if [[ -n "$issue" ]]; then
695
+ # Working on specific issue
696
+ if [[ -n "$title" ]]; then
697
+ title="$title: $issue"
698
+ else
699
+ title="$issue"
700
+ fi
701
+ # Add branch in parens if different from main
702
+ if [[ -n "$branch" && "$branch" != "main" && "$branch" != "master" ]]; then
703
+ title="$title ($branch)"
704
+ fi
705
+ elif [[ -n "$phase" ]]; then
706
+ # In a workflow phase (explore, spec, plan, etc.)
707
+ if [[ -n "$title" ]]; then
708
+ title="$title: $phase"
709
+ else
710
+ title="$phase"
711
+ fi
712
+ elif [[ -n "$branch" ]]; then
713
+ # Just show branch
714
+ if [[ -n "$title" ]]; then
715
+ title="$title: $branch"
716
+ else
717
+ title="$branch"
718
+ fi
719
+ fi
720
+
721
+ # Set terminal title using ANSI escape sequence (OSC 0)
722
+ # Works in: iTerm2, Warp, Terminal.app, etc.
723
+ if [[ -n "$title" ]]; then
724
+ printf '\033]0;%s\007' "$title"
725
+ fi
726
+ }
727
+
728
+ # ============================================================================
729
+ # Read Claude Code Input
730
+ # ============================================================================
731
+
732
+ # Read JSON from stdin (Claude Code provides this)
733
+ INPUT=$(cat)
734
+
735
+ # Sync data to agent registry for HUD (runs in background, non-blocking)
736
+ SCRIPT_DIR="$(dirname "$0")"
737
+ if [[ -f "$SCRIPT_DIR/statusline_agent_sync.py" ]]; then
738
+ echo "$INPUT" | uv run "$SCRIPT_DIR/statusline_agent_sync.py" 2>/dev/null &
739
+ fi
740
+
741
+ # Parse Claude Code data with fallbacks for missing fields
742
+ parse_claude_input() {
743
+ local input="$1"
744
+
745
+ # Model name (extract short name from display_name)
746
+ MODEL=$(echo "$input" | jq -r '.model.display_name // "Claude"' 2>/dev/null || echo "Claude")
747
+ # Shorten model name (e.g., "Claude 3.5 Sonnet" -> "Sonnet", "Claude Opus 4" -> "Opus")
748
+ if [[ "$MODEL" == *"Opus"* ]]; then
749
+ MODEL="Opus"
750
+ elif [[ "$MODEL" == *"Sonnet"* ]]; then
751
+ MODEL="Sonnet"
752
+ elif [[ "$MODEL" == *"Haiku"* ]]; then
753
+ MODEL="Haiku"
754
+ fi
755
+
756
+ # Context window size and usage
757
+ CONTEXT_SIZE=$(echo "$input" | jq -r '.context_window.context_window_size // 200000' 2>/dev/null || echo "200000")
758
+
759
+ # Get individual token counts for breakdown display
760
+ INPUT_TOKENS=$(echo "$input" | jq -r '.context_window.current_usage.input_tokens // 0' 2>/dev/null || echo "0")
761
+ CACHE_TOKENS=$(echo "$input" | jq -r '.context_window.current_usage.cache_read_input_tokens // 0' 2>/dev/null || echo "0")
762
+ CACHE_WRITE_TOKENS=$(echo "$input" | jq -r '.context_window.current_usage.cache_creation_input_tokens // 0' 2>/dev/null || echo "0")
763
+
764
+ # Calculate current tokens (input + cache creation)
765
+ CURRENT_TOKENS=$((INPUT_TOKENS + CACHE_WRITE_TOKENS))
766
+
767
+ # Calculate percentage (avoid division by zero)
768
+ if [[ "$CONTEXT_SIZE" -gt 0 ]]; then
769
+ CONTEXT_PCT=$((CURRENT_TOKENS * 100 / CONTEXT_SIZE))
770
+ else
771
+ CONTEXT_PCT=0
772
+ fi
773
+
774
+ # Session cost - now read from registry for aggregated view (ANV-135)
775
+ # Fallback to per-agent cost if registry not available
776
+ COST=$(echo "$input" | jq -r '.cost.total_cost_usd // 0' 2>/dev/null || echo "0")
777
+ COST=$(printf "%.2f" "$COST")
778
+
779
+ # Current directory (basename only)
780
+ DIR=$(echo "$input" | jq -r '.workspace.current_dir // "."' 2>/dev/null || echo ".")
781
+ DIR=$(basename "$DIR")
782
+
783
+ # Transcript path for todo extraction
784
+ TRANSCRIPT_PATH=$(echo "$input" | jq -r '.transcript_path // empty' 2>/dev/null || echo "")
785
+ }
786
+
787
+ # ============================================================================
788
+ # Read Anvil State
789
+ # ============================================================================
790
+
791
+ parse_anvil_state() {
792
+ PHASE=""
793
+ ISSUE=""
794
+ BRANCH=""
795
+ PROJECT=""
796
+
797
+ local state_file="$ANVIL_STATE_FILE"
798
+
799
+ if [[ -f "$state_file" ]]; then
800
+ # Read phase and issue from state file
801
+ PHASE=$(jq -r '.session.phase // empty' "$state_file" 2>/dev/null || echo "")
802
+ ISSUE=$(jq -r '.session.activeIssue // empty' "$state_file" 2>/dev/null || echo "")
803
+
804
+ # Try to get cached git branch
805
+ local cached_at
806
+ cached_at=$(jq -r '.cache.git.cachedAt // empty' "$state_file" 2>/dev/null || echo "")
807
+
808
+ if [[ -n "$cached_at" ]]; then
809
+ # Check if cache is still valid (within 5 seconds)
810
+ local ttl
811
+ ttl=$(jq -r '.cache.git.ttlSeconds // 5' "$state_file" 2>/dev/null || echo "5")
812
+
813
+ local cached_epoch current_epoch
814
+ if [[ "$(uname)" == "Darwin" ]]; then
815
+ # macOS: Use TZ=UTC to ensure proper UTC parsing
816
+ cached_epoch=$(TZ=UTC date -j -f "%Y-%m-%dT%H:%M:%SZ" "$cached_at" "+%s" 2>/dev/null || echo 0)
817
+ else
818
+ # Linux: date -d handles ISO format natively with UTC suffix
819
+ cached_epoch=$(date -d "$cached_at" "+%s" 2>/dev/null || echo 0)
820
+ fi
821
+ current_epoch=$(date -u "+%s")
822
+
823
+ local age=$((current_epoch - cached_epoch))
824
+ if [[ $age -lt $ttl ]]; then
825
+ BRANCH=$(jq -r '.cache.git.branch // empty' "$state_file" 2>/dev/null || echo "")
826
+ fi
827
+ fi
828
+ fi
829
+
830
+ # Get fresh git branch if not cached
831
+ if [[ -z "$BRANCH" ]]; then
832
+ BRANCH=$(git branch --show-current 2>/dev/null || echo "")
833
+ fi
834
+
835
+ # Extract project name from current working directory
836
+ PROJECT=$(basename "$(pwd)" 2>/dev/null || echo "")
837
+ }
838
+
839
+ # ============================================================================
840
+ # Format Output
841
+ # ============================================================================
842
+
843
+ format_full() {
844
+ local output=""
845
+
846
+ # Agent codename prefix (A1, A2, etc.) before the bracket (ANV-135)
847
+ if [[ -n "$MY_CODENAME" ]]; then
848
+ output+="${CYAN}${MY_CODENAME}${RESET} "
849
+ fi
850
+
851
+ # Model with turns estimate (ANV-176) or fallback to percentage
852
+ local turns_display
853
+ turns_display=$(format_turns_display "$ESTIMATED_TURNS")
854
+
855
+ # ANV-192: Get CCS level, trend, and checkpoint status
856
+ local context_level context_trend ckpt_status
857
+ context_level=$(get_context_level "$CONTEXT_PCT")
858
+ context_trend=$(get_context_trend "$CONTEXT_PCT")
859
+ ckpt_status=$(get_checkpoint_status "$context_level")
860
+
861
+ # ANV-226: Removed model name from display (was showing unreliable "Opus 0%")
862
+ # Now shows just context info: [~16 turns] or [███░░░░░░░░ 45%]
863
+ if [[ -n "$turns_display" ]]; then
864
+ output+="${BOLD}[${turns_display}]${RESET}"
865
+ else
866
+ local context_bar context_color
867
+ context_bar=$(generate_context_bar "$CONTEXT_PCT")
868
+ context_color=$(get_context_color "$CONTEXT_PCT")
869
+ output+="${BOLD}${context_color}[${context_bar} ${CONTEXT_PCT}%]${RESET}"
870
+ fi
871
+
872
+ # ANV-192: CCS Level indicator (L1/L2/L3) with trend arrow
873
+ local level_indicator trend_indicator
874
+ level_indicator=$(format_level_indicator "$context_level")
875
+ trend_indicator=$(format_trend_indicator "$context_trend" "$context_level")
876
+
877
+ if [[ -n "$level_indicator" ]]; then
878
+ output+=" ${level_indicator}"
879
+ if [[ -n "$trend_indicator" ]]; then
880
+ output+="${trend_indicator}"
881
+ fi
882
+ fi
883
+
884
+ # ANV-192: Checkpoint readiness indicator
885
+ local ckpt_indicator
886
+ ckpt_indicator=$(format_checkpoint_indicator "$ckpt_status")
887
+ if [[ -n "$ckpt_indicator" ]]; then
888
+ output+=" ${ckpt_indicator}"
889
+ fi
890
+
891
+ # Token breakdown at 85%+ (shows input+cache) - L2 and L3
892
+ if [[ $CONTEXT_PCT -ge $THRESHOLD_CRITICAL ]]; then
893
+ local input_fmt cache_fmt
894
+ input_fmt=$(format_tokens "$INPUT_TOKENS")
895
+ cache_fmt=$(format_tokens "$CACHE_TOKENS")
896
+ output+=" ${DIM}${input_fmt}+${cache_fmt}${RESET}"
897
+ fi
898
+
899
+ # Project name display based on SHOW_PROJECT config (ANV-176)
900
+ # Options: "never" (default), "always", "multi" (only when >1 agent)
901
+ if [[ -n "$PROJECT" ]]; then
902
+ case "$SHOW_PROJECT" in
903
+ "always")
904
+ output+=" ${DIM}|${RESET} ${CYAN}📁 ${PROJECT}${RESET}"
905
+ ;;
906
+ "multi")
907
+ if [[ -n "$AGENT_COUNT" && "$AGENT_COUNT" -gt 1 ]]; then
908
+ output+=" ${DIM}|${RESET} ${CYAN}📁 ${PROJECT}${RESET}"
909
+ fi
910
+ ;;
911
+ # "never" or default: don't show
912
+ esac
913
+ fi
914
+
915
+ # Git branch (if available, skip at COMPACT)
916
+ if [[ -n "$BRANCH" && $CONTEXT_PCT -lt $THRESHOLD_COMPACT ]]; then
917
+ output+=" ${DIM}|${RESET} ${GREEN}🌿 ${BRANCH}${RESET}"
918
+ fi
919
+
920
+ # Active issue (if set)
921
+ if [[ -n "$ISSUE" ]]; then
922
+ output+=" ${DIM}|${RESET} ${YELLOW}🎯 ${ISSUE}${RESET}"
923
+ fi
924
+
925
+ # Todo or Phase display
926
+ # Priority: Show todo if in_progress exists, otherwise show phase
927
+ local todo_display=""
928
+ if [[ -n "$TODO_DATA" ]]; then
929
+ todo_display=$(format_todo_progress "$TODO_DATA")
930
+ fi
931
+
932
+ if [[ -n "$todo_display" ]]; then
933
+ output+=" ${DIM}|${RESET} ${MAGENTA}${todo_display}${RESET}"
934
+ elif [[ -n "$PHASE" ]]; then
935
+ local phase_upper
936
+ phase_upper=$(echo "$PHASE" | tr '[:lower:]' '[:upper:]')
937
+ output+=" ${DIM}|${RESET} ${MAGENTA}🔄 ${phase_upper}${RESET}"
938
+ fi
939
+
940
+ # Cost display based on COST_DISPLAY config (ANV-176)
941
+ # Options: "agent" (default), "total", "both", "none"
942
+ if [[ $CONTEXT_PCT -lt $THRESHOLD_COMPACT && "$COST_DISPLAY" != "none" ]]; then
943
+ local cost_text=""
944
+ case "$COST_DISPLAY" in
945
+ "total")
946
+ cost_text="\$${TOTAL_COST:-0.00}"
947
+ ;;
948
+ "both")
949
+ cost_text="\$${COST}/\$${TOTAL_COST:-0.00}"
950
+ ;;
951
+ "agent"|*)
952
+ cost_text="\$${COST}"
953
+ ;;
954
+ esac
955
+ output+=" ${DIM}|${RESET} ${BLUE}💰 ${cost_text}${RESET}"
956
+ fi
957
+
958
+ # Agent count (show when >1 agent active)
959
+ if [[ -n "$AGENT_COUNT" && "$AGENT_COUNT" -gt 1 ]]; then
960
+ output+=" ${DIM}|${RESET} ${CYAN}👥 ${AGENT_COUNT} agents${RESET}"
961
+ fi
962
+
963
+ echo -e "$output"
964
+ }
965
+
966
+ format_minimal() {
967
+ local output=""
968
+
969
+ # ANV-226: Removed model display (was unreliable), show context percentage instead
970
+ local context_color
971
+ context_color=$(get_context_color "$CONTEXT_PCT")
972
+ output+="${BOLD}${context_color}[${CONTEXT_PCT}%]${RESET}"
973
+
974
+ # Active issue (if set)
975
+ if [[ -n "$ISSUE" ]]; then
976
+ output+=" ${YELLOW}${ISSUE}${RESET}"
977
+ fi
978
+
979
+ # Workflow phase (if set)
980
+ if [[ -n "$PHASE" ]]; then
981
+ local phase_upper
982
+ phase_upper=$(echo "$PHASE" | tr '[:lower:]' '[:upper:]')
983
+ if [[ -n "$ISSUE" ]]; then
984
+ output+=" ${DIM}|${RESET}"
985
+ fi
986
+ output+=" ${MAGENTA}${phase_upper}${RESET}"
987
+ fi
988
+
989
+ echo -e "$output"
990
+ }
991
+
992
+ format_no_anvil() {
993
+ # Fallback format when no Anvil state exists
994
+ local output=""
995
+
996
+ # Generate context bar and get color
997
+ local context_bar context_color
998
+ context_bar=$(generate_context_bar "$CONTEXT_PCT")
999
+ context_color=$(get_context_color "$CONTEXT_PCT")
1000
+
1001
+ # ANV-192: Get CCS level (trend not available without state file)
1002
+ local context_level
1003
+ context_level=$(get_context_level "$CONTEXT_PCT")
1004
+
1005
+ # ANV-226: Removed model name from display (was unreliable "Opus 0%")
1006
+ output+="${BOLD}${context_color}[${context_bar} ${CONTEXT_PCT}%]${RESET}"
1007
+
1008
+ # ANV-192: CCS Level indicator (L1/L2/L3)
1009
+ local level_indicator
1010
+ level_indicator=$(format_level_indicator "$context_level")
1011
+ if [[ -n "$level_indicator" ]]; then
1012
+ output+=" ${level_indicator}"
1013
+ fi
1014
+
1015
+ # Directory
1016
+ output+=" ${DIR}"
1017
+
1018
+ # Git branch (if available)
1019
+ if [[ -n "$BRANCH" ]]; then
1020
+ output+=" ${DIM}|${RESET} ${GREEN}🌿 ${BRANCH}${RESET}"
1021
+ fi
1022
+
1023
+ # Agent count (show when >1 agent active)
1024
+ if [[ -n "$AGENT_COUNT" && "$AGENT_COUNT" -gt 1 ]]; then
1025
+ output+=" ${DIM}|${RESET} ${CYAN}👥 ${AGENT_COUNT} agents${RESET}"
1026
+ fi
1027
+
1028
+ echo -e "$output"
1029
+ }
1030
+
1031
+ # ============================================================================
1032
+ # Main
1033
+ # ============================================================================
1034
+
1035
+ main() {
1036
+ # Check configuration
1037
+ local config
1038
+ config=$(get_statusline_config)
1039
+ local enabled variant
1040
+ # ANV-176: Parse new config options for cost and project display
1041
+ read -r enabled variant COST_DISPLAY SHOW_PROJECT <<< "$config"
1042
+
1043
+ # Exit early if disabled
1044
+ if [[ "$enabled" == "false" ]]; then
1045
+ exit 0
1046
+ fi
1047
+
1048
+ # Parse inputs
1049
+ parse_claude_input "$INPUT"
1050
+ parse_anvil_state
1051
+
1052
+ # Get todo data from transcript (if available)
1053
+ TODO_DATA=""
1054
+ if [[ -n "$TRANSCRIPT_PATH" ]]; then
1055
+ TODO_DATA=$(get_current_todo "$TRANSCRIPT_PATH")
1056
+ fi
1057
+
1058
+ # Get agent codename, count, and per-agent cost from registry (ANV-176)
1059
+ # Always fetch - codename needed for terminal title, count/cost for statusline
1060
+ MY_CODENAME=""
1061
+ AGENT_COUNT=0
1062
+ COST="0.00"
1063
+ TOTAL_COST="0.00"
1064
+ local registry_data
1065
+ registry_data=$(get_registry_data)
1066
+ if [[ -n "$registry_data" ]]; then
1067
+ IFS='|' read -r MY_CODENAME AGENT_COUNT COST TOTAL_COST <<< "$registry_data"
1068
+ # Format costs to 2 decimal places
1069
+ COST=$(printf "%.2f" "${COST:-0}")
1070
+ TOTAL_COST=$(printf "%.2f" "${TOTAL_COST:-0}")
1071
+ fi
1072
+
1073
+ # Get estimated turns until compaction (ANV-176)
1074
+ ESTIMATED_TURNS=$(get_estimated_turns)
1075
+
1076
+ # Set terminal window title for agent identification
1077
+ # Priority: codename + (issue > phase > branch)
1078
+ set_terminal_title "$MY_CODENAME" "$ISSUE" "$BRANCH" "$PHASE"
1079
+
1080
+ # Early exit if statusline is disabled (skip state updates and alert checks)
1081
+ if [[ "$variant" == "off" ]]; then
1082
+ exit 0
1083
+ fi
1084
+
1085
+ # ANV-192: Update context percentage in state for trend tracking
1086
+ update_context_state "$CONTEXT_PCT"
1087
+
1088
+ # ANV-193: Check for threshold crossing alerts
1089
+ local context_level alert_message
1090
+ context_level=$(get_context_level "$CONTEXT_PCT")
1091
+ alert_message=$(check_and_get_alert "$context_level" "$CONTEXT_PCT")
1092
+
1093
+ # Output alert first if threshold was crossed
1094
+ if [[ -n "$alert_message" ]]; then
1095
+ format_alert_output "$alert_message"
1096
+ fi
1097
+
1098
+ # Output based on variant
1099
+ case "$variant" in
1100
+ "minimal")
1101
+ format_minimal
1102
+ ;;
1103
+ "full"|*)
1104
+ # Always use full format when variant is "full"
1105
+ format_full
1106
+ ;;
1107
+ esac
1108
+ }
1109
+
1110
+ main