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,1202 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ ralph_state.py - Ralph Wiggum State Management (ANV-164)
4
+
5
+ Python module for managing Ralph Wiggum autonomous execution state.
6
+ Provides a clean API for the /ralph skill to interact with state files.
7
+
8
+ Usage:
9
+ from global.lib.ralph_state import RalphState, ContextCheckpoint
10
+
11
+ # Initialize a new Ralph session
12
+ state = RalphState.initialize(
13
+ task_name="Migrate Jest to Vitest",
14
+ objective="Convert all test files from Jest to Vitest",
15
+ todo_items=["Update config", "Migrate tests", "Run tests"]
16
+ )
17
+
18
+ # Load existing state
19
+ state = RalphState.load()
20
+
21
+ # Update state
22
+ state.increment_iteration()
23
+ state.update_status("completed")
24
+ state.save()
25
+
26
+ # Get status report
27
+ print(state.status_report())
28
+ """
29
+
30
+ import json
31
+ from dataclasses import dataclass, field
32
+ from datetime import datetime, timezone
33
+ from pathlib import Path
34
+ import importlib.util
35
+ import re
36
+ from typing import Any, Dict, List, Optional, Tuple
37
+
38
+
39
+ # =============================================================================
40
+ # Configuration
41
+ # =============================================================================
42
+
43
+ DEFAULT_STATE_FILE = ".claude/ralph-state.json"
44
+ DEFAULT_MAX_ITERATIONS = 50
45
+ DEFAULT_COMPLETION_PROMISE = "COMPLETE"
46
+ DEFAULT_PROGRESS_FILE = "progress.txt"
47
+ DEFAULT_FIX_PLAN_FILE = "fix_plan.md"
48
+ DEFAULT_PROMPT_FILE = "PROMPT.md"
49
+
50
+
51
+
52
+
53
+ # =============================================================================
54
+ # Linear Provider Dynamic Import
55
+ # =============================================================================
56
+
57
+
58
+ def _get_linear_provider():
59
+ """Dynamically import LinearProvider to avoid 'global' keyword issues."""
60
+ lib_dir = Path(__file__).parent
61
+ spec = importlib.util.spec_from_file_location("linear_provider", lib_dir / "linear_provider.py")
62
+ if spec is None or spec.loader is None:
63
+ raise ImportError(f"Could not find linear_provider.py in {lib_dir}")
64
+ module = importlib.util.module_from_spec(spec)
65
+ spec.loader.exec_module(module)
66
+ return module.LinearProvider
67
+
68
+
69
+ def _get_linear_config() -> Dict[str, str]:
70
+ """Load Linear team configuration from .claude/linear.yaml.
71
+
72
+ Returns:
73
+ Dict with team_key and team_id, or empty dict if not configured.
74
+
75
+ Fixes ANV-120: Ensures LinearProvider always gets proper team context
76
+ to prevent state UUID cache discrepancies between teams.
77
+ """
78
+ try:
79
+ import yaml
80
+ except ImportError:
81
+ return {}
82
+
83
+ config_path = Path(".claude/linear.yaml")
84
+ if not config_path.exists():
85
+ return {}
86
+
87
+ try:
88
+ config = yaml.safe_load(config_path.read_text())
89
+ return {
90
+ "team_key": config.get("team_key", ""),
91
+ "team_id": config.get("team_id", ""),
92
+ }
93
+ except Exception:
94
+ return {}
95
+
96
+
97
+ def _create_linear_provider():
98
+ """Create a LinearProvider instance with proper team configuration.
99
+
100
+ This is the preferred way to instantiate LinearProvider in ralph_state.py.
101
+ It loads team config from .claude/linear.yaml to ensure the state cache
102
+ is populated with the correct team's workflow state UUIDs.
103
+
104
+ Fixes ANV-120: Linear state UUID caching causes team/state discrepancy errors.
105
+ """
106
+ LinearProvider = _get_linear_provider()
107
+ config = _get_linear_config()
108
+ return LinearProvider(
109
+ team_key=config.get("team_key", ""),
110
+ team_id=config.get("team_id", ""),
111
+ )
112
+
113
+
114
+ # =============================================================================
115
+ # Linear Integration Data Classes
116
+ # =============================================================================
117
+
118
+
119
+ @dataclass
120
+ class LinearSubtask:
121
+ """A Linear subtask tracked by Ralph."""
122
+ id: str
123
+ identifier: str
124
+ title: str
125
+ status: str = "todo"
126
+ completed_at: Optional[str] = None
127
+ skip_reason: Optional[str] = None
128
+
129
+ def to_dict(self) -> Dict[str, Any]:
130
+ """Serialize to dictionary."""
131
+ return {"id": self.id, "identifier": self.identifier, "title": self.title,
132
+ "status": self.status, "completed_at": self.completed_at, "skip_reason": self.skip_reason}
133
+
134
+ @classmethod
135
+ def from_dict(cls, data: Dict[str, Any]) -> "LinearSubtask":
136
+ """Deserialize from dictionary."""
137
+ return cls(id=data["id"], identifier=data["identifier"], title=data["title"],
138
+ status=data.get("status", "todo"), completed_at=data.get("completed_at"),
139
+ skip_reason=data.get("skip_reason"))
140
+
141
+
142
+ @dataclass
143
+ class LinearIntegration:
144
+ """Linear integration state for Ralph."""
145
+ enabled: bool = False
146
+ mode: str = "issue"
147
+ parent_issue: Optional[str] = None
148
+ parent_id: Optional[str] = None
149
+ project_name: Optional[str] = None
150
+ subtasks: List[LinearSubtask] = None
151
+ last_sync: str = ""
152
+ no_sync: bool = False
153
+
154
+ def __post_init__(self):
155
+ if self.subtasks is None:
156
+ self.subtasks = []
157
+
158
+ def to_dict(self) -> Dict[str, Any]:
159
+ """Serialize to dictionary."""
160
+ return {"enabled": self.enabled, "mode": self.mode, "parent_issue": self.parent_issue,
161
+ "parent_id": self.parent_id, "project_name": self.project_name,
162
+ "subtasks": [s.to_dict() for s in self.subtasks], "last_sync": self.last_sync,
163
+ "no_sync": self.no_sync}
164
+
165
+ @classmethod
166
+ def from_dict(cls, data: Dict[str, Any]) -> "LinearIntegration":
167
+ """Deserialize from dictionary."""
168
+ subtasks = [LinearSubtask.from_dict(s) for s in data.get("subtasks", [])]
169
+ return cls(enabled=data.get("enabled", False), mode=data.get("mode", "issue"),
170
+ parent_issue=data.get("parent_issue"), parent_id=data.get("parent_id"),
171
+ project_name=data.get("project_name"), subtasks=subtasks,
172
+ last_sync=data.get("last_sync", ""), no_sync=data.get("no_sync", False))
173
+
174
+
175
+ # =============================================================================
176
+ # Data Classes
177
+ # =============================================================================
178
+
179
+
180
+ @dataclass
181
+ class ContextCheckpoint:
182
+ """Context checkpoint state for CCS integration (ANV-198).
183
+
184
+ Tracks checkpoint information when context limits are approached,
185
+ enabling seamless session handoffs during Ralph autonomous execution.
186
+ """
187
+
188
+ active: bool = False
189
+ level: str = "" # L1, L2, L3
190
+ percent_at_checkpoint: int = 0
191
+ timestamp: str = ""
192
+ handoff_file: str = ""
193
+ resume_summary: str = ""
194
+ files_in_progress: List[Dict[str, Any]] = field(default_factory=list)
195
+ current_todo_item: str = ""
196
+ progress_on_item: str = ""
197
+
198
+ def to_dict(self) -> Dict[str, Any]:
199
+ """Convert to dictionary for serialization."""
200
+ return {
201
+ "active": self.active,
202
+ "level": self.level,
203
+ "percent_at_checkpoint": self.percent_at_checkpoint,
204
+ "timestamp": self.timestamp,
205
+ "handoff_file": self.handoff_file,
206
+ "resume_summary": self.resume_summary,
207
+ "files_in_progress": self.files_in_progress,
208
+ "current_todo_item": self.current_todo_item,
209
+ "progress_on_item": self.progress_on_item,
210
+ }
211
+
212
+ @classmethod
213
+ def from_dict(cls, data: Dict[str, Any]) -> "ContextCheckpoint":
214
+ """Create from dictionary."""
215
+ return cls(
216
+ active=data.get("active", False),
217
+ level=data.get("level", ""),
218
+ percent_at_checkpoint=data.get("percent_at_checkpoint", 0),
219
+ timestamp=data.get("timestamp", ""),
220
+ handoff_file=data.get("handoff_file", ""),
221
+ resume_summary=data.get("resume_summary", ""),
222
+ files_in_progress=data.get("files_in_progress", []),
223
+ current_todo_item=data.get("current_todo_item", ""),
224
+ progress_on_item=data.get("progress_on_item", ""),
225
+ )
226
+
227
+
228
+ @dataclass
229
+ class RalphState:
230
+ """Ralph Wiggum execution state."""
231
+
232
+ task_name: str
233
+ objective: str
234
+ iteration: int = 0
235
+ started_at: str = ""
236
+ status: str = "running"
237
+ no_change_count: int = 0
238
+ last_diff_hash: str = ""
239
+ error_hashes: List[str] = field(default_factory=list)
240
+ max_iterations: int = DEFAULT_MAX_ITERATIONS
241
+ completion_promise: str = DEFAULT_COMPLETION_PROMISE
242
+ todo_items: List[str] = field(default_factory=list)
243
+ completed_items: List[str] = field(default_factory=list)
244
+
245
+ # CCS checkpoint state (ANV-198)
246
+ context_checkpoint: Optional[ContextCheckpoint] = None
247
+ context_history: List[Dict[str, Any]] = field(default_factory=list)
248
+
249
+ # Linear integration (ANV-211)
250
+ linear_integration: Optional[LinearIntegration] = None
251
+
252
+ # File paths
253
+ state_file: str = DEFAULT_STATE_FILE
254
+ progress_file: str = DEFAULT_PROGRESS_FILE
255
+ fix_plan_file: str = DEFAULT_FIX_PLAN_FILE
256
+ prompt_file: str = DEFAULT_PROMPT_FILE
257
+
258
+ @classmethod
259
+ def initialize(
260
+ cls,
261
+ task_name: str,
262
+ objective: str,
263
+ todo_items: Optional[List[str]] = None,
264
+ max_iterations: int = DEFAULT_MAX_ITERATIONS,
265
+ completion_promise: str = DEFAULT_COMPLETION_PROMISE,
266
+ state_file: str = DEFAULT_STATE_FILE,
267
+ linear_integration: Optional[LinearIntegration] = None,
268
+ ) -> "RalphState":
269
+ """Initialize a new Ralph session."""
270
+ now = datetime.now(timezone.utc).isoformat()
271
+
272
+ state = cls(
273
+ task_name=task_name,
274
+ objective=objective,
275
+ iteration=0,
276
+ started_at=now,
277
+ status="running",
278
+ no_change_count=0,
279
+ last_diff_hash="",
280
+ error_hashes=[],
281
+ max_iterations=max_iterations,
282
+ completion_promise=completion_promise,
283
+ todo_items=todo_items or [],
284
+ completed_items=[],
285
+ context_checkpoint=None,
286
+ context_history=[],
287
+ linear_integration=linear_integration,
288
+ state_file=state_file,
289
+ )
290
+
291
+ # Ensure directory exists
292
+ Path(state_file).parent.mkdir(parents=True, exist_ok=True)
293
+
294
+ state.save()
295
+ return state
296
+
297
+ @classmethod
298
+ def load(cls, state_file: str = DEFAULT_STATE_FILE) -> Optional["RalphState"]:
299
+ """Load existing Ralph state from file."""
300
+ if not Path(state_file).exists():
301
+ return None
302
+
303
+ try:
304
+ with open(state_file, "r") as f:
305
+ data = json.load(f)
306
+
307
+ # Handle legacy state files without all fields
308
+ # Parse context_checkpoint if present (ANV-198)
309
+ checkpoint_data = data.get("context_checkpoint")
310
+ context_checkpoint = (
311
+ ContextCheckpoint.from_dict(checkpoint_data)
312
+ if checkpoint_data
313
+ else None
314
+ )
315
+
316
+ # Parse linear_integration if present (ANV-211)
317
+ linear_data = data.get("linear_integration")
318
+ linear_integration = (
319
+ LinearIntegration.from_dict(linear_data)
320
+ if linear_data
321
+ else None
322
+ )
323
+
324
+ return cls(
325
+ task_name=data.get("task_name", "unknown"),
326
+ objective=data.get("objective", ""),
327
+ iteration=data.get("iteration", 0),
328
+ started_at=data.get("started_at", ""),
329
+ status=data.get("status", "unknown"),
330
+ no_change_count=data.get("no_change_count", 0),
331
+ last_diff_hash=data.get("last_diff_hash", ""),
332
+ error_hashes=data.get("error_hashes", []),
333
+ max_iterations=data.get("max_iterations", DEFAULT_MAX_ITERATIONS),
334
+ completion_promise=data.get(
335
+ "completion_promise", DEFAULT_COMPLETION_PROMISE
336
+ ),
337
+ todo_items=data.get("todo_items", []),
338
+ completed_items=data.get("completed_items", []),
339
+ context_checkpoint=context_checkpoint,
340
+ context_history=data.get("context_history", []),
341
+ linear_integration=linear_integration,
342
+ state_file=state_file,
343
+ progress_file=data.get("progress_file", DEFAULT_PROGRESS_FILE),
344
+ fix_plan_file=data.get("fix_plan_file", DEFAULT_FIX_PLAN_FILE),
345
+ prompt_file=data.get("prompt_file", DEFAULT_PROMPT_FILE),
346
+ )
347
+ except (json.JSONDecodeError, KeyError) as e:
348
+ print(f"Error loading state: {e}")
349
+ return None
350
+
351
+ @classmethod
352
+ def exists(cls, state_file: str = DEFAULT_STATE_FILE) -> bool:
353
+ """Check if a Ralph session is active."""
354
+ return Path(state_file).exists()
355
+
356
+ def save(self) -> None:
357
+ """Save state to file."""
358
+ Path(self.state_file).parent.mkdir(parents=True, exist_ok=True)
359
+
360
+ data = {
361
+ "task_name": self.task_name,
362
+ "objective": self.objective,
363
+ "iteration": self.iteration,
364
+ "started_at": self.started_at,
365
+ "status": self.status,
366
+ "no_change_count": self.no_change_count,
367
+ "last_diff_hash": self.last_diff_hash,
368
+ "error_hashes": self.error_hashes,
369
+ "max_iterations": self.max_iterations,
370
+ "completion_promise": self.completion_promise,
371
+ "todo_items": self.todo_items,
372
+ "completed_items": self.completed_items,
373
+ "progress_file": self.progress_file,
374
+ "fix_plan_file": self.fix_plan_file,
375
+ "prompt_file": self.prompt_file,
376
+ # CCS checkpoint state (ANV-198)
377
+ "context_checkpoint": (
378
+ self.context_checkpoint.to_dict()
379
+ if self.context_checkpoint
380
+ else None
381
+ ),
382
+ "context_history": self.context_history,
383
+ # Linear integration (ANV-211)
384
+ "linear_integration": (
385
+ self.linear_integration.to_dict()
386
+ if self.linear_integration
387
+ else None
388
+ ),
389
+ }
390
+
391
+ with open(self.state_file, "w") as f:
392
+ json.dump(data, f, indent=2)
393
+
394
+ def delete(self) -> None:
395
+ """Delete state file (cleanup)."""
396
+ if Path(self.state_file).exists():
397
+ Path(self.state_file).unlink()
398
+
399
+ # =========================================================================
400
+ # State Updates
401
+ # =========================================================================
402
+
403
+ def increment_iteration(self) -> int:
404
+ """Increment iteration counter and save."""
405
+ self.iteration += 1
406
+ self.save()
407
+ return self.iteration
408
+
409
+ def update_status(self, status: str) -> None:
410
+ """Update status and save."""
411
+ valid_statuses = [
412
+ "running",
413
+ "completed",
414
+ "fatal_error",
415
+ "circuit_breaker",
416
+ "max_iterations",
417
+ "stopped",
418
+ "interrupted",
419
+ ]
420
+ if status not in valid_statuses:
421
+ raise ValueError(
422
+ f"Invalid status: {status}. Must be one of {valid_statuses}"
423
+ )
424
+
425
+ self.status = status
426
+ self.save()
427
+
428
+ def mark_item_complete(self, item: str) -> None:
429
+ """Mark a todo item as complete."""
430
+ if item in self.todo_items:
431
+ self.todo_items.remove(item)
432
+ if item not in self.completed_items:
433
+ self.completed_items.append(item)
434
+ self.save()
435
+
436
+ def add_todo_item(self, item: str) -> None:
437
+ """Add a new todo item."""
438
+ if item not in self.todo_items:
439
+ self.todo_items.append(item)
440
+ self.save()
441
+
442
+ def record_no_change(self, diff_hash: str) -> int:
443
+ """Record a no-change iteration, return count."""
444
+ if diff_hash == self.last_diff_hash and self.last_diff_hash:
445
+ self.no_change_count += 1
446
+ else:
447
+ self.no_change_count = 0
448
+ self.last_diff_hash = diff_hash
449
+ self.save()
450
+ return self.no_change_count
451
+
452
+ def record_error(self, error_hash: str) -> int:
453
+ """Record an error hash, return count of this specific error."""
454
+ self.error_hashes.append(error_hash)
455
+ count = self.error_hashes.count(error_hash)
456
+ self.save()
457
+ return count
458
+
459
+ # =========================================================================
460
+ # Queries
461
+ # =========================================================================
462
+
463
+ @property
464
+ def remaining_items(self) -> int:
465
+ """Number of remaining todo items."""
466
+ return len(self.todo_items)
467
+
468
+ @property
469
+ def total_items(self) -> int:
470
+ """Total number of items (complete + remaining)."""
471
+ return len(self.todo_items) + len(self.completed_items)
472
+
473
+ @property
474
+ def progress_percent(self) -> float:
475
+ """Completion percentage."""
476
+ total = self.total_items
477
+ if total == 0:
478
+ return 0.0
479
+ return (len(self.completed_items) / total) * 100
480
+
481
+ @property
482
+ def is_complete(self) -> bool:
483
+ """Check if all items are complete."""
484
+ return len(self.todo_items) == 0 and len(self.completed_items) > 0
485
+
486
+ @property
487
+ def duration_minutes(self) -> float:
488
+ """Duration since start in minutes."""
489
+ if not self.started_at:
490
+ return 0.0
491
+ try:
492
+ start = datetime.fromisoformat(self.started_at.replace("Z", "+00:00"))
493
+ now = datetime.now(timezone.utc)
494
+ return (now - start).total_seconds() / 60
495
+ except ValueError:
496
+ return 0.0
497
+
498
+ # =========================================================================
499
+ # Reporting
500
+ # =========================================================================
501
+
502
+ def status_report(self) -> str:
503
+ """Generate a status report for display."""
504
+ duration = self.duration_minutes
505
+
506
+ if duration < 60:
507
+ duration_str = f"{int(duration)} minutes"
508
+ else:
509
+ hours = int(duration // 60)
510
+ mins = int(duration % 60)
511
+ duration_str = f"{hours}h {mins}m"
512
+
513
+ report = f"""## Ralph Wiggum Status
514
+
515
+ | Metric | Value |
516
+ |--------|-------|
517
+ | Status | {self.status.replace('_', ' ').title()} |
518
+ | Iteration | {self.iteration} of {self.max_iterations} |
519
+ | Started | {self.started_at} |
520
+ | Duration | {duration_str} |
521
+ | Items Complete | {len(self.completed_items)} of {self.total_items} |
522
+ | Progress | {self.progress_percent:.0f}% |
523
+ """
524
+
525
+ if self.completed_items:
526
+ report += "\n### Recently Completed\n"
527
+ for item in self.completed_items[-3:]:
528
+ report += f"- {item}\n"
529
+
530
+ if self.todo_items:
531
+ report += "\n### Remaining Items\n"
532
+ for item in self.todo_items[:5]:
533
+ report += f"- [ ] {item}\n"
534
+ if len(self.todo_items) > 5:
535
+ report += f"- ... and {len(self.todo_items) - 5} more\n"
536
+
537
+ if self.no_change_count > 0:
538
+ report += (
539
+ f"\n**Warning**: {self.no_change_count} iterations "
540
+ "with no file changes\n"
541
+ )
542
+
543
+ # Add Linear integration status (ANV-212)
544
+ if self.linear_integration and self.linear_integration.enabled:
545
+ completed, skipped, remaining = self.get_linear_progress()
546
+ sync_status = "Disabled" if self.linear_integration.no_sync else "Enabled"
547
+ parent = self.linear_integration.parent_issue or "Unknown"
548
+
549
+ report += f"""
550
+ ### Linear Integration
551
+ | Field | Value |
552
+ |-------|-------|
553
+ | Parent Issue | {parent} |
554
+ | Subtasks | {completed} done, {skipped} skipped, {remaining} remaining |
555
+ | Last Sync | {self.linear_integration.last_sync or "Never"} |
556
+ | Sync Status | {sync_status} |
557
+ """
558
+ # Show current subtask
559
+ next_subtask = self.get_next_subtask()
560
+ if next_subtask:
561
+ report += f"\n### Current Subtask\n[{next_subtask.identifier}] {next_subtask.title}\n"
562
+
563
+ return report
564
+
565
+ def stop_summary(self) -> str:
566
+ """Generate a summary for manual stop."""
567
+ return f"""## Ralph Wiggum Stopped
568
+
569
+ Task: {self.task_name}
570
+ Iterations completed: {self.iteration}
571
+ Items completed: {len(self.completed_items)} of {self.total_items}
572
+ Status: Manually stopped
573
+
574
+ ### Remaining items:
575
+ {chr(10).join(f'- [ ] {item}' for item in self.todo_items)}
576
+
577
+ To resume later, run: /ralph start "{self.objective}"
578
+ """
579
+
580
+
581
+
582
+ # =========================================================================
583
+ # Linear Integration (ANV-211)
584
+ # =========================================================================
585
+
586
+ @classmethod
587
+ def initialize_from_linear(
588
+ cls,
589
+ issue_id: str,
590
+ subtask_filter: Optional[str] = None,
591
+ no_sync: bool = False,
592
+ state_file: str = DEFAULT_STATE_FILE,
593
+ ) -> "RalphState":
594
+ """Initialize Ralph session from a Linear issue.
595
+
596
+ Args:
597
+ issue_id: Linear issue identifier (e.g., "ANV-209")
598
+ subtask_filter: Optional filter for subtasks (e.g., "ANV-1..ANV-5" or "ANV-1,ANV-3")
599
+ no_sync: If True, don't sync status updates back to Linear
600
+ state_file: Path to state file
601
+
602
+ Returns:
603
+ Initialized RalphState with Linear integration enabled
604
+ """
605
+ # ANV-120: Use _create_linear_provider() to ensure proper team config
606
+ provider = _create_linear_provider()
607
+
608
+ # Fetch issue details
609
+ issue = provider.get_issue(issue_id)
610
+ if not issue:
611
+ raise ValueError(f"Issue {issue_id} not found in Linear")
612
+
613
+ # Fetch subtasks (children) - Issue objects use attribute access
614
+ children = provider.get_children(issue.id)
615
+ if not children:
616
+ raise ValueError(f"Issue {issue_id} has no subtasks to process")
617
+
618
+ # Filter subtasks if specified
619
+ if subtask_filter:
620
+ children = cls._filter_subtasks(children, subtask_filter)
621
+ if not children:
622
+ raise ValueError(f"No subtasks match filter: {subtask_filter}")
623
+
624
+ # Build LinearSubtask objects from Issue dataclass instances
625
+ subtasks = []
626
+ todo_items = []
627
+ for child in children:
628
+ # Issue.status is an IssueStatus enum - check if done
629
+ child_status = child.status.value.lower() if child.status else ""
630
+ status = "completed" if child_status in ("done", "completed", "closed") else "todo"
631
+ subtask = LinearSubtask(
632
+ id=child.id,
633
+ identifier=child.identifier,
634
+ title=child.title,
635
+ status=status,
636
+ )
637
+ subtasks.append(subtask)
638
+ if status == "todo":
639
+ todo_items.append(f"[{child.identifier}] {child.title}")
640
+
641
+ # Create LinearIntegration
642
+ linear_integration = LinearIntegration(
643
+ enabled=True,
644
+ mode="issue",
645
+ parent_issue=issue.identifier,
646
+ parent_id=issue.id,
647
+ project_name=None, # Issue dataclass doesn't expose project directly
648
+ subtasks=subtasks,
649
+ last_sync=datetime.now(timezone.utc).isoformat(),
650
+ no_sync=no_sync,
651
+ )
652
+
653
+ # Initialize state
654
+ state = cls(
655
+ task_name=f"Linear: {issue.identifier}",
656
+ objective=issue.title or "Complete Linear subtasks",
657
+ iteration=0,
658
+ started_at=datetime.now(timezone.utc).isoformat(),
659
+ status="running",
660
+ no_change_count=0,
661
+ last_diff_hash="",
662
+ error_hashes=[],
663
+ max_iterations=DEFAULT_MAX_ITERATIONS,
664
+ completion_promise=DEFAULT_COMPLETION_PROMISE,
665
+ todo_items=todo_items,
666
+ completed_items=[],
667
+ context_checkpoint=None,
668
+ context_history=[],
669
+ linear_integration=linear_integration,
670
+ state_file=state_file,
671
+ )
672
+
673
+ Path(state_file).parent.mkdir(parents=True, exist_ok=True)
674
+ state.save()
675
+ return state
676
+
677
+ @staticmethod
678
+ def _filter_subtasks(
679
+ subtasks: List[Any], filter_spec: str
680
+ ) -> List[Any]:
681
+ """Filter subtasks by range or list specification.
682
+
683
+ Args:
684
+ subtasks: List of Issue objects from LinearProvider
685
+ filter_spec: Filter specification:
686
+ - Range: "ANV-1..ANV-5" (inclusive)
687
+ - List: "ANV-1,ANV-3,ANV-7"
688
+
689
+ Returns:
690
+ Filtered list of Issue objects
691
+ """
692
+ # Range syntax: PREFIX-START..PREFIX-END
693
+ range_match = re.match(r"([A-Z]+-)(\d+)\.\.\1(\d+)", filter_spec)
694
+ if range_match:
695
+ prefix = range_match.group(1)
696
+ start_num = int(range_match.group(2))
697
+ end_num = int(range_match.group(3))
698
+ valid_ids = {f"{prefix}{n}" for n in range(start_num, end_num + 1)}
699
+ return [s for s in subtasks if s.identifier in valid_ids]
700
+
701
+ # List syntax: ID1,ID2,ID3
702
+ if "," in filter_spec:
703
+ valid_ids = {id.strip() for id in filter_spec.split(",")}
704
+ return [s for s in subtasks if s.identifier in valid_ids]
705
+
706
+ # Single ID
707
+ return [s for s in subtasks if s.identifier == filter_spec]
708
+
709
+ def get_next_subtask(self) -> Optional[LinearSubtask]:
710
+ """Get the next uncompleted Linear subtask.
711
+
712
+ Returns:
713
+ Next LinearSubtask with status 'todo', or None if all complete
714
+ """
715
+ if not self.linear_integration or not self.linear_integration.enabled:
716
+ return None
717
+
718
+ for subtask in self.linear_integration.subtasks:
719
+ if subtask.status == "todo":
720
+ return subtask
721
+ return None
722
+
723
+ def mark_subtask_complete(
724
+ self,
725
+ identifier: str,
726
+ skip_reason: Optional[str] = None,
727
+ ) -> bool:
728
+ """Mark a Linear subtask as complete and sync to Linear.
729
+
730
+ Args:
731
+ identifier: Linear issue identifier (e.g., "ANV-212")
732
+ skip_reason: If provided, mark as skipped instead of completed
733
+
734
+ Returns:
735
+ True if subtask was found and updated
736
+ """
737
+ if not self.linear_integration or not self.linear_integration.enabled:
738
+ return False
739
+
740
+ for subtask in self.linear_integration.subtasks:
741
+ if subtask.identifier == identifier:
742
+ now = datetime.now(timezone.utc).isoformat()
743
+ if skip_reason:
744
+ subtask.status = "skipped"
745
+ subtask.skip_reason = skip_reason
746
+ else:
747
+ subtask.status = "completed"
748
+ subtask.completed_at = now
749
+
750
+ # Update todo_items
751
+ item_prefix = f"[{identifier}]"
752
+ self.todo_items = [t for t in self.todo_items if not t.startswith(item_prefix)]
753
+ if not skip_reason:
754
+ self.completed_items.append(f"[{identifier}] {subtask.title}")
755
+
756
+ # Sync to Linear if not disabled
757
+ if not self.linear_integration.no_sync and not skip_reason:
758
+ self.sync_to_linear(identifier, "done")
759
+
760
+ self.save()
761
+ return True
762
+ return False
763
+
764
+ def sync_to_linear(self, identifier: str, state_name: str) -> bool:
765
+ """Sync subtask status to Linear.
766
+
767
+ Args:
768
+ identifier: Linear issue identifier
769
+ state_name: Target state name ("done", "in_progress", etc.)
770
+
771
+ Returns:
772
+ True if sync succeeded
773
+ """
774
+ if not self.linear_integration or self.linear_integration.no_sync:
775
+ return False
776
+
777
+ try:
778
+ # ANV-120: Use _create_linear_provider() to ensure proper team config
779
+ provider = _create_linear_provider()
780
+
781
+ # Import IssueStatus for state conversion
782
+ try:
783
+ from .issue_models import IssueStatus
784
+ except ImportError:
785
+ from issue_models import IssueStatus
786
+
787
+ # Convert state name to IssueStatus enum
788
+ target_status = IssueStatus.from_linear_state(state_name)
789
+
790
+ # Update issue with the converted status
791
+ provider.update_issue(identifier, status=target_status)
792
+ self.linear_integration.last_sync = datetime.now(timezone.utc).isoformat()
793
+ self.save()
794
+ return True
795
+ except (ImportError, AttributeError, ValueError, OSError) as e:
796
+ # Log the error for debugging but don't crash
797
+ # Catch: ImportError (module issues), AttributeError (API mismatch),
798
+ # ValueError (bad data), OSError (network/auth failures)
799
+ print(f"Warning: Failed to sync {identifier} to Linear: {e}")
800
+ return False
801
+
802
+ def get_linear_progress(self) -> Tuple[int, int, int]:
803
+ """Get Linear subtask progress counts.
804
+
805
+ Returns:
806
+ Tuple of (completed, skipped, remaining)
807
+ """
808
+ if not self.linear_integration or not self.linear_integration.enabled:
809
+ return (0, 0, 0)
810
+
811
+ completed = sum(1 for s in self.linear_integration.subtasks if s.status == "completed")
812
+ skipped = sum(1 for s in self.linear_integration.subtasks if s.status == "skipped")
813
+ remaining = sum(1 for s in self.linear_integration.subtasks if s.status == "todo")
814
+ return (completed, skipped, remaining)
815
+
816
+
817
+ # =============================================================================
818
+ # File Generation
819
+ # =============================================================================
820
+
821
+
822
+ def create_fix_plan(
823
+ task_name: str,
824
+ objective: str,
825
+ todo_items: List[str],
826
+ completion_promise: str = DEFAULT_COMPLETION_PROMISE,
827
+ output_file: str = DEFAULT_FIX_PLAN_FILE,
828
+ ) -> None:
829
+ """Create fix_plan.md from template."""
830
+ timestamp = datetime.now(timezone.utc).isoformat()
831
+
832
+ items_md = "\n".join(f"- [ ] {item}" for item in todo_items)
833
+
834
+ content = f"""# Fix Plan: {task_name}
835
+
836
+ > Generated: {timestamp}
837
+ > Task: {objective}
838
+
839
+ ---
840
+
841
+ ## TODO Items
842
+
843
+ Complete these items **one at a time**, in order:
844
+
845
+ {items_md}
846
+
847
+ ---
848
+
849
+ ## Completion Criteria
850
+
851
+ All items must be checked AND:
852
+ - [ ] All tests passing
853
+ - [ ] All lint checks passing
854
+ - [ ] Code reviewed for quality
855
+
856
+ When complete, output: `<promise>{completion_promise}</promise>`
857
+
858
+ ---
859
+
860
+ ## Notes
861
+
862
+ - Mark items complete with `- [x]` when done
863
+ - Do NOT skip items - complete in order
864
+ - If stuck on an item for 3+ iterations, output `<fatal>reason</fatal>`
865
+ """
866
+
867
+ with open(output_file, "w") as f:
868
+ f.write(content)
869
+
870
+
871
+ def create_progress_file(
872
+ task_name: str,
873
+ objective: str,
874
+ total_items: int,
875
+ output_file: str = DEFAULT_PROGRESS_FILE,
876
+ ) -> None:
877
+ """Create progress.txt from template."""
878
+ timestamp = datetime.now(timezone.utc).isoformat()
879
+
880
+ content = f"""# Ralph Wiggum Progress Log
881
+
882
+ > Task: {task_name}
883
+ > Started: {timestamp}
884
+ > Objective: {objective}
885
+
886
+ ---
887
+
888
+ ## Iteration 0 - Initialization
889
+
890
+ ### Completed
891
+ - Created fix_plan.md with {total_items} items
892
+ - Initialized progress tracking
893
+
894
+ ### Next
895
+ - Begin with first TODO item
896
+
897
+ ### Blockers
898
+ - None
899
+
900
+ ---
901
+
902
+ <!-- Each iteration should append a new section below -->
903
+
904
+ """
905
+
906
+ with open(output_file, "w") as f:
907
+ f.write(content)
908
+
909
+
910
+ def create_prompt_file(
911
+ task_name: str,
912
+ objective: str,
913
+ iteration: int = 0,
914
+ last_action: str = "None (starting)",
915
+ remaining_count: int = 0,
916
+ total_items: int = 0,
917
+ started_at: str = "",
918
+ completion_promise: str = DEFAULT_COMPLETION_PROMISE,
919
+ additional_context: str = "",
920
+ output_file: str = DEFAULT_PROMPT_FILE,
921
+ ) -> None:
922
+ """Create PROMPT.md from template."""
923
+ timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
924
+
925
+ content = f"""# Task: {task_name}
926
+
927
+ ## Objective
928
+ {objective}
929
+
930
+ ## Current State
931
+ - **Iteration**: {iteration}
932
+ - **Last action**: {last_action}
933
+ - **Remaining items**: {remaining_count} of {total_items}
934
+ - **Started**: {started_at}
935
+
936
+ ---
937
+
938
+ ## Instructions
939
+
940
+ ### Step 1: Review Progress
941
+ 1. **Read `progress.txt`** first - learn from what previous iterations attempted
942
+ 2. Read `fix_plan.md` for the current TODO list
943
+
944
+ ### Step 2: Execute ONE Item
945
+ 3. Find the **first unchecked item** in fix_plan.md
946
+ 4. Complete **ONLY that one item** - do not attempt multiple items
947
+ 5. Run tests after completion
948
+
949
+ ### Step 3: Update State
950
+ 6. If tests pass, mark the item complete in fix_plan.md: `- [x]`
951
+ 7. **Update `progress.txt`** with what you did this iteration
952
+
953
+ ### Step 4: Check Completion
954
+ 8. If ALL items are complete and tests pass, output:
955
+ ```
956
+ <promise>{completion_promise}</promise>
957
+ ```
958
+
959
+ ---
960
+
961
+ ## Progress Tracking (REQUIRED)
962
+
963
+ After each iteration, **append** to `progress.txt`:
964
+
965
+ ```markdown
966
+ ## Iteration {iteration} - {timestamp}
967
+
968
+ ### Completed
969
+ - [What you finished this iteration]
970
+
971
+ ### Attempted (if any failed)
972
+ - [What you tried but couldn't complete]
973
+
974
+ ### Next
975
+ - [What the next iteration should focus on]
976
+
977
+ ### Blockers
978
+ - [Anything blocking progress, or "None"]
979
+ ```
980
+
981
+ ---
982
+
983
+ ## Constraints
984
+
985
+ | Rule | Reason |
986
+ |------|--------|
987
+ | **ONE item per iteration** | Prevents context overflow |
988
+ | **Always update progress.txt** | Prevents repeated mistakes |
989
+ | **Run tests before marking complete** | Ensures quality |
990
+ | **Commit on successful item** | Preserves progress |
991
+ | **Use subagents for research** | Keeps main context clean |
992
+
993
+ ---
994
+
995
+ ## Completion Signal
996
+
997
+ When ALL items are done and tests pass:
998
+ ```
999
+ <promise>{completion_promise}</promise>
1000
+ ```
1001
+
1002
+ ## Fatal Error Signal
1003
+
1004
+ If stuck or unrecoverable after multiple attempts:
1005
+ ```
1006
+ <fatal>Description of the blocking issue</fatal>
1007
+ ```
1008
+
1009
+ ---
1010
+
1011
+ ## Context
1012
+
1013
+ {additional_context if additional_context else "No additional context provided."}
1014
+ """
1015
+
1016
+ with open(output_file, "w") as f:
1017
+ f.write(content)
1018
+
1019
+
1020
+ # =============================================================================
1021
+ # CLI Interface
1022
+ # =============================================================================
1023
+
1024
+
1025
+ def main():
1026
+ """CLI for ralph_state.py."""
1027
+ import argparse
1028
+
1029
+ parser = argparse.ArgumentParser(description="Ralph Wiggum State Management")
1030
+ subparsers = parser.add_subparsers(dest="command", help="Command to run")
1031
+
1032
+ # Init command
1033
+ init_parser = subparsers.add_parser("init", help="Initialize Ralph session")
1034
+ init_parser.add_argument("--task", required=True, help="Task name")
1035
+ init_parser.add_argument("--objective", required=True, help="Task objective")
1036
+ init_parser.add_argument("--items", nargs="+", help="Todo items")
1037
+ init_parser.add_argument("--max-iterations", type=int, default=50)
1038
+
1039
+ # Status command
1040
+ subparsers.add_parser("status", help="Show current status")
1041
+
1042
+ # Stop command
1043
+ subparsers.add_parser("stop", help="Stop Ralph session")
1044
+
1045
+ # Init-linear command (ANV-211)
1046
+ init_linear_parser = subparsers.add_parser(
1047
+ "init-linear", help="Initialize Ralph from Linear issue"
1048
+ )
1049
+ init_linear_parser.add_argument(
1050
+ "--issue", required=True, help="Linear issue ID (e.g., ANV-209)"
1051
+ )
1052
+ init_linear_parser.add_argument(
1053
+ "--filter", help="Subtask filter (e.g., ANV-1..ANV-5 or ANV-1,ANV-3)"
1054
+ )
1055
+ init_linear_parser.add_argument(
1056
+ "--no-sync", action="store_true", help="Don't sync status back to Linear"
1057
+ )
1058
+
1059
+ # Sync command (ANV-211)
1060
+ sync_parser = subparsers.add_parser("sync", help="Sync status with Linear")
1061
+ # --complete and --skip are mutually exclusive
1062
+ sync_action_group = sync_parser.add_mutually_exclusive_group()
1063
+ sync_action_group.add_argument(
1064
+ "--complete", help="Mark subtask complete (issue ID)"
1065
+ )
1066
+ sync_action_group.add_argument(
1067
+ "--skip", help="Mark subtask skipped (issue ID)"
1068
+ )
1069
+ sync_parser.add_argument(
1070
+ "--reason", help="Skip reason (used with --skip)"
1071
+ )
1072
+
1073
+ args = parser.parse_args()
1074
+
1075
+ if args.command == "init":
1076
+ state = RalphState.initialize(
1077
+ task_name=args.task,
1078
+ objective=args.objective,
1079
+ todo_items=args.items or [],
1080
+ max_iterations=args.max_iterations,
1081
+ )
1082
+
1083
+ # Create supporting files
1084
+ create_fix_plan(
1085
+ args.task,
1086
+ args.objective,
1087
+ args.items or [],
1088
+ )
1089
+ create_progress_file(
1090
+ args.task,
1091
+ args.objective,
1092
+ len(args.items or []),
1093
+ )
1094
+ create_prompt_file(
1095
+ args.task,
1096
+ args.objective,
1097
+ remaining_count=len(args.items or []),
1098
+ total_items=len(args.items or []),
1099
+ started_at=state.started_at,
1100
+ )
1101
+
1102
+ print(f"Ralph session initialized: {args.task}")
1103
+ print(f" State file: {state.state_file}")
1104
+ print(f" Items: {len(args.items or [])} todo items")
1105
+
1106
+ elif args.command == "status":
1107
+ state = RalphState.load()
1108
+ if state:
1109
+ print(state.status_report())
1110
+ else:
1111
+ print("No active Ralph session")
1112
+
1113
+ elif args.command == "stop":
1114
+ state = RalphState.load()
1115
+ if state:
1116
+ print(state.stop_summary())
1117
+ state.update_status("stopped")
1118
+ state.delete()
1119
+ print("\nRalph session stopped and cleaned up")
1120
+ else:
1121
+ print("No active Ralph session to stop")
1122
+
1123
+ elif args.command == "init-linear":
1124
+ try:
1125
+ state = RalphState.initialize_from_linear(
1126
+ issue_id=args.issue,
1127
+ subtask_filter=args.filter,
1128
+ no_sync=args.no_sync,
1129
+ )
1130
+
1131
+ # Create supporting files
1132
+ create_fix_plan(
1133
+ state.task_name,
1134
+ state.objective,
1135
+ state.todo_items,
1136
+ )
1137
+ create_progress_file(
1138
+ state.task_name,
1139
+ state.objective,
1140
+ len(state.todo_items),
1141
+ )
1142
+ create_prompt_file(
1143
+ state.task_name,
1144
+ state.objective,
1145
+ remaining_count=len(state.todo_items),
1146
+ total_items=len(state.todo_items),
1147
+ started_at=state.started_at,
1148
+ )
1149
+
1150
+ completed, skipped, remaining = state.get_linear_progress()
1151
+ print(f"Ralph session initialized from Linear: {args.issue}")
1152
+ print(f" State file: {state.state_file}")
1153
+ print(f" Subtasks: {remaining} todo, {completed} already done")
1154
+ if args.filter:
1155
+ print(f" Filter: {args.filter}")
1156
+ if args.no_sync:
1157
+ print(" Sync: DISABLED (changes won't update Linear)")
1158
+
1159
+ except ValueError as e:
1160
+ print(f"Error: {e}")
1161
+ except ImportError as e:
1162
+ print(f"Error: Could not load Linear provider - {e}")
1163
+
1164
+ elif args.command == "sync":
1165
+ state = RalphState.load()
1166
+ if not state:
1167
+ print("No active Ralph session")
1168
+ elif not state.linear_integration or not state.linear_integration.enabled:
1169
+ print("Linear integration not enabled for this session")
1170
+ else:
1171
+ if args.complete:
1172
+ if state.mark_subtask_complete(args.complete):
1173
+ print(f"Marked {args.complete} as complete")
1174
+ if not state.linear_integration.no_sync:
1175
+ print(" -> Synced to Linear")
1176
+ else:
1177
+ print(f"Subtask {args.complete} not found")
1178
+ elif args.skip:
1179
+ reason = args.reason or "Skipped via CLI"
1180
+ if state.mark_subtask_complete(args.skip, skip_reason=reason):
1181
+ print(f"Marked {args.skip} as skipped: {reason}")
1182
+ else:
1183
+ print(f"Subtask {args.skip} not found")
1184
+ else:
1185
+ # Show Linear progress
1186
+ completed, skipped, remaining = state.get_linear_progress()
1187
+ print(f"Linear Integration: {state.linear_integration.parent_issue}")
1188
+ print(f" Completed: {completed}")
1189
+ print(f" Skipped: {skipped}")
1190
+ print(f" Remaining: {remaining}")
1191
+ print(f" Last sync: {state.linear_integration.last_sync}")
1192
+
1193
+ next_task = state.get_next_subtask()
1194
+ if next_task:
1195
+ print(f"\n Next: [{next_task.identifier}] {next_task.title}")
1196
+
1197
+ else:
1198
+ parser.print_help()
1199
+
1200
+
1201
+ if __name__ == "__main__":
1202
+ main()