anvil-dev-framework 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/README.md +719 -0
  2. package/VERSION +1 -0
  3. package/docs/ANVIL-REPO-IMPLEMENTATION-PLAN.md +441 -0
  4. package/docs/FIRST-SKILL-TUTORIAL.md +408 -0
  5. package/docs/INSTALLATION-RETRO-NOTES.md +458 -0
  6. package/docs/INSTALLATION.md +984 -0
  7. package/docs/anvil-hud.md +469 -0
  8. package/docs/anvil-init.md +255 -0
  9. package/docs/anvil-state.md +210 -0
  10. package/docs/boris-cherny-ralph-wiggum-insights.md +608 -0
  11. package/docs/command-reference.md +2022 -0
  12. package/docs/hooks-tts.md +368 -0
  13. package/docs/implementation-guide.md +810 -0
  14. package/docs/linear-github-integration.md +247 -0
  15. package/docs/local-issues.md +677 -0
  16. package/docs/patterns/README.md +419 -0
  17. package/docs/planning-responsibilities.md +139 -0
  18. package/docs/session-workflow.md +573 -0
  19. package/docs/simplification-plan-template.md +297 -0
  20. package/docs/simplification-principles.md +129 -0
  21. package/docs/specifications/CCS-RALPH-INTEGRATION-DESIGN.md +633 -0
  22. package/docs/specifications/CCS-RESEARCH-REPORT.md +169 -0
  23. package/docs/specifications/PLAN-ANV-verification-ralph-wiggum.md +403 -0
  24. package/docs/specifications/PLAN-parallel-tracks-anvil-memory-ccs.md +494 -0
  25. package/docs/specifications/SPEC-ANV-VRW/component-01-verify.md +208 -0
  26. package/docs/specifications/SPEC-ANV-VRW/component-02-stop-gate.md +226 -0
  27. package/docs/specifications/SPEC-ANV-VRW/component-03-posttooluse.md +209 -0
  28. package/docs/specifications/SPEC-ANV-VRW/component-04-ralph-wiggum.md +604 -0
  29. package/docs/specifications/SPEC-ANV-VRW/component-05-atomic-actions.md +311 -0
  30. package/docs/specifications/SPEC-ANV-VRW/component-06-verify-subagent.md +264 -0
  31. package/docs/specifications/SPEC-ANV-VRW/component-07-claude-md.md +363 -0
  32. package/docs/specifications/SPEC-ANV-VRW/index.md +182 -0
  33. package/docs/specifications/SPEC-ANV-anvil-memory.md +573 -0
  34. package/docs/specifications/SPEC-ANV-context-checkpoints.md +781 -0
  35. package/docs/specifications/SPEC-ANV-verification-ralph-wiggum.md +789 -0
  36. package/docs/sync.md +122 -0
  37. package/global/CLAUDE.md +140 -0
  38. package/global/agents/verify-app.md +164 -0
  39. package/global/commands/anvil-settings.md +527 -0
  40. package/global/commands/anvil-sync.md +121 -0
  41. package/global/commands/change.md +197 -0
  42. package/global/commands/clarify.md +252 -0
  43. package/global/commands/cleanup.md +292 -0
  44. package/global/commands/commit-push-pr.md +207 -0
  45. package/global/commands/decay-review.md +127 -0
  46. package/global/commands/discover.md +158 -0
  47. package/global/commands/doc-coverage.md +122 -0
  48. package/global/commands/evidence.md +307 -0
  49. package/global/commands/explore.md +121 -0
  50. package/global/commands/force-exit.md +135 -0
  51. package/global/commands/handoff.md +191 -0
  52. package/global/commands/healthcheck.md +302 -0
  53. package/global/commands/hud.md +84 -0
  54. package/global/commands/insights.md +319 -0
  55. package/global/commands/linear-setup.md +184 -0
  56. package/global/commands/lint-fix.md +198 -0
  57. package/global/commands/orient.md +510 -0
  58. package/global/commands/plan.md +228 -0
  59. package/global/commands/ralph.md +346 -0
  60. package/global/commands/ready.md +182 -0
  61. package/global/commands/release.md +305 -0
  62. package/global/commands/retro.md +96 -0
  63. package/global/commands/shard.md +166 -0
  64. package/global/commands/spec.md +227 -0
  65. package/global/commands/sprint.md +184 -0
  66. package/global/commands/tasks.md +228 -0
  67. package/global/commands/test-and-commit.md +151 -0
  68. package/global/commands/validate.md +132 -0
  69. package/global/commands/verify.md +251 -0
  70. package/global/commands/weekly-review.md +156 -0
  71. package/global/hooks/__pycache__/ralph_context_monitor.cpython-314.pyc +0 -0
  72. package/global/hooks/__pycache__/statusline_agent_sync.cpython-314.pyc +0 -0
  73. package/global/hooks/anvil_memory_observe.ts +322 -0
  74. package/global/hooks/anvil_memory_session.ts +166 -0
  75. package/global/hooks/anvil_memory_stop.ts +187 -0
  76. package/global/hooks/parse_transcript.py +116 -0
  77. package/global/hooks/post_merge_cleanup.sh +132 -0
  78. package/global/hooks/post_tool_format.sh +215 -0
  79. package/global/hooks/ralph_context_monitor.py +240 -0
  80. package/global/hooks/ralph_stop.sh +502 -0
  81. package/global/hooks/statusline.sh +1110 -0
  82. package/global/hooks/statusline_agent_sync.py +224 -0
  83. package/global/hooks/stop_gate.sh +250 -0
  84. package/global/lib/.claude/anvil-state.json +21 -0
  85. package/global/lib/__pycache__/agent_registry.cpython-314.pyc +0 -0
  86. package/global/lib/__pycache__/claim_service.cpython-314.pyc +0 -0
  87. package/global/lib/__pycache__/coderabbit_service.cpython-314.pyc +0 -0
  88. package/global/lib/__pycache__/config_service.cpython-314.pyc +0 -0
  89. package/global/lib/__pycache__/coordination_service.cpython-314.pyc +0 -0
  90. package/global/lib/__pycache__/doc_coverage_service.cpython-314.pyc +0 -0
  91. package/global/lib/__pycache__/gate_logger.cpython-314.pyc +0 -0
  92. package/global/lib/__pycache__/github_service.cpython-314.pyc +0 -0
  93. package/global/lib/__pycache__/hygiene_service.cpython-314.pyc +0 -0
  94. package/global/lib/__pycache__/issue_models.cpython-314.pyc +0 -0
  95. package/global/lib/__pycache__/issue_provider.cpython-314.pyc +0 -0
  96. package/global/lib/__pycache__/linear_data_service.cpython-314.pyc +0 -0
  97. package/global/lib/__pycache__/linear_provider.cpython-314.pyc +0 -0
  98. package/global/lib/__pycache__/local_provider.cpython-314.pyc +0 -0
  99. package/global/lib/__pycache__/quality_service.cpython-314.pyc +0 -0
  100. package/global/lib/__pycache__/ralph_state.cpython-314.pyc +0 -0
  101. package/global/lib/__pycache__/state_manager.cpython-314.pyc +0 -0
  102. package/global/lib/__pycache__/transcript_parser.cpython-314.pyc +0 -0
  103. package/global/lib/__pycache__/verification_runner.cpython-314.pyc +0 -0
  104. package/global/lib/__pycache__/verify_iteration.cpython-314.pyc +0 -0
  105. package/global/lib/__pycache__/verify_subagent.cpython-314.pyc +0 -0
  106. package/global/lib/agent_registry.py +995 -0
  107. package/global/lib/anvil-state.sh +435 -0
  108. package/global/lib/claim_service.py +515 -0
  109. package/global/lib/coderabbit_service.py +314 -0
  110. package/global/lib/config_service.py +423 -0
  111. package/global/lib/coordination_service.py +331 -0
  112. package/global/lib/doc_coverage_service.py +1305 -0
  113. package/global/lib/gate_logger.py +316 -0
  114. package/global/lib/github_service.py +310 -0
  115. package/global/lib/handoff_generator.py +775 -0
  116. package/global/lib/hygiene_service.py +712 -0
  117. package/global/lib/issue_models.py +257 -0
  118. package/global/lib/issue_provider.py +339 -0
  119. package/global/lib/linear_data_service.py +210 -0
  120. package/global/lib/linear_provider.py +987 -0
  121. package/global/lib/linear_provider.py.backup +671 -0
  122. package/global/lib/local_provider.py +486 -0
  123. package/global/lib/orient_fast.py +457 -0
  124. package/global/lib/quality_service.py +470 -0
  125. package/global/lib/ralph_prompt_generator.py +563 -0
  126. package/global/lib/ralph_state.py +1202 -0
  127. package/global/lib/state_manager.py +417 -0
  128. package/global/lib/transcript_parser.py +597 -0
  129. package/global/lib/verification_runner.py +557 -0
  130. package/global/lib/verify_iteration.py +490 -0
  131. package/global/lib/verify_subagent.py +250 -0
  132. package/global/skills/README.md +155 -0
  133. package/global/skills/quality-gates/SKILL.md +252 -0
  134. package/global/skills/skill-template/SKILL.md +109 -0
  135. package/global/skills/testing-strategies/SKILL.md +337 -0
  136. package/global/templates/CHANGE-template.md +105 -0
  137. package/global/templates/HANDOFF-template.md +63 -0
  138. package/global/templates/PLAN-template.md +111 -0
  139. package/global/templates/SPEC-template.md +93 -0
  140. package/global/templates/ralph/PROMPT.md.template +89 -0
  141. package/global/templates/ralph/fix_plan.md.template +31 -0
  142. package/global/templates/ralph/progress.txt.template +23 -0
  143. package/global/tests/__pycache__/test_doc_coverage.cpython-314.pyc +0 -0
  144. package/global/tests/test_doc_coverage.py +520 -0
  145. package/global/tests/test_issue_models.py +299 -0
  146. package/global/tests/test_local_provider.py +323 -0
  147. package/global/tools/README.md +178 -0
  148. package/global/tools/__pycache__/anvil-hud.cpython-314.pyc +0 -0
  149. package/global/tools/anvil-hud.py +3622 -0
  150. package/global/tools/anvil-hud.py.bak +3318 -0
  151. package/global/tools/anvil-issue.py +432 -0
  152. package/global/tools/anvil-memory/CLAUDE.md +49 -0
  153. package/global/tools/anvil-memory/README.md +42 -0
  154. package/global/tools/anvil-memory/bun.lock +25 -0
  155. package/global/tools/anvil-memory/bunfig.toml +9 -0
  156. package/global/tools/anvil-memory/package.json +23 -0
  157. package/global/tools/anvil-memory/src/__tests__/ccs/context-monitor.test.ts +535 -0
  158. package/global/tools/anvil-memory/src/__tests__/ccs/edge-cases.test.ts +645 -0
  159. package/global/tools/anvil-memory/src/__tests__/ccs/fixtures.ts +363 -0
  160. package/global/tools/anvil-memory/src/__tests__/ccs/index.ts +8 -0
  161. package/global/tools/anvil-memory/src/__tests__/ccs/integration.test.ts +417 -0
  162. package/global/tools/anvil-memory/src/__tests__/ccs/prompt-generator.test.ts +571 -0
  163. package/global/tools/anvil-memory/src/__tests__/ccs/ralph-stop.test.ts +440 -0
  164. package/global/tools/anvil-memory/src/__tests__/ccs/test-utils.ts +252 -0
  165. package/global/tools/anvil-memory/src/__tests__/commands.test.ts +657 -0
  166. package/global/tools/anvil-memory/src/__tests__/db.test.ts +641 -0
  167. package/global/tools/anvil-memory/src/__tests__/hooks.test.ts +272 -0
  168. package/global/tools/anvil-memory/src/__tests__/performance.test.ts +427 -0
  169. package/global/tools/anvil-memory/src/__tests__/test-utils.ts +113 -0
  170. package/global/tools/anvil-memory/src/commands/checkpoint.ts +197 -0
  171. package/global/tools/anvil-memory/src/commands/get.ts +115 -0
  172. package/global/tools/anvil-memory/src/commands/init.ts +94 -0
  173. package/global/tools/anvil-memory/src/commands/observe.ts +163 -0
  174. package/global/tools/anvil-memory/src/commands/search.ts +112 -0
  175. package/global/tools/anvil-memory/src/db.ts +638 -0
  176. package/global/tools/anvil-memory/src/index.ts +205 -0
  177. package/global/tools/anvil-memory/src/types.ts +122 -0
  178. package/global/tools/anvil-memory/tsconfig.json +29 -0
  179. package/global/tools/ralph-loop.sh +359 -0
  180. package/package.json +45 -0
  181. package/scripts/anvil +822 -0
  182. package/scripts/extract_patterns.py +222 -0
  183. package/scripts/init-project.sh +541 -0
  184. package/scripts/install.sh +229 -0
  185. package/scripts/postinstall.js +41 -0
  186. package/scripts/rollback.sh +188 -0
  187. package/scripts/sync.sh +623 -0
  188. package/scripts/test-statusline.sh +248 -0
  189. package/scripts/update_claude_md.py +224 -0
  190. package/scripts/verify.sh +255 -0
