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,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()
|