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,331 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
coordination_service.py - Agent coordination and conflict detection for HUD (ANV-106-110)
|
|
4
|
+
|
|
5
|
+
Tracks file locks between agents, detects conflicts when multiple agents edit
|
|
6
|
+
the same file, and provides branch age information for trunk-based development.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from coordination_service import CoordinationService
|
|
10
|
+
|
|
11
|
+
service = CoordinationService()
|
|
12
|
+
service.update_from_registry(agents)
|
|
13
|
+
conflicts = service.get_conflicts()
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import subprocess
|
|
17
|
+
import time
|
|
18
|
+
from datetime import datetime, timezone
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any, Dict, List, Optional, Set, TypedDict
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class FileLock(TypedDict, total=False):
|
|
24
|
+
"""Lock on a file by an agent."""
|
|
25
|
+
agent_id: str # Agent holding the lock
|
|
26
|
+
agent_display: str # Short display name for agent
|
|
27
|
+
file_path: str # Full path to file
|
|
28
|
+
pattern: str # Directory pattern (e.g., "src/auth/*")
|
|
29
|
+
since: str # ISO timestamp when lock acquired
|
|
30
|
+
operation: str # Type of operation (Edit, Write, etc.)
|
|
31
|
+
duration_seconds: int # How long the lock has been held
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class FileConflict(TypedDict, total=False):
|
|
35
|
+
"""Conflict when multiple agents edit the same file."""
|
|
36
|
+
file_path: str # Full path to conflicting file
|
|
37
|
+
pattern: str # Directory pattern
|
|
38
|
+
agents: List[str] # Agent IDs involved in conflict
|
|
39
|
+
detected: str # ISO timestamp when detected
|
|
40
|
+
suggestion: str # Suggested action
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class BranchInfo(TypedDict, total=False):
|
|
44
|
+
"""Branch age information for an agent."""
|
|
45
|
+
agent_id: str
|
|
46
|
+
branch: str # Branch name
|
|
47
|
+
age_seconds: int # Age of branch in seconds
|
|
48
|
+
age_display: str # Human-readable age (e.g., "2h 15m")
|
|
49
|
+
is_stale: bool # True if branch age exceeds threshold
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class CoordinationData(TypedDict, total=False):
|
|
53
|
+
"""Aggregated coordination data."""
|
|
54
|
+
file_locks: Dict[str, FileLock] # pattern -> lock
|
|
55
|
+
conflicts: List[FileConflict] # List of conflicts
|
|
56
|
+
branches: Dict[str, BranchInfo] # agent_id -> branch info
|
|
57
|
+
last_update: float # Timestamp
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# Thresholds
|
|
61
|
+
STALE_BRANCH_THRESHOLD = 4 * 60 * 60 # 4 hours in seconds
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class CoordinationService:
|
|
65
|
+
"""Service for tracking agent coordination and conflicts (ANV-106-110)."""
|
|
66
|
+
|
|
67
|
+
def __init__(self):
|
|
68
|
+
"""Initialize the coordination service."""
|
|
69
|
+
self._locks: Dict[str, FileLock] = {} # pattern -> lock
|
|
70
|
+
self._lock_timestamps: Dict[str, float] = {} # pattern -> timestamp
|
|
71
|
+
self._conflicts: List[FileConflict] = []
|
|
72
|
+
self._branches: Dict[str, BranchInfo] = {} # agent_id -> branch
|
|
73
|
+
self._last_update = 0.0
|
|
74
|
+
|
|
75
|
+
def update_from_registry(self, agents: Dict[str, Any]) -> CoordinationData:
|
|
76
|
+
"""Update coordination data from agent registry.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
agents: Dictionary of agent_id -> agent data
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
CoordinationData with all coordination information
|
|
83
|
+
"""
|
|
84
|
+
now = time.time()
|
|
85
|
+
new_locks: Dict[str, FileLock] = {}
|
|
86
|
+
agent_files: Dict[str, Set[str]] = {} # agent_id -> set of file patterns
|
|
87
|
+
|
|
88
|
+
for agent_id, agent in agents.items():
|
|
89
|
+
project = agent.get("project", "")
|
|
90
|
+
display_id = agent_id[:12] if len(agent_id) > 12 else agent_id
|
|
91
|
+
|
|
92
|
+
# Track files from recent tool calls
|
|
93
|
+
recent_tools = agent.get("recentTools", [])
|
|
94
|
+
if not recent_tools:
|
|
95
|
+
# Try to get from detailed state
|
|
96
|
+
detailed = agent.get("detailed", {})
|
|
97
|
+
recent_tools = detailed.get("recentTools", [])
|
|
98
|
+
|
|
99
|
+
files_touched: Set[str] = set()
|
|
100
|
+
for tool in recent_tools:
|
|
101
|
+
tool_name = tool.get("name", "")
|
|
102
|
+
# Only track write operations
|
|
103
|
+
if tool_name in ("Edit", "Write", "MultiEdit"):
|
|
104
|
+
file_path = tool.get("args", {}).get("file_path", "")
|
|
105
|
+
if file_path:
|
|
106
|
+
# Get directory pattern
|
|
107
|
+
pattern = self._get_pattern(file_path, project)
|
|
108
|
+
files_touched.add(pattern)
|
|
109
|
+
|
|
110
|
+
# Create or update lock
|
|
111
|
+
if pattern not in new_locks:
|
|
112
|
+
# Check if we had this lock before
|
|
113
|
+
since = self._lock_timestamps.get(pattern, now)
|
|
114
|
+
self._lock_timestamps[pattern] = since
|
|
115
|
+
|
|
116
|
+
new_locks[pattern] = {
|
|
117
|
+
"agent_id": agent_id,
|
|
118
|
+
"agent_display": display_id,
|
|
119
|
+
"file_path": file_path,
|
|
120
|
+
"pattern": pattern,
|
|
121
|
+
"since": datetime.fromtimestamp(since, timezone.utc).isoformat(),
|
|
122
|
+
"operation": tool_name,
|
|
123
|
+
"duration_seconds": int(now - since),
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
agent_files[agent_id] = files_touched
|
|
127
|
+
|
|
128
|
+
# Get branch info
|
|
129
|
+
branch_info = self._get_branch_info(agent_id, project)
|
|
130
|
+
if branch_info:
|
|
131
|
+
self._branches[agent_id] = branch_info
|
|
132
|
+
|
|
133
|
+
# Update locks
|
|
134
|
+
self._locks = new_locks
|
|
135
|
+
|
|
136
|
+
# Detect conflicts (same pattern touched by multiple agents)
|
|
137
|
+
self._conflicts = []
|
|
138
|
+
pattern_agents: Dict[str, List[str]] = {} # pattern -> [agent_ids]
|
|
139
|
+
|
|
140
|
+
for agent_id, patterns in agent_files.items():
|
|
141
|
+
for pattern in patterns:
|
|
142
|
+
if pattern not in pattern_agents:
|
|
143
|
+
pattern_agents[pattern] = []
|
|
144
|
+
pattern_agents[pattern].append(agent_id)
|
|
145
|
+
|
|
146
|
+
# Find patterns with multiple agents
|
|
147
|
+
for pattern, agent_ids in pattern_agents.items():
|
|
148
|
+
if len(agent_ids) > 1:
|
|
149
|
+
self._conflicts.append({
|
|
150
|
+
"file_path": pattern,
|
|
151
|
+
"pattern": pattern,
|
|
152
|
+
"agents": agent_ids,
|
|
153
|
+
"detected": datetime.now(timezone.utc).isoformat(),
|
|
154
|
+
"suggestion": "Coordinate via /handoff or stagger work",
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
self._last_update = now
|
|
158
|
+
|
|
159
|
+
return self.get_data()
|
|
160
|
+
|
|
161
|
+
def get_data(self) -> CoordinationData:
|
|
162
|
+
"""Get current coordination data."""
|
|
163
|
+
return {
|
|
164
|
+
"file_locks": self._locks,
|
|
165
|
+
"conflicts": self._conflicts,
|
|
166
|
+
"branches": self._branches,
|
|
167
|
+
"last_update": self._last_update,
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
def get_locks(self) -> Dict[str, FileLock]:
|
|
171
|
+
"""Get current file locks."""
|
|
172
|
+
return self._locks
|
|
173
|
+
|
|
174
|
+
def get_conflicts(self) -> List[FileConflict]:
|
|
175
|
+
"""Get current conflicts."""
|
|
176
|
+
return self._conflicts
|
|
177
|
+
|
|
178
|
+
def get_branches(self) -> Dict[str, BranchInfo]:
|
|
179
|
+
"""Get branch information."""
|
|
180
|
+
return self._branches
|
|
181
|
+
|
|
182
|
+
def _get_pattern(self, file_path: str, project: str) -> str:
|
|
183
|
+
"""Get directory pattern from file path.
|
|
184
|
+
|
|
185
|
+
Converts full path to a pattern like "src/auth/*"
|
|
186
|
+
"""
|
|
187
|
+
try:
|
|
188
|
+
path = Path(file_path)
|
|
189
|
+
if project:
|
|
190
|
+
try:
|
|
191
|
+
path = path.relative_to(project)
|
|
192
|
+
except ValueError:
|
|
193
|
+
pass
|
|
194
|
+
|
|
195
|
+
# Get parent directory
|
|
196
|
+
parent = path.parent
|
|
197
|
+
return str(parent) + "/*" if str(parent) != "." else str(path)
|
|
198
|
+
except Exception:
|
|
199
|
+
return file_path
|
|
200
|
+
|
|
201
|
+
def _get_branch_info(self, agent_id: str, project: str) -> Optional[BranchInfo]:
|
|
202
|
+
"""Get branch age information for an agent's project."""
|
|
203
|
+
if not project or not Path(project).exists():
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
# Get current branch
|
|
208
|
+
result = subprocess.run(
|
|
209
|
+
["git", "branch", "--show-current"],
|
|
210
|
+
cwd=project,
|
|
211
|
+
capture_output=True,
|
|
212
|
+
text=True,
|
|
213
|
+
timeout=5,
|
|
214
|
+
)
|
|
215
|
+
branch = result.stdout.strip()
|
|
216
|
+
if not branch:
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
# Get branch creation/diverge time (commits since main)
|
|
220
|
+
result = subprocess.run(
|
|
221
|
+
["git", "log", "main.." + branch, "--format=%ci", "--reverse"],
|
|
222
|
+
cwd=project,
|
|
223
|
+
capture_output=True,
|
|
224
|
+
text=True,
|
|
225
|
+
timeout=5,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
age_seconds = 0
|
|
229
|
+
if result.stdout.strip():
|
|
230
|
+
# Get first commit time
|
|
231
|
+
first_commit = result.stdout.strip().split("\n")[0]
|
|
232
|
+
try:
|
|
233
|
+
# Parse git date format: "2024-01-15 10:30:00 +0000"
|
|
234
|
+
# Split off timezone, replace first space with T
|
|
235
|
+
parts = first_commit.rsplit(" ", 1) # Split off timezone
|
|
236
|
+
if len(parts) == 2:
|
|
237
|
+
dt_str, tz_str = parts
|
|
238
|
+
# dt_str is "2024-01-15 10:30:00", tz_str is "+0000"
|
|
239
|
+
iso_str = dt_str.replace(" ", "T") + tz_str.replace(":", "")
|
|
240
|
+
commit_time = datetime.fromisoformat(iso_str)
|
|
241
|
+
age_seconds = int((datetime.now(timezone.utc) - commit_time.astimezone(timezone.utc)).total_seconds())
|
|
242
|
+
except Exception:
|
|
243
|
+
pass
|
|
244
|
+
|
|
245
|
+
# If no divergence from main, use branch checkout time instead
|
|
246
|
+
if age_seconds == 0:
|
|
247
|
+
# Fallback: use reflog
|
|
248
|
+
result = subprocess.run(
|
|
249
|
+
["git", "reflog", "show", "--format=%ci", "-1", branch],
|
|
250
|
+
cwd=project,
|
|
251
|
+
capture_output=True,
|
|
252
|
+
text=True,
|
|
253
|
+
timeout=5,
|
|
254
|
+
)
|
|
255
|
+
if result.stdout.strip():
|
|
256
|
+
try:
|
|
257
|
+
# Parse git date format: "2024-01-15 10:30:00 +0000"
|
|
258
|
+
reflog_date = result.stdout.strip()
|
|
259
|
+
parts = reflog_date.rsplit(" ", 1) # Split off timezone
|
|
260
|
+
if len(parts) == 2:
|
|
261
|
+
dt_str, tz_str = parts
|
|
262
|
+
iso_str = dt_str.replace(" ", "T") + tz_str.replace(":", "")
|
|
263
|
+
checkout_time = datetime.fromisoformat(iso_str)
|
|
264
|
+
age_seconds = int((datetime.now(timezone.utc) - checkout_time.astimezone(timezone.utc)).total_seconds())
|
|
265
|
+
except Exception:
|
|
266
|
+
pass
|
|
267
|
+
|
|
268
|
+
# Format age
|
|
269
|
+
if age_seconds < 60:
|
|
270
|
+
age_display = f"{age_seconds}s"
|
|
271
|
+
elif age_seconds < 3600:
|
|
272
|
+
age_display = f"{age_seconds // 60}m"
|
|
273
|
+
else:
|
|
274
|
+
hours = age_seconds // 3600
|
|
275
|
+
mins = (age_seconds % 3600) // 60
|
|
276
|
+
age_display = f"{hours}h {mins}m"
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
"agent_id": agent_id,
|
|
280
|
+
"branch": branch,
|
|
281
|
+
"age_seconds": age_seconds,
|
|
282
|
+
"age_display": age_display,
|
|
283
|
+
"is_stale": age_seconds > STALE_BRANCH_THRESHOLD,
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
except Exception:
|
|
287
|
+
return None
|
|
288
|
+
|
|
289
|
+
def clear(self) -> None:
|
|
290
|
+
"""Clear all coordination data."""
|
|
291
|
+
self._locks.clear()
|
|
292
|
+
self._lock_timestamps.clear()
|
|
293
|
+
self._conflicts.clear()
|
|
294
|
+
self._branches.clear()
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
# Singleton instance
|
|
298
|
+
_service: Optional[CoordinationService] = None
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def get_coordination_service() -> CoordinationService:
|
|
302
|
+
"""Get singleton service instance."""
|
|
303
|
+
global _service
|
|
304
|
+
if _service is None:
|
|
305
|
+
_service = CoordinationService()
|
|
306
|
+
return _service
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
if __name__ == "__main__":
|
|
310
|
+
# Simple test
|
|
311
|
+
service = CoordinationService()
|
|
312
|
+
|
|
313
|
+
# Simulate two agents editing same file
|
|
314
|
+
test_agents = {
|
|
315
|
+
"agent-1": {
|
|
316
|
+
"project": "/tmp/test-project",
|
|
317
|
+
"recentTools": [
|
|
318
|
+
{"name": "Edit", "args": {"file_path": "/tmp/test-project/src/auth/login.ts"}},
|
|
319
|
+
],
|
|
320
|
+
},
|
|
321
|
+
"agent-2": {
|
|
322
|
+
"project": "/tmp/test-project",
|
|
323
|
+
"recentTools": [
|
|
324
|
+
{"name": "Edit", "args": {"file_path": "/tmp/test-project/src/auth/logout.ts"}},
|
|
325
|
+
],
|
|
326
|
+
},
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
data = service.update_from_registry(test_agents)
|
|
330
|
+
print("Locks:", data["file_locks"])
|
|
331
|
+
print("Conflicts:", data["conflicts"])
|