ralph-code 0.5.0__py3-none-any.whl
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.
- ralph/__init__.py +20 -0
- ralph/__main__.py +34 -0
- ralph/app.py +1328 -0
- ralph/claude_runner.py +22 -0
- ralph/colors.py +183 -0
- ralph/config.py +227 -0
- ralph/git_manager.py +304 -0
- ralph/harness.py +393 -0
- ralph/harness_runner.py +972 -0
- ralph/prd_manager.py +348 -0
- ralph/schemas/ralph_tasks_schema.json +95 -0
- ralph/schemas/task_schema.json +92 -0
- ralph/spinner.py +287 -0
- ralph/storage.py +77 -0
- ralph/tasks.py +298 -0
- ralph/user_stories.py +283 -0
- ralph/workflow.py +1036 -0
- ralph_code-0.5.0.dist-info/METADATA +79 -0
- ralph_code-0.5.0.dist-info/RECORD +23 -0
- ralph_code-0.5.0.dist-info/WHEEL +5 -0
- ralph_code-0.5.0.dist-info/entry_points.txt +2 -0
- ralph_code-0.5.0.dist-info/licenses/LICENSE +21 -0
- ralph_code-0.5.0.dist-info/top_level.txt +1 -0
ralph/prd_manager.py
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""PRD (Product Requirements Document) management for ralph-coding.
|
|
2
|
+
|
|
3
|
+
PRDs are stored as files in the PRD folder:
|
|
4
|
+
- .txt files = unspecced tasks (simple descriptions)
|
|
5
|
+
- .md files = specced PRDs (full specifications)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import re
|
|
10
|
+
import uuid
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Literal
|
|
15
|
+
|
|
16
|
+
PRDStatus = Literal["unspecced", "pending", "questions", "in_progress", "completed", "errored"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class PRDQuestion:
|
|
21
|
+
"""Represents an open question in a PRD."""
|
|
22
|
+
question: str
|
|
23
|
+
options: list[str] | None = None # Multi-choice options if any
|
|
24
|
+
answer: str | None = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def slugify(text: str) -> str:
|
|
28
|
+
"""Convert text to kebab-case slug for filenames."""
|
|
29
|
+
# Lowercase and replace spaces/underscores with hyphens
|
|
30
|
+
slug = text.lower().strip()
|
|
31
|
+
slug = re.sub(r'[_\s]+', '-', slug)
|
|
32
|
+
# Remove non-alphanumeric characters except hyphens
|
|
33
|
+
slug = re.sub(r'[^a-z0-9-]', '', slug)
|
|
34
|
+
# Remove multiple consecutive hyphens
|
|
35
|
+
slug = re.sub(r'-+', '-', slug)
|
|
36
|
+
# Remove leading/trailing hyphens
|
|
37
|
+
slug = slug.strip('-')
|
|
38
|
+
return slug[:50] # Limit length
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class PRD:
|
|
43
|
+
"""Represents a Product Requirements Document."""
|
|
44
|
+
id: str
|
|
45
|
+
name: str
|
|
46
|
+
file_path: Path
|
|
47
|
+
is_specced: bool
|
|
48
|
+
status: PRDStatus = "pending"
|
|
49
|
+
description: str = ""
|
|
50
|
+
created_at: str = field(default_factory=lambda: datetime.utcnow().isoformat())
|
|
51
|
+
started_at: str | None = None
|
|
52
|
+
completed_at: str | None = None
|
|
53
|
+
questions: list[PRDQuestion] = field(default_factory=list)
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def from_txt_file(cls, file_path: Path) -> "PRD":
|
|
57
|
+
"""Create an unspecced PRD from a .txt file."""
|
|
58
|
+
content = file_path.read_text().strip()
|
|
59
|
+
name = file_path.stem # filename without extension
|
|
60
|
+
return cls(
|
|
61
|
+
id=str(uuid.uuid4()),
|
|
62
|
+
name=name,
|
|
63
|
+
file_path=file_path,
|
|
64
|
+
is_specced=False,
|
|
65
|
+
status="unspecced",
|
|
66
|
+
description=content,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def from_md_file(cls, file_path: Path) -> "PRD":
|
|
71
|
+
"""Create a specced PRD from a .md file."""
|
|
72
|
+
content = file_path.read_text()
|
|
73
|
+
name = file_path.stem
|
|
74
|
+
|
|
75
|
+
# Try to extract title from first heading
|
|
76
|
+
title_match = re.search(r'^#\s+(?:PRD:\s*)?(.+)$', content, re.MULTILINE)
|
|
77
|
+
if title_match:
|
|
78
|
+
name = title_match.group(1).strip()
|
|
79
|
+
|
|
80
|
+
# Parse the State section
|
|
81
|
+
status: PRDStatus = "pending"
|
|
82
|
+
questions: list[PRDQuestion] = []
|
|
83
|
+
|
|
84
|
+
# Look for ## State section
|
|
85
|
+
state_match = re.search(r'^## State\s*\n(.*?)(?=^## |\Z)', content, re.MULTILINE | re.DOTALL)
|
|
86
|
+
if state_match:
|
|
87
|
+
state_section = state_match.group(1).strip()
|
|
88
|
+
|
|
89
|
+
# Check state value
|
|
90
|
+
if "ready to implement" in state_section.lower():
|
|
91
|
+
status = "pending"
|
|
92
|
+
elif "open questions" in state_section.lower():
|
|
93
|
+
status = "questions"
|
|
94
|
+
# Parse questions (lines after the state indicator)
|
|
95
|
+
lines = state_section.split('\n')
|
|
96
|
+
for line in lines[1:]: # Skip the "- Open Questions" line
|
|
97
|
+
line = line.strip()
|
|
98
|
+
if not line or line.startswith('-'):
|
|
99
|
+
continue
|
|
100
|
+
|
|
101
|
+
# Check for JSON options at end of line
|
|
102
|
+
json_match = re.search(r'\[.*\]\s*$', line)
|
|
103
|
+
if json_match:
|
|
104
|
+
try:
|
|
105
|
+
options = json.loads(json_match.group())
|
|
106
|
+
question_text = line[:json_match.start()].strip()
|
|
107
|
+
questions.append(PRDQuestion(question=question_text, options=options))
|
|
108
|
+
except json.JSONDecodeError:
|
|
109
|
+
questions.append(PRDQuestion(question=line))
|
|
110
|
+
else:
|
|
111
|
+
questions.append(PRDQuestion(question=line))
|
|
112
|
+
|
|
113
|
+
# Check for status markers (legacy/override)
|
|
114
|
+
if "<!-- STATUS: completed -->" in content:
|
|
115
|
+
status = "completed"
|
|
116
|
+
elif "<!-- STATUS: in_progress -->" in content:
|
|
117
|
+
status = "in_progress"
|
|
118
|
+
elif "<!-- STATUS: errored -->" in content:
|
|
119
|
+
status = "errored"
|
|
120
|
+
|
|
121
|
+
return cls(
|
|
122
|
+
id=str(uuid.uuid4()),
|
|
123
|
+
name=name,
|
|
124
|
+
file_path=file_path,
|
|
125
|
+
is_specced=True,
|
|
126
|
+
status=status,
|
|
127
|
+
description=content[:500], # First 500 chars as description
|
|
128
|
+
questions=questions,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def start(self) -> None:
|
|
132
|
+
"""Mark the PRD as in progress."""
|
|
133
|
+
self.status = "in_progress"
|
|
134
|
+
self.started_at = datetime.utcnow().isoformat()
|
|
135
|
+
|
|
136
|
+
def complete(self) -> None:
|
|
137
|
+
"""Mark the PRD as completed."""
|
|
138
|
+
self.status = "completed"
|
|
139
|
+
self.completed_at = datetime.utcnow().isoformat()
|
|
140
|
+
|
|
141
|
+
def error(self, reason: str = "") -> None:
|
|
142
|
+
"""Mark the PRD as errored."""
|
|
143
|
+
self.status = "errored"
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def content(self) -> str:
|
|
147
|
+
"""Get the full content of the PRD file."""
|
|
148
|
+
if self.file_path.exists():
|
|
149
|
+
return self.file_path.read_text()
|
|
150
|
+
return self.description
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class PRDManager:
|
|
154
|
+
"""Manages PRD files in the PRD folder."""
|
|
155
|
+
|
|
156
|
+
def __init__(self, project_dir: Path):
|
|
157
|
+
self.project_dir = project_dir
|
|
158
|
+
self.prd_dir = project_dir / "PRD"
|
|
159
|
+
self._ensure_prd_dir()
|
|
160
|
+
self._prds: list[PRD] = []
|
|
161
|
+
self._load()
|
|
162
|
+
|
|
163
|
+
def _ensure_prd_dir(self) -> None:
|
|
164
|
+
"""Ensure PRD directory exists."""
|
|
165
|
+
self.prd_dir.mkdir(parents=True, exist_ok=True)
|
|
166
|
+
|
|
167
|
+
def _load(self) -> None:
|
|
168
|
+
"""Load all PRDs from the PRD folder."""
|
|
169
|
+
self._prds = []
|
|
170
|
+
|
|
171
|
+
# Load unspecced tasks (.txt files)
|
|
172
|
+
for txt_file in self.prd_dir.glob("*.txt"):
|
|
173
|
+
self._prds.append(PRD.from_txt_file(txt_file))
|
|
174
|
+
|
|
175
|
+
# Load specced PRDs (.md files)
|
|
176
|
+
for md_file in self.prd_dir.glob("*.md"):
|
|
177
|
+
self._prds.append(PRD.from_md_file(md_file))
|
|
178
|
+
|
|
179
|
+
def reload(self) -> None:
|
|
180
|
+
"""Reload PRDs from disk."""
|
|
181
|
+
self._load()
|
|
182
|
+
|
|
183
|
+
def get_all_prds(self) -> list[PRD]:
|
|
184
|
+
"""Get all PRDs."""
|
|
185
|
+
return list(self._prds)
|
|
186
|
+
|
|
187
|
+
def get_unspecced_prds(self) -> list[PRD]:
|
|
188
|
+
"""Get all unspecced PRDs (.txt files)."""
|
|
189
|
+
return [p for p in self._prds if not p.is_specced]
|
|
190
|
+
|
|
191
|
+
def get_specced_prds(self) -> list[PRD]:
|
|
192
|
+
"""Get all specced PRDs (.md files)."""
|
|
193
|
+
return [p for p in self._prds if p.is_specced]
|
|
194
|
+
|
|
195
|
+
def get_pending_prds(self) -> list[PRD]:
|
|
196
|
+
"""Get specced PRDs that are pending implementation."""
|
|
197
|
+
return [p for p in self._prds if p.is_specced and p.status == "pending"]
|
|
198
|
+
|
|
199
|
+
def get_in_progress_prd(self) -> PRD | None:
|
|
200
|
+
"""Get the currently in-progress PRD."""
|
|
201
|
+
for prd in self._prds:
|
|
202
|
+
if prd.status == "in_progress":
|
|
203
|
+
return prd
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
def get_completed_prds(self) -> list[PRD]:
|
|
207
|
+
"""Get all completed PRDs."""
|
|
208
|
+
return [p for p in self._prds if p.status == "completed"]
|
|
209
|
+
|
|
210
|
+
def get_errored_prds(self) -> list[PRD]:
|
|
211
|
+
"""Get all errored PRDs."""
|
|
212
|
+
return [p for p in self._prds if p.status == "errored"]
|
|
213
|
+
|
|
214
|
+
def get_questions_prds(self) -> list[PRD]:
|
|
215
|
+
"""Get PRDs that have open questions."""
|
|
216
|
+
return [p for p in self._prds if p.status == "questions"]
|
|
217
|
+
|
|
218
|
+
def get_prd_by_id(self, prd_id: str) -> PRD | None:
|
|
219
|
+
"""Get a PRD by its ID."""
|
|
220
|
+
for prd in self._prds:
|
|
221
|
+
if prd.id == prd_id:
|
|
222
|
+
return prd
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
def spec_prd(self, prd: PRD, prd_content: str, new_name: str | None = None) -> PRD:
|
|
226
|
+
"""
|
|
227
|
+
Convert an unspecced PRD (.txt) to a specced PRD (.md).
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
prd: The unspecced PRD to convert
|
|
231
|
+
prd_content: The full PRD content in markdown
|
|
232
|
+
new_name: Optional new name for the PRD file (will be slugified)
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
The new specced PRD
|
|
236
|
+
"""
|
|
237
|
+
if prd.is_specced:
|
|
238
|
+
raise ValueError("PRD is already specced")
|
|
239
|
+
|
|
240
|
+
# Determine new filename
|
|
241
|
+
if new_name:
|
|
242
|
+
slug = slugify(new_name)
|
|
243
|
+
else:
|
|
244
|
+
# Try to extract name from PRD content
|
|
245
|
+
title_match = re.search(r'^#\s+(?:PRD:\s*)?(.+)$', prd_content, re.MULTILINE)
|
|
246
|
+
if title_match:
|
|
247
|
+
slug = slugify(title_match.group(1))
|
|
248
|
+
else:
|
|
249
|
+
slug = slugify(prd.name)
|
|
250
|
+
|
|
251
|
+
new_file = self.prd_dir / f"{slug}.md"
|
|
252
|
+
|
|
253
|
+
# Handle name collision
|
|
254
|
+
counter = 1
|
|
255
|
+
while new_file.exists():
|
|
256
|
+
new_file = self.prd_dir / f"{slug}-{counter}.md"
|
|
257
|
+
counter += 1
|
|
258
|
+
|
|
259
|
+
# Write the new .md file
|
|
260
|
+
new_file.write_text(prd_content)
|
|
261
|
+
|
|
262
|
+
# Delete the old .txt file
|
|
263
|
+
if prd.file_path.exists() and prd.file_path.suffix == ".txt":
|
|
264
|
+
prd.file_path.unlink()
|
|
265
|
+
|
|
266
|
+
# Reload to pick up changes
|
|
267
|
+
self._load()
|
|
268
|
+
|
|
269
|
+
# Return the new PRD
|
|
270
|
+
for p in self._prds:
|
|
271
|
+
if p.file_path == new_file:
|
|
272
|
+
return p
|
|
273
|
+
|
|
274
|
+
raise RuntimeError("Failed to find newly created PRD")
|
|
275
|
+
|
|
276
|
+
def update_prd_status(self, prd: PRD, status: PRDStatus) -> None:
|
|
277
|
+
"""Update a PRD's status by modifying its file."""
|
|
278
|
+
if not prd.is_specced:
|
|
279
|
+
return # Can't update status of unspecced PRDs
|
|
280
|
+
|
|
281
|
+
content = prd.file_path.read_text()
|
|
282
|
+
|
|
283
|
+
# Remove any existing status line
|
|
284
|
+
content = re.sub(r'\n*<!-- STATUS: \w+ -->\n*', '\n', content)
|
|
285
|
+
|
|
286
|
+
# Add status at the end
|
|
287
|
+
content = content.rstrip() + f"\n\n<!-- STATUS: {status} -->\n"
|
|
288
|
+
|
|
289
|
+
prd.file_path.write_text(content)
|
|
290
|
+
prd.status = status
|
|
291
|
+
|
|
292
|
+
def answer_prd_questions(self, prd: PRD, answers: list[str]) -> list[tuple[str, str]]:
|
|
293
|
+
"""
|
|
294
|
+
Update a PRD with answers to its questions.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
prd: The PRD with questions
|
|
298
|
+
answers: List of answers corresponding to each question
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
List of (question, answer) tuples that might be useful as learnings
|
|
302
|
+
"""
|
|
303
|
+
if not prd.is_specced or prd.status != "questions":
|
|
304
|
+
return []
|
|
305
|
+
|
|
306
|
+
content = prd.file_path.read_text()
|
|
307
|
+
|
|
308
|
+
# Build the answered questions section
|
|
309
|
+
answered_lines = []
|
|
310
|
+
learnings = []
|
|
311
|
+
|
|
312
|
+
for i, question in enumerate(prd.questions):
|
|
313
|
+
answer = answers[i] if i < len(answers) else ""
|
|
314
|
+
question.answer = answer
|
|
315
|
+
answered_lines.append(f"Q: {question.question}")
|
|
316
|
+
answered_lines.append(f"A: {answer}")
|
|
317
|
+
answered_lines.append("")
|
|
318
|
+
learnings.append((question.question, answer))
|
|
319
|
+
|
|
320
|
+
# Replace the State section with "Ready to Implement" and answered questions
|
|
321
|
+
new_state = "## State\n- Ready to Implement\n\n### Answered Questions\n" + "\n".join(answered_lines)
|
|
322
|
+
|
|
323
|
+
# Find and replace the State section
|
|
324
|
+
state_pattern = r'^## State\s*\n(.*?)(?=^## |\Z)'
|
|
325
|
+
if re.search(state_pattern, content, re.MULTILINE | re.DOTALL):
|
|
326
|
+
content = re.sub(state_pattern, new_state + "\n", content, flags=re.MULTILINE | re.DOTALL)
|
|
327
|
+
else:
|
|
328
|
+
# Insert State section after first heading
|
|
329
|
+
first_heading_end = content.find('\n', content.find('#'))
|
|
330
|
+
if first_heading_end != -1:
|
|
331
|
+
content = content[:first_heading_end + 1] + "\n" + new_state + "\n" + content[first_heading_end + 1:]
|
|
332
|
+
|
|
333
|
+
prd.file_path.write_text(content)
|
|
334
|
+
prd.status = "pending"
|
|
335
|
+
prd.questions = []
|
|
336
|
+
|
|
337
|
+
return learnings
|
|
338
|
+
|
|
339
|
+
def get_stats(self) -> dict[str, int]:
|
|
340
|
+
"""Get PRD statistics."""
|
|
341
|
+
return {
|
|
342
|
+
"unspecced": len(self.get_unspecced_prds()),
|
|
343
|
+
"pending": len(self.get_pending_prds()),
|
|
344
|
+
"questions": len(self.get_questions_prds()),
|
|
345
|
+
"in_progress": 1 if self.get_in_progress_prd() else 0,
|
|
346
|
+
"completed": len(self.get_completed_prds()),
|
|
347
|
+
"errored": len(self.get_errored_prds()),
|
|
348
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "Ralph Tasks Schema",
|
|
4
|
+
"description": "Schema for ralph tasks.json - user stories derived from PRDs",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["project", "branchName", "description", "userStories"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"project": {
|
|
9
|
+
"type": "string",
|
|
10
|
+
"minLength": 1,
|
|
11
|
+
"description": "Project name"
|
|
12
|
+
},
|
|
13
|
+
"branchName": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"pattern": "^ralph/[a-z0-9-]+$",
|
|
16
|
+
"description": "Git branch name for this feature (ralph/feature-name)"
|
|
17
|
+
},
|
|
18
|
+
"description": {
|
|
19
|
+
"type": "string",
|
|
20
|
+
"description": "Feature description from PRD"
|
|
21
|
+
},
|
|
22
|
+
"prdFile": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"description": "Path to the source PRD file"
|
|
25
|
+
},
|
|
26
|
+
"userStories": {
|
|
27
|
+
"type": "array",
|
|
28
|
+
"items": {
|
|
29
|
+
"$ref": "#/definitions/userStory"
|
|
30
|
+
},
|
|
31
|
+
"description": "List of user stories to implement"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"definitions": {
|
|
35
|
+
"userStory": {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"required": ["id", "title", "description", "acceptanceCriteria", "priority", "passes"],
|
|
38
|
+
"properties": {
|
|
39
|
+
"id": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"pattern": "^US-[0-9]{3}$",
|
|
42
|
+
"description": "Story ID in format US-XXX"
|
|
43
|
+
},
|
|
44
|
+
"title": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"minLength": 1,
|
|
47
|
+
"description": "Short descriptive title"
|
|
48
|
+
},
|
|
49
|
+
"description": {
|
|
50
|
+
"type": "string",
|
|
51
|
+
"description": "User story in format: As a [user], I want [feature] so that [benefit]"
|
|
52
|
+
},
|
|
53
|
+
"acceptanceCriteria": {
|
|
54
|
+
"type": "array",
|
|
55
|
+
"items": {
|
|
56
|
+
"type": "string"
|
|
57
|
+
},
|
|
58
|
+
"minItems": 1,
|
|
59
|
+
"description": "Verifiable acceptance criteria"
|
|
60
|
+
},
|
|
61
|
+
"priority": {
|
|
62
|
+
"type": "integer",
|
|
63
|
+
"minimum": 1,
|
|
64
|
+
"description": "Execution priority (1 = first)"
|
|
65
|
+
},
|
|
66
|
+
"passes": {
|
|
67
|
+
"type": "boolean",
|
|
68
|
+
"description": "Whether this story has passed acceptance criteria"
|
|
69
|
+
},
|
|
70
|
+
"notes": {
|
|
71
|
+
"type": "string",
|
|
72
|
+
"default": "",
|
|
73
|
+
"description": "Notes from implementation attempts"
|
|
74
|
+
},
|
|
75
|
+
"attempts": {
|
|
76
|
+
"type": "integer",
|
|
77
|
+
"default": 0,
|
|
78
|
+
"description": "Number of implementation attempts"
|
|
79
|
+
},
|
|
80
|
+
"blocked": {
|
|
81
|
+
"type": "boolean",
|
|
82
|
+
"default": false,
|
|
83
|
+
"description": "Whether this story is blocked due to exceeding max attempts"
|
|
84
|
+
},
|
|
85
|
+
"needsIntervention": {
|
|
86
|
+
"type": "boolean",
|
|
87
|
+
"default": false,
|
|
88
|
+
"description": "Whether this story needs human intervention to resolve"
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
"additionalProperties": false
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
"additionalProperties": false
|
|
95
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "Ralph Task Schema",
|
|
4
|
+
"description": "Schema for validating ralph task specifications",
|
|
5
|
+
"definitions": {
|
|
6
|
+
"thin_task": {
|
|
7
|
+
"type": "string",
|
|
8
|
+
"minLength": 1,
|
|
9
|
+
"description": "A simple task description string"
|
|
10
|
+
},
|
|
11
|
+
"full_task": {
|
|
12
|
+
"type": "object",
|
|
13
|
+
"required": ["id", "name", "status", "created_at"],
|
|
14
|
+
"properties": {
|
|
15
|
+
"id": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
|
|
18
|
+
"description": "UUID identifier for the task"
|
|
19
|
+
},
|
|
20
|
+
"name": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"minLength": 1,
|
|
23
|
+
"description": "Short name/title for the task"
|
|
24
|
+
},
|
|
25
|
+
"description": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"description": "Detailed description of what the task involves"
|
|
28
|
+
},
|
|
29
|
+
"status": {
|
|
30
|
+
"type": "string",
|
|
31
|
+
"enum": ["pending", "in_progress", "completed", "errored"],
|
|
32
|
+
"description": "Current status of the task"
|
|
33
|
+
},
|
|
34
|
+
"prerequisites": {
|
|
35
|
+
"type": "array",
|
|
36
|
+
"items": {
|
|
37
|
+
"type": "string"
|
|
38
|
+
},
|
|
39
|
+
"description": "List of task IDs that must be completed before this task"
|
|
40
|
+
},
|
|
41
|
+
"acceptance_criteria": {
|
|
42
|
+
"type": "array",
|
|
43
|
+
"items": {
|
|
44
|
+
"type": "string"
|
|
45
|
+
},
|
|
46
|
+
"description": "List of criteria that must be met for the task to be considered complete"
|
|
47
|
+
},
|
|
48
|
+
"files_to_modify": {
|
|
49
|
+
"type": "array",
|
|
50
|
+
"items": {
|
|
51
|
+
"type": "string"
|
|
52
|
+
},
|
|
53
|
+
"description": "List of files that will need to be created or modified"
|
|
54
|
+
},
|
|
55
|
+
"notes": {
|
|
56
|
+
"type": "string",
|
|
57
|
+
"description": "Additional notes or context about the task"
|
|
58
|
+
},
|
|
59
|
+
"created_at": {
|
|
60
|
+
"type": "string",
|
|
61
|
+
"format": "date-time",
|
|
62
|
+
"description": "ISO datetime when the task was created"
|
|
63
|
+
},
|
|
64
|
+
"started_at": {
|
|
65
|
+
"type": ["string", "null"],
|
|
66
|
+
"format": "date-time",
|
|
67
|
+
"description": "ISO datetime when work on the task began"
|
|
68
|
+
},
|
|
69
|
+
"completed_at": {
|
|
70
|
+
"type": ["string", "null"],
|
|
71
|
+
"format": "date-time",
|
|
72
|
+
"description": "ISO datetime when the task was completed"
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
"additionalProperties": false
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
"type": "object",
|
|
79
|
+
"required": ["tasks"],
|
|
80
|
+
"properties": {
|
|
81
|
+
"tasks": {
|
|
82
|
+
"type": "array",
|
|
83
|
+
"items": {
|
|
84
|
+
"oneOf": [
|
|
85
|
+
{ "$ref": "#/definitions/thin_task" },
|
|
86
|
+
{ "$ref": "#/definitions/full_task" }
|
|
87
|
+
]
|
|
88
|
+
},
|
|
89
|
+
"description": "List of tasks (can be thin tasks or full specs)"
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|