orbit-auto 3.0.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.
- orbit_auto/__init__.py +32 -0
- orbit_auto/__main__.py +13 -0
- orbit_auto/claude_runner.py +439 -0
- orbit_auto/cli.py +325 -0
- orbit_auto/code_reviewer.py +187 -0
- orbit_auto/dag.py +336 -0
- orbit_auto/db_logger.py +293 -0
- orbit_auto/display.py +382 -0
- orbit_auto/init_task.py +79 -0
- orbit_auto/models.py +195 -0
- orbit_auto/parallel.py +464 -0
- orbit_auto/plan_validator.py +128 -0
- orbit_auto/runnable.py +273 -0
- orbit_auto/sequential.py +597 -0
- orbit_auto/state.py +286 -0
- orbit_auto/task_parser.py +300 -0
- orbit_auto/templates/__init__.py +126 -0
- orbit_auto/worker.py +483 -0
- orbit_auto/worktree.py +224 -0
- orbit_auto-3.0.0.dist-info/METADATA +176 -0
- orbit_auto-3.0.0.dist-info/RECORD +23 -0
- orbit_auto-3.0.0.dist-info/WHEEL +4 -0
- orbit_auto-3.0.0.dist-info/entry_points.txt +2 -0
orbit_auto/__init__.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Orbit Auto - Autonomous AI Development Tool
|
|
3
|
+
|
|
4
|
+
A Python implementation of the Orbit Auto technique for autonomous
|
|
5
|
+
AI-assisted development. Supports both sequential and parallel execution
|
|
6
|
+
with orbit integration.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
orbit-auto <task-name> # Parallel (default, 8 workers)
|
|
10
|
+
orbit-auto <task-name> -w 12 # Parallel with 12 workers
|
|
11
|
+
orbit-auto <task-name> --sequential # Sequential mode
|
|
12
|
+
orbit-auto <task-name> --dry-run # Show execution plan
|
|
13
|
+
orbit-auto init <task-name> "desc" # Initialize task
|
|
14
|
+
orbit-auto status <task-name> # Show task status
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
__version__ = "3.0.0"
|
|
18
|
+
__author__ = "Tom Brami"
|
|
19
|
+
|
|
20
|
+
from orbit_auto.models import Task, State, Config, ExecutionResult
|
|
21
|
+
from orbit_auto.dag import DAG
|
|
22
|
+
from orbit_auto.state import StateManager
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"Task",
|
|
26
|
+
"State",
|
|
27
|
+
"Config",
|
|
28
|
+
"ExecutionResult",
|
|
29
|
+
"DAG",
|
|
30
|
+
"StateManager",
|
|
31
|
+
"__version__",
|
|
32
|
+
]
|
orbit_auto/__main__.py
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Claude CLI integration for Orbit Auto.
|
|
3
|
+
|
|
4
|
+
Handles invoking the Claude CLI, parsing streaming output,
|
|
5
|
+
and extracting structured results from responses.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import subprocess
|
|
12
|
+
import time
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Callable
|
|
16
|
+
|
|
17
|
+
from orbit_auto.models import ExecutionResult, Visibility
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class StreamContext:
|
|
22
|
+
"""Context for stream processing."""
|
|
23
|
+
|
|
24
|
+
start_time: float = field(default_factory=time.time)
|
|
25
|
+
tool_count: int = 0
|
|
26
|
+
files_modified: list[str] = field(default_factory=list)
|
|
27
|
+
accumulated_text: str = ""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ClaudeRunner:
|
|
31
|
+
"""
|
|
32
|
+
Runs Claude CLI and processes output.
|
|
33
|
+
|
|
34
|
+
Supports streaming output parsing, tool visibility modes,
|
|
35
|
+
and extraction of learning tags from responses.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
visibility: Visibility = Visibility.VERBOSE,
|
|
41
|
+
on_tool_use: Callable[[str, str], None] | None = None,
|
|
42
|
+
) -> None:
|
|
43
|
+
"""
|
|
44
|
+
Initialize ClaudeRunner.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
visibility: Output visibility level
|
|
48
|
+
on_tool_use: Optional callback for tool use events (tool_name, display_info)
|
|
49
|
+
"""
|
|
50
|
+
self.visibility = visibility
|
|
51
|
+
self.on_tool_use = on_tool_use
|
|
52
|
+
|
|
53
|
+
def run(
|
|
54
|
+
self,
|
|
55
|
+
prompt: str,
|
|
56
|
+
working_dir: Path,
|
|
57
|
+
print_output: bool = True,
|
|
58
|
+
log_file: Path | None = None,
|
|
59
|
+
timeout: int | None = None,
|
|
60
|
+
session_name: str | None = None,
|
|
61
|
+
) -> ExecutionResult:
|
|
62
|
+
"""
|
|
63
|
+
Run Claude with a prompt and parse the response.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
prompt: The prompt to send to Claude
|
|
67
|
+
working_dir: Working directory for the Claude process
|
|
68
|
+
print_output: Whether to print tool visibility output
|
|
69
|
+
log_file: Optional path to write full Claude output for debugging
|
|
70
|
+
timeout: Max seconds to wait for completion (None = no limit)
|
|
71
|
+
session_name: Optional display name for the session (--name flag)
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
ExecutionResult with parsed response data
|
|
75
|
+
"""
|
|
76
|
+
start_time = time.time()
|
|
77
|
+
ctx = StreamContext(start_time=start_time)
|
|
78
|
+
|
|
79
|
+
# Build command
|
|
80
|
+
# Note: --verbose is required when using --print with --output-format=stream-json
|
|
81
|
+
# --exclude-dynamic-system-prompt-sections improves prompt-cache hits across parallel workers.
|
|
82
|
+
cmd = ["claude", "--print", "--output-format", "stream-json", "--verbose", "--exclude-dynamic-system-prompt-sections"]
|
|
83
|
+
if session_name:
|
|
84
|
+
cmd.extend(["--name", session_name])
|
|
85
|
+
|
|
86
|
+
# Set up environment to signal autonomous execution
|
|
87
|
+
# This allows hooks to skip when running in orbit-auto mode
|
|
88
|
+
env = os.environ.copy()
|
|
89
|
+
env["ORBIT_AUTO_MODE"] = "1"
|
|
90
|
+
|
|
91
|
+
# Run Claude
|
|
92
|
+
process = subprocess.Popen(
|
|
93
|
+
cmd,
|
|
94
|
+
stdin=subprocess.PIPE,
|
|
95
|
+
stdout=subprocess.PIPE,
|
|
96
|
+
stderr=subprocess.PIPE,
|
|
97
|
+
cwd=working_dir,
|
|
98
|
+
text=True,
|
|
99
|
+
env=env,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Send prompt (with optional timeout)
|
|
103
|
+
try:
|
|
104
|
+
stdout, stderr = process.communicate(input=prompt, timeout=timeout)
|
|
105
|
+
except subprocess.TimeoutExpired:
|
|
106
|
+
process.kill()
|
|
107
|
+
stdout, stderr = process.communicate() # drain pipes after kill
|
|
108
|
+
result = ExecutionResult(
|
|
109
|
+
task_id="",
|
|
110
|
+
success=False,
|
|
111
|
+
output="",
|
|
112
|
+
duration=time.time() - start_time,
|
|
113
|
+
)
|
|
114
|
+
result.cli_error = f"Task timed out after {timeout}s"
|
|
115
|
+
if log_file:
|
|
116
|
+
self._write_log_file(
|
|
117
|
+
log_file,
|
|
118
|
+
prompt,
|
|
119
|
+
stdout or "",
|
|
120
|
+
stderr or "",
|
|
121
|
+
result,
|
|
122
|
+
start_time,
|
|
123
|
+
)
|
|
124
|
+
return result
|
|
125
|
+
|
|
126
|
+
# Process output
|
|
127
|
+
result = self._process_output(stdout, ctx, print_output)
|
|
128
|
+
result.duration = time.time() - start_time
|
|
129
|
+
|
|
130
|
+
# Capture CLI errors from stderr (e.g., rate limits, auth errors, flag errors)
|
|
131
|
+
if stderr and stderr.strip():
|
|
132
|
+
# Filter out noise - only capture actual errors
|
|
133
|
+
error_lines = [
|
|
134
|
+
line
|
|
135
|
+
for line in stderr.strip().split("\n")
|
|
136
|
+
if line.strip() and not line.startswith("Warning:")
|
|
137
|
+
]
|
|
138
|
+
if error_lines:
|
|
139
|
+
result.cli_error = "\n".join(error_lines[:5]) # Limit to first 5 lines
|
|
140
|
+
|
|
141
|
+
# Write log file if requested
|
|
142
|
+
if log_file:
|
|
143
|
+
self._write_log_file(log_file, prompt, stdout, stderr, result, start_time)
|
|
144
|
+
|
|
145
|
+
return result
|
|
146
|
+
|
|
147
|
+
def _write_log_file(
|
|
148
|
+
self,
|
|
149
|
+
log_file: Path,
|
|
150
|
+
prompt: str,
|
|
151
|
+
stdout: str,
|
|
152
|
+
stderr: str,
|
|
153
|
+
result: ExecutionResult,
|
|
154
|
+
start_time: float,
|
|
155
|
+
) -> None:
|
|
156
|
+
"""Write worker execution log to file."""
|
|
157
|
+
from datetime import datetime
|
|
158
|
+
|
|
159
|
+
log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
160
|
+
|
|
161
|
+
with open(log_file, "w") as f:
|
|
162
|
+
f.write("=== Orbit Auto Worker Log ===\n")
|
|
163
|
+
f.write(f"Started: {datetime.fromtimestamp(start_time).isoformat()}\n")
|
|
164
|
+
f.write(f"Duration: {result.duration:.1f}s\n")
|
|
165
|
+
f.write("\n")
|
|
166
|
+
|
|
167
|
+
f.write("--- PROMPT ---\n")
|
|
168
|
+
f.write(prompt[:500] + "...\n" if len(prompt) > 500 else prompt + "\n")
|
|
169
|
+
f.write("\n")
|
|
170
|
+
|
|
171
|
+
f.write("--- RAW CLAUDE OUTPUT (stream-json) ---\n")
|
|
172
|
+
f.write(stdout if stdout else "(no stdout)\n")
|
|
173
|
+
f.write("\n")
|
|
174
|
+
|
|
175
|
+
if stderr and stderr.strip():
|
|
176
|
+
f.write("--- STDERR ---\n")
|
|
177
|
+
f.write(stderr)
|
|
178
|
+
f.write("\n")
|
|
179
|
+
|
|
180
|
+
f.write("--- EXECUTION RESULT ---\n")
|
|
181
|
+
f.write(f"Success: {result.success}\n")
|
|
182
|
+
f.write(f"Tools used: {result.tools_used}\n")
|
|
183
|
+
f.write(f"Files modified: {result.files_modified}\n")
|
|
184
|
+
if result.cli_error:
|
|
185
|
+
f.write(f"CLI error: {result.cli_error}\n")
|
|
186
|
+
if result.what_worked:
|
|
187
|
+
f.write(f"What worked: {result.what_worked}\n")
|
|
188
|
+
if result.what_failed:
|
|
189
|
+
f.write(f"What failed: {result.what_failed}\n")
|
|
190
|
+
if result.is_blocked:
|
|
191
|
+
f.write("Status: BLOCKED (waiting for human)\n")
|
|
192
|
+
|
|
193
|
+
def _process_output(
|
|
194
|
+
self,
|
|
195
|
+
output: str,
|
|
196
|
+
ctx: StreamContext,
|
|
197
|
+
print_output: bool,
|
|
198
|
+
) -> ExecutionResult:
|
|
199
|
+
"""Process Claude's stream-json output."""
|
|
200
|
+
for line in output.split("\n"):
|
|
201
|
+
line = line.strip()
|
|
202
|
+
if not line:
|
|
203
|
+
continue
|
|
204
|
+
|
|
205
|
+
# Parse tool use
|
|
206
|
+
if '"type":"tool_use"' in line:
|
|
207
|
+
self._handle_tool_use(line, ctx, print_output)
|
|
208
|
+
|
|
209
|
+
# Capture text content
|
|
210
|
+
if '"type":"assistant"' in line:
|
|
211
|
+
text_content = self._extract_text_content(line)
|
|
212
|
+
if text_content:
|
|
213
|
+
ctx.accumulated_text += text_content
|
|
214
|
+
|
|
215
|
+
if '"type":"result"' in line:
|
|
216
|
+
result_content = self._extract_result_content(line)
|
|
217
|
+
if result_content:
|
|
218
|
+
ctx.accumulated_text += result_content
|
|
219
|
+
|
|
220
|
+
# Build result
|
|
221
|
+
return self._build_result(ctx)
|
|
222
|
+
|
|
223
|
+
def _handle_tool_use(
|
|
224
|
+
self,
|
|
225
|
+
line: str,
|
|
226
|
+
ctx: StreamContext,
|
|
227
|
+
print_output: bool,
|
|
228
|
+
) -> None:
|
|
229
|
+
"""Handle a tool use event from the stream."""
|
|
230
|
+
tool_name = self._extract_json_field(line, "name")
|
|
231
|
+
if not tool_name:
|
|
232
|
+
return
|
|
233
|
+
|
|
234
|
+
ctx.tool_count += 1
|
|
235
|
+
display_info = ""
|
|
236
|
+
|
|
237
|
+
if self.visibility == Visibility.VERBOSE:
|
|
238
|
+
display_info = self._get_verbose_info(line, tool_name, ctx)
|
|
239
|
+
elif self.visibility == Visibility.MINIMAL:
|
|
240
|
+
display_info = self._get_minimal_info(line, tool_name, ctx)
|
|
241
|
+
|
|
242
|
+
if print_output and self.on_tool_use:
|
|
243
|
+
self.on_tool_use(tool_name, display_info)
|
|
244
|
+
|
|
245
|
+
def _get_verbose_info(self, line: str, tool_name: str, ctx: StreamContext) -> str:
|
|
246
|
+
"""Extract verbose display info for a tool call."""
|
|
247
|
+
if tool_name in ("Read", "Write", "Edit"):
|
|
248
|
+
file_path = self._extract_input_field(line, "file_path")
|
|
249
|
+
if file_path and tool_name in ("Write", "Edit"):
|
|
250
|
+
ctx.files_modified.append(file_path)
|
|
251
|
+
return file_path or ""
|
|
252
|
+
elif tool_name == "Bash":
|
|
253
|
+
cmd = self._extract_input_field(line, "command")
|
|
254
|
+
if cmd:
|
|
255
|
+
# Strip ANSI escape sequences
|
|
256
|
+
cmd = re.sub(r"\x1b\[[0-9;]*m", "", cmd)
|
|
257
|
+
if len(cmd) > 50:
|
|
258
|
+
cmd = cmd[:47] + "..."
|
|
259
|
+
return cmd or ""
|
|
260
|
+
elif tool_name in ("Glob", "Grep"):
|
|
261
|
+
return self._extract_input_field(line, "pattern") or ""
|
|
262
|
+
return ""
|
|
263
|
+
|
|
264
|
+
def _get_minimal_info(self, line: str, tool_name: str, ctx: StreamContext) -> str:
|
|
265
|
+
"""Extract minimal display info for a tool call."""
|
|
266
|
+
if tool_name in ("Read", "Write", "Edit"):
|
|
267
|
+
file_path = self._extract_input_field(line, "file_path")
|
|
268
|
+
if file_path:
|
|
269
|
+
if tool_name in ("Write", "Edit"):
|
|
270
|
+
ctx.files_modified.append(file_path)
|
|
271
|
+
return Path(file_path).name
|
|
272
|
+
return ""
|
|
273
|
+
elif tool_name == "Bash":
|
|
274
|
+
cmd = self._extract_input_field(line, "command")
|
|
275
|
+
if cmd:
|
|
276
|
+
cmd = re.sub(r"\x1b\[[0-9;]*m", "", cmd)
|
|
277
|
+
return cmd.split()[0] if cmd.split() else ""
|
|
278
|
+
return ""
|
|
279
|
+
return ""
|
|
280
|
+
|
|
281
|
+
def _extract_json_field(self, line: str, field: str) -> str:
|
|
282
|
+
"""Extract a field value from JSON line."""
|
|
283
|
+
pattern = rf'"{field}":"([^"]*)"'
|
|
284
|
+
match = re.search(pattern, line)
|
|
285
|
+
return match.group(1) if match else ""
|
|
286
|
+
|
|
287
|
+
def _extract_input_field(self, line: str, field: str) -> str:
|
|
288
|
+
"""Extract a field from the input object in a tool_use message."""
|
|
289
|
+
# Try to parse as JSON
|
|
290
|
+
try:
|
|
291
|
+
data = json.loads(line)
|
|
292
|
+
# Check nested path first
|
|
293
|
+
if "message" in data and "content" in data["message"]:
|
|
294
|
+
for item in data["message"]["content"]:
|
|
295
|
+
if item.get("type") == "tool_use" and "input" in item:
|
|
296
|
+
return item["input"].get(field, "")
|
|
297
|
+
# Check top-level input
|
|
298
|
+
if "input" in data:
|
|
299
|
+
return data["input"].get(field, "")
|
|
300
|
+
except json.JSONDecodeError:
|
|
301
|
+
pass
|
|
302
|
+
return ""
|
|
303
|
+
|
|
304
|
+
def _extract_text_content(self, line: str) -> str:
|
|
305
|
+
"""Extract text content from an assistant message."""
|
|
306
|
+
try:
|
|
307
|
+
data = json.loads(line)
|
|
308
|
+
if "message" in data and "content" in data["message"]:
|
|
309
|
+
for item in data["message"]["content"]:
|
|
310
|
+
if item.get("type") == "text":
|
|
311
|
+
return item.get("text", "")
|
|
312
|
+
except json.JSONDecodeError:
|
|
313
|
+
pass
|
|
314
|
+
return ""
|
|
315
|
+
|
|
316
|
+
def _extract_result_content(self, line: str) -> str:
|
|
317
|
+
"""Extract result content from a result message."""
|
|
318
|
+
try:
|
|
319
|
+
data = json.loads(line)
|
|
320
|
+
return data.get("result", "")
|
|
321
|
+
except json.JSONDecodeError:
|
|
322
|
+
pass
|
|
323
|
+
return ""
|
|
324
|
+
|
|
325
|
+
def _build_result(self, ctx: StreamContext) -> ExecutionResult:
|
|
326
|
+
"""Build ExecutionResult from accumulated context."""
|
|
327
|
+
text = ctx.accumulated_text
|
|
328
|
+
|
|
329
|
+
# Extract learning tags
|
|
330
|
+
learnings = self._extract_tag(text, "learnings")
|
|
331
|
+
what_worked = self._extract_tag(text, "what_worked")
|
|
332
|
+
what_failed = self._extract_tag(text, "what_failed")
|
|
333
|
+
dont_retry = self._extract_tag(text, "dont_retry")
|
|
334
|
+
try_next = self._extract_tag(text, "try_next")
|
|
335
|
+
pattern = self._extract_tag(text, "pattern_discovered")
|
|
336
|
+
gotcha = self._extract_tag(text, "gotcha")
|
|
337
|
+
|
|
338
|
+
# Check status signals
|
|
339
|
+
is_complete = "<promise>COMPLETE</promise>" in text
|
|
340
|
+
is_blocked = "<blocker>WAITING_FOR_HUMAN</blocker>" in text
|
|
341
|
+
|
|
342
|
+
# Determine success - require explicit positive signal
|
|
343
|
+
# Task is successful ONLY if:
|
|
344
|
+
# 1. <promise>COMPLETE</promise> is present, OR
|
|
345
|
+
# 2. <what_worked> tag is present (explicit success signal)
|
|
346
|
+
# This prevents marking tasks complete when Claude crashes, rate limits, or outputs nothing
|
|
347
|
+
success = is_complete or (what_worked is not None and not is_blocked)
|
|
348
|
+
|
|
349
|
+
return ExecutionResult(
|
|
350
|
+
task_id="", # Set by caller
|
|
351
|
+
success=success,
|
|
352
|
+
output=text,
|
|
353
|
+
duration=time.time() - ctx.start_time,
|
|
354
|
+
tools_used=ctx.tool_count,
|
|
355
|
+
files_modified=list(set(ctx.files_modified)),
|
|
356
|
+
learnings=learnings,
|
|
357
|
+
what_worked=what_worked,
|
|
358
|
+
what_failed=what_failed,
|
|
359
|
+
dont_retry=dont_retry,
|
|
360
|
+
try_next=try_next,
|
|
361
|
+
pattern_discovered=pattern,
|
|
362
|
+
gotcha=gotcha,
|
|
363
|
+
is_complete=is_complete,
|
|
364
|
+
is_blocked=is_blocked,
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
def _extract_tag(self, text: str, tag: str) -> str | None:
|
|
368
|
+
"""Extract content between XML-style tags."""
|
|
369
|
+
start_tag = f"<{tag}>"
|
|
370
|
+
end_tag = f"</{tag}>"
|
|
371
|
+
|
|
372
|
+
if start_tag not in text or end_tag not in text:
|
|
373
|
+
return None
|
|
374
|
+
|
|
375
|
+
start_idx = text.index(start_tag) + len(start_tag)
|
|
376
|
+
end_idx = text.index(end_tag)
|
|
377
|
+
|
|
378
|
+
if end_idx > start_idx:
|
|
379
|
+
return text[start_idx:end_idx].strip()
|
|
380
|
+
return None
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def build_generic_prompt(
|
|
384
|
+
task_number: str,
|
|
385
|
+
task_title: str,
|
|
386
|
+
tasks_file: Path,
|
|
387
|
+
context_file: Path,
|
|
388
|
+
auto_log: Path | None = None,
|
|
389
|
+
) -> str:
|
|
390
|
+
"""
|
|
391
|
+
Build a generic prompt for tasks without optimized prompts.
|
|
392
|
+
|
|
393
|
+
This replicates the prompt structure used by the bash implementation.
|
|
394
|
+
"""
|
|
395
|
+
prompt_parts = [
|
|
396
|
+
"You are working on an autonomous development task using Orbit Auto.",
|
|
397
|
+
"",
|
|
398
|
+
"## Current Task",
|
|
399
|
+
f"Task {task_number}: {task_title}",
|
|
400
|
+
"",
|
|
401
|
+
"## Files to Reference",
|
|
402
|
+
f"- Tasks file: {tasks_file}",
|
|
403
|
+
f"- Context file: {context_file}",
|
|
404
|
+
]
|
|
405
|
+
|
|
406
|
+
if auto_log and auto_log.exists():
|
|
407
|
+
prompt_parts.append(f"- Auto log: {auto_log}")
|
|
408
|
+
|
|
409
|
+
prompt_parts.extend(
|
|
410
|
+
[
|
|
411
|
+
"",
|
|
412
|
+
"## Instructions",
|
|
413
|
+
"1. Read the tasks file to understand the full task list",
|
|
414
|
+
"2. Read the context file for project-specific information",
|
|
415
|
+
"3. Complete the current task following the acceptance criteria",
|
|
416
|
+
"4. DO NOT mark the task checkbox - orbit-auto handles task completion tracking",
|
|
417
|
+
"",
|
|
418
|
+
"## Output Tags (REQUIRED)",
|
|
419
|
+
"",
|
|
420
|
+
"**CRITICAL:** orbit-auto detects success via these tags. Without them, the task is marked FAILED.",
|
|
421
|
+
"",
|
|
422
|
+
"Always include:",
|
|
423
|
+
"- <learnings>What you learned from this attempt</learnings>",
|
|
424
|
+
"",
|
|
425
|
+
"**On SUCCESS (REQUIRED for task completion):**",
|
|
426
|
+
"- <what_worked>The approach that succeeded</what_worked>",
|
|
427
|
+
"",
|
|
428
|
+
"On FAILURE:",
|
|
429
|
+
"- <what_failed>What went wrong</what_failed>",
|
|
430
|
+
"- <dont_retry>What not to try again</dont_retry>",
|
|
431
|
+
"- <try_next>What to try next</try_next>",
|
|
432
|
+
"",
|
|
433
|
+
"When ALL tasks are complete:",
|
|
434
|
+
"- <run_summary>Summary of all work done</run_summary>",
|
|
435
|
+
"- <promise>COMPLETE</promise>",
|
|
436
|
+
]
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
return "\n".join(prompt_parts)
|