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.
- package/README.md +719 -0
- package/VERSION +1 -0
- package/docs/ANVIL-REPO-IMPLEMENTATION-PLAN.md +441 -0
- package/docs/FIRST-SKILL-TUTORIAL.md +408 -0
- package/docs/INSTALLATION-RETRO-NOTES.md +458 -0
- package/docs/INSTALLATION.md +984 -0
- package/docs/anvil-hud.md +469 -0
- package/docs/anvil-init.md +255 -0
- package/docs/anvil-state.md +210 -0
- package/docs/boris-cherny-ralph-wiggum-insights.md +608 -0
- package/docs/command-reference.md +2022 -0
- package/docs/hooks-tts.md +368 -0
- package/docs/implementation-guide.md +810 -0
- package/docs/linear-github-integration.md +247 -0
- package/docs/local-issues.md +677 -0
- package/docs/patterns/README.md +419 -0
- package/docs/planning-responsibilities.md +139 -0
- package/docs/session-workflow.md +573 -0
- package/docs/simplification-plan-template.md +297 -0
- package/docs/simplification-principles.md +129 -0
- package/docs/specifications/CCS-RALPH-INTEGRATION-DESIGN.md +633 -0
- package/docs/specifications/CCS-RESEARCH-REPORT.md +169 -0
- package/docs/specifications/PLAN-ANV-verification-ralph-wiggum.md +403 -0
- package/docs/specifications/PLAN-parallel-tracks-anvil-memory-ccs.md +494 -0
- package/docs/specifications/SPEC-ANV-VRW/component-01-verify.md +208 -0
- package/docs/specifications/SPEC-ANV-VRW/component-02-stop-gate.md +226 -0
- package/docs/specifications/SPEC-ANV-VRW/component-03-posttooluse.md +209 -0
- package/docs/specifications/SPEC-ANV-VRW/component-04-ralph-wiggum.md +604 -0
- package/docs/specifications/SPEC-ANV-VRW/component-05-atomic-actions.md +311 -0
- package/docs/specifications/SPEC-ANV-VRW/component-06-verify-subagent.md +264 -0
- package/docs/specifications/SPEC-ANV-VRW/component-07-claude-md.md +363 -0
- package/docs/specifications/SPEC-ANV-VRW/index.md +182 -0
- package/docs/specifications/SPEC-ANV-anvil-memory.md +573 -0
- package/docs/specifications/SPEC-ANV-context-checkpoints.md +781 -0
- package/docs/specifications/SPEC-ANV-verification-ralph-wiggum.md +789 -0
- package/docs/sync.md +122 -0
- package/global/CLAUDE.md +140 -0
- package/global/agents/verify-app.md +164 -0
- package/global/commands/anvil-settings.md +527 -0
- package/global/commands/anvil-sync.md +121 -0
- package/global/commands/change.md +197 -0
- package/global/commands/clarify.md +252 -0
- package/global/commands/cleanup.md +292 -0
- package/global/commands/commit-push-pr.md +207 -0
- package/global/commands/decay-review.md +127 -0
- package/global/commands/discover.md +158 -0
- package/global/commands/doc-coverage.md +122 -0
- package/global/commands/evidence.md +307 -0
- package/global/commands/explore.md +121 -0
- package/global/commands/force-exit.md +135 -0
- package/global/commands/handoff.md +191 -0
- package/global/commands/healthcheck.md +302 -0
- package/global/commands/hud.md +84 -0
- package/global/commands/insights.md +319 -0
- package/global/commands/linear-setup.md +184 -0
- package/global/commands/lint-fix.md +198 -0
- package/global/commands/orient.md +510 -0
- package/global/commands/plan.md +228 -0
- package/global/commands/ralph.md +346 -0
- package/global/commands/ready.md +182 -0
- package/global/commands/release.md +305 -0
- package/global/commands/retro.md +96 -0
- package/global/commands/shard.md +166 -0
- package/global/commands/spec.md +227 -0
- package/global/commands/sprint.md +184 -0
- package/global/commands/tasks.md +228 -0
- package/global/commands/test-and-commit.md +151 -0
- package/global/commands/validate.md +132 -0
- package/global/commands/verify.md +251 -0
- package/global/commands/weekly-review.md +156 -0
- package/global/hooks/__pycache__/ralph_context_monitor.cpython-314.pyc +0 -0
- package/global/hooks/__pycache__/statusline_agent_sync.cpython-314.pyc +0 -0
- package/global/hooks/anvil_memory_observe.ts +322 -0
- package/global/hooks/anvil_memory_session.ts +166 -0
- package/global/hooks/anvil_memory_stop.ts +187 -0
- package/global/hooks/parse_transcript.py +116 -0
- package/global/hooks/post_merge_cleanup.sh +132 -0
- package/global/hooks/post_tool_format.sh +215 -0
- package/global/hooks/ralph_context_monitor.py +240 -0
- package/global/hooks/ralph_stop.sh +502 -0
- package/global/hooks/statusline.sh +1110 -0
- package/global/hooks/statusline_agent_sync.py +224 -0
- package/global/hooks/stop_gate.sh +250 -0
- package/global/lib/.claude/anvil-state.json +21 -0
- package/global/lib/__pycache__/agent_registry.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/claim_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/coderabbit_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/config_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/coordination_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/doc_coverage_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/gate_logger.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/github_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/hygiene_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/issue_models.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/issue_provider.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/linear_data_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/linear_provider.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/local_provider.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/quality_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/ralph_state.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/state_manager.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/transcript_parser.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/verification_runner.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/verify_iteration.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/verify_subagent.cpython-314.pyc +0 -0
- package/global/lib/agent_registry.py +995 -0
- package/global/lib/anvil-state.sh +435 -0
- package/global/lib/claim_service.py +515 -0
- package/global/lib/coderabbit_service.py +314 -0
- package/global/lib/config_service.py +423 -0
- package/global/lib/coordination_service.py +331 -0
- package/global/lib/doc_coverage_service.py +1305 -0
- package/global/lib/gate_logger.py +316 -0
- package/global/lib/github_service.py +310 -0
- package/global/lib/handoff_generator.py +775 -0
- package/global/lib/hygiene_service.py +712 -0
- package/global/lib/issue_models.py +257 -0
- package/global/lib/issue_provider.py +339 -0
- package/global/lib/linear_data_service.py +210 -0
- package/global/lib/linear_provider.py +987 -0
- package/global/lib/linear_provider.py.backup +671 -0
- package/global/lib/local_provider.py +486 -0
- package/global/lib/orient_fast.py +457 -0
- package/global/lib/quality_service.py +470 -0
- package/global/lib/ralph_prompt_generator.py +563 -0
- package/global/lib/ralph_state.py +1202 -0
- package/global/lib/state_manager.py +417 -0
- package/global/lib/transcript_parser.py +597 -0
- package/global/lib/verification_runner.py +557 -0
- package/global/lib/verify_iteration.py +490 -0
- package/global/lib/verify_subagent.py +250 -0
- package/global/skills/README.md +155 -0
- package/global/skills/quality-gates/SKILL.md +252 -0
- package/global/skills/skill-template/SKILL.md +109 -0
- package/global/skills/testing-strategies/SKILL.md +337 -0
- package/global/templates/CHANGE-template.md +105 -0
- package/global/templates/HANDOFF-template.md +63 -0
- package/global/templates/PLAN-template.md +111 -0
- package/global/templates/SPEC-template.md +93 -0
- package/global/templates/ralph/PROMPT.md.template +89 -0
- package/global/templates/ralph/fix_plan.md.template +31 -0
- package/global/templates/ralph/progress.txt.template +23 -0
- package/global/tests/__pycache__/test_doc_coverage.cpython-314.pyc +0 -0
- package/global/tests/test_doc_coverage.py +520 -0
- package/global/tests/test_issue_models.py +299 -0
- package/global/tests/test_local_provider.py +323 -0
- package/global/tools/README.md +178 -0
- package/global/tools/__pycache__/anvil-hud.cpython-314.pyc +0 -0
- package/global/tools/anvil-hud.py +3622 -0
- package/global/tools/anvil-hud.py.bak +3318 -0
- package/global/tools/anvil-issue.py +432 -0
- package/global/tools/anvil-memory/CLAUDE.md +49 -0
- package/global/tools/anvil-memory/README.md +42 -0
- package/global/tools/anvil-memory/bun.lock +25 -0
- package/global/tools/anvil-memory/bunfig.toml +9 -0
- package/global/tools/anvil-memory/package.json +23 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/context-monitor.test.ts +535 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/edge-cases.test.ts +645 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/fixtures.ts +363 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/index.ts +8 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/integration.test.ts +417 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/prompt-generator.test.ts +571 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/ralph-stop.test.ts +440 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/test-utils.ts +252 -0
- package/global/tools/anvil-memory/src/__tests__/commands.test.ts +657 -0
- package/global/tools/anvil-memory/src/__tests__/db.test.ts +641 -0
- package/global/tools/anvil-memory/src/__tests__/hooks.test.ts +272 -0
- package/global/tools/anvil-memory/src/__tests__/performance.test.ts +427 -0
- package/global/tools/anvil-memory/src/__tests__/test-utils.ts +113 -0
- package/global/tools/anvil-memory/src/commands/checkpoint.ts +197 -0
- package/global/tools/anvil-memory/src/commands/get.ts +115 -0
- package/global/tools/anvil-memory/src/commands/init.ts +94 -0
- package/global/tools/anvil-memory/src/commands/observe.ts +163 -0
- package/global/tools/anvil-memory/src/commands/search.ts +112 -0
- package/global/tools/anvil-memory/src/db.ts +638 -0
- package/global/tools/anvil-memory/src/index.ts +205 -0
- package/global/tools/anvil-memory/src/types.ts +122 -0
- package/global/tools/anvil-memory/tsconfig.json +29 -0
- package/global/tools/ralph-loop.sh +359 -0
- package/package.json +45 -0
- package/scripts/anvil +822 -0
- package/scripts/extract_patterns.py +222 -0
- package/scripts/init-project.sh +541 -0
- package/scripts/install.sh +229 -0
- package/scripts/postinstall.js +41 -0
- package/scripts/rollback.sh +188 -0
- package/scripts/sync.sh +623 -0
- package/scripts/test-statusline.sh +248 -0
- package/scripts/update_claude_md.py +224 -0
- 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
|