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,257 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Issue data models for Anvil Framework.
|
|
3
|
+
|
|
4
|
+
Provides a common data model for issue tracking that works with both
|
|
5
|
+
Linear and local JSON storage backends.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from issue_models import Issue, IssueStatus, Priority
|
|
9
|
+
|
|
10
|
+
issue = Issue(
|
|
11
|
+
id="local-001",
|
|
12
|
+
identifier="LOCAL-001",
|
|
13
|
+
title="Implement feature X",
|
|
14
|
+
status=IssueStatus.TODO,
|
|
15
|
+
priority=Priority.MEDIUM
|
|
16
|
+
)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from dataclasses import dataclass, field, asdict
|
|
20
|
+
from datetime import datetime, timezone
|
|
21
|
+
from enum import Enum
|
|
22
|
+
from typing import Optional
|
|
23
|
+
import json
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class IssueStatus(Enum):
|
|
27
|
+
"""Issue workflow states, compatible with Linear."""
|
|
28
|
+
BACKLOG = "backlog"
|
|
29
|
+
TODO = "todo"
|
|
30
|
+
IN_PROGRESS = "in_progress"
|
|
31
|
+
IN_REVIEW = "in_review"
|
|
32
|
+
DONE = "done"
|
|
33
|
+
CANCELLED = "cancelled"
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def from_linear_state(cls, state_name: str) -> "IssueStatus":
|
|
37
|
+
"""Map Linear state name to IssueStatus."""
|
|
38
|
+
mapping = {
|
|
39
|
+
"backlog": cls.BACKLOG,
|
|
40
|
+
"todo": cls.TODO,
|
|
41
|
+
"in progress": cls.IN_PROGRESS,
|
|
42
|
+
"in review": cls.IN_REVIEW,
|
|
43
|
+
"done": cls.DONE,
|
|
44
|
+
"canceled": cls.CANCELLED,
|
|
45
|
+
"cancelled": cls.CANCELLED,
|
|
46
|
+
"duplicate": cls.CANCELLED,
|
|
47
|
+
}
|
|
48
|
+
return mapping.get(state_name.lower(), cls.BACKLOG)
|
|
49
|
+
|
|
50
|
+
def to_display(self) -> str:
|
|
51
|
+
"""Human-readable display name."""
|
|
52
|
+
return self.value.replace("_", " ").title()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Priority(Enum):
|
|
56
|
+
"""Issue priority levels, compatible with Linear."""
|
|
57
|
+
URGENT = 0 # P0 - Drop everything
|
|
58
|
+
HIGH = 1 # P1 - Next up
|
|
59
|
+
MEDIUM = 2 # P2 - This sprint
|
|
60
|
+
LOW = 3 # P3 - Backlog
|
|
61
|
+
NONE = 4 # No priority set
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def from_linear_priority(cls, priority: int) -> "Priority":
|
|
65
|
+
"""Map Linear priority (0-4) to Priority enum."""
|
|
66
|
+
try:
|
|
67
|
+
return cls(priority)
|
|
68
|
+
except ValueError:
|
|
69
|
+
return cls.NONE
|
|
70
|
+
|
|
71
|
+
def to_display(self) -> str:
|
|
72
|
+
"""Short display format (P0, P1, etc.)."""
|
|
73
|
+
if self == Priority.NONE:
|
|
74
|
+
return "—"
|
|
75
|
+
return f"P{self.value}"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class Issue:
|
|
80
|
+
"""
|
|
81
|
+
Core issue model, compatible with Linear schema.
|
|
82
|
+
|
|
83
|
+
Attributes:
|
|
84
|
+
id: Internal unique identifier (UUID or generated ID)
|
|
85
|
+
identifier: Human-readable identifier (e.g., "ANV-72", "LOCAL-001")
|
|
86
|
+
title: Issue title
|
|
87
|
+
description: Detailed description (markdown supported)
|
|
88
|
+
status: Current workflow status
|
|
89
|
+
priority: Priority level (P0-P4)
|
|
90
|
+
parent_id: Parent issue ID for sub-issues
|
|
91
|
+
labels: List of label names
|
|
92
|
+
assigned_agent: Anvil agent ID if claimed
|
|
93
|
+
created_at: Creation timestamp
|
|
94
|
+
updated_at: Last modification timestamp
|
|
95
|
+
completed_at: Completion timestamp (if done)
|
|
96
|
+
estimate: Time estimate string (e.g., "2h", "1d")
|
|
97
|
+
provider: Source provider ("local" or "linear")
|
|
98
|
+
external_id: External system ID (Linear UUID if synced)
|
|
99
|
+
"""
|
|
100
|
+
id: str
|
|
101
|
+
identifier: str
|
|
102
|
+
title: str
|
|
103
|
+
description: str = ""
|
|
104
|
+
status: IssueStatus = IssueStatus.TODO
|
|
105
|
+
priority: Priority = Priority.MEDIUM
|
|
106
|
+
|
|
107
|
+
# Relationships
|
|
108
|
+
parent_id: Optional[str] = None
|
|
109
|
+
labels: list[str] = field(default_factory=list)
|
|
110
|
+
assigned_agent: Optional[str] = None
|
|
111
|
+
|
|
112
|
+
# Metadata
|
|
113
|
+
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
114
|
+
updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
115
|
+
completed_at: Optional[datetime] = None
|
|
116
|
+
|
|
117
|
+
# Estimates
|
|
118
|
+
estimate: Optional[str] = None
|
|
119
|
+
|
|
120
|
+
# Source tracking
|
|
121
|
+
provider: str = "local"
|
|
122
|
+
external_id: Optional[str] = None
|
|
123
|
+
|
|
124
|
+
def to_dict(self) -> dict:
|
|
125
|
+
"""Convert to dictionary for JSON serialization."""
|
|
126
|
+
data = asdict(self)
|
|
127
|
+
# Convert enums to values
|
|
128
|
+
data["status"] = self.status.value
|
|
129
|
+
data["priority"] = self.priority.value
|
|
130
|
+
# Convert datetimes to ISO strings
|
|
131
|
+
data["created_at"] = self.created_at.isoformat() if self.created_at else None
|
|
132
|
+
data["updated_at"] = self.updated_at.isoformat() if self.updated_at else None
|
|
133
|
+
data["completed_at"] = self.completed_at.isoformat() if self.completed_at else None
|
|
134
|
+
return data
|
|
135
|
+
|
|
136
|
+
def to_json(self) -> str:
|
|
137
|
+
"""Convert to JSON string."""
|
|
138
|
+
return json.dumps(self.to_dict(), indent=2)
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def from_dict(cls, data: dict) -> "Issue":
|
|
142
|
+
"""Create Issue from dictionary."""
|
|
143
|
+
# Parse status
|
|
144
|
+
status_val = data.get("status", "todo")
|
|
145
|
+
if isinstance(status_val, str):
|
|
146
|
+
status = IssueStatus(status_val)
|
|
147
|
+
else:
|
|
148
|
+
status = status_val
|
|
149
|
+
|
|
150
|
+
# Parse priority
|
|
151
|
+
priority_val = data.get("priority", 2)
|
|
152
|
+
if isinstance(priority_val, int):
|
|
153
|
+
priority = Priority(priority_val)
|
|
154
|
+
elif isinstance(priority_val, str):
|
|
155
|
+
priority = Priority(int(priority_val))
|
|
156
|
+
else:
|
|
157
|
+
priority = priority_val
|
|
158
|
+
|
|
159
|
+
# Parse datetimes
|
|
160
|
+
def parse_dt(val) -> Optional[datetime]:
|
|
161
|
+
if val is None:
|
|
162
|
+
return None
|
|
163
|
+
if isinstance(val, datetime):
|
|
164
|
+
return val
|
|
165
|
+
return datetime.fromisoformat(val.replace('Z', '+00:00'))
|
|
166
|
+
|
|
167
|
+
return cls(
|
|
168
|
+
id=data["id"],
|
|
169
|
+
identifier=data["identifier"],
|
|
170
|
+
title=data["title"],
|
|
171
|
+
description=data.get("description", ""),
|
|
172
|
+
status=status,
|
|
173
|
+
priority=priority,
|
|
174
|
+
parent_id=data.get("parent_id"),
|
|
175
|
+
labels=data.get("labels", []),
|
|
176
|
+
assigned_agent=data.get("assigned_agent"),
|
|
177
|
+
created_at=parse_dt(data.get("created_at")) or datetime.now(timezone.utc),
|
|
178
|
+
updated_at=parse_dt(data.get("updated_at")) or datetime.now(timezone.utc),
|
|
179
|
+
completed_at=parse_dt(data.get("completed_at")),
|
|
180
|
+
estimate=data.get("estimate"),
|
|
181
|
+
provider=data.get("provider", "local"),
|
|
182
|
+
external_id=data.get("external_id"),
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
@classmethod
|
|
186
|
+
def from_json(cls, json_str: str) -> "Issue":
|
|
187
|
+
"""Create Issue from JSON string."""
|
|
188
|
+
return cls.from_dict(json.loads(json_str))
|
|
189
|
+
|
|
190
|
+
@classmethod
|
|
191
|
+
def from_linear(cls, linear_data: dict) -> "Issue":
|
|
192
|
+
"""Create Issue from Linear API response."""
|
|
193
|
+
state = linear_data.get("state", {})
|
|
194
|
+
state_name = state.get("name", "Backlog") if isinstance(state, dict) else "Backlog"
|
|
195
|
+
|
|
196
|
+
return cls(
|
|
197
|
+
id=linear_data.get("id", ""),
|
|
198
|
+
identifier=linear_data.get("identifier", ""),
|
|
199
|
+
title=linear_data.get("title", ""),
|
|
200
|
+
description=linear_data.get("description", ""),
|
|
201
|
+
status=IssueStatus.from_linear_state(state_name),
|
|
202
|
+
priority=Priority.from_linear_priority(linear_data.get("priority", 4)),
|
|
203
|
+
parent_id=linear_data.get("parent", {}).get("id") if linear_data.get("parent") else None,
|
|
204
|
+
labels=[lbl.get("name", "") for lbl in linear_data.get("labels", {}).get("nodes", [])],
|
|
205
|
+
assigned_agent=None, # Linear doesn't track Anvil agents
|
|
206
|
+
created_at=datetime.fromisoformat(
|
|
207
|
+
linear_data.get("createdAt", "").replace('Z', '+00:00')
|
|
208
|
+
) if linear_data.get("createdAt") else datetime.now(timezone.utc),
|
|
209
|
+
updated_at=datetime.fromisoformat(
|
|
210
|
+
linear_data.get("updatedAt", "").replace('Z', '+00:00')
|
|
211
|
+
) if linear_data.get("updatedAt") else datetime.now(timezone.utc),
|
|
212
|
+
completed_at=datetime.fromisoformat(
|
|
213
|
+
linear_data.get("completedAt", "").replace('Z', '+00:00')
|
|
214
|
+
) if linear_data.get("completedAt") else None,
|
|
215
|
+
estimate=linear_data.get("estimate"),
|
|
216
|
+
provider="linear",
|
|
217
|
+
external_id=linear_data.get("id"),
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
def is_ready(self) -> bool:
|
|
221
|
+
"""Check if issue is ready to work on (Todo, no blocking deps)."""
|
|
222
|
+
return self.status == IssueStatus.TODO
|
|
223
|
+
|
|
224
|
+
def is_active(self) -> bool:
|
|
225
|
+
"""Check if issue is actively being worked on."""
|
|
226
|
+
return self.status in (IssueStatus.IN_PROGRESS, IssueStatus.IN_REVIEW)
|
|
227
|
+
|
|
228
|
+
def is_complete(self) -> bool:
|
|
229
|
+
"""Check if issue is finished."""
|
|
230
|
+
return self.status in (IssueStatus.DONE, IssueStatus.CANCELLED)
|
|
231
|
+
|
|
232
|
+
def claim(self, agent_id: str) -> None:
|
|
233
|
+
"""Assign this issue to an agent."""
|
|
234
|
+
self.assigned_agent = agent_id
|
|
235
|
+
self.updated_at = datetime.now(timezone.utc)
|
|
236
|
+
|
|
237
|
+
def unclaim(self) -> None:
|
|
238
|
+
"""Remove agent assignment."""
|
|
239
|
+
self.assigned_agent = None
|
|
240
|
+
self.updated_at = datetime.now(timezone.utc)
|
|
241
|
+
|
|
242
|
+
def transition_to(self, new_status: IssueStatus) -> None:
|
|
243
|
+
"""Transition issue to a new status."""
|
|
244
|
+
self.status = new_status
|
|
245
|
+
self.updated_at = datetime.now(timezone.utc)
|
|
246
|
+
if new_status == IssueStatus.DONE:
|
|
247
|
+
self.completed_at = datetime.now(timezone.utc)
|
|
248
|
+
elif self.completed_at and new_status != IssueStatus.CANCELLED:
|
|
249
|
+
self.completed_at = None # Reopen
|
|
250
|
+
|
|
251
|
+
def __str__(self) -> str:
|
|
252
|
+
"""String representation for display."""
|
|
253
|
+
agent_str = f" [{self.assigned_agent}]" if self.assigned_agent else ""
|
|
254
|
+
return f"{self.identifier}: {self.title} ({self.status.to_display()}){agent_str}"
|
|
255
|
+
|
|
256
|
+
def __repr__(self) -> str:
|
|
257
|
+
return f"Issue(identifier={self.identifier!r}, title={self.title!r}, status={self.status})"
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"""
|
|
2
|
+
IssueProvider protocol for Anvil Framework.
|
|
3
|
+
|
|
4
|
+
Defines the abstract interface for issue tracking backends.
|
|
5
|
+
Implementations include LocalJsonProvider and LinearProvider.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from issue_provider import IssueProvider, get_provider
|
|
9
|
+
|
|
10
|
+
# Auto-detect provider based on project config
|
|
11
|
+
provider = get_provider(Path.cwd())
|
|
12
|
+
|
|
13
|
+
# List issues
|
|
14
|
+
issues = provider.list_issues(status=IssueStatus.TODO)
|
|
15
|
+
|
|
16
|
+
# Create issue
|
|
17
|
+
issue = provider.create_issue(title="New feature", priority=Priority.HIGH)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from abc import ABC, abstractmethod
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Optional, Protocol, runtime_checkable
|
|
23
|
+
try:
|
|
24
|
+
import yaml
|
|
25
|
+
except ImportError:
|
|
26
|
+
yaml = None # YAML is optional for config loading
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
from .issue_models import Issue, IssueStatus, Priority
|
|
30
|
+
except ImportError:
|
|
31
|
+
from issue_models import Issue, IssueStatus, Priority
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@runtime_checkable
|
|
35
|
+
class IssueProvider(Protocol):
|
|
36
|
+
"""
|
|
37
|
+
Abstract interface for issue tracking backends.
|
|
38
|
+
|
|
39
|
+
This protocol defines the contract that all issue providers must implement.
|
|
40
|
+
Currently supported providers:
|
|
41
|
+
- LocalJsonProvider: File-based storage in ~/.anvil/issues/
|
|
42
|
+
- LinearProvider: Adapter for Linear API via linear-skill
|
|
43
|
+
|
|
44
|
+
The provider pattern allows Anvil commands (/orient, /sprint, /ready)
|
|
45
|
+
to work identically regardless of the underlying storage backend.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def name(self) -> str:
|
|
50
|
+
"""Provider identifier (e.g., 'local', 'linear')."""
|
|
51
|
+
...
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def is_available(self) -> bool:
|
|
55
|
+
"""Check if provider is configured and accessible."""
|
|
56
|
+
...
|
|
57
|
+
|
|
58
|
+
# ─────────────────────────────────────────────────────────────────────
|
|
59
|
+
# Read Operations
|
|
60
|
+
# ─────────────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
def list_issues(
|
|
63
|
+
self,
|
|
64
|
+
status: Optional[IssueStatus] = None,
|
|
65
|
+
project: Optional[str] = None,
|
|
66
|
+
limit: int = 50
|
|
67
|
+
) -> list[Issue]:
|
|
68
|
+
"""
|
|
69
|
+
List issues, optionally filtered by status.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
status: Filter to specific status (None = all)
|
|
73
|
+
project: Filter to specific project (provider-dependent)
|
|
74
|
+
limit: Maximum number of issues to return
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
List of Issue objects, sorted by priority then updated_at
|
|
78
|
+
"""
|
|
79
|
+
...
|
|
80
|
+
|
|
81
|
+
def get_issue(self, identifier: str) -> Optional[Issue]:
|
|
82
|
+
"""
|
|
83
|
+
Get a single issue by identifier.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
identifier: Human-readable identifier (e.g., 'ANV-72', 'LOCAL-001')
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Issue object or None if not found
|
|
90
|
+
"""
|
|
91
|
+
...
|
|
92
|
+
|
|
93
|
+
def get_ready_issues(self) -> list[Issue]:
|
|
94
|
+
"""
|
|
95
|
+
Get issues that are ready to work on.
|
|
96
|
+
|
|
97
|
+
Returns issues in Todo status with no blocking dependencies.
|
|
98
|
+
Sorted by priority (P0 first) then by age (oldest first).
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
List of Issue objects ready to be claimed
|
|
102
|
+
"""
|
|
103
|
+
...
|
|
104
|
+
|
|
105
|
+
def get_in_progress_issues(self) -> list[Issue]:
|
|
106
|
+
"""
|
|
107
|
+
Get issues currently being worked on.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
List of Issue objects with In Progress or In Review status
|
|
111
|
+
"""
|
|
112
|
+
...
|
|
113
|
+
|
|
114
|
+
# ─────────────────────────────────────────────────────────────────────
|
|
115
|
+
# Write Operations
|
|
116
|
+
# ─────────────────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
def create_issue(
|
|
119
|
+
self,
|
|
120
|
+
title: str,
|
|
121
|
+
description: str = "",
|
|
122
|
+
status: IssueStatus = IssueStatus.TODO,
|
|
123
|
+
priority: Priority = Priority.MEDIUM,
|
|
124
|
+
parent_id: Optional[str] = None,
|
|
125
|
+
labels: Optional[list[str]] = None
|
|
126
|
+
) -> Issue:
|
|
127
|
+
"""
|
|
128
|
+
Create a new issue.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
title: Issue title
|
|
132
|
+
description: Detailed description (markdown supported)
|
|
133
|
+
status: Initial status (default: Todo)
|
|
134
|
+
priority: Priority level (default: Medium/P2)
|
|
135
|
+
parent_id: Parent issue identifier for sub-issues
|
|
136
|
+
labels: List of label names
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Created Issue object with generated ID
|
|
140
|
+
"""
|
|
141
|
+
...
|
|
142
|
+
|
|
143
|
+
def update_issue(
|
|
144
|
+
self,
|
|
145
|
+
identifier: str,
|
|
146
|
+
title: Optional[str] = None,
|
|
147
|
+
description: Optional[str] = None,
|
|
148
|
+
status: Optional[IssueStatus] = None,
|
|
149
|
+
priority: Optional[Priority] = None,
|
|
150
|
+
labels: Optional[list[str]] = None
|
|
151
|
+
) -> Issue:
|
|
152
|
+
"""
|
|
153
|
+
Update an existing issue.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
identifier: Issue identifier to update
|
|
157
|
+
title: New title (None = no change)
|
|
158
|
+
description: New description (None = no change)
|
|
159
|
+
status: New status (None = no change)
|
|
160
|
+
priority: New priority (None = no change)
|
|
161
|
+
labels: New labels (None = no change)
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Updated Issue object
|
|
165
|
+
|
|
166
|
+
Raises:
|
|
167
|
+
KeyError: If issue not found
|
|
168
|
+
"""
|
|
169
|
+
...
|
|
170
|
+
|
|
171
|
+
def delete_issue(self, identifier: str) -> bool:
|
|
172
|
+
"""
|
|
173
|
+
Delete an issue.
|
|
174
|
+
|
|
175
|
+
For most providers, this is a soft delete (marks as cancelled).
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
identifier: Issue identifier to delete
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
True if deleted, False if not found
|
|
182
|
+
"""
|
|
183
|
+
...
|
|
184
|
+
|
|
185
|
+
# ─────────────────────────────────────────────────────────────────────
|
|
186
|
+
# Agent Integration
|
|
187
|
+
# ─────────────────────────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
def assign_to_agent(self, identifier: str, agent_id: str) -> Issue:
|
|
190
|
+
"""
|
|
191
|
+
Assign an issue to an Anvil agent.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
identifier: Issue identifier
|
|
195
|
+
agent_id: Anvil agent ID (e.g., 'swift-falcon-a3f2')
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Updated Issue object
|
|
199
|
+
|
|
200
|
+
Raises:
|
|
201
|
+
KeyError: If issue not found
|
|
202
|
+
"""
|
|
203
|
+
...
|
|
204
|
+
|
|
205
|
+
def unassign_agent(self, identifier: str) -> Issue:
|
|
206
|
+
"""
|
|
207
|
+
Remove agent assignment from an issue.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
identifier: Issue identifier
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Updated Issue object
|
|
214
|
+
|
|
215
|
+
Raises:
|
|
216
|
+
KeyError: If issue not found
|
|
217
|
+
"""
|
|
218
|
+
...
|
|
219
|
+
|
|
220
|
+
def get_agent_issues(self, agent_id: str) -> list[Issue]:
|
|
221
|
+
"""
|
|
222
|
+
Get all issues assigned to a specific agent.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
agent_id: Anvil agent ID
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
List of Issue objects assigned to this agent
|
|
229
|
+
"""
|
|
230
|
+
...
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class BaseProvider(ABC):
|
|
234
|
+
"""
|
|
235
|
+
Base class for issue providers with common functionality.
|
|
236
|
+
|
|
237
|
+
Provides default implementations for some methods and
|
|
238
|
+
shared utilities for concrete providers.
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
@property
|
|
242
|
+
@abstractmethod
|
|
243
|
+
def name(self) -> str:
|
|
244
|
+
"""Provider identifier."""
|
|
245
|
+
pass
|
|
246
|
+
|
|
247
|
+
@property
|
|
248
|
+
def is_available(self) -> bool:
|
|
249
|
+
"""Default: always available. Override for remote providers."""
|
|
250
|
+
return True
|
|
251
|
+
|
|
252
|
+
def get_ready_issues(self) -> list[Issue]:
|
|
253
|
+
"""Default implementation: Todo issues sorted by priority."""
|
|
254
|
+
issues = self.list_issues(status=IssueStatus.TODO)
|
|
255
|
+
# Sort by priority (lower = higher priority), then by age
|
|
256
|
+
return sorted(issues, key=lambda i: (i.priority.value, i.created_at))
|
|
257
|
+
|
|
258
|
+
def get_in_progress_issues(self) -> list[Issue]:
|
|
259
|
+
"""Default implementation: In Progress + In Review issues."""
|
|
260
|
+
in_progress = self.list_issues(status=IssueStatus.IN_PROGRESS)
|
|
261
|
+
in_review = self.list_issues(status=IssueStatus.IN_REVIEW)
|
|
262
|
+
return in_progress + in_review
|
|
263
|
+
|
|
264
|
+
def get_agent_issues(self, agent_id: str) -> list[Issue]:
|
|
265
|
+
"""Default implementation: filter all issues by agent."""
|
|
266
|
+
all_issues = self.list_issues()
|
|
267
|
+
return [i for i in all_issues if i.assigned_agent == agent_id]
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def get_provider(project_path: Optional[Path] = None) -> IssueProvider:
|
|
271
|
+
"""
|
|
272
|
+
Get the appropriate issue provider for a project.
|
|
273
|
+
|
|
274
|
+
Auto-detection logic:
|
|
275
|
+
1. If .claude/linear.yaml exists → LinearProvider
|
|
276
|
+
2. Otherwise → LocalJsonProvider
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
project_path: Project root directory (default: current directory)
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
Configured IssueProvider instance
|
|
283
|
+
"""
|
|
284
|
+
if project_path is None:
|
|
285
|
+
project_path = Path.cwd()
|
|
286
|
+
|
|
287
|
+
linear_config = project_path / ".claude" / "linear.yaml"
|
|
288
|
+
|
|
289
|
+
if linear_config.exists():
|
|
290
|
+
try:
|
|
291
|
+
if yaml is None:
|
|
292
|
+
raise ImportError("PyYAML required for Linear config")
|
|
293
|
+
config = yaml.safe_load(linear_config.read_text())
|
|
294
|
+
# Import here to avoid circular imports
|
|
295
|
+
try:
|
|
296
|
+
from .linear_provider import LinearProvider
|
|
297
|
+
except ImportError:
|
|
298
|
+
from linear_provider import LinearProvider
|
|
299
|
+
return LinearProvider(
|
|
300
|
+
team_key=config.get("team_key", ""),
|
|
301
|
+
team_id=config.get("team_id", "")
|
|
302
|
+
)
|
|
303
|
+
except Exception:
|
|
304
|
+
pass # Fall through to local provider
|
|
305
|
+
|
|
306
|
+
# Default to local provider
|
|
307
|
+
try:
|
|
308
|
+
from .local_provider import LocalJsonProvider
|
|
309
|
+
except ImportError:
|
|
310
|
+
from local_provider import LocalJsonProvider
|
|
311
|
+
return LocalJsonProvider(project_path)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def get_provider_name(project_path: Optional[Path] = None) -> str:
|
|
315
|
+
"""
|
|
316
|
+
Get the name of the provider that would be used for a project.
|
|
317
|
+
|
|
318
|
+
Useful for displaying provider info without fully initializing it.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
project_path: Project root directory (default: current directory)
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
Provider name string ('linear' or 'local')
|
|
325
|
+
"""
|
|
326
|
+
if project_path is None:
|
|
327
|
+
project_path = Path.cwd()
|
|
328
|
+
|
|
329
|
+
linear_config = project_path / ".claude" / "linear.yaml"
|
|
330
|
+
|
|
331
|
+
if linear_config.exists():
|
|
332
|
+
try:
|
|
333
|
+
config = yaml.safe_load(linear_config.read_text())
|
|
334
|
+
team_key = config.get("team_key", "")
|
|
335
|
+
return f"linear:{team_key}" if team_key else "linear"
|
|
336
|
+
except Exception:
|
|
337
|
+
pass
|
|
338
|
+
|
|
339
|
+
return "local"
|