@pjmendonca/devflow 1.13.2 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/agent.md +1 -1
- package/.claude/commands/bugfix.md +21 -0
- package/.claude/commands/checkpoint.md +0 -1
- package/.claude/commands/collab.md +0 -1
- package/.claude/commands/costs.md +88 -18
- package/.claude/commands/devflow.md +26 -0
- package/.claude/commands/handoff.md +0 -1
- package/.claude/commands/init.md +287 -0
- package/.claude/commands/memory.md +0 -1
- package/.claude/commands/pair.md +0 -1
- package/.claude/commands/review.md +27 -0
- package/.claude/commands/route.md +0 -1
- package/.claude/commands/swarm.md +0 -1
- package/.claude/commands/validate.md +55 -0
- package/.claude/hooks/session-notification.sh +44 -0
- package/.claude/hooks/session-startup.sh +427 -0
- package/.claude/hooks/session-stop.sh +38 -0
- package/.claude/hooks/session_tracker.py +272 -0
- package/.claude/settings.json +38 -0
- package/.claude/skills/costs/SKILL.md +156 -0
- package/.claude/skills/validate/SKILL.md +101 -0
- package/CHANGELOG.md +243 -0
- package/README.md +207 -10
- package/bin/devflow-install.js +2 -1
- package/bin/devflow.js +4 -0
- package/lib/constants.js +0 -1
- package/lib/exec-python.js +1 -1
- package/package.json +1 -1
- package/tooling/.automation/.checkpoint_lock +1 -0
- package/tooling/.automation/agents/architect.md +19 -0
- package/tooling/.automation/agents/ba.md +19 -0
- package/tooling/.automation/agents/maintainer.md +19 -0
- package/tooling/.automation/agents/pm.md +19 -0
- package/tooling/.automation/agents/reviewer.md +1 -1
- package/tooling/.automation/agents/writer.md +19 -0
- package/tooling/.automation/benchmarks/benchmark_20251230_100119.json +314 -0
- package/tooling/.automation/benchmarks/benchmark_20251230_100216.json +314 -0
- package/tooling/.automation/costs/config.json +31 -0
- package/tooling/.automation/costs/sessions/2025-12-29_20251229_164128.json +22 -0
- package/tooling/.automation/memory/knowledge/kg_integration-test.json +707 -1
- package/tooling/.automation/memory/knowledge/kg_test-story.json +3273 -2
- package/tooling/.automation/memory/shared/shared_integration-test.json +181 -1
- package/tooling/.automation/memory/shared/shared_test-story.json +721 -1
- package/tooling/.automation/memory/shared/shared_test.json +1254 -0
- package/tooling/.automation/memory/shared/shared_validation-check.json +227 -0
- package/tooling/.automation/overrides/templates/architect/cloud-native.yaml +5 -5
- package/tooling/.automation/overrides/templates/architect/enterprise-architect.yaml +23 -5
- package/tooling/.automation/overrides/templates/architect/pragmatic-minimalist.yaml +24 -6
- package/tooling/.automation/overrides/templates/ba/agile-storyteller.yaml +4 -4
- package/tooling/.automation/overrides/templates/ba/domain-expert.yaml +4 -4
- package/tooling/.automation/overrides/templates/ba/requirements-engineer.yaml +4 -4
- package/tooling/.automation/overrides/templates/dev/performance-engineer.yaml +18 -0
- package/tooling/.automation/overrides/templates/dev/rapid-prototyper.yaml +19 -1
- package/tooling/.automation/overrides/templates/dev/security-focused.yaml +18 -0
- package/tooling/.automation/overrides/templates/dev/user-advocate.yaml +54 -0
- package/tooling/.automation/overrides/templates/maintainer/devops-maintainer.yaml +4 -4
- package/tooling/.automation/overrides/templates/maintainer/legacy-steward.yaml +4 -4
- package/tooling/.automation/overrides/templates/maintainer/oss-maintainer.yaml +4 -4
- package/tooling/.automation/overrides/templates/maintainer/reliability-engineer.yaml +55 -0
- package/tooling/.automation/overrides/templates/pm/agile-pm.yaml +4 -4
- package/tooling/.automation/overrides/templates/pm/hybrid-delivery.yaml +3 -3
- package/tooling/.automation/overrides/templates/pm/traditional-pm.yaml +4 -4
- package/tooling/.automation/overrides/templates/reviewer/quick-sanity.yaml +18 -0
- package/tooling/.automation/overrides/templates/reviewer/thorough-critic.yaml +18 -0
- package/tooling/.automation/overrides/templates/sm/agile-coach.yaml +2 -2
- package/tooling/.automation/overrides/templates/sm/startup-pm.yaml +3 -3
- package/tooling/.automation/overrides/templates/writer/api-documentarian.yaml +5 -5
- package/tooling/.automation/overrides/templates/writer/docs-as-code.yaml +4 -4
- package/tooling/.automation/overrides/templates/writer/user-guide-author.yaml +5 -5
- package/tooling/.automation/validation/history/2025-12-29_val_002a28c1.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_01273bb1.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_03369914.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_07a449ba.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_0df1f0a2.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_10ff3d34.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_110771d7.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_13f3a7f9.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_17ba9d21.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_22247089.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_227ea6a4.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_2335d5ae.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_246824bb.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_28b4b9cd.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_2abd12cc.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_2c801b2f.json +59 -0
- package/tooling/.automation/validation/history/2025-12-29_val_2c8cfa8e.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_2ce76eb0.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_30351948.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_30eb7229.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_34df0e77.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_376e4d6a.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_3a4e8a1a.json +59 -0
- package/tooling/.automation/validation/history/2025-12-29_val_3b77a628.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_3ea4e1cf.json +59 -0
- package/tooling/.automation/validation/history/2025-12-29_val_44aacdb4.json +59 -0
- package/tooling/.automation/validation/history/2025-12-29_val_457ddfa8.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_45af6238.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_4735dba1.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_486b203c.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_49dc56cd.json +59 -0
- package/tooling/.automation/validation/history/2025-12-29_val_4d863d6d.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_5149a808.json +59 -0
- package/tooling/.automation/validation/history/2025-12-29_val_52e0bb43.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_585d6319.json +59 -0
- package/tooling/.automation/validation/history/2025-12-29_val_5b2d859a.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_635a7081.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_64df4905.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_70634cee.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_714553f9.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_7f7bfdbf.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_7faad91d.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_81821f8f.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_8249f3c9.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_8422b50f.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_8446c134.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_879f4e26.json +59 -0
- package/tooling/.automation/validation/history/2025-12-29_val_8b6d5bd7.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_8c5cd787.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_91d20bc7.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_958a12b7.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_95d91108.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_980dbb74.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_9e40c79b.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_9f499b7c.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_9f7c3b57.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_a30d5bd4.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_a6eb09c7.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_a86f7b83.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_ad5347e1.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_b0a5a993.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_bcb0192e.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_bf3c9aaa.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_c461ff88.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_c4f4e258.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_c7f0fa6d.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_c911b0e6.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_cc581964.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_cdd5a33b.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_cfd42495.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_d1c7a4ee.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_d2280d0e.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_d2a6ff69.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_d8c53ab2.json +59 -0
- package/tooling/.automation/validation/history/2025-12-29_val_d9c1247a.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_d9d58569.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_dabb4fd9.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_dd8fe359.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_decdffc9.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_e3a95476.json +59 -0
- package/tooling/.automation/validation/history/2025-12-29_val_e776dfca.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_ea70969f.json +59 -0
- package/tooling/.automation/validation/history/2025-12-29_val_ef41ea95.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_f384f9b1.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_f8adc38c.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_fa40b69e.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_fc538d54.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_fe814665.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_ffea4b12.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_02d001e5.json +59 -0
- package/tooling/.automation/validation/history/2025-12-30_val_0b8966dc.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_15455fbf.json +59 -0
- package/tooling/.automation/validation/history/2025-12-30_val_157e34b9.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_28d1d933.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_3442a52c.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_37f1ce1e.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_4f1d8a93.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_56ff1de3.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_664fd4e2.json +41 -0
- package/tooling/.automation/validation/history/2025-12-30_val_66afb0a7.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_7634663c.json +41 -0
- package/tooling/.automation/validation/history/2025-12-30_val_8ea830c3.json +41 -0
- package/tooling/.automation/validation/history/2025-12-30_val_998957c2.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_a52177db.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_a5b65a63.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_ae391d0e.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_c7895339.json +41 -0
- package/tooling/.automation/validation/history/2025-12-30_val_ca416593.json +41 -0
- package/tooling/.automation/validation/history/2025-12-30_val_cee19422.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_ddd4f4e6.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_f2e1394b.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_f4a7fa06.json +41 -0
- package/tooling/.automation/validation/history/2025-12-30_val_ffea3369.json +32 -0
- package/tooling/.automation/validation-config.yaml +103 -0
- package/tooling/completions/DevflowCompletion.ps1 +21 -21
- package/tooling/completions/_run-story +3 -3
- package/tooling/completions/run-story-completion.bash +8 -8
- package/tooling/docs/DOC-STANDARD.md +14 -14
- package/tooling/docs/templates/migration-spec.md +4 -4
- package/tooling/scripts/context_checkpoint.py +5 -15
- package/tooling/scripts/cost_dashboard.py +610 -13
- package/tooling/scripts/create-persona.py +1 -12
- package/tooling/scripts/create-persona.sh +44 -44
- package/tooling/scripts/lib/__init__.py +12 -1
- package/tooling/scripts/lib/agent_handoff.py +11 -2
- package/tooling/scripts/lib/agent_router.py +31 -10
- package/tooling/scripts/lib/colors.py +106 -0
- package/tooling/scripts/lib/context_monitor.py +766 -0
- package/tooling/scripts/lib/cost_config.py +229 -10
- package/tooling/scripts/lib/cost_display.py +20 -45
- package/tooling/scripts/lib/cost_tracker.py +462 -15
- package/tooling/scripts/lib/currency_converter.py +28 -5
- package/tooling/scripts/lib/pair_programming.py +102 -3
- package/tooling/scripts/lib/personality_system.py +949 -0
- package/tooling/scripts/lib/platform.py +55 -0
- package/tooling/scripts/lib/shared_memory.py +9 -3
- package/tooling/scripts/lib/swarm_orchestrator.py +514 -75
- package/tooling/scripts/lib/validation_loop.py +1014 -0
- package/tooling/scripts/memory_summarize.py +9 -2
- package/tooling/scripts/new-doc.py +2 -9
- package/tooling/scripts/personalize_agent.py +1 -12
- package/tooling/scripts/rollback-migration.sh +60 -60
- package/tooling/scripts/run-collab.ps1 +16 -16
- package/tooling/scripts/run-collab.py +88 -53
- package/tooling/scripts/run-collab.sh +4 -4
- package/tooling/scripts/run-story.py +278 -20
- package/tooling/scripts/run-story.sh +3 -3
- package/tooling/scripts/setup-checkpoint-service.py +2 -9
- package/tooling/scripts/tech-debt-tracker.py +1 -12
- package/tooling/scripts/test_adversarial_swarm.py +452 -0
- package/tooling/scripts/validate-overrides.py +1 -10
- package/tooling/scripts/validate-overrides.sh +40 -40
- package/tooling/scripts/validate_loop.py +162 -0
- package/tooling/scripts/validate_setup.py +2 -30
- package/.claude/skills/init/SKILL.md +0 -496
|
@@ -0,0 +1,766 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Context Monitor - Real-time context usage tracking and alerting.
|
|
4
|
+
|
|
5
|
+
Monitors Claude Code context window usage and provides:
|
|
6
|
+
1. Real-time context estimation based on token usage
|
|
7
|
+
2. Proactive warnings before compaction thresholds
|
|
8
|
+
3. Auto-checkpoint triggers at critical levels
|
|
9
|
+
4. Integration with cost tracking for unified status display
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
from lib.context_monitor import ContextMonitor, StatusLine
|
|
13
|
+
|
|
14
|
+
monitor = ContextMonitor(story_key="STORY-123")
|
|
15
|
+
monitor.update_from_tokens(input_tokens=50000, output_tokens=10000)
|
|
16
|
+
|
|
17
|
+
# Get status line for display
|
|
18
|
+
status = StatusLine(monitor)
|
|
19
|
+
print(status.render())
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import sys
|
|
25
|
+
import threading
|
|
26
|
+
import time
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from datetime import datetime
|
|
29
|
+
from enum import Enum
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import Callable, Optional
|
|
32
|
+
|
|
33
|
+
# Add parent for imports
|
|
34
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
35
|
+
|
|
36
|
+
from colors import Colors
|
|
37
|
+
|
|
38
|
+
# Configuration
|
|
39
|
+
PROJECT_ROOT = Path(__file__).parent.parent.parent.parent
|
|
40
|
+
CONTEXT_STATE_DIR = PROJECT_ROOT / "tooling" / ".automation" / "context"
|
|
41
|
+
|
|
42
|
+
# Context window sizes (tokens) - Claude model estimates
|
|
43
|
+
CONTEXT_WINDOWS = {
|
|
44
|
+
"opus": 200_000,
|
|
45
|
+
"sonnet": 200_000,
|
|
46
|
+
"haiku": 200_000,
|
|
47
|
+
"default": 200_000,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# Thresholds (percentage of context window)
|
|
51
|
+
THRESHOLD_SAFE = 0.50 # 50% - Normal operation
|
|
52
|
+
THRESHOLD_CAUTION = 0.65 # 65% - Start being careful
|
|
53
|
+
THRESHOLD_WARNING = 0.75 # 75% - Visible warning
|
|
54
|
+
THRESHOLD_CRITICAL = 0.85 # 85% - Auto-checkpoint recommended
|
|
55
|
+
THRESHOLD_EMERGENCY = 0.95 # 95% - Compaction imminent
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ContextLevel(Enum):
|
|
59
|
+
"""Context usage level classifications."""
|
|
60
|
+
|
|
61
|
+
SAFE = "safe" # < 50%
|
|
62
|
+
CAUTION = "caution" # 50-65%
|
|
63
|
+
WARNING = "warning" # 65-75%
|
|
64
|
+
CRITICAL = "critical" # 75-85%
|
|
65
|
+
EMERGENCY = "emergency" # > 85%
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class ContextState:
|
|
70
|
+
"""Current context window state."""
|
|
71
|
+
|
|
72
|
+
story_key: str
|
|
73
|
+
model: str = "sonnet"
|
|
74
|
+
context_window: int = 200_000
|
|
75
|
+
|
|
76
|
+
# Token tracking
|
|
77
|
+
total_input_tokens: int = 0
|
|
78
|
+
total_output_tokens: int = 0
|
|
79
|
+
estimated_context_tokens: int = 0
|
|
80
|
+
|
|
81
|
+
# History for trend analysis
|
|
82
|
+
token_history: list = field(default_factory=list)
|
|
83
|
+
|
|
84
|
+
# Timestamps
|
|
85
|
+
session_start: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
86
|
+
last_update: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
87
|
+
|
|
88
|
+
# Checkpoint tracking
|
|
89
|
+
last_checkpoint_at: Optional[float] = None # Context % at last checkpoint
|
|
90
|
+
checkpoint_count: int = 0
|
|
91
|
+
|
|
92
|
+
# Current activity tracking
|
|
93
|
+
current_agent: Optional[str] = None
|
|
94
|
+
current_task: Optional[str] = None
|
|
95
|
+
current_phase: Optional[str] = None
|
|
96
|
+
phase_start_time: Optional[str] = None
|
|
97
|
+
phases_completed: int = 0
|
|
98
|
+
total_phases: int = 0
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def total_tokens(self) -> int:
|
|
102
|
+
"""Total tokens used (in + out)."""
|
|
103
|
+
return self.total_input_tokens + self.total_output_tokens
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def context_usage_ratio(self) -> float:
|
|
107
|
+
"""Context usage as ratio (0.0 to 1.0)."""
|
|
108
|
+
if self.context_window <= 0:
|
|
109
|
+
return 0.0
|
|
110
|
+
# Estimate context usage - conversation grows with each exchange
|
|
111
|
+
# Input tokens accumulate in context, output tokens become part of history
|
|
112
|
+
return min(1.0, self.estimated_context_tokens / self.context_window)
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def context_usage_percent(self) -> float:
|
|
116
|
+
"""Context usage as percentage."""
|
|
117
|
+
return self.context_usage_ratio * 100
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def context_level(self) -> ContextLevel:
|
|
121
|
+
"""Current context level classification."""
|
|
122
|
+
ratio = self.context_usage_ratio
|
|
123
|
+
if ratio >= THRESHOLD_EMERGENCY:
|
|
124
|
+
return ContextLevel.EMERGENCY
|
|
125
|
+
elif ratio >= THRESHOLD_CRITICAL:
|
|
126
|
+
return ContextLevel.CRITICAL
|
|
127
|
+
elif ratio >= THRESHOLD_WARNING:
|
|
128
|
+
return ContextLevel.WARNING
|
|
129
|
+
elif ratio >= THRESHOLD_CAUTION:
|
|
130
|
+
return ContextLevel.CAUTION
|
|
131
|
+
return ContextLevel.SAFE
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def tokens_remaining(self) -> int:
|
|
135
|
+
"""Estimated tokens remaining before compaction."""
|
|
136
|
+
return max(0, self.context_window - self.estimated_context_tokens)
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def exchanges_remaining(self) -> int:
|
|
140
|
+
"""Estimated exchanges remaining (assuming avg 5K tokens per exchange)."""
|
|
141
|
+
avg_exchange_tokens = 5000
|
|
142
|
+
return max(0, self.tokens_remaining // avg_exchange_tokens)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class ContextMonitor:
|
|
146
|
+
"""
|
|
147
|
+
Monitors and tracks context window usage.
|
|
148
|
+
|
|
149
|
+
Provides real-time estimation, warnings, and checkpoint triggers.
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
def __init__(
|
|
153
|
+
self,
|
|
154
|
+
story_key: str,
|
|
155
|
+
model: str = "sonnet",
|
|
156
|
+
on_threshold: Optional[Callable[[ContextLevel, ContextState], None]] = None,
|
|
157
|
+
state_dir: Optional[Path] = None,
|
|
158
|
+
):
|
|
159
|
+
"""
|
|
160
|
+
Initialize context monitor.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
story_key: Story identifier for state persistence
|
|
164
|
+
model: Model name for context window size
|
|
165
|
+
on_threshold: Callback when threshold crossed (level, state)
|
|
166
|
+
state_dir: Optional custom state directory (for testing)
|
|
167
|
+
"""
|
|
168
|
+
self.story_key = story_key
|
|
169
|
+
self.model = model.lower()
|
|
170
|
+
self.on_threshold = on_threshold
|
|
171
|
+
self._lock = threading.Lock()
|
|
172
|
+
self._state_dir = state_dir or CONTEXT_STATE_DIR
|
|
173
|
+
|
|
174
|
+
# Initialize state
|
|
175
|
+
context_window = CONTEXT_WINDOWS.get(self.model, CONTEXT_WINDOWS["default"])
|
|
176
|
+
self.state = ContextState(
|
|
177
|
+
story_key=story_key, model=self.model, context_window=context_window
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# Track previous level for threshold crossing detection
|
|
181
|
+
self._previous_level = ContextLevel.SAFE
|
|
182
|
+
|
|
183
|
+
# Ensure state directory exists
|
|
184
|
+
self._state_dir.mkdir(parents=True, exist_ok=True)
|
|
185
|
+
|
|
186
|
+
# Try to load existing state
|
|
187
|
+
self._load_state()
|
|
188
|
+
|
|
189
|
+
def _get_state_file(self) -> Path:
|
|
190
|
+
"""Get path to state file."""
|
|
191
|
+
return self._state_dir / f"context_{self.story_key}.json"
|
|
192
|
+
|
|
193
|
+
def _load_state(self):
|
|
194
|
+
"""Load state from file if exists."""
|
|
195
|
+
state_file = self._get_state_file()
|
|
196
|
+
if state_file.exists():
|
|
197
|
+
try:
|
|
198
|
+
with open(state_file) as f:
|
|
199
|
+
data = json.load(f)
|
|
200
|
+
self.state = ContextState(
|
|
201
|
+
story_key=data.get("story_key", self.story_key),
|
|
202
|
+
model=data.get("model", self.model),
|
|
203
|
+
context_window=data.get(
|
|
204
|
+
"context_window", CONTEXT_WINDOWS.get(self.model, 200_000)
|
|
205
|
+
),
|
|
206
|
+
total_input_tokens=data.get("total_input_tokens", 0),
|
|
207
|
+
total_output_tokens=data.get("total_output_tokens", 0),
|
|
208
|
+
estimated_context_tokens=data.get("estimated_context_tokens", 0),
|
|
209
|
+
token_history=data.get("token_history", []),
|
|
210
|
+
session_start=data.get("session_start", datetime.now().isoformat()),
|
|
211
|
+
last_update=data.get("last_update", datetime.now().isoformat()),
|
|
212
|
+
last_checkpoint_at=data.get("last_checkpoint_at"),
|
|
213
|
+
checkpoint_count=data.get("checkpoint_count", 0),
|
|
214
|
+
)
|
|
215
|
+
except (json.JSONDecodeError, KeyError):
|
|
216
|
+
pass # Use default state
|
|
217
|
+
|
|
218
|
+
def _save_state(self):
|
|
219
|
+
"""Save state to file."""
|
|
220
|
+
state_file = self._get_state_file()
|
|
221
|
+
with open(state_file, "w") as f:
|
|
222
|
+
json.dump(
|
|
223
|
+
{
|
|
224
|
+
"story_key": self.state.story_key,
|
|
225
|
+
"model": self.state.model,
|
|
226
|
+
"context_window": self.state.context_window,
|
|
227
|
+
"total_input_tokens": self.state.total_input_tokens,
|
|
228
|
+
"total_output_tokens": self.state.total_output_tokens,
|
|
229
|
+
"estimated_context_tokens": self.state.estimated_context_tokens,
|
|
230
|
+
"token_history": self.state.token_history[-100:], # Keep last 100
|
|
231
|
+
"session_start": self.state.session_start,
|
|
232
|
+
"last_update": self.state.last_update,
|
|
233
|
+
"last_checkpoint_at": self.state.last_checkpoint_at,
|
|
234
|
+
"checkpoint_count": self.state.checkpoint_count,
|
|
235
|
+
},
|
|
236
|
+
f,
|
|
237
|
+
indent=2,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
def update_from_tokens(
|
|
241
|
+
self, input_tokens: int = 0, output_tokens: int = 0, is_new_exchange: bool = True
|
|
242
|
+
):
|
|
243
|
+
"""
|
|
244
|
+
Update context estimation from token counts.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
input_tokens: New input tokens used
|
|
248
|
+
output_tokens: New output tokens generated
|
|
249
|
+
is_new_exchange: Whether this is a new exchange (vs continuation)
|
|
250
|
+
"""
|
|
251
|
+
with self._lock:
|
|
252
|
+
self.state.total_input_tokens += input_tokens
|
|
253
|
+
self.state.total_output_tokens += output_tokens
|
|
254
|
+
|
|
255
|
+
# Estimate context growth
|
|
256
|
+
# Context accumulates: previous context + new input + output
|
|
257
|
+
if is_new_exchange:
|
|
258
|
+
# New exchange adds both input and output to context
|
|
259
|
+
self.state.estimated_context_tokens += input_tokens + output_tokens
|
|
260
|
+
else:
|
|
261
|
+
# Continuation just adds the delta
|
|
262
|
+
self.state.estimated_context_tokens += output_tokens
|
|
263
|
+
|
|
264
|
+
# Record in history
|
|
265
|
+
self.state.token_history.append(
|
|
266
|
+
{
|
|
267
|
+
"timestamp": datetime.now().isoformat(),
|
|
268
|
+
"input": input_tokens,
|
|
269
|
+
"output": output_tokens,
|
|
270
|
+
"context": self.state.estimated_context_tokens,
|
|
271
|
+
}
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
self.state.last_update = datetime.now().isoformat()
|
|
275
|
+
|
|
276
|
+
# Check for threshold crossing
|
|
277
|
+
current_level = self.state.context_level
|
|
278
|
+
if current_level != self._previous_level:
|
|
279
|
+
self._handle_threshold_crossing(current_level)
|
|
280
|
+
self._previous_level = current_level
|
|
281
|
+
|
|
282
|
+
# Persist state
|
|
283
|
+
self._save_state()
|
|
284
|
+
|
|
285
|
+
def update_from_cost_entry(self, input_tokens: int, output_tokens: int):
|
|
286
|
+
"""Update from a cost tracker entry."""
|
|
287
|
+
self.update_from_tokens(input_tokens, output_tokens, is_new_exchange=True)
|
|
288
|
+
|
|
289
|
+
def _handle_threshold_crossing(self, new_level: ContextLevel):
|
|
290
|
+
"""Handle threshold crossing event."""
|
|
291
|
+
if self.on_threshold:
|
|
292
|
+
self.on_threshold(new_level, self.state)
|
|
293
|
+
|
|
294
|
+
def record_checkpoint(self):
|
|
295
|
+
"""Record that a checkpoint was created."""
|
|
296
|
+
with self._lock:
|
|
297
|
+
self.state.last_checkpoint_at = self.state.context_usage_ratio
|
|
298
|
+
self.state.checkpoint_count += 1
|
|
299
|
+
self._save_state()
|
|
300
|
+
|
|
301
|
+
def reset_context(self):
|
|
302
|
+
"""Reset context tracking (after compaction/clear)."""
|
|
303
|
+
with self._lock:
|
|
304
|
+
self.state.estimated_context_tokens = 0
|
|
305
|
+
self.state.token_history = []
|
|
306
|
+
self.state.last_update = datetime.now().isoformat()
|
|
307
|
+
self._previous_level = ContextLevel.SAFE
|
|
308
|
+
self._save_state()
|
|
309
|
+
|
|
310
|
+
def set_current_activity(
|
|
311
|
+
self,
|
|
312
|
+
agent: Optional[str] = None,
|
|
313
|
+
task: Optional[str] = None,
|
|
314
|
+
phase: Optional[str] = None,
|
|
315
|
+
phases_completed: Optional[int] = None,
|
|
316
|
+
total_phases: Optional[int] = None,
|
|
317
|
+
):
|
|
318
|
+
"""
|
|
319
|
+
Set current activity information for status display.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
agent: Current agent name (e.g., "SM", "DEV", "REVIEWER")
|
|
323
|
+
task: Short description of current task
|
|
324
|
+
phase: Current phase name (e.g., "Context", "Development", "Review")
|
|
325
|
+
phases_completed: Number of phases completed
|
|
326
|
+
total_phases: Total number of phases
|
|
327
|
+
"""
|
|
328
|
+
with self._lock:
|
|
329
|
+
if agent is not None:
|
|
330
|
+
self.state.current_agent = agent
|
|
331
|
+
if task is not None:
|
|
332
|
+
self.state.current_task = task
|
|
333
|
+
if phase is not None:
|
|
334
|
+
self.state.current_phase = phase
|
|
335
|
+
self.state.phase_start_time = datetime.now().isoformat()
|
|
336
|
+
if phases_completed is not None:
|
|
337
|
+
self.state.phases_completed = phases_completed
|
|
338
|
+
if total_phases is not None:
|
|
339
|
+
self.state.total_phases = total_phases
|
|
340
|
+
self._save_state()
|
|
341
|
+
|
|
342
|
+
def clear_current_activity(self):
|
|
343
|
+
"""Clear current activity (when idle)."""
|
|
344
|
+
with self._lock:
|
|
345
|
+
self.state.current_agent = None
|
|
346
|
+
self.state.current_task = None
|
|
347
|
+
self.state.current_phase = None
|
|
348
|
+
self.state.phase_start_time = None
|
|
349
|
+
self._save_state()
|
|
350
|
+
|
|
351
|
+
def get_recommendation(self) -> str:
|
|
352
|
+
"""Get recommended action based on current state."""
|
|
353
|
+
level = self.state.context_level
|
|
354
|
+
|
|
355
|
+
if level == ContextLevel.EMERGENCY:
|
|
356
|
+
return "CHECKPOINT NOW - Compaction imminent. Save state and clear session."
|
|
357
|
+
elif level == ContextLevel.CRITICAL:
|
|
358
|
+
return "Consider checkpoint - Context window filling up. Wrap up current task."
|
|
359
|
+
elif level == ContextLevel.WARNING:
|
|
360
|
+
return "Monitor closely - Plan to checkpoint within a few exchanges."
|
|
361
|
+
elif level == ContextLevel.CAUTION:
|
|
362
|
+
return "Context growing - Be aware of window limits."
|
|
363
|
+
return "Context healthy - Plenty of room remaining."
|
|
364
|
+
|
|
365
|
+
def should_checkpoint(self) -> bool:
|
|
366
|
+
"""Check if checkpoint is recommended."""
|
|
367
|
+
return self.state.context_level in (ContextLevel.CRITICAL, ContextLevel.EMERGENCY)
|
|
368
|
+
|
|
369
|
+
def should_warn(self) -> bool:
|
|
370
|
+
"""Check if warning should be displayed."""
|
|
371
|
+
return self.state.context_level in (
|
|
372
|
+
ContextLevel.WARNING,
|
|
373
|
+
ContextLevel.CRITICAL,
|
|
374
|
+
ContextLevel.EMERGENCY,
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
class StatusLine:
|
|
379
|
+
"""
|
|
380
|
+
Persistent status line for CLI display.
|
|
381
|
+
|
|
382
|
+
Combines context, cost, and activity information in a compact format.
|
|
383
|
+
Shows: Agent | Task | Context% | Cost | Time
|
|
384
|
+
"""
|
|
385
|
+
|
|
386
|
+
# Agent display names and colors
|
|
387
|
+
AGENT_COLORS = {
|
|
388
|
+
"SM": Colors.CYAN,
|
|
389
|
+
"DEV": Colors.GREEN,
|
|
390
|
+
"REVIEWER": Colors.MAGENTA,
|
|
391
|
+
"ARCHITECT": Colors.BLUE,
|
|
392
|
+
"BA": Colors.YELLOW,
|
|
393
|
+
"PM": Colors.CYAN,
|
|
394
|
+
"MAINTAINER": Colors.YELLOW,
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
def __init__(
|
|
398
|
+
self,
|
|
399
|
+
context_monitor: Optional[ContextMonitor] = None,
|
|
400
|
+
cost_tracker: Optional[object] = None, # Avoid circular import
|
|
401
|
+
width: int = 80,
|
|
402
|
+
):
|
|
403
|
+
"""
|
|
404
|
+
Initialize status line.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
context_monitor: ContextMonitor instance
|
|
408
|
+
cost_tracker: Optional CostTracker instance
|
|
409
|
+
width: Display width
|
|
410
|
+
"""
|
|
411
|
+
self.context_monitor = context_monitor
|
|
412
|
+
self.cost_tracker = cost_tracker
|
|
413
|
+
self.width = width
|
|
414
|
+
self._last_render = ""
|
|
415
|
+
self._start_time = datetime.now()
|
|
416
|
+
|
|
417
|
+
def _get_activity_indicator(self) -> str:
|
|
418
|
+
"""Get current activity indicator (agent + task + phase)."""
|
|
419
|
+
if not self.context_monitor:
|
|
420
|
+
return ""
|
|
421
|
+
|
|
422
|
+
state = self.context_monitor.state
|
|
423
|
+
|
|
424
|
+
parts = []
|
|
425
|
+
|
|
426
|
+
# Phase progress (e.g., "[2/3]")
|
|
427
|
+
if state.total_phases > 0:
|
|
428
|
+
parts.append(f"[{state.phases_completed + 1}/{state.total_phases}]")
|
|
429
|
+
|
|
430
|
+
# Current agent with color
|
|
431
|
+
if state.current_agent:
|
|
432
|
+
color = self.AGENT_COLORS.get(state.current_agent, Colors.WHITE)
|
|
433
|
+
parts.append(f"{color}{Colors.BOLD}{state.current_agent}{Colors.RESET}")
|
|
434
|
+
|
|
435
|
+
# Current phase/task (truncated if long)
|
|
436
|
+
if state.current_phase:
|
|
437
|
+
phase = state.current_phase
|
|
438
|
+
if len(phase) > 20:
|
|
439
|
+
phase = phase[:17] + "..."
|
|
440
|
+
parts.append(f"{Colors.DIM}{phase}{Colors.RESET}")
|
|
441
|
+
elif state.current_task:
|
|
442
|
+
task = state.current_task
|
|
443
|
+
if len(task) > 25:
|
|
444
|
+
task = task[:22] + "..."
|
|
445
|
+
parts.append(f"{Colors.DIM}{task}{Colors.RESET}")
|
|
446
|
+
|
|
447
|
+
# Phase elapsed time
|
|
448
|
+
if state.phase_start_time:
|
|
449
|
+
try:
|
|
450
|
+
start = datetime.fromisoformat(state.phase_start_time)
|
|
451
|
+
elapsed = datetime.now() - start
|
|
452
|
+
mins = int(elapsed.total_seconds() // 60)
|
|
453
|
+
secs = int(elapsed.total_seconds() % 60)
|
|
454
|
+
parts.append(f"{Colors.DIM}({mins}:{secs:02d}){Colors.RESET}")
|
|
455
|
+
except (ValueError, TypeError):
|
|
456
|
+
pass
|
|
457
|
+
|
|
458
|
+
if not parts:
|
|
459
|
+
return f"{Colors.DIM}Idle{Colors.RESET}"
|
|
460
|
+
|
|
461
|
+
return " ".join(parts)
|
|
462
|
+
|
|
463
|
+
def _get_context_indicator(self) -> str:
|
|
464
|
+
"""Get context usage indicator with color."""
|
|
465
|
+
if not self.context_monitor:
|
|
466
|
+
return ""
|
|
467
|
+
|
|
468
|
+
state = self.context_monitor.state
|
|
469
|
+
pct = state.context_usage_percent
|
|
470
|
+
level = state.context_level
|
|
471
|
+
|
|
472
|
+
# Color and icon based on level
|
|
473
|
+
if level == ContextLevel.EMERGENCY:
|
|
474
|
+
color = Colors.BG_RED + Colors.WHITE
|
|
475
|
+
icon = "[!!!]"
|
|
476
|
+
elif level == ContextLevel.CRITICAL:
|
|
477
|
+
color = Colors.BOLD_RED
|
|
478
|
+
icon = "[!!]"
|
|
479
|
+
elif level == ContextLevel.WARNING:
|
|
480
|
+
color = Colors.BOLD_YELLOW
|
|
481
|
+
icon = "[!]"
|
|
482
|
+
elif level == ContextLevel.CAUTION:
|
|
483
|
+
color = Colors.YELLOW
|
|
484
|
+
icon = ""
|
|
485
|
+
else:
|
|
486
|
+
color = Colors.GREEN
|
|
487
|
+
icon = ""
|
|
488
|
+
|
|
489
|
+
# Format: Ctx: 45% [===== ] ~12 left
|
|
490
|
+
bar_width = 10
|
|
491
|
+
filled = int((pct / 100) * bar_width)
|
|
492
|
+
bar = "=" * filled + " " * (bar_width - filled)
|
|
493
|
+
|
|
494
|
+
remaining = state.exchanges_remaining
|
|
495
|
+
remaining_str = f"~{remaining} left" if remaining < 50 else ""
|
|
496
|
+
|
|
497
|
+
return f"{color}Ctx: {pct:.0f}% [{bar}] {icon}{remaining_str}{Colors.RESET}"
|
|
498
|
+
|
|
499
|
+
def _get_cost_indicator(self) -> str:
|
|
500
|
+
"""Get cost indicator with color."""
|
|
501
|
+
if not self.cost_tracker:
|
|
502
|
+
return ""
|
|
503
|
+
|
|
504
|
+
try:
|
|
505
|
+
session = self.cost_tracker.session
|
|
506
|
+
pct = session.budget_used_percent
|
|
507
|
+
cost = session.total_cost_usd
|
|
508
|
+
|
|
509
|
+
if pct >= 90:
|
|
510
|
+
color = Colors.RED
|
|
511
|
+
elif pct >= 75:
|
|
512
|
+
color = Colors.YELLOW
|
|
513
|
+
else:
|
|
514
|
+
color = Colors.GREEN
|
|
515
|
+
|
|
516
|
+
return f"{color}Cost: ${cost:.2f} ({pct:.0f}%){Colors.RESET}"
|
|
517
|
+
except Exception:
|
|
518
|
+
return ""
|
|
519
|
+
|
|
520
|
+
def _get_time_indicator(self) -> str:
|
|
521
|
+
"""Get elapsed time indicator."""
|
|
522
|
+
return f"{Colors.DIM}{datetime.now().strftime('%H:%M:%S')}{Colors.RESET}"
|
|
523
|
+
|
|
524
|
+
def render(self, include_border: bool = False) -> str:
|
|
525
|
+
"""
|
|
526
|
+
Render the status line.
|
|
527
|
+
|
|
528
|
+
Format: [1/3] DEV Development (0:45) | Ctx: 45% [===== ] | Cost: $1.23 (12%) | 14:32:01
|
|
529
|
+
|
|
530
|
+
Args:
|
|
531
|
+
include_border: Whether to include a top border line
|
|
532
|
+
"""
|
|
533
|
+
parts = []
|
|
534
|
+
|
|
535
|
+
# Activity (agent + task + phase progress)
|
|
536
|
+
activity = self._get_activity_indicator()
|
|
537
|
+
if activity:
|
|
538
|
+
parts.append(activity)
|
|
539
|
+
|
|
540
|
+
# Context usage
|
|
541
|
+
ctx = self._get_context_indicator()
|
|
542
|
+
if ctx:
|
|
543
|
+
parts.append(ctx)
|
|
544
|
+
|
|
545
|
+
# Cost
|
|
546
|
+
cost = self._get_cost_indicator()
|
|
547
|
+
if cost:
|
|
548
|
+
parts.append(cost)
|
|
549
|
+
|
|
550
|
+
# Time
|
|
551
|
+
parts.append(self._get_time_indicator())
|
|
552
|
+
|
|
553
|
+
# Join with separator
|
|
554
|
+
content = f" {Colors.DIM}|{Colors.RESET} ".join(parts)
|
|
555
|
+
|
|
556
|
+
if include_border:
|
|
557
|
+
border = f"{Colors.DIM}{'─' * self.width}{Colors.RESET}"
|
|
558
|
+
return f"{border}\n{content}"
|
|
559
|
+
|
|
560
|
+
self._last_render = content
|
|
561
|
+
return content
|
|
562
|
+
|
|
563
|
+
def render_warning(self) -> Optional[str]:
|
|
564
|
+
"""Render warning message if threshold crossed."""
|
|
565
|
+
if not self.context_monitor:
|
|
566
|
+
return None
|
|
567
|
+
|
|
568
|
+
if not self.context_monitor.should_warn():
|
|
569
|
+
return None
|
|
570
|
+
|
|
571
|
+
state = self.context_monitor.state
|
|
572
|
+
level = state.context_level
|
|
573
|
+
rec = self.context_monitor.get_recommendation()
|
|
574
|
+
|
|
575
|
+
if level == ContextLevel.EMERGENCY:
|
|
576
|
+
return f"\n{Colors.BG_RED}{Colors.WHITE} CONTEXT EMERGENCY {Colors.RESET} {rec}"
|
|
577
|
+
elif level == ContextLevel.CRITICAL:
|
|
578
|
+
return f"\n{Colors.BOLD_RED}[CRITICAL]{Colors.RESET} {rec}"
|
|
579
|
+
elif level == ContextLevel.WARNING:
|
|
580
|
+
return f"\n{Colors.YELLOW}[WARNING]{Colors.RESET} {rec}"
|
|
581
|
+
|
|
582
|
+
return None
|
|
583
|
+
|
|
584
|
+
def print(self, newline: bool = True):
|
|
585
|
+
"""Print status line to terminal."""
|
|
586
|
+
output = self.render()
|
|
587
|
+
warning = self.render_warning()
|
|
588
|
+
|
|
589
|
+
if warning:
|
|
590
|
+
output += warning
|
|
591
|
+
|
|
592
|
+
if newline:
|
|
593
|
+
print(output)
|
|
594
|
+
else:
|
|
595
|
+
print(f"\r{output}", end="", flush=True)
|
|
596
|
+
|
|
597
|
+
def print_header(self, title: str = ""):
|
|
598
|
+
"""Print status line as a header with title."""
|
|
599
|
+
border = f"{Colors.DIM}{'─' * self.width}{Colors.RESET}"
|
|
600
|
+
status = self.render()
|
|
601
|
+
|
|
602
|
+
if title:
|
|
603
|
+
print(f"{border}")
|
|
604
|
+
print(f"{Colors.BOLD}{title}{Colors.RESET}")
|
|
605
|
+
print(f"{status}")
|
|
606
|
+
print(f"{border}")
|
|
607
|
+
else:
|
|
608
|
+
print(f"{border}")
|
|
609
|
+
print(f"{status}")
|
|
610
|
+
print(f"{border}")
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
class StatusLineManager:
|
|
614
|
+
"""
|
|
615
|
+
Manages persistent status line updates across CLI operations.
|
|
616
|
+
|
|
617
|
+
Provides a consistent status display that can be updated from anywhere.
|
|
618
|
+
"""
|
|
619
|
+
|
|
620
|
+
_instance = None
|
|
621
|
+
_lock = threading.Lock()
|
|
622
|
+
|
|
623
|
+
def __new__(cls, *args, **kwargs):
|
|
624
|
+
"""Singleton pattern."""
|
|
625
|
+
if cls._instance is None:
|
|
626
|
+
with cls._lock:
|
|
627
|
+
if cls._instance is None:
|
|
628
|
+
cls._instance = super().__new__(cls)
|
|
629
|
+
cls._instance._initialized = False
|
|
630
|
+
return cls._instance
|
|
631
|
+
|
|
632
|
+
def __init__(
|
|
633
|
+
self, story_key: Optional[str] = None, model: str = "sonnet", auto_refresh: bool = False
|
|
634
|
+
):
|
|
635
|
+
"""
|
|
636
|
+
Initialize or update the status line manager.
|
|
637
|
+
|
|
638
|
+
Args:
|
|
639
|
+
story_key: Story identifier
|
|
640
|
+
model: Model name
|
|
641
|
+
auto_refresh: Whether to auto-refresh on a timer
|
|
642
|
+
"""
|
|
643
|
+
if self._initialized and story_key is None:
|
|
644
|
+
return # Already initialized, no update needed
|
|
645
|
+
|
|
646
|
+
self.story_key = story_key or "default"
|
|
647
|
+
self.model = model
|
|
648
|
+
self.auto_refresh = auto_refresh
|
|
649
|
+
|
|
650
|
+
# Create monitor
|
|
651
|
+
self.context_monitor = ContextMonitor(
|
|
652
|
+
story_key=self.story_key, model=self.model, on_threshold=self._on_threshold
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
# Cost tracker set externally
|
|
656
|
+
self.cost_tracker = None
|
|
657
|
+
|
|
658
|
+
# Status line
|
|
659
|
+
self.status_line = StatusLine(context_monitor=self.context_monitor)
|
|
660
|
+
|
|
661
|
+
# Auto-refresh thread
|
|
662
|
+
self._refresh_thread = None
|
|
663
|
+
self._stop_refresh = threading.Event()
|
|
664
|
+
|
|
665
|
+
if auto_refresh:
|
|
666
|
+
self._start_auto_refresh()
|
|
667
|
+
|
|
668
|
+
self._initialized = True
|
|
669
|
+
|
|
670
|
+
def _on_threshold(self, level: ContextLevel, state: ContextState):
|
|
671
|
+
"""Handle threshold crossing."""
|
|
672
|
+
if level in (ContextLevel.CRITICAL, ContextLevel.EMERGENCY):
|
|
673
|
+
# Print immediate warning
|
|
674
|
+
warning = self.status_line.render_warning()
|
|
675
|
+
if warning:
|
|
676
|
+
print(warning)
|
|
677
|
+
|
|
678
|
+
def set_cost_tracker(self, tracker):
|
|
679
|
+
"""Set cost tracker for combined display."""
|
|
680
|
+
self.cost_tracker = tracker
|
|
681
|
+
self.status_line.cost_tracker = tracker
|
|
682
|
+
|
|
683
|
+
def update_tokens(self, input_tokens: int, output_tokens: int):
|
|
684
|
+
"""Update context from token usage."""
|
|
685
|
+
self.context_monitor.update_from_tokens(input_tokens, output_tokens)
|
|
686
|
+
|
|
687
|
+
def print_status(self, newline: bool = True):
|
|
688
|
+
"""Print current status line."""
|
|
689
|
+
self.status_line.print(newline=newline)
|
|
690
|
+
|
|
691
|
+
def print_header(self, title: str = ""):
|
|
692
|
+
"""Print status as header."""
|
|
693
|
+
self.status_line.print_header(title)
|
|
694
|
+
|
|
695
|
+
def should_checkpoint(self) -> bool:
|
|
696
|
+
"""Check if checkpoint recommended."""
|
|
697
|
+
return self.context_monitor.should_checkpoint()
|
|
698
|
+
|
|
699
|
+
def record_checkpoint(self):
|
|
700
|
+
"""Record checkpoint event."""
|
|
701
|
+
self.context_monitor.record_checkpoint()
|
|
702
|
+
|
|
703
|
+
def reset(self):
|
|
704
|
+
"""Reset context tracking."""
|
|
705
|
+
self.context_monitor.reset_context()
|
|
706
|
+
|
|
707
|
+
def _start_auto_refresh(self, interval: float = 5.0):
|
|
708
|
+
"""Start auto-refresh thread."""
|
|
709
|
+
if self._refresh_thread is not None:
|
|
710
|
+
return
|
|
711
|
+
|
|
712
|
+
def refresh_loop():
|
|
713
|
+
while not self._stop_refresh.is_set():
|
|
714
|
+
self.print_status(newline=False)
|
|
715
|
+
time.sleep(interval)
|
|
716
|
+
|
|
717
|
+
self._refresh_thread = threading.Thread(target=refresh_loop, daemon=True)
|
|
718
|
+
self._refresh_thread.start()
|
|
719
|
+
|
|
720
|
+
def stop_auto_refresh(self):
|
|
721
|
+
"""Stop auto-refresh thread."""
|
|
722
|
+
self._stop_refresh.set()
|
|
723
|
+
if self._refresh_thread:
|
|
724
|
+
self._refresh_thread.join(timeout=1.0)
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
# Global instance getter
|
|
728
|
+
def get_status_manager(
|
|
729
|
+
story_key: Optional[str] = None, model: str = "sonnet"
|
|
730
|
+
) -> StatusLineManager:
|
|
731
|
+
"""Get or create the global status line manager."""
|
|
732
|
+
return StatusLineManager(story_key=story_key, model=model)
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
if __name__ == "__main__":
|
|
736
|
+
# Demo
|
|
737
|
+
print("Context Monitor Demo\n")
|
|
738
|
+
|
|
739
|
+
monitor = ContextMonitor(story_key="demo-story", model="sonnet")
|
|
740
|
+
|
|
741
|
+
# Simulate token usage growth
|
|
742
|
+
status = StatusLine(context_monitor=monitor)
|
|
743
|
+
|
|
744
|
+
print("Initial state:")
|
|
745
|
+
status.print_header("DEVFLOW STATUS")
|
|
746
|
+
|
|
747
|
+
# Simulate exchanges
|
|
748
|
+
exchanges = [
|
|
749
|
+
(15000, 3000), # Small exchange
|
|
750
|
+
(25000, 8000), # Medium exchange
|
|
751
|
+
(40000, 12000), # Large exchange
|
|
752
|
+
(30000, 10000), # Another exchange
|
|
753
|
+
(50000, 15000), # Big exchange - should trigger caution
|
|
754
|
+
]
|
|
755
|
+
|
|
756
|
+
for i, (inp, out) in enumerate(exchanges, 1):
|
|
757
|
+
print(f"\nExchange {i}: +{inp:,} input, +{out:,} output")
|
|
758
|
+
monitor.update_from_tokens(inp, out)
|
|
759
|
+
status.print()
|
|
760
|
+
|
|
761
|
+
if monitor.should_warn():
|
|
762
|
+
print(f" Recommendation: {monitor.get_recommendation()}")
|
|
763
|
+
|
|
764
|
+
print("\n" + "=" * 60)
|
|
765
|
+
print("Final state:")
|
|
766
|
+
status.print_header("DEVFLOW STATUS")
|