anvil-dev-framework 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +719 -0
- package/VERSION +1 -0
- package/docs/ANVIL-REPO-IMPLEMENTATION-PLAN.md +441 -0
- package/docs/FIRST-SKILL-TUTORIAL.md +408 -0
- package/docs/INSTALLATION-RETRO-NOTES.md +458 -0
- package/docs/INSTALLATION.md +984 -0
- package/docs/anvil-hud.md +469 -0
- package/docs/anvil-init.md +255 -0
- package/docs/anvil-state.md +210 -0
- package/docs/boris-cherny-ralph-wiggum-insights.md +608 -0
- package/docs/command-reference.md +2022 -0
- package/docs/hooks-tts.md +368 -0
- package/docs/implementation-guide.md +810 -0
- package/docs/linear-github-integration.md +247 -0
- package/docs/local-issues.md +677 -0
- package/docs/patterns/README.md +419 -0
- package/docs/planning-responsibilities.md +139 -0
- package/docs/session-workflow.md +573 -0
- package/docs/simplification-plan-template.md +297 -0
- package/docs/simplification-principles.md +129 -0
- package/docs/specifications/CCS-RALPH-INTEGRATION-DESIGN.md +633 -0
- package/docs/specifications/CCS-RESEARCH-REPORT.md +169 -0
- package/docs/specifications/PLAN-ANV-verification-ralph-wiggum.md +403 -0
- package/docs/specifications/PLAN-parallel-tracks-anvil-memory-ccs.md +494 -0
- package/docs/specifications/SPEC-ANV-VRW/component-01-verify.md +208 -0
- package/docs/specifications/SPEC-ANV-VRW/component-02-stop-gate.md +226 -0
- package/docs/specifications/SPEC-ANV-VRW/component-03-posttooluse.md +209 -0
- package/docs/specifications/SPEC-ANV-VRW/component-04-ralph-wiggum.md +604 -0
- package/docs/specifications/SPEC-ANV-VRW/component-05-atomic-actions.md +311 -0
- package/docs/specifications/SPEC-ANV-VRW/component-06-verify-subagent.md +264 -0
- package/docs/specifications/SPEC-ANV-VRW/component-07-claude-md.md +363 -0
- package/docs/specifications/SPEC-ANV-VRW/index.md +182 -0
- package/docs/specifications/SPEC-ANV-anvil-memory.md +573 -0
- package/docs/specifications/SPEC-ANV-context-checkpoints.md +781 -0
- package/docs/specifications/SPEC-ANV-verification-ralph-wiggum.md +789 -0
- package/docs/sync.md +122 -0
- package/global/CLAUDE.md +140 -0
- package/global/agents/verify-app.md +164 -0
- package/global/commands/anvil-settings.md +527 -0
- package/global/commands/anvil-sync.md +121 -0
- package/global/commands/change.md +197 -0
- package/global/commands/clarify.md +252 -0
- package/global/commands/cleanup.md +292 -0
- package/global/commands/commit-push-pr.md +207 -0
- package/global/commands/decay-review.md +127 -0
- package/global/commands/discover.md +158 -0
- package/global/commands/doc-coverage.md +122 -0
- package/global/commands/evidence.md +307 -0
- package/global/commands/explore.md +121 -0
- package/global/commands/force-exit.md +135 -0
- package/global/commands/handoff.md +191 -0
- package/global/commands/healthcheck.md +302 -0
- package/global/commands/hud.md +84 -0
- package/global/commands/insights.md +319 -0
- package/global/commands/linear-setup.md +184 -0
- package/global/commands/lint-fix.md +198 -0
- package/global/commands/orient.md +510 -0
- package/global/commands/plan.md +228 -0
- package/global/commands/ralph.md +346 -0
- package/global/commands/ready.md +182 -0
- package/global/commands/release.md +305 -0
- package/global/commands/retro.md +96 -0
- package/global/commands/shard.md +166 -0
- package/global/commands/spec.md +227 -0
- package/global/commands/sprint.md +184 -0
- package/global/commands/tasks.md +228 -0
- package/global/commands/test-and-commit.md +151 -0
- package/global/commands/validate.md +132 -0
- package/global/commands/verify.md +251 -0
- package/global/commands/weekly-review.md +156 -0
- package/global/hooks/__pycache__/ralph_context_monitor.cpython-314.pyc +0 -0
- package/global/hooks/__pycache__/statusline_agent_sync.cpython-314.pyc +0 -0
- package/global/hooks/anvil_memory_observe.ts +322 -0
- package/global/hooks/anvil_memory_session.ts +166 -0
- package/global/hooks/anvil_memory_stop.ts +187 -0
- package/global/hooks/parse_transcript.py +116 -0
- package/global/hooks/post_merge_cleanup.sh +132 -0
- package/global/hooks/post_tool_format.sh +215 -0
- package/global/hooks/ralph_context_monitor.py +240 -0
- package/global/hooks/ralph_stop.sh +502 -0
- package/global/hooks/statusline.sh +1110 -0
- package/global/hooks/statusline_agent_sync.py +224 -0
- package/global/hooks/stop_gate.sh +250 -0
- package/global/lib/.claude/anvil-state.json +21 -0
- package/global/lib/__pycache__/agent_registry.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/claim_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/coderabbit_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/config_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/coordination_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/doc_coverage_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/gate_logger.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/github_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/hygiene_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/issue_models.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/issue_provider.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/linear_data_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/linear_provider.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/local_provider.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/quality_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/ralph_state.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/state_manager.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/transcript_parser.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/verification_runner.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/verify_iteration.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/verify_subagent.cpython-314.pyc +0 -0
- package/global/lib/agent_registry.py +995 -0
- package/global/lib/anvil-state.sh +435 -0
- package/global/lib/claim_service.py +515 -0
- package/global/lib/coderabbit_service.py +314 -0
- package/global/lib/config_service.py +423 -0
- package/global/lib/coordination_service.py +331 -0
- package/global/lib/doc_coverage_service.py +1305 -0
- package/global/lib/gate_logger.py +316 -0
- package/global/lib/github_service.py +310 -0
- package/global/lib/handoff_generator.py +775 -0
- package/global/lib/hygiene_service.py +712 -0
- package/global/lib/issue_models.py +257 -0
- package/global/lib/issue_provider.py +339 -0
- package/global/lib/linear_data_service.py +210 -0
- package/global/lib/linear_provider.py +987 -0
- package/global/lib/linear_provider.py.backup +671 -0
- package/global/lib/local_provider.py +486 -0
- package/global/lib/orient_fast.py +457 -0
- package/global/lib/quality_service.py +470 -0
- package/global/lib/ralph_prompt_generator.py +563 -0
- package/global/lib/ralph_state.py +1202 -0
- package/global/lib/state_manager.py +417 -0
- package/global/lib/transcript_parser.py +597 -0
- package/global/lib/verification_runner.py +557 -0
- package/global/lib/verify_iteration.py +490 -0
- package/global/lib/verify_subagent.py +250 -0
- package/global/skills/README.md +155 -0
- package/global/skills/quality-gates/SKILL.md +252 -0
- package/global/skills/skill-template/SKILL.md +109 -0
- package/global/skills/testing-strategies/SKILL.md +337 -0
- package/global/templates/CHANGE-template.md +105 -0
- package/global/templates/HANDOFF-template.md +63 -0
- package/global/templates/PLAN-template.md +111 -0
- package/global/templates/SPEC-template.md +93 -0
- package/global/templates/ralph/PROMPT.md.template +89 -0
- package/global/templates/ralph/fix_plan.md.template +31 -0
- package/global/templates/ralph/progress.txt.template +23 -0
- package/global/tests/__pycache__/test_doc_coverage.cpython-314.pyc +0 -0
- package/global/tests/test_doc_coverage.py +520 -0
- package/global/tests/test_issue_models.py +299 -0
- package/global/tests/test_local_provider.py +323 -0
- package/global/tools/README.md +178 -0
- package/global/tools/__pycache__/anvil-hud.cpython-314.pyc +0 -0
- package/global/tools/anvil-hud.py +3622 -0
- package/global/tools/anvil-hud.py.bak +3318 -0
- package/global/tools/anvil-issue.py +432 -0
- package/global/tools/anvil-memory/CLAUDE.md +49 -0
- package/global/tools/anvil-memory/README.md +42 -0
- package/global/tools/anvil-memory/bun.lock +25 -0
- package/global/tools/anvil-memory/bunfig.toml +9 -0
- package/global/tools/anvil-memory/package.json +23 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/context-monitor.test.ts +535 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/edge-cases.test.ts +645 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/fixtures.ts +363 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/index.ts +8 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/integration.test.ts +417 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/prompt-generator.test.ts +571 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/ralph-stop.test.ts +440 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/test-utils.ts +252 -0
- package/global/tools/anvil-memory/src/__tests__/commands.test.ts +657 -0
- package/global/tools/anvil-memory/src/__tests__/db.test.ts +641 -0
- package/global/tools/anvil-memory/src/__tests__/hooks.test.ts +272 -0
- package/global/tools/anvil-memory/src/__tests__/performance.test.ts +427 -0
- package/global/tools/anvil-memory/src/__tests__/test-utils.ts +113 -0
- package/global/tools/anvil-memory/src/commands/checkpoint.ts +197 -0
- package/global/tools/anvil-memory/src/commands/get.ts +115 -0
- package/global/tools/anvil-memory/src/commands/init.ts +94 -0
- package/global/tools/anvil-memory/src/commands/observe.ts +163 -0
- package/global/tools/anvil-memory/src/commands/search.ts +112 -0
- package/global/tools/anvil-memory/src/db.ts +638 -0
- package/global/tools/anvil-memory/src/index.ts +205 -0
- package/global/tools/anvil-memory/src/types.ts +122 -0
- package/global/tools/anvil-memory/tsconfig.json +29 -0
- package/global/tools/ralph-loop.sh +359 -0
- package/package.json +45 -0
- package/scripts/anvil +822 -0
- package/scripts/extract_patterns.py +222 -0
- package/scripts/init-project.sh +541 -0
- package/scripts/install.sh +229 -0
- package/scripts/postinstall.js +41 -0
- package/scripts/rollback.sh +188 -0
- package/scripts/sync.sh +623 -0
- package/scripts/test-statusline.sh +248 -0
- package/scripts/update_claude_md.py +224 -0
- package/scripts/verify.sh +255 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
gate_logger.py - Stop Gate Event Logging (ANV-155)
|
|
4
|
+
|
|
5
|
+
Logs all verification gate events for debugging and audit purposes.
|
|
6
|
+
Events are written to .claude/logs/stop_gate.log.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from gate_logger import GateLogger
|
|
10
|
+
|
|
11
|
+
logger = GateLogger()
|
|
12
|
+
logger.log_gate_trigger()
|
|
13
|
+
logger.log_check_result("tests", passed=True, count=47)
|
|
14
|
+
logger.log_gate_result(passed=True)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from datetime import datetime
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any, Dict, List, Literal, Optional
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class GateEvent:
|
|
26
|
+
"""A single gate event."""
|
|
27
|
+
timestamp: str
|
|
28
|
+
level: Literal["INFO", "WARN", "ERROR", "OK"]
|
|
29
|
+
event_type: str
|
|
30
|
+
message: str
|
|
31
|
+
details: Dict[str, Any] = field(default_factory=dict)
|
|
32
|
+
|
|
33
|
+
def to_log_line(self) -> str:
|
|
34
|
+
"""Format as log line."""
|
|
35
|
+
base = f"[{self.timestamp}] [{self.level}] {self.event_type}: {self.message}"
|
|
36
|
+
if self.details:
|
|
37
|
+
base += f" | {json.dumps(self.details)}"
|
|
38
|
+
return base
|
|
39
|
+
|
|
40
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
41
|
+
return {
|
|
42
|
+
"timestamp": self.timestamp,
|
|
43
|
+
"level": self.level,
|
|
44
|
+
"event_type": self.event_type,
|
|
45
|
+
"message": self.message,
|
|
46
|
+
"details": self.details,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class GateLogger:
|
|
51
|
+
"""Logger for stop gate events."""
|
|
52
|
+
|
|
53
|
+
DEFAULT_LOG_FILE = ".claude/logs/stop_gate.log"
|
|
54
|
+
MAX_LOG_SIZE = 1024 * 1024 # 1MB
|
|
55
|
+
MAX_LOG_FILES = 5
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
log_file: Optional[str] = None,
|
|
60
|
+
project_root: Optional[Path] = None,
|
|
61
|
+
):
|
|
62
|
+
self.project_root = project_root or Path.cwd()
|
|
63
|
+
self.log_file = self.project_root / (log_file or self.DEFAULT_LOG_FILE)
|
|
64
|
+
self.session_events: List[GateEvent] = []
|
|
65
|
+
|
|
66
|
+
def _ensure_log_dir(self) -> None:
|
|
67
|
+
"""Ensure log directory exists."""
|
|
68
|
+
self.log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
|
|
70
|
+
def _rotate_if_needed(self) -> None:
|
|
71
|
+
"""Rotate log file if too large."""
|
|
72
|
+
if not self.log_file.exists():
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
if self.log_file.stat().st_size < self.MAX_LOG_SIZE:
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
# Rotate existing files
|
|
79
|
+
for i in range(self.MAX_LOG_FILES - 1, 0, -1):
|
|
80
|
+
old_file = self.log_file.with_suffix(f".log.{i}")
|
|
81
|
+
new_file = self.log_file.with_suffix(f".log.{i + 1}")
|
|
82
|
+
if old_file.exists():
|
|
83
|
+
if i + 1 >= self.MAX_LOG_FILES:
|
|
84
|
+
old_file.unlink()
|
|
85
|
+
else:
|
|
86
|
+
old_file.rename(new_file)
|
|
87
|
+
|
|
88
|
+
# Rotate current file
|
|
89
|
+
self.log_file.rename(self.log_file.with_suffix(".log.1"))
|
|
90
|
+
|
|
91
|
+
def _timestamp(self) -> str:
|
|
92
|
+
"""Get current timestamp in ISO format."""
|
|
93
|
+
return datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
94
|
+
|
|
95
|
+
def _write_event(self, event: GateEvent) -> None:
|
|
96
|
+
"""Write event to log file."""
|
|
97
|
+
self._ensure_log_dir()
|
|
98
|
+
self._rotate_if_needed()
|
|
99
|
+
|
|
100
|
+
with open(self.log_file, "a") as f:
|
|
101
|
+
f.write(event.to_log_line() + "\n")
|
|
102
|
+
|
|
103
|
+
self.session_events.append(event)
|
|
104
|
+
|
|
105
|
+
def log(
|
|
106
|
+
self,
|
|
107
|
+
level: Literal["INFO", "WARN", "ERROR", "OK"],
|
|
108
|
+
event_type: str,
|
|
109
|
+
message: str,
|
|
110
|
+
details: Optional[Dict[str, Any]] = None,
|
|
111
|
+
) -> None:
|
|
112
|
+
"""Log a gate event."""
|
|
113
|
+
event = GateEvent(
|
|
114
|
+
timestamp=self._timestamp(),
|
|
115
|
+
level=level,
|
|
116
|
+
event_type=event_type,
|
|
117
|
+
message=message,
|
|
118
|
+
details=details or {},
|
|
119
|
+
)
|
|
120
|
+
self._write_event(event)
|
|
121
|
+
|
|
122
|
+
# ==========================================================================
|
|
123
|
+
# Convenience Methods
|
|
124
|
+
# ==========================================================================
|
|
125
|
+
|
|
126
|
+
def log_gate_trigger(self, reason: str = "stop_event") -> None:
|
|
127
|
+
"""Log that the gate was triggered."""
|
|
128
|
+
self.log("INFO", "GATE_TRIGGER", f"Stop gate triggered: {reason}")
|
|
129
|
+
|
|
130
|
+
def log_config(
|
|
131
|
+
self,
|
|
132
|
+
require_verification: bool,
|
|
133
|
+
force_exit: bool,
|
|
134
|
+
project_type: str,
|
|
135
|
+
) -> None:
|
|
136
|
+
"""Log gate configuration."""
|
|
137
|
+
self.log(
|
|
138
|
+
"INFO",
|
|
139
|
+
"CONFIG",
|
|
140
|
+
"Gate config loaded",
|
|
141
|
+
{
|
|
142
|
+
"require_verification": require_verification,
|
|
143
|
+
"force_exit": force_exit,
|
|
144
|
+
"project_type": project_type,
|
|
145
|
+
},
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
def log_check_start(self, check_type: str) -> None:
|
|
149
|
+
"""Log start of a verification check."""
|
|
150
|
+
self.log("INFO", "CHECK_START", f"Starting {check_type} check")
|
|
151
|
+
|
|
152
|
+
def log_check_result(
|
|
153
|
+
self,
|
|
154
|
+
check_type: str,
|
|
155
|
+
passed: bool,
|
|
156
|
+
count: Optional[int] = None,
|
|
157
|
+
error_count: Optional[int] = None,
|
|
158
|
+
duration_ms: Optional[int] = None,
|
|
159
|
+
) -> None:
|
|
160
|
+
"""Log result of a verification check."""
|
|
161
|
+
level = "OK" if passed else "ERROR"
|
|
162
|
+
status = "passed" if passed else "failed"
|
|
163
|
+
|
|
164
|
+
details: Dict[str, Any] = {"passed": passed}
|
|
165
|
+
if count is not None:
|
|
166
|
+
details["count"] = count
|
|
167
|
+
if error_count is not None:
|
|
168
|
+
details["error_count"] = error_count
|
|
169
|
+
if duration_ms is not None:
|
|
170
|
+
details["duration_ms"] = duration_ms
|
|
171
|
+
|
|
172
|
+
self.log(level, "CHECK_RESULT", f"{check_type} {status}", details)
|
|
173
|
+
|
|
174
|
+
def log_gate_result(
|
|
175
|
+
self,
|
|
176
|
+
passed: bool,
|
|
177
|
+
failing_checks: Optional[List[str]] = None,
|
|
178
|
+
) -> None:
|
|
179
|
+
"""Log final gate decision."""
|
|
180
|
+
if passed:
|
|
181
|
+
self.log("OK", "GATE_RESULT", "Gate PASSED - allowing exit")
|
|
182
|
+
else:
|
|
183
|
+
checks = ", ".join(failing_checks or ["unknown"])
|
|
184
|
+
self.log(
|
|
185
|
+
"ERROR",
|
|
186
|
+
"GATE_RESULT",
|
|
187
|
+
f"Gate BLOCKED - failures: {checks}",
|
|
188
|
+
{"failing_checks": failing_checks or []},
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
def log_force_exit(self, reason: str) -> None:
|
|
192
|
+
"""Log force exit bypass."""
|
|
193
|
+
self.log(
|
|
194
|
+
"WARN",
|
|
195
|
+
"FORCE_EXIT",
|
|
196
|
+
"Force exit requested",
|
|
197
|
+
{"reason": reason},
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
def log_verification_state_used(self, status: str) -> None:
|
|
201
|
+
"""Log that cached verification state was used."""
|
|
202
|
+
self.log(
|
|
203
|
+
"INFO",
|
|
204
|
+
"STATE_CACHE",
|
|
205
|
+
f"Using cached verification state: {status}",
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
def log_error(self, error: str, details: Optional[Dict[str, Any]] = None) -> None:
|
|
209
|
+
"""Log an error."""
|
|
210
|
+
self.log("ERROR", "ERROR", error, details)
|
|
211
|
+
|
|
212
|
+
# ==========================================================================
|
|
213
|
+
# Session Summary
|
|
214
|
+
# ==========================================================================
|
|
215
|
+
|
|
216
|
+
def get_session_summary(self) -> Dict[str, Any]:
|
|
217
|
+
"""Get summary of events in current session."""
|
|
218
|
+
checks_passed = []
|
|
219
|
+
checks_failed = []
|
|
220
|
+
gate_result = None
|
|
221
|
+
|
|
222
|
+
for event in self.session_events:
|
|
223
|
+
if event.event_type == "CHECK_RESULT":
|
|
224
|
+
check_type = event.message.split()[0]
|
|
225
|
+
if event.details.get("passed"):
|
|
226
|
+
checks_passed.append(check_type)
|
|
227
|
+
else:
|
|
228
|
+
checks_failed.append(check_type)
|
|
229
|
+
elif event.event_type == "GATE_RESULT":
|
|
230
|
+
gate_result = "passed" if "PASSED" in event.message else "blocked"
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
"event_count": len(self.session_events),
|
|
234
|
+
"checks_passed": checks_passed,
|
|
235
|
+
"checks_failed": checks_failed,
|
|
236
|
+
"gate_result": gate_result,
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
def read_recent_events(self, count: int = 50) -> List[Dict[str, Any]]:
|
|
240
|
+
"""Read recent events from log file."""
|
|
241
|
+
if not self.log_file.exists():
|
|
242
|
+
return []
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
with open(self.log_file, "r") as f:
|
|
246
|
+
lines = f.readlines()
|
|
247
|
+
|
|
248
|
+
events = []
|
|
249
|
+
for line in lines[-count:]:
|
|
250
|
+
# Parse log line
|
|
251
|
+
line = line.strip()
|
|
252
|
+
if not line:
|
|
253
|
+
continue
|
|
254
|
+
|
|
255
|
+
# Basic parsing: [timestamp] [level] event_type: message
|
|
256
|
+
try:
|
|
257
|
+
parts = line.split("] ", 2)
|
|
258
|
+
timestamp = parts[0][1:]
|
|
259
|
+
level = parts[1][1:]
|
|
260
|
+
rest = parts[2]
|
|
261
|
+
event_type, message = rest.split(": ", 1)
|
|
262
|
+
|
|
263
|
+
events.append({
|
|
264
|
+
"timestamp": timestamp,
|
|
265
|
+
"level": level,
|
|
266
|
+
"event_type": event_type,
|
|
267
|
+
"message": message,
|
|
268
|
+
})
|
|
269
|
+
except (IndexError, ValueError):
|
|
270
|
+
continue
|
|
271
|
+
|
|
272
|
+
return events
|
|
273
|
+
|
|
274
|
+
except Exception:
|
|
275
|
+
return []
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
# =============================================================================
|
|
279
|
+
# CLI Interface
|
|
280
|
+
# =============================================================================
|
|
281
|
+
|
|
282
|
+
def main():
|
|
283
|
+
"""CLI entry point for gate logger."""
|
|
284
|
+
import argparse
|
|
285
|
+
|
|
286
|
+
parser = argparse.ArgumentParser(description="Gate logger utility")
|
|
287
|
+
parser.add_argument("command", choices=["tail", "summary", "clear"])
|
|
288
|
+
parser.add_argument("--count", type=int, default=20, help="Number of events")
|
|
289
|
+
parser.add_argument("--project", type=str, help="Project root path")
|
|
290
|
+
|
|
291
|
+
args = parser.parse_args()
|
|
292
|
+
|
|
293
|
+
project_root = Path(args.project) if args.project else Path.cwd()
|
|
294
|
+
logger = GateLogger(project_root=project_root)
|
|
295
|
+
|
|
296
|
+
if args.command == "tail":
|
|
297
|
+
events = logger.read_recent_events(args.count)
|
|
298
|
+
for event in events:
|
|
299
|
+
level = event.get("level", "INFO")
|
|
300
|
+
msg = event.get("message", "")
|
|
301
|
+
print(f"[{level}] {msg}")
|
|
302
|
+
|
|
303
|
+
elif args.command == "summary":
|
|
304
|
+
summary = logger.get_session_summary()
|
|
305
|
+
print(json.dumps(summary, indent=2))
|
|
306
|
+
|
|
307
|
+
elif args.command == "clear":
|
|
308
|
+
if logger.log_file.exists():
|
|
309
|
+
logger.log_file.unlink()
|
|
310
|
+
print("Log file cleared")
|
|
311
|
+
else:
|
|
312
|
+
print("No log file to clear")
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
if __name__ == "__main__":
|
|
316
|
+
main()
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
github_service.py - GitHub and CI status integration for HUD (ANV-111-113)
|
|
4
|
+
|
|
5
|
+
Fetches PR and CI status using the `gh` CLI tool with caching.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from github_service import GitHubService
|
|
9
|
+
|
|
10
|
+
service = GitHubService()
|
|
11
|
+
status = service.get_pr_status(project_path="/path/to/repo")
|
|
12
|
+
print(status["pr_state"], status["ci_status"])
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import subprocess
|
|
17
|
+
import time
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any, Dict, List, Optional, TypedDict
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ReviewInfo(TypedDict, total=False):
|
|
23
|
+
"""Information about a PR review."""
|
|
24
|
+
state: str # APPROVED, CHANGES_REQUESTED, COMMENTED, PENDING
|
|
25
|
+
author: str # Reviewer username
|
|
26
|
+
submitted_at: str # ISO timestamp
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CheckRun(TypedDict, total=False):
|
|
30
|
+
"""Information about a CI check run."""
|
|
31
|
+
name: str # Check name
|
|
32
|
+
status: str # queued, in_progress, completed
|
|
33
|
+
conclusion: str # success, failure, neutral, cancelled, skipped
|
|
34
|
+
url: str # URL to check details
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class MergeQueueEntry(TypedDict, total=False):
|
|
38
|
+
"""Merge queue position info."""
|
|
39
|
+
position: int # Position in queue
|
|
40
|
+
estimated_time_to_merge: int # Estimated seconds until merge
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class PRStatus(TypedDict, total=False):
|
|
44
|
+
"""PR and CI status information."""
|
|
45
|
+
project_path: str
|
|
46
|
+
branch: str
|
|
47
|
+
pr_number: Optional[int]
|
|
48
|
+
pr_url: str
|
|
49
|
+
pr_state: str # OPEN, MERGED, CLOSED
|
|
50
|
+
pr_title: str
|
|
51
|
+
ci_status: str # passed, failed, running, pending
|
|
52
|
+
ci_conclusion: str # success, failure, etc.
|
|
53
|
+
reviews: List[ReviewInfo]
|
|
54
|
+
reviews_summary: str # e.g., "2 approved, 1 pending"
|
|
55
|
+
check_runs: List[CheckRun]
|
|
56
|
+
merge_queue: Optional[MergeQueueEntry]
|
|
57
|
+
last_updated: float
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# Cache TTL in seconds
|
|
61
|
+
CACHE_TTL = 30.0
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class GitHubService:
|
|
65
|
+
"""Service for fetching GitHub PR and CI status (ANV-111-113)."""
|
|
66
|
+
|
|
67
|
+
def __init__(self):
|
|
68
|
+
"""Initialize the GitHub service."""
|
|
69
|
+
self._cache: Dict[str, PRStatus] = {}
|
|
70
|
+
self._cache_timestamps: Dict[str, float] = {}
|
|
71
|
+
|
|
72
|
+
def get_pr_status(self, project_path: str, force_refresh: bool = False) -> Optional[PRStatus]:
|
|
73
|
+
"""Get PR and CI status for a project.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
project_path: Path to the git repository
|
|
77
|
+
force_refresh: Force refresh even if cache is fresh
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
PRStatus or None if no PR exists or gh CLI not available
|
|
81
|
+
"""
|
|
82
|
+
cache_key = project_path
|
|
83
|
+
now = time.time()
|
|
84
|
+
|
|
85
|
+
# Check cache
|
|
86
|
+
if not force_refresh and cache_key in self._cache:
|
|
87
|
+
if now - self._cache_timestamps.get(cache_key, 0) < CACHE_TTL:
|
|
88
|
+
return self._cache[cache_key]
|
|
89
|
+
|
|
90
|
+
# Fetch fresh data
|
|
91
|
+
status = self._fetch_pr_status(project_path)
|
|
92
|
+
if status:
|
|
93
|
+
self._cache[cache_key] = status
|
|
94
|
+
self._cache_timestamps[cache_key] = now
|
|
95
|
+
|
|
96
|
+
return status
|
|
97
|
+
|
|
98
|
+
def get_status_for_agents(self, agents: Dict[str, Any]) -> Dict[str, PRStatus]:
|
|
99
|
+
"""Get PR status for all agent projects.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
agents: Dictionary of agent_id -> agent data
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Dictionary of project_path -> PRStatus
|
|
106
|
+
"""
|
|
107
|
+
results: Dict[str, PRStatus] = {}
|
|
108
|
+
|
|
109
|
+
for agent_id, agent in agents.items():
|
|
110
|
+
project = agent.get("project")
|
|
111
|
+
if project and project not in results:
|
|
112
|
+
status = self.get_pr_status(project)
|
|
113
|
+
if status:
|
|
114
|
+
results[project] = status
|
|
115
|
+
|
|
116
|
+
return results
|
|
117
|
+
|
|
118
|
+
def _fetch_pr_status(self, project_path: str) -> Optional[PRStatus]:
|
|
119
|
+
"""Fetch PR status from GitHub using gh CLI."""
|
|
120
|
+
if not Path(project_path).exists():
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
# Get current branch first
|
|
124
|
+
branch = self._get_current_branch(project_path)
|
|
125
|
+
if not branch:
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
# Try to get PR for current branch
|
|
130
|
+
result = subprocess.run(
|
|
131
|
+
[
|
|
132
|
+
"gh", "pr", "view",
|
|
133
|
+
"--json", "number,state,title,url,reviews,statusCheckRollup,mergeQueueEntry",
|
|
134
|
+
],
|
|
135
|
+
cwd=project_path,
|
|
136
|
+
capture_output=True,
|
|
137
|
+
text=True,
|
|
138
|
+
timeout=10,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if result.returncode != 0:
|
|
142
|
+
# No PR exists for this branch
|
|
143
|
+
return {
|
|
144
|
+
"project_path": project_path,
|
|
145
|
+
"branch": branch,
|
|
146
|
+
"pr_number": None,
|
|
147
|
+
"pr_url": "",
|
|
148
|
+
"pr_state": "NO_PR",
|
|
149
|
+
"pr_title": "",
|
|
150
|
+
"ci_status": "none",
|
|
151
|
+
"ci_conclusion": "",
|
|
152
|
+
"reviews": [],
|
|
153
|
+
"reviews_summary": "",
|
|
154
|
+
"check_runs": [],
|
|
155
|
+
"merge_queue": None,
|
|
156
|
+
"last_updated": time.time(),
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
data = json.loads(result.stdout)
|
|
160
|
+
return self._parse_pr_data(data, project_path, branch)
|
|
161
|
+
|
|
162
|
+
except subprocess.TimeoutExpired:
|
|
163
|
+
return None
|
|
164
|
+
except (json.JSONDecodeError, Exception):
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
def _get_current_branch(self, project_path: str) -> Optional[str]:
|
|
168
|
+
"""Get current git branch name."""
|
|
169
|
+
try:
|
|
170
|
+
result = subprocess.run(
|
|
171
|
+
["git", "branch", "--show-current"],
|
|
172
|
+
cwd=project_path,
|
|
173
|
+
capture_output=True,
|
|
174
|
+
text=True,
|
|
175
|
+
timeout=5,
|
|
176
|
+
)
|
|
177
|
+
return result.stdout.strip() or None
|
|
178
|
+
except Exception:
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
def _parse_pr_data(self, data: Dict[str, Any], project_path: str, branch: str) -> PRStatus:
|
|
182
|
+
"""Parse gh CLI PR data into PRStatus."""
|
|
183
|
+
# Parse reviews
|
|
184
|
+
reviews: List[ReviewInfo] = []
|
|
185
|
+
for review in data.get("reviews", []) or []:
|
|
186
|
+
reviews.append({
|
|
187
|
+
"state": review.get("state", ""),
|
|
188
|
+
"author": review.get("author", {}).get("login", ""),
|
|
189
|
+
"submitted_at": review.get("submittedAt", ""),
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
# Summarize reviews
|
|
193
|
+
approved = sum(1 for r in reviews if r["state"] == "APPROVED")
|
|
194
|
+
changes_requested = sum(1 for r in reviews if r["state"] == "CHANGES_REQUESTED")
|
|
195
|
+
pending = sum(1 for r in reviews if r["state"] == "PENDING")
|
|
196
|
+
|
|
197
|
+
parts = []
|
|
198
|
+
if approved:
|
|
199
|
+
parts.append(f"{approved} approved")
|
|
200
|
+
if changes_requested:
|
|
201
|
+
parts.append(f"{changes_requested} changes requested")
|
|
202
|
+
if pending:
|
|
203
|
+
parts.append(f"{pending} pending")
|
|
204
|
+
reviews_summary = ", ".join(parts) if parts else "No reviews"
|
|
205
|
+
|
|
206
|
+
# Parse status check rollup
|
|
207
|
+
status_rollup = data.get("statusCheckRollup", []) or []
|
|
208
|
+
check_runs: List[CheckRun] = []
|
|
209
|
+
ci_status = "pending"
|
|
210
|
+
ci_conclusion = ""
|
|
211
|
+
|
|
212
|
+
for check in status_rollup:
|
|
213
|
+
typename = check.get("__typename", "")
|
|
214
|
+
if typename == "CheckRun":
|
|
215
|
+
check_runs.append({
|
|
216
|
+
"name": check.get("name", ""),
|
|
217
|
+
"status": check.get("status", ""),
|
|
218
|
+
"conclusion": check.get("conclusion", ""),
|
|
219
|
+
"url": check.get("detailsUrl", ""),
|
|
220
|
+
})
|
|
221
|
+
elif typename == "StatusContext":
|
|
222
|
+
# StatusContext state contains values like "success", "pending", "failure"
|
|
223
|
+
state = check.get("state", "")
|
|
224
|
+
check_runs.append({
|
|
225
|
+
"name": check.get("context", ""),
|
|
226
|
+
"status": "completed" if state and state.lower() != "pending" else "pending",
|
|
227
|
+
"conclusion": state.lower() if state else "",
|
|
228
|
+
"url": check.get("targetUrl", ""),
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
# Determine overall CI status
|
|
232
|
+
if not check_runs:
|
|
233
|
+
ci_status = "none"
|
|
234
|
+
else:
|
|
235
|
+
statuses = [c.get("status", "").lower() for c in check_runs]
|
|
236
|
+
conclusions = [c.get("conclusion", "").lower() for c in check_runs]
|
|
237
|
+
|
|
238
|
+
if "in_progress" in statuses or "queued" in statuses:
|
|
239
|
+
ci_status = "running"
|
|
240
|
+
elif all(c == "completed" for c in statuses):
|
|
241
|
+
if "failure" in conclusions or "cancelled" in conclusions:
|
|
242
|
+
ci_status = "failed"
|
|
243
|
+
ci_conclusion = "failure"
|
|
244
|
+
elif all(c in ("success", "skipped", "neutral") for c in conclusions if c):
|
|
245
|
+
ci_status = "passed"
|
|
246
|
+
ci_conclusion = "success"
|
|
247
|
+
else:
|
|
248
|
+
ci_status = "mixed"
|
|
249
|
+
|
|
250
|
+
# Parse merge queue
|
|
251
|
+
merge_queue_data = data.get("mergeQueueEntry")
|
|
252
|
+
merge_queue: Optional[MergeQueueEntry] = None
|
|
253
|
+
if merge_queue_data:
|
|
254
|
+
merge_queue = {
|
|
255
|
+
"position": merge_queue_data.get("position", 0),
|
|
256
|
+
"estimated_time_to_merge": merge_queue_data.get("estimatedTimeToMerge", 0),
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
"project_path": project_path,
|
|
261
|
+
"branch": branch,
|
|
262
|
+
"pr_number": data.get("number"),
|
|
263
|
+
"pr_url": data.get("url", ""),
|
|
264
|
+
"pr_state": data.get("state", "OPEN"),
|
|
265
|
+
"pr_title": data.get("title", ""),
|
|
266
|
+
"ci_status": ci_status,
|
|
267
|
+
"ci_conclusion": ci_conclusion,
|
|
268
|
+
"reviews": reviews,
|
|
269
|
+
"reviews_summary": reviews_summary,
|
|
270
|
+
"check_runs": check_runs,
|
|
271
|
+
"merge_queue": merge_queue,
|
|
272
|
+
"last_updated": time.time(),
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
def clear_cache(self) -> None:
|
|
276
|
+
"""Clear cached PR status."""
|
|
277
|
+
self._cache.clear()
|
|
278
|
+
self._cache_timestamps.clear()
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
# Singleton instance
|
|
282
|
+
_service: Optional[GitHubService] = None
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def get_github_service() -> GitHubService:
|
|
286
|
+
"""Get singleton service instance."""
|
|
287
|
+
global _service
|
|
288
|
+
if _service is None:
|
|
289
|
+
_service = GitHubService()
|
|
290
|
+
return _service
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
if __name__ == "__main__":
|
|
294
|
+
# Simple test
|
|
295
|
+
import sys
|
|
296
|
+
|
|
297
|
+
project = sys.argv[1] if len(sys.argv) > 1 else "."
|
|
298
|
+
service = GitHubService()
|
|
299
|
+
status = service.get_pr_status(project)
|
|
300
|
+
|
|
301
|
+
if status:
|
|
302
|
+
print(f"Project: {status['project_path']}")
|
|
303
|
+
print(f"Branch: {status['branch']}")
|
|
304
|
+
print(f"PR: #{status.get('pr_number', 'None')} - {status['pr_state']}")
|
|
305
|
+
print(f"CI: {status['ci_status']}")
|
|
306
|
+
print(f"Reviews: {status['reviews_summary']}")
|
|
307
|
+
if status.get('merge_queue'):
|
|
308
|
+
print(f"Merge Queue: Position {status['merge_queue']['position']}")
|
|
309
|
+
else:
|
|
310
|
+
print("Could not fetch PR status")
|