@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.
Files changed (224) hide show
  1. package/.claude/commands/agent.md +1 -1
  2. package/.claude/commands/bugfix.md +21 -0
  3. package/.claude/commands/checkpoint.md +0 -1
  4. package/.claude/commands/collab.md +0 -1
  5. package/.claude/commands/costs.md +88 -18
  6. package/.claude/commands/devflow.md +26 -0
  7. package/.claude/commands/handoff.md +0 -1
  8. package/.claude/commands/init.md +287 -0
  9. package/.claude/commands/memory.md +0 -1
  10. package/.claude/commands/pair.md +0 -1
  11. package/.claude/commands/review.md +27 -0
  12. package/.claude/commands/route.md +0 -1
  13. package/.claude/commands/swarm.md +0 -1
  14. package/.claude/commands/validate.md +55 -0
  15. package/.claude/hooks/session-notification.sh +44 -0
  16. package/.claude/hooks/session-startup.sh +427 -0
  17. package/.claude/hooks/session-stop.sh +38 -0
  18. package/.claude/hooks/session_tracker.py +272 -0
  19. package/.claude/settings.json +38 -0
  20. package/.claude/skills/costs/SKILL.md +156 -0
  21. package/.claude/skills/validate/SKILL.md +101 -0
  22. package/CHANGELOG.md +243 -0
  23. package/README.md +207 -10
  24. package/bin/devflow-install.js +2 -1
  25. package/bin/devflow.js +4 -0
  26. package/lib/constants.js +0 -1
  27. package/lib/exec-python.js +1 -1
  28. package/package.json +1 -1
  29. package/tooling/.automation/.checkpoint_lock +1 -0
  30. package/tooling/.automation/agents/architect.md +19 -0
  31. package/tooling/.automation/agents/ba.md +19 -0
  32. package/tooling/.automation/agents/maintainer.md +19 -0
  33. package/tooling/.automation/agents/pm.md +19 -0
  34. package/tooling/.automation/agents/reviewer.md +1 -1
  35. package/tooling/.automation/agents/writer.md +19 -0
  36. package/tooling/.automation/benchmarks/benchmark_20251230_100119.json +314 -0
  37. package/tooling/.automation/benchmarks/benchmark_20251230_100216.json +314 -0
  38. package/tooling/.automation/costs/config.json +31 -0
  39. package/tooling/.automation/costs/sessions/2025-12-29_20251229_164128.json +22 -0
  40. package/tooling/.automation/memory/knowledge/kg_integration-test.json +707 -1
  41. package/tooling/.automation/memory/knowledge/kg_test-story.json +3273 -2
  42. package/tooling/.automation/memory/shared/shared_integration-test.json +181 -1
  43. package/tooling/.automation/memory/shared/shared_test-story.json +721 -1
  44. package/tooling/.automation/memory/shared/shared_test.json +1254 -0
  45. package/tooling/.automation/memory/shared/shared_validation-check.json +227 -0
  46. package/tooling/.automation/overrides/templates/architect/cloud-native.yaml +5 -5
  47. package/tooling/.automation/overrides/templates/architect/enterprise-architect.yaml +23 -5
  48. package/tooling/.automation/overrides/templates/architect/pragmatic-minimalist.yaml +24 -6
  49. package/tooling/.automation/overrides/templates/ba/agile-storyteller.yaml +4 -4
  50. package/tooling/.automation/overrides/templates/ba/domain-expert.yaml +4 -4
  51. package/tooling/.automation/overrides/templates/ba/requirements-engineer.yaml +4 -4
  52. package/tooling/.automation/overrides/templates/dev/performance-engineer.yaml +18 -0
  53. package/tooling/.automation/overrides/templates/dev/rapid-prototyper.yaml +19 -1
  54. package/tooling/.automation/overrides/templates/dev/security-focused.yaml +18 -0
  55. package/tooling/.automation/overrides/templates/dev/user-advocate.yaml +54 -0
  56. package/tooling/.automation/overrides/templates/maintainer/devops-maintainer.yaml +4 -4
  57. package/tooling/.automation/overrides/templates/maintainer/legacy-steward.yaml +4 -4
  58. package/tooling/.automation/overrides/templates/maintainer/oss-maintainer.yaml +4 -4
  59. package/tooling/.automation/overrides/templates/maintainer/reliability-engineer.yaml +55 -0
  60. package/tooling/.automation/overrides/templates/pm/agile-pm.yaml +4 -4
  61. package/tooling/.automation/overrides/templates/pm/hybrid-delivery.yaml +3 -3
  62. package/tooling/.automation/overrides/templates/pm/traditional-pm.yaml +4 -4
  63. package/tooling/.automation/overrides/templates/reviewer/quick-sanity.yaml +18 -0
  64. package/tooling/.automation/overrides/templates/reviewer/thorough-critic.yaml +18 -0
  65. package/tooling/.automation/overrides/templates/sm/agile-coach.yaml +2 -2
  66. package/tooling/.automation/overrides/templates/sm/startup-pm.yaml +3 -3
  67. package/tooling/.automation/overrides/templates/writer/api-documentarian.yaml +5 -5
  68. package/tooling/.automation/overrides/templates/writer/docs-as-code.yaml +4 -4
  69. package/tooling/.automation/overrides/templates/writer/user-guide-author.yaml +5 -5
  70. package/tooling/.automation/validation/history/2025-12-29_val_002a28c1.json +32 -0
  71. package/tooling/.automation/validation/history/2025-12-29_val_01273bb1.json +32 -0
  72. package/tooling/.automation/validation/history/2025-12-29_val_03369914.json +41 -0
  73. package/tooling/.automation/validation/history/2025-12-29_val_07a449ba.json +32 -0
  74. package/tooling/.automation/validation/history/2025-12-29_val_0df1f0a2.json +41 -0
  75. package/tooling/.automation/validation/history/2025-12-29_val_10ff3d34.json +41 -0
  76. package/tooling/.automation/validation/history/2025-12-29_val_110771d7.json +32 -0
  77. package/tooling/.automation/validation/history/2025-12-29_val_13f3a7f9.json +32 -0
  78. package/tooling/.automation/validation/history/2025-12-29_val_17ba9d21.json +41 -0
  79. package/tooling/.automation/validation/history/2025-12-29_val_22247089.json +32 -0
  80. package/tooling/.automation/validation/history/2025-12-29_val_227ea6a4.json +32 -0
  81. package/tooling/.automation/validation/history/2025-12-29_val_2335d5ae.json +32 -0
  82. package/tooling/.automation/validation/history/2025-12-29_val_246824bb.json +41 -0
  83. package/tooling/.automation/validation/history/2025-12-29_val_28b4b9cd.json +32 -0
  84. package/tooling/.automation/validation/history/2025-12-29_val_2abd12cc.json +32 -0
  85. package/tooling/.automation/validation/history/2025-12-29_val_2c801b2f.json +59 -0
  86. package/tooling/.automation/validation/history/2025-12-29_val_2c8cfa8e.json +32 -0
  87. package/tooling/.automation/validation/history/2025-12-29_val_2ce76eb0.json +32 -0
  88. package/tooling/.automation/validation/history/2025-12-29_val_30351948.json +41 -0
  89. package/tooling/.automation/validation/history/2025-12-29_val_30eb7229.json +41 -0
  90. package/tooling/.automation/validation/history/2025-12-29_val_34df0e77.json +41 -0
  91. package/tooling/.automation/validation/history/2025-12-29_val_376e4d6a.json +32 -0
  92. package/tooling/.automation/validation/history/2025-12-29_val_3a4e8a1a.json +59 -0
  93. package/tooling/.automation/validation/history/2025-12-29_val_3b77a628.json +32 -0
  94. package/tooling/.automation/validation/history/2025-12-29_val_3ea4e1cf.json +59 -0
  95. package/tooling/.automation/validation/history/2025-12-29_val_44aacdb4.json +59 -0
  96. package/tooling/.automation/validation/history/2025-12-29_val_457ddfa8.json +32 -0
  97. package/tooling/.automation/validation/history/2025-12-29_val_45af6238.json +41 -0
  98. package/tooling/.automation/validation/history/2025-12-29_val_4735dba1.json +41 -0
  99. package/tooling/.automation/validation/history/2025-12-29_val_486b203c.json +41 -0
  100. package/tooling/.automation/validation/history/2025-12-29_val_49dc56cd.json +59 -0
  101. package/tooling/.automation/validation/history/2025-12-29_val_4d863d6d.json +32 -0
  102. package/tooling/.automation/validation/history/2025-12-29_val_5149a808.json +59 -0
  103. package/tooling/.automation/validation/history/2025-12-29_val_52e0bb43.json +32 -0
  104. package/tooling/.automation/validation/history/2025-12-29_val_585d6319.json +59 -0
  105. package/tooling/.automation/validation/history/2025-12-29_val_5b2d859a.json +32 -0
  106. package/tooling/.automation/validation/history/2025-12-29_val_635a7081.json +41 -0
  107. package/tooling/.automation/validation/history/2025-12-29_val_64df4905.json +32 -0
  108. package/tooling/.automation/validation/history/2025-12-29_val_70634cee.json +41 -0
  109. package/tooling/.automation/validation/history/2025-12-29_val_714553f9.json +32 -0
  110. package/tooling/.automation/validation/history/2025-12-29_val_7f7bfdbf.json +41 -0
  111. package/tooling/.automation/validation/history/2025-12-29_val_7faad91d.json +32 -0
  112. package/tooling/.automation/validation/history/2025-12-29_val_81821f8f.json +41 -0
  113. package/tooling/.automation/validation/history/2025-12-29_val_8249f3c9.json +32 -0
  114. package/tooling/.automation/validation/history/2025-12-29_val_8422b50f.json +41 -0
  115. package/tooling/.automation/validation/history/2025-12-29_val_8446c134.json +32 -0
  116. package/tooling/.automation/validation/history/2025-12-29_val_879f4e26.json +59 -0
  117. package/tooling/.automation/validation/history/2025-12-29_val_8b6d5bd7.json +32 -0
  118. package/tooling/.automation/validation/history/2025-12-29_val_8c5cd787.json +32 -0
  119. package/tooling/.automation/validation/history/2025-12-29_val_91d20bc7.json +32 -0
  120. package/tooling/.automation/validation/history/2025-12-29_val_958a12b7.json +41 -0
  121. package/tooling/.automation/validation/history/2025-12-29_val_95d91108.json +41 -0
  122. package/tooling/.automation/validation/history/2025-12-29_val_980dbb74.json +32 -0
  123. package/tooling/.automation/validation/history/2025-12-29_val_9e40c79b.json +32 -0
  124. package/tooling/.automation/validation/history/2025-12-29_val_9f499b7c.json +32 -0
  125. package/tooling/.automation/validation/history/2025-12-29_val_9f7c3b57.json +32 -0
  126. package/tooling/.automation/validation/history/2025-12-29_val_a30d5bd4.json +32 -0
  127. package/tooling/.automation/validation/history/2025-12-29_val_a6eb09c7.json +32 -0
  128. package/tooling/.automation/validation/history/2025-12-29_val_a86f7b83.json +41 -0
  129. package/tooling/.automation/validation/history/2025-12-29_val_ad5347e1.json +41 -0
  130. package/tooling/.automation/validation/history/2025-12-29_val_b0a5a993.json +32 -0
  131. package/tooling/.automation/validation/history/2025-12-29_val_bcb0192e.json +32 -0
  132. package/tooling/.automation/validation/history/2025-12-29_val_bf3c9aaa.json +32 -0
  133. package/tooling/.automation/validation/history/2025-12-29_val_c461ff88.json +32 -0
  134. package/tooling/.automation/validation/history/2025-12-29_val_c4f4e258.json +41 -0
  135. package/tooling/.automation/validation/history/2025-12-29_val_c7f0fa6d.json +41 -0
  136. package/tooling/.automation/validation/history/2025-12-29_val_c911b0e6.json +32 -0
  137. package/tooling/.automation/validation/history/2025-12-29_val_cc581964.json +32 -0
  138. package/tooling/.automation/validation/history/2025-12-29_val_cdd5a33b.json +32 -0
  139. package/tooling/.automation/validation/history/2025-12-29_val_cfd42495.json +32 -0
  140. package/tooling/.automation/validation/history/2025-12-29_val_d1c7a4ee.json +41 -0
  141. package/tooling/.automation/validation/history/2025-12-29_val_d2280d0e.json +32 -0
  142. package/tooling/.automation/validation/history/2025-12-29_val_d2a6ff69.json +32 -0
  143. package/tooling/.automation/validation/history/2025-12-29_val_d8c53ab2.json +59 -0
  144. package/tooling/.automation/validation/history/2025-12-29_val_d9c1247a.json +41 -0
  145. package/tooling/.automation/validation/history/2025-12-29_val_d9d58569.json +32 -0
  146. package/tooling/.automation/validation/history/2025-12-29_val_dabb4fd9.json +32 -0
  147. package/tooling/.automation/validation/history/2025-12-29_val_dd8fe359.json +32 -0
  148. package/tooling/.automation/validation/history/2025-12-29_val_decdffc9.json +32 -0
  149. package/tooling/.automation/validation/history/2025-12-29_val_e3a95476.json +59 -0
  150. package/tooling/.automation/validation/history/2025-12-29_val_e776dfca.json +32 -0
  151. package/tooling/.automation/validation/history/2025-12-29_val_ea70969f.json +59 -0
  152. package/tooling/.automation/validation/history/2025-12-29_val_ef41ea95.json +32 -0
  153. package/tooling/.automation/validation/history/2025-12-29_val_f384f9b1.json +32 -0
  154. package/tooling/.automation/validation/history/2025-12-29_val_f8adc38c.json +41 -0
  155. package/tooling/.automation/validation/history/2025-12-29_val_fa40b69e.json +32 -0
  156. package/tooling/.automation/validation/history/2025-12-29_val_fc538d54.json +41 -0
  157. package/tooling/.automation/validation/history/2025-12-29_val_fe814665.json +32 -0
  158. package/tooling/.automation/validation/history/2025-12-29_val_ffea4b12.json +32 -0
  159. package/tooling/.automation/validation/history/2025-12-30_val_02d001e5.json +59 -0
  160. package/tooling/.automation/validation/history/2025-12-30_val_0b8966dc.json +32 -0
  161. package/tooling/.automation/validation/history/2025-12-30_val_15455fbf.json +59 -0
  162. package/tooling/.automation/validation/history/2025-12-30_val_157e34b9.json +32 -0
  163. package/tooling/.automation/validation/history/2025-12-30_val_28d1d933.json +32 -0
  164. package/tooling/.automation/validation/history/2025-12-30_val_3442a52c.json +32 -0
  165. package/tooling/.automation/validation/history/2025-12-30_val_37f1ce1e.json +32 -0
  166. package/tooling/.automation/validation/history/2025-12-30_val_4f1d8a93.json +32 -0
  167. package/tooling/.automation/validation/history/2025-12-30_val_56ff1de3.json +32 -0
  168. package/tooling/.automation/validation/history/2025-12-30_val_664fd4e2.json +41 -0
  169. package/tooling/.automation/validation/history/2025-12-30_val_66afb0a7.json +32 -0
  170. package/tooling/.automation/validation/history/2025-12-30_val_7634663c.json +41 -0
  171. package/tooling/.automation/validation/history/2025-12-30_val_8ea830c3.json +41 -0
  172. package/tooling/.automation/validation/history/2025-12-30_val_998957c2.json +32 -0
  173. package/tooling/.automation/validation/history/2025-12-30_val_a52177db.json +32 -0
  174. package/tooling/.automation/validation/history/2025-12-30_val_a5b65a63.json +32 -0
  175. package/tooling/.automation/validation/history/2025-12-30_val_ae391d0e.json +32 -0
  176. package/tooling/.automation/validation/history/2025-12-30_val_c7895339.json +41 -0
  177. package/tooling/.automation/validation/history/2025-12-30_val_ca416593.json +41 -0
  178. package/tooling/.automation/validation/history/2025-12-30_val_cee19422.json +32 -0
  179. package/tooling/.automation/validation/history/2025-12-30_val_ddd4f4e6.json +32 -0
  180. package/tooling/.automation/validation/history/2025-12-30_val_f2e1394b.json +32 -0
  181. package/tooling/.automation/validation/history/2025-12-30_val_f4a7fa06.json +41 -0
  182. package/tooling/.automation/validation/history/2025-12-30_val_ffea3369.json +32 -0
  183. package/tooling/.automation/validation-config.yaml +103 -0
  184. package/tooling/completions/DevflowCompletion.ps1 +21 -21
  185. package/tooling/completions/_run-story +3 -3
  186. package/tooling/completions/run-story-completion.bash +8 -8
  187. package/tooling/docs/DOC-STANDARD.md +14 -14
  188. package/tooling/docs/templates/migration-spec.md +4 -4
  189. package/tooling/scripts/context_checkpoint.py +5 -15
  190. package/tooling/scripts/cost_dashboard.py +610 -13
  191. package/tooling/scripts/create-persona.py +1 -12
  192. package/tooling/scripts/create-persona.sh +44 -44
  193. package/tooling/scripts/lib/__init__.py +12 -1
  194. package/tooling/scripts/lib/agent_handoff.py +11 -2
  195. package/tooling/scripts/lib/agent_router.py +31 -10
  196. package/tooling/scripts/lib/colors.py +106 -0
  197. package/tooling/scripts/lib/context_monitor.py +766 -0
  198. package/tooling/scripts/lib/cost_config.py +229 -10
  199. package/tooling/scripts/lib/cost_display.py +20 -45
  200. package/tooling/scripts/lib/cost_tracker.py +462 -15
  201. package/tooling/scripts/lib/currency_converter.py +28 -5
  202. package/tooling/scripts/lib/pair_programming.py +102 -3
  203. package/tooling/scripts/lib/personality_system.py +949 -0
  204. package/tooling/scripts/lib/platform.py +55 -0
  205. package/tooling/scripts/lib/shared_memory.py +9 -3
  206. package/tooling/scripts/lib/swarm_orchestrator.py +514 -75
  207. package/tooling/scripts/lib/validation_loop.py +1014 -0
  208. package/tooling/scripts/memory_summarize.py +9 -2
  209. package/tooling/scripts/new-doc.py +2 -9
  210. package/tooling/scripts/personalize_agent.py +1 -12
  211. package/tooling/scripts/rollback-migration.sh +60 -60
  212. package/tooling/scripts/run-collab.ps1 +16 -16
  213. package/tooling/scripts/run-collab.py +88 -53
  214. package/tooling/scripts/run-collab.sh +4 -4
  215. package/tooling/scripts/run-story.py +278 -20
  216. package/tooling/scripts/run-story.sh +3 -3
  217. package/tooling/scripts/setup-checkpoint-service.py +2 -9
  218. package/tooling/scripts/tech-debt-tracker.py +1 -12
  219. package/tooling/scripts/test_adversarial_swarm.py +452 -0
  220. package/tooling/scripts/validate-overrides.py +1 -10
  221. package/tooling/scripts/validate-overrides.sh +40 -40
  222. package/tooling/scripts/validate_loop.py +162 -0
  223. package/tooling/scripts/validate_setup.py +2 -30
  224. 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")