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,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"