@@ -0,0 +1,423 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ config_service.py - Configuration management for HUD (ANV-116-118)
4
+
5
+ Manages HUD configuration including budget thresholds, refresh intervals,
6
+ and feature toggles. Supports both global defaults and per-project overrides.
7
+
8
+ Usage:
9
+ from config_service import get_config
10
+
11
+ config = get_config()
12
+ print(config.budget.agent_warn_threshold) # $5.00
13
+
14
+ Configuration files:
15
+ - ~/.anvil/hud-config.yaml (global defaults)
16
+ - .anvil/hud-config.yaml (project overrides)
17
+ """
18
+
19
+ import os
20
+ from dataclasses import dataclass, field
21
+ from pathlib import Path
22
+ from typing import Any, Dict, List, Literal, Optional
23
+
24
+ try:
25
+ import yaml
26
+ HAS_YAML = True
27
+ except ImportError:
28
+ HAS_YAML = False
29
+
30
+
31
+ @dataclass
32
+ class BudgetConfig:
33
+ """Budget threshold configuration."""
34
+ # Billing model: "api" (pay-per-use) or "subscription" (Pro/Max flat rate)
35
+ # When "subscription", cost alerts are disabled (costs shown for info only)
36
+ billing_model: Literal["api", "subscription"] = "api"
37
+ agent_warn_threshold: float = 5.0 # Warn when agent exceeds $5
38
+ agent_crit_threshold: float = 10.0 # Critical when agent exceeds $10
39
+ total_warn_threshold: float = 15.0 # Warn when total exceeds $15
40
+ total_crit_threshold: float = 25.0 # Critical when total exceeds $25
41
+ daily_budget: Optional[float] = None # Optional daily budget limit
42
+ weekly_budget: Optional[float] = None # Optional weekly budget limit
43
+
44
+
45
+ @dataclass
46
+ class ContextConfig:
47
+ """Context usage threshold configuration."""
48
+ warn_threshold_pct: int = 80 # Warn at 80% context
49
+ crit_threshold_pct: int = 90 # Critical at 90% context
50
+ compaction_reminder: bool = True # Show compaction reminders
51
+ estimated_turns_warn: int = 10 # Warn when <10 turns estimated
52
+
53
+
54
+ @dataclass
55
+ class CoordinationConfig:
56
+ """Coordination/branch age configuration."""
57
+ stale_branch_hours: int = 4 # Branch stale after 4 hours
58
+ conflict_detection: bool = True # Enable conflict detection
59
+ lock_timeout_minutes: int = 30 # File lock timeout
60
+
61
+
62
+ @dataclass
63
+ class RefreshConfig:
64
+ """Refresh interval configuration."""
65
+ agent_refresh_seconds: float = 2.0 # Agent list refresh
66
+ quality_cache_seconds: float = 30.0 # Quality check cache TTL
67
+ github_cache_seconds: float = 30.0 # GitHub status cache TTL
68
+ coderabbit_cache_seconds: float = 60.0 # CodeRabbit cache TTL
69
+
70
+
71
+ @dataclass
72
+ class DisplayConfig:
73
+ """Display/UI configuration."""
74
+ show_cost_panel: bool = True
75
+ show_task_panel: bool = True
76
+ show_quality_panel: bool = True
77
+ show_coordination_panel: bool = True
78
+ truncate_agent_id_length: int = 16
79
+ max_issues_shown: int = 5
80
+ max_locks_shown: int = 5
81
+ max_conflicts_shown: int = 3
82
+
83
+
84
+ @dataclass
85
+ class IntegrationConfig:
86
+ """External integration configuration."""
87
+ enable_linear: bool = True
88
+ enable_github: bool = True
89
+ enable_coderabbit: bool = True
90
+ enable_quality_checks: bool = True
91
+
92
+
93
+ @dataclass
94
+ class HUDModeConfig:
95
+ """HUD v3 mode configuration (ANV-128)."""
96
+ default_mode: Literal["focused", "full"] = "focused" # Default to Focused Mode
97
+ remember_last_tab: bool = True # Remember tab across sessions
98
+ show_tab_bar: bool = True # Show tab bar in Focused Mode
99
+
100
+
101
+ @dataclass
102
+ class HUDConfig:
103
+ """Main HUD configuration."""
104
+ budget: BudgetConfig = field(default_factory=BudgetConfig)
105
+ context: ContextConfig = field(default_factory=ContextConfig)
106
+ coordination: CoordinationConfig = field(default_factory=CoordinationConfig)
107
+ refresh: RefreshConfig = field(default_factory=RefreshConfig)
108
+ display: DisplayConfig = field(default_factory=DisplayConfig)
109
+ integrations: IntegrationConfig = field(default_factory=IntegrationConfig)
110
+ hud: HUDModeConfig = field(default_factory=HUDModeConfig) # HUD v3 (ANV-128)
111
+
112
+ # Feature flags
113
+ demo_mode: bool = False
114
+ debug_mode: bool = False
115
+ notifications_enabled: bool = True
116
+ sound_enabled: bool = False
117
+
118
+
119
+ def _deep_update(base: Dict[str, Any], updates: Dict[str, Any]) -> Dict[str, Any]:
120
+ """Recursively update a dict with another dict."""
121
+ result = dict(base)
122
+ for key, value in updates.items():
123
+ if key in result and isinstance(result[key], dict) and isinstance(value, dict):
124
+ result[key] = _deep_update(result[key], value)
125
+ else:
126
+ result[key] = value
127
+ return result
128
+
129
+
130
+ def _dict_to_config(data: Dict[str, Any]) -> HUDConfig:
131
+ """Convert a dictionary to HUDConfig."""
132
+ config = HUDConfig()
133
+
134
+ if "budget" in data:
135
+ for key, value in data["budget"].items():
136
+ if hasattr(config.budget, key):
137
+ setattr(config.budget, key, value)
138
+
139
+ if "context" in data:
140
+ for key, value in data["context"].items():
141
+ if hasattr(config.context, key):
142
+ setattr(config.context, key, value)
143
+
144
+ if "coordination" in data:
145
+ for key, value in data["coordination"].items():
146
+ if hasattr(config.coordination, key):
147
+ setattr(config.coordination, key, value)
148
+
149
+ if "refresh" in data:
150
+ for key, value in data["refresh"].items():
151
+ if hasattr(config.refresh, key):
152
+ setattr(config.refresh, key, value)
153
+
154
+ if "display" in data:
155
+ for key, value in data["display"].items():
156
+ if hasattr(config.display, key):
157
+ setattr(config.display, key, value)
158
+
159
+ if "integrations" in data:
160
+ for key, value in data["integrations"].items():
161
+ if hasattr(config.integrations, key):
162
+ setattr(config.integrations, key, value)
163
+
164
+ # HUD v3 mode config (ANV-128)
165
+ if "hud" in data:
166
+ for key, value in data["hud"].items():
167
+ if hasattr(config.hud, key):
168
+ setattr(config.hud, key, value)
169
+
170
+ # Top-level flags
171
+ for key in ["demo_mode", "debug_mode", "notifications_enabled", "sound_enabled"]:
172
+ if key in data:
173
+ setattr(config, key, data[key])
174
+
175
+ return config
176
+
177
+
178
+ class ConfigService:
179
+ """Service for loading and managing HUD configuration (ANV-116-118)."""
180
+
181
+ def __init__(self, project_path: Optional[str] = None):
182
+ """Initialize the config service.
183
+
184
+ Args:
185
+ project_path: Optional project path for project-specific config
186
+ """
187
+ self.project_path = Path(project_path) if project_path else None
188
+ self._config: Optional[HUDConfig] = None
189
+ self._config_sources: List[str] = []
190
+
191
+ @property
192
+ def config(self) -> HUDConfig:
193
+ """Get the current configuration, loading if needed."""
194
+ if self._config is None:
195
+ self._config = self._load_config()
196
+ return self._config
197
+
198
+ def reload(self) -> HUDConfig:
199
+ """Force reload configuration from disk."""
200
+ self._config = None
201
+ return self.config
202
+
203
+ def get_config_sources(self) -> List[str]:
204
+ """Get list of config files that were loaded."""
205
+ return self._config_sources
206
+
207
+ def _load_config(self) -> HUDConfig:
208
+ """Load configuration from files, merging global and project configs."""
209
+ self._config_sources = []
210
+ merged_data: Dict[str, Any] = {}
211
+
212
+ # Load global config from ~/.anvil/hud-config.yaml
213
+ global_config_path = Path.home() / ".anvil" / "hud-config.yaml"
214
+ if global_config_path.exists():
215
+ global_data = self._load_yaml_file(global_config_path)
216
+ if global_data:
217
+ merged_data = _deep_update(merged_data, global_data)
218
+ self._config_sources.append(str(global_config_path))
219
+
220
+ # Load project config from .anvil/hud-config.yaml
221
+ if self.project_path:
222
+ project_config_path = self.project_path / ".anvil" / "hud-config.yaml"
223
+ if project_config_path.exists():
224
+ project_data = self._load_yaml_file(project_config_path)
225
+ if project_data:
226
+ merged_data = _deep_update(merged_data, project_data)
227
+ self._config_sources.append(str(project_config_path))
228
+
229
+ # Also check for env var overrides
230
+ merged_data = self._apply_env_overrides(merged_data)
231
+
232
+ return _dict_to_config(merged_data)
233
+
234
+ def _load_yaml_file(self, path: Path) -> Optional[Dict[str, Any]]:
235
+ """Load a YAML config file."""
236
+ if not HAS_YAML:
237
+ return None
238
+
239
+ try:
240
+ with open(path, "r") as f:
241
+ return yaml.safe_load(f) or {}
242
+ except Exception:
243
+ return None
244
+
245
+ def _apply_env_overrides(self, data: Dict[str, Any]) -> Dict[str, Any]:
246
+ """Apply environment variable overrides to config."""
247
+ # Support env var overrides for common settings
248
+ env_mappings = {
249
+ "ANVIL_HUD_DEMO": ("demo_mode", lambda x: x.lower() == "true"),
250
+ "ANVIL_HUD_DEBUG": ("debug_mode", lambda x: x.lower() == "true"),
251
+ # Billing model: "api" (pay-per-use) or "subscription" (Pro/Max)
252
+ "ANVIL_BILLING_MODEL": ("budget.billing_model", lambda x: x.lower() if x.lower() in ["api", "subscription"] else "api"),
253
+ "ANVIL_BUDGET_WARN": ("budget.agent_warn_threshold", float),
254
+ "ANVIL_BUDGET_CRIT": ("budget.agent_crit_threshold", float),
255
+ "ANVIL_CONTEXT_WARN": ("context.warn_threshold_pct", int),
256
+ "ANVIL_CONTEXT_CRIT": ("context.crit_threshold_pct", int),
257
+ "ANVIL_STALE_HOURS": ("coordination.stale_branch_hours", int),
258
+ "ANVIL_REFRESH_SECONDS": ("refresh.agent_refresh_seconds", float),
259
+ # HUD v3 mode (ANV-128)
260
+ "ANVIL_HUD_MODE": ("hud.default_mode", lambda x: x.lower() if x.lower() in ["focused", "full"] else "focused"),
261
+ }
262
+
263
+ for env_var, (config_path, converter) in env_mappings.items():
264
+ value = os.environ.get(env_var)
265
+ if value is not None:
266
+ try:
267
+ converted = converter(value)
268
+ # Set nested value
269
+ parts = config_path.split(".")
270
+ if len(parts) == 1:
271
+ data[parts[0]] = converted
272
+ else:
273
+ if parts[0] not in data:
274
+ data[parts[0]] = {}
275
+ data[parts[0]][parts[1]] = converted
276
+ except (ValueError, TypeError):
277
+ pass
278
+
279
+ return data
280
+
281
+ def save_global_config(self, config: HUDConfig) -> bool:
282
+ """Save configuration to global config file."""
283
+ if not HAS_YAML:
284
+ return False
285
+
286
+ config_path = Path.home() / ".anvil" / "hud-config.yaml"
287
+ return self._save_config(config, config_path)
288
+
289
+ def save_project_config(self, config: HUDConfig) -> bool:
290
+ """Save configuration to project config file."""
291
+ if not HAS_YAML or not self.project_path:
292
+ return False
293
+
294
+ config_path = self.project_path / ".anvil" / "hud-config.yaml"
295
+ return self._save_config(config, config_path)
296
+
297
+ def _save_config(self, config: HUDConfig, path: Path) -> bool:
298
+ """Save configuration to a file."""
299
+ try:
300
+ # Ensure directory exists
301
+ path.parent.mkdir(parents=True, exist_ok=True)
302
+
303
+ # Convert config to dict
304
+ data = self._config_to_dict(config)
305
+
306
+ with open(path, "w") as f:
307
+ yaml.dump(data, f, default_flow_style=False, sort_keys=False)
308
+
309
+ return True
310
+ except Exception:
311
+ return False
312
+
313
+ def _config_to_dict(self, config: HUDConfig) -> Dict[str, Any]:
314
+ """Convert HUDConfig to a dictionary for saving."""
315
+ return {
316
+ "budget": {
317
+ "billing_model": config.budget.billing_model, # "api" or "subscription"
318
+ "agent_warn_threshold": config.budget.agent_warn_threshold,
319
+ "agent_crit_threshold": config.budget.agent_crit_threshold,
320
+ "total_warn_threshold": config.budget.total_warn_threshold,
321
+ "total_crit_threshold": config.budget.total_crit_threshold,
322
+ "daily_budget": config.budget.daily_budget,
323
+ "weekly_budget": config.budget.weekly_budget,
324
+ },
325
+ "context": {
326
+ "warn_threshold_pct": config.context.warn_threshold_pct,
327
+ "crit_threshold_pct": config.context.crit_threshold_pct,
328
+ "compaction_reminder": config.context.compaction_reminder,
329
+ "estimated_turns_warn": config.context.estimated_turns_warn,
330
+ },
331
+ "coordination": {
332
+ "stale_branch_hours": config.coordination.stale_branch_hours,
333
+ "conflict_detection": config.coordination.conflict_detection,
334
+ "lock_timeout_minutes": config.coordination.lock_timeout_minutes,
335
+ },
336
+ "refresh": {
337
+ "agent_refresh_seconds": config.refresh.agent_refresh_seconds,
338
+ "quality_cache_seconds": config.refresh.quality_cache_seconds,
339
+ "github_cache_seconds": config.refresh.github_cache_seconds,
340
+ "coderabbit_cache_seconds": config.refresh.coderabbit_cache_seconds,
341
+ },
342
+ "display": {
343
+ "show_cost_panel": config.display.show_cost_panel,
344
+ "show_task_panel": config.display.show_task_panel,
345
+ "show_quality_panel": config.display.show_quality_panel,
346
+ "show_coordination_panel": config.display.show_coordination_panel,
347
+ "truncate_agent_id_length": config.display.truncate_agent_id_length,
348
+ "max_issues_shown": config.display.max_issues_shown,
349
+ "max_locks_shown": config.display.max_locks_shown,
350
+ "max_conflicts_shown": config.display.max_conflicts_shown,
351
+ },
352
+ "integrations": {
353
+ "enable_linear": config.integrations.enable_linear,
354
+ "enable_github": config.integrations.enable_github,
355
+ "enable_coderabbit": config.integrations.enable_coderabbit,
356
+ "enable_quality_checks": config.integrations.enable_quality_checks,
357
+ },
358
+ # HUD v3 mode config (ANV-128)
359
+ "hud": {
360
+ "default_mode": config.hud.default_mode,
361
+ "remember_last_tab": config.hud.remember_last_tab,
362
+ "show_tab_bar": config.hud.show_tab_bar,
363
+ },
364
+ "demo_mode": config.demo_mode,
365
+ "debug_mode": config.debug_mode,
366
+ "notifications_enabled": config.notifications_enabled,
367
+ "sound_enabled": config.sound_enabled,
368
+ }
369
+
370
+
371
+ # Singleton instance
372
+ _service: Optional[ConfigService] = None
373
+
374
+
375
+ def get_config_service(project_path: Optional[str] = None) -> ConfigService:
376
+ """Get singleton config service instance."""
377
+ global _service
378
+ # Normalize paths for comparison (handles relative/absolute, trailing slashes, symlinks)
379
+ norm_project = Path(project_path).resolve() if project_path else None
380
+ norm_existing = _service.project_path.resolve() if _service and _service.project_path else None
381
+
382
+ if _service is None or (norm_project and norm_project != norm_existing):
383
+ _service = ConfigService(project_path)
384
+ return _service
385
+
386
+
387
+ def get_config(project_path: Optional[str] = None) -> HUDConfig:
388
+ """Convenience function to get config directly."""
389
+ return get_config_service(project_path).config
390
+
391
+
392
+ def generate_default_config() -> str:
393
+ """Generate a default config file content."""
394
+ if not HAS_YAML:
395
+ return "# YAML not available. Install pyyaml to use config files."
396
+
397
+ config = HUDConfig()
398
+ service = ConfigService()
399
+ data = service._config_to_dict(config)
400
+
401
+ header = """# Anvil HUD Configuration
402
+ # This file configures the Anvil HUD multi-agent dashboard.
403
+ #
404
+ # Location:
405
+ # Global: ~/.anvil/hud-config.yaml
406
+ # Project: .anvil/hud-config.yaml (overrides global)
407
+ #
408
+ # Environment variables can override settings:
409
+ # ANVIL_HUD_DEMO=true - Enable demo mode
410
+ # ANVIL_HUD_DEBUG=true - Enable debug mode
411
+ # ANVIL_HUD_MODE=focused - Default mode: focused or full (ANV-128)
412
+ # ANVIL_BUDGET_WARN=5.0 - Agent budget warning threshold
413
+ # ANVIL_CONTEXT_WARN=80 - Context usage warning %
414
+ # ANVIL_STALE_HOURS=4 - Stale branch threshold hours
415
+ # ANVIL_REFRESH_SECONDS=2.0 - Agent refresh interval
416
+
417
+ """
418
+ return header + yaml.dump(data, default_flow_style=False, sort_keys=False)
419
+
420
+
421
+ if __name__ == "__main__":
422
+ # Generate and print default config
423
+ print(generate_default_config())