sdxloop 0.1.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.
- sdxloop/__init__.py +26 -0
- sdxloop/_vendor/sdxloop_worker-0.1.0-py3-none-any.whl +0 -0
- sdxloop/cli/__init__.py +1 -0
- sdxloop/cli/app.py +359 -0
- sdxloop/cli/doctor.py +145 -0
- sdxloop/cli/tui.py +129 -0
- sdxloop/config.py +180 -0
- sdxloop/engine/__init__.py +26 -0
- sdxloop/engine/engine.py +380 -0
- sdxloop/engine/model.py +151 -0
- sdxloop/engine/phases.py +223 -0
- sdxloop/engine/prompts/__init__.py +31 -0
- sdxloop/engine/prompts/decompose.md +41 -0
- sdxloop/engine/prompts/execute.md +31 -0
- sdxloop/engine/prompts/plan.md +35 -0
- sdxloop/engine/prompts/scrutinize.md +43 -0
- sdxloop/engine/prompts/validate.md +38 -0
- sdxloop/engine/store.py +257 -0
- sdxloop/errors.py +73 -0
- sdxloop/events.py +87 -0
- sdxloop/gh/__init__.py +6 -0
- sdxloop/gh/ops.py +140 -0
- sdxloop/gh/reporter.py +75 -0
- sdxloop/ids.py +35 -0
- sdxloop/py.typed +0 -0
- sdxloop/sbx/__init__.py +6 -0
- sdxloop/sbx/cli.py +210 -0
- sdxloop/sbx/models.py +83 -0
- sdxloop/sbx/pair.py +125 -0
- sdxloop/sbx/parse.py +67 -0
- sdxloop/sbx/provision.py +209 -0
- sdxloop/sbx/sandbox.py +70 -0
- sdxloop/worker/__init__.py +6 -0
- sdxloop/worker/client.py +260 -0
- sdxloop/worker/wheel.py +73 -0
- sdxloop-0.1.0.dist-info/METADATA +32 -0
- sdxloop-0.1.0.dist-info/RECORD +39 -0
- sdxloop-0.1.0.dist-info/WHEEL +4 -0
- sdxloop-0.1.0.dist-info/entry_points.txt +2 -0
sdxloop/engine/model.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Engine domain models: runs, tasks, plans, verdicts, the task graph."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from graphlib import CycleError, TopologicalSorter
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
9
|
+
|
|
10
|
+
RunState = Literal[
|
|
11
|
+
"created",
|
|
12
|
+
"provisioning",
|
|
13
|
+
"decomposing",
|
|
14
|
+
"running",
|
|
15
|
+
"finalizing",
|
|
16
|
+
"completed",
|
|
17
|
+
"failed",
|
|
18
|
+
"cancelled",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
TaskState = Literal[
|
|
22
|
+
"pending",
|
|
23
|
+
"planning",
|
|
24
|
+
"executing",
|
|
25
|
+
"scrutinizing",
|
|
26
|
+
"verifying",
|
|
27
|
+
"validating",
|
|
28
|
+
"done",
|
|
29
|
+
"failed",
|
|
30
|
+
"skipped",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
TERMINAL_TASK_STATES: frozenset[str] = frozenset({"done", "failed", "skipped"})
|
|
34
|
+
RESUMABLE_RUN_STATES: frozenset[str] = frozenset(
|
|
35
|
+
{"created", "provisioning", "decomposing", "running", "finalizing", "failed"}
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
Phase = Literal["decompose", "plan", "execute", "scrutinize", "verify", "validate"]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class _Model(BaseModel):
|
|
42
|
+
model_config = ConfigDict(extra="forbid")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class TaskSpec(_Model):
|
|
46
|
+
"""One unit of the decomposed outcome (agent-authored, engine-validated)."""
|
|
47
|
+
|
|
48
|
+
id: str
|
|
49
|
+
title: str
|
|
50
|
+
description: str = ""
|
|
51
|
+
depends_on: list[str] = Field(default_factory=list)
|
|
52
|
+
acceptance_criteria: list[str] = Field(default_factory=list)
|
|
53
|
+
verify_commands: list[str] = Field(default_factory=list)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class TaskGraph(_Model):
|
|
57
|
+
tasks: list[TaskSpec]
|
|
58
|
+
|
|
59
|
+
@model_validator(mode="after")
|
|
60
|
+
def _check_graph(self) -> TaskGraph:
|
|
61
|
+
if not self.tasks:
|
|
62
|
+
raise ValueError("task graph must contain at least one task")
|
|
63
|
+
ids = [t.id for t in self.tasks]
|
|
64
|
+
if len(set(ids)) != len(ids):
|
|
65
|
+
raise ValueError(f"duplicate task ids: {ids}")
|
|
66
|
+
known = set(ids)
|
|
67
|
+
for task in self.tasks:
|
|
68
|
+
unknown = [d for d in task.depends_on if d not in known]
|
|
69
|
+
if unknown:
|
|
70
|
+
raise ValueError(f"task {task.id} depends on unknown tasks: {unknown}")
|
|
71
|
+
if task.id in task.depends_on:
|
|
72
|
+
raise ValueError(f"task {task.id} depends on itself")
|
|
73
|
+
try:
|
|
74
|
+
self.topo_order()
|
|
75
|
+
except CycleError as exc:
|
|
76
|
+
raise ValueError(f"task graph contains a cycle: {exc.args}") from exc
|
|
77
|
+
return self
|
|
78
|
+
|
|
79
|
+
def topo_order(self) -> list[TaskSpec]:
|
|
80
|
+
"""Dependency order; ties broken by authored order (depth, position)."""
|
|
81
|
+
by_id = {t.id: t for t in self.tasks}
|
|
82
|
+
# TopologicalSorter validates the cycle; the actual ordering is
|
|
83
|
+
# deterministic: dependency depth first, authored position second.
|
|
84
|
+
list(TopologicalSorter({t.id: set(t.depends_on) for t in self.tasks}).static_order())
|
|
85
|
+
position = {t.id: i for i, t in enumerate(self.tasks)}
|
|
86
|
+
return sorted(
|
|
87
|
+
self.tasks,
|
|
88
|
+
key=lambda t: (self._depth(t, by_id), position[t.id]),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def _depth(self, task: TaskSpec, by_id: dict[str, TaskSpec]) -> int:
|
|
92
|
+
if not task.depends_on:
|
|
93
|
+
return 0
|
|
94
|
+
return 1 + max(self._depth(by_id[d], by_id) for d in task.depends_on)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class PlanModel(_Model):
|
|
98
|
+
"""The agent's plan for one task."""
|
|
99
|
+
|
|
100
|
+
steps: list[str]
|
|
101
|
+
expected_artifacts: list[str] = Field(default_factory=list)
|
|
102
|
+
verify_commands: list[str] = Field(default_factory=list)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class Issue(_Model):
|
|
106
|
+
severity: Literal["low", "medium", "high"] = "medium"
|
|
107
|
+
detail: str
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class Verdict(_Model):
|
|
111
|
+
"""Critic output: scrutinize uses pass/revise, validate uses accept/reject."""
|
|
112
|
+
|
|
113
|
+
verdict: Literal["pass", "revise", "accept", "reject"]
|
|
114
|
+
issues: list[Issue] = Field(default_factory=list)
|
|
115
|
+
feedback: str = ""
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class TaskRecord(_Model):
|
|
119
|
+
"""Persisted per-task state."""
|
|
120
|
+
|
|
121
|
+
spec: TaskSpec
|
|
122
|
+
state: TaskState = "pending"
|
|
123
|
+
revisions: int = 0
|
|
124
|
+
replans: int = 0
|
|
125
|
+
last_feedback: str = ""
|
|
126
|
+
session_id: str | None = None
|
|
127
|
+
plan: PlanModel | None = None
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def terminal(self) -> bool:
|
|
131
|
+
return self.state in TERMINAL_TASK_STATES
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class RunRecord(_Model):
|
|
135
|
+
run_id: str
|
|
136
|
+
outcome: str
|
|
137
|
+
state: RunState
|
|
138
|
+
created_at: float
|
|
139
|
+
updated_at: float
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class RunResult(_Model):
|
|
143
|
+
"""What LoopEngine.start()/resume() returns to callers."""
|
|
144
|
+
|
|
145
|
+
run_id: str
|
|
146
|
+
state: RunState
|
|
147
|
+
tasks: list[TaskRecord] = Field(default_factory=list)
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def succeeded(self) -> bool:
|
|
151
|
+
return self.state == "completed"
|
sdxloop/engine/phases.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Phase handlers: each turns engine state into one or more worker jobs.
|
|
2
|
+
|
|
3
|
+
Session strategy per phase (a deliberate design decision):
|
|
4
|
+
|
|
5
|
+
- DECOMPOSE / PLAN / EXECUTE run as fresh agent sessions with full ("auto")
|
|
6
|
+
permissions — the microVM is the security boundary.
|
|
7
|
+
- SCRUTINIZE / VALIDATE run as **fresh sessions in the same agent sandbox
|
|
8
|
+
with read-only permissions**: a fresh session removes conversational
|
|
9
|
+
anchoring to the executor's claims, read-only permissions stop the critic
|
|
10
|
+
from "fixing" things itself, and reusing the sandbox preserves the
|
|
11
|
+
workspace state the critic must inspect (a fresh sandbox per critic would
|
|
12
|
+
cost minutes and lose it).
|
|
13
|
+
- VERIFY is mechanical — shell commands, no LLM, no opinions.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import Literal, TypeVar
|
|
19
|
+
|
|
20
|
+
from pydantic import BaseModel, ValidationError
|
|
21
|
+
|
|
22
|
+
from sdxloop.config import Config
|
|
23
|
+
from sdxloop.engine.model import PlanModel, TaskGraph, TaskRecord, Verdict
|
|
24
|
+
from sdxloop.engine.prompts import bullet_list, render
|
|
25
|
+
from sdxloop.errors import WorkerError
|
|
26
|
+
from sdxloop.ids import new_job_id
|
|
27
|
+
from sdxloop.worker.client import WorkerClient
|
|
28
|
+
from sdxloop_worker.protocol import JobRequest, JobResult
|
|
29
|
+
|
|
30
|
+
EVIDENCE_COMMANDS: tuple[tuple[str, str], ...] = (
|
|
31
|
+
("git status", "git status --short 2>&1 | head -50"),
|
|
32
|
+
("git diff stat", "git diff --stat 2>&1 | head -50"),
|
|
33
|
+
("recent files", "find . -type f -newer /tmp -not -path './.git/*' 2>/dev/null | head -30"),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
OUTPUT_CLIP = 6_000
|
|
37
|
+
|
|
38
|
+
ModelT = TypeVar("ModelT", bound=BaseModel)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def clip(text: str | None, limit: int = OUTPUT_CLIP) -> str:
|
|
42
|
+
text = text or ""
|
|
43
|
+
if len(text) <= limit:
|
|
44
|
+
return text
|
|
45
|
+
return f"...(clipped)...\n{text[-limit:]}"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class PhaseRunner:
|
|
49
|
+
"""Runs the six phases for one run against the agent sandbox's worker."""
|
|
50
|
+
|
|
51
|
+
def __init__(self, agent: WorkerClient, config: Config, run_id: str, outcome: str) -> None:
|
|
52
|
+
self.agent = agent
|
|
53
|
+
self.config = config
|
|
54
|
+
self.run_id = run_id
|
|
55
|
+
self.outcome = outcome
|
|
56
|
+
|
|
57
|
+
# -- job plumbing ------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
def _agent_job(
|
|
60
|
+
self,
|
|
61
|
+
prompt: str,
|
|
62
|
+
*,
|
|
63
|
+
permission_mode: Literal["auto", "read_only"],
|
|
64
|
+
expect: Literal["text", "json"],
|
|
65
|
+
) -> JobResult:
|
|
66
|
+
job = JobRequest(
|
|
67
|
+
job_id=new_job_id(),
|
|
68
|
+
run_id=self.run_id,
|
|
69
|
+
kind="agent.session",
|
|
70
|
+
prompt=prompt,
|
|
71
|
+
model=self.config.model,
|
|
72
|
+
permission_mode=permission_mode,
|
|
73
|
+
expect=expect,
|
|
74
|
+
timeout_s=self.config.budgets.per_job_timeout_s,
|
|
75
|
+
)
|
|
76
|
+
result = self.agent.submit(job)
|
|
77
|
+
if result.status != "ok":
|
|
78
|
+
assert result.error is not None
|
|
79
|
+
raise WorkerError(f"agent job failed ({result.error.type}): {result.error.message}")
|
|
80
|
+
return result
|
|
81
|
+
|
|
82
|
+
def _agent_json(
|
|
83
|
+
self,
|
|
84
|
+
model_cls: type[ModelT],
|
|
85
|
+
prompt_name: str,
|
|
86
|
+
context: dict[str, str],
|
|
87
|
+
*,
|
|
88
|
+
permission_mode: Literal["auto", "read_only"] = "auto",
|
|
89
|
+
) -> ModelT:
|
|
90
|
+
"""Run a JSON-expecting agent job; one retry with the validation error."""
|
|
91
|
+
retry_context = ""
|
|
92
|
+
last_error: Exception | None = None
|
|
93
|
+
for _ in range(2):
|
|
94
|
+
prompt = render(prompt_name, retry_context=retry_context, **context)
|
|
95
|
+
result = self._agent_job(prompt, permission_mode=permission_mode, expect="json")
|
|
96
|
+
try:
|
|
97
|
+
return model_cls.model_validate(result.output_json)
|
|
98
|
+
except ValidationError as exc:
|
|
99
|
+
last_error = exc
|
|
100
|
+
retry_context = (
|
|
101
|
+
"\n## Previous attempt was invalid\n\n"
|
|
102
|
+
"Your previous response failed validation with:\n\n"
|
|
103
|
+
f"```\n{exc}\n```\n\nFix the structure and respond again."
|
|
104
|
+
)
|
|
105
|
+
raise WorkerError(f"{prompt_name} produced invalid output twice: {last_error}")
|
|
106
|
+
|
|
107
|
+
def shell(self, command: str, *, cwd: str | None = None) -> JobResult:
|
|
108
|
+
job = JobRequest(
|
|
109
|
+
job_id=new_job_id(),
|
|
110
|
+
run_id=self.run_id,
|
|
111
|
+
kind="shell.check",
|
|
112
|
+
argv=["sh", "-c", command],
|
|
113
|
+
cwd=cwd,
|
|
114
|
+
timeout_s=self.config.budgets.per_job_timeout_s,
|
|
115
|
+
)
|
|
116
|
+
result = self.agent.submit(job)
|
|
117
|
+
if result.status != "ok":
|
|
118
|
+
assert result.error is not None
|
|
119
|
+
raise WorkerError(f"shell job failed ({result.error.type}): {result.error.message}")
|
|
120
|
+
return result
|
|
121
|
+
|
|
122
|
+
# -- phases ------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
def decompose(self) -> TaskGraph:
|
|
125
|
+
return self._agent_json(
|
|
126
|
+
TaskGraph,
|
|
127
|
+
"decompose",
|
|
128
|
+
{
|
|
129
|
+
"outcome": self.outcome,
|
|
130
|
+
"max_tasks": str(self.config.budgets.max_tasks),
|
|
131
|
+
},
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def plan(self, task: TaskRecord) -> PlanModel:
|
|
135
|
+
return self._agent_json(
|
|
136
|
+
PlanModel,
|
|
137
|
+
"plan",
|
|
138
|
+
{
|
|
139
|
+
"outcome": self.outcome,
|
|
140
|
+
"task_id": task.spec.id,
|
|
141
|
+
"task_title": task.spec.title,
|
|
142
|
+
"task_description": task.spec.description or "(no further description)",
|
|
143
|
+
"acceptance_criteria": bullet_list(task.spec.acceptance_criteria),
|
|
144
|
+
"feedback": task.last_feedback or "(none — first attempt)",
|
|
145
|
+
},
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
def execute(self, task: TaskRecord, plan: PlanModel) -> JobResult:
|
|
149
|
+
prompt = render(
|
|
150
|
+
"execute",
|
|
151
|
+
outcome=self.outcome,
|
|
152
|
+
task_id=task.spec.id,
|
|
153
|
+
task_title=task.spec.title,
|
|
154
|
+
task_description=task.spec.description or "(no further description)",
|
|
155
|
+
plan_steps=bullet_list(plan.steps),
|
|
156
|
+
expected_artifacts=bullet_list(plan.expected_artifacts),
|
|
157
|
+
feedback=task.last_feedback or "(none — first attempt)",
|
|
158
|
+
)
|
|
159
|
+
return self._agent_job(prompt, permission_mode="auto", expect="text")
|
|
160
|
+
|
|
161
|
+
def scrutinize(self, task: TaskRecord, plan: PlanModel, executor_report: str) -> Verdict:
|
|
162
|
+
evidence_parts: list[str] = []
|
|
163
|
+
for label, command in EVIDENCE_COMMANDS:
|
|
164
|
+
try:
|
|
165
|
+
result = self.shell(command)
|
|
166
|
+
except WorkerError:
|
|
167
|
+
continue
|
|
168
|
+
output = clip(result.output_text, 1_500).strip()
|
|
169
|
+
if output:
|
|
170
|
+
evidence_parts.append(f"### {label}\n```\n{output}\n```")
|
|
171
|
+
verdict = self._agent_json(
|
|
172
|
+
Verdict,
|
|
173
|
+
"scrutinize",
|
|
174
|
+
{
|
|
175
|
+
"task_id": task.spec.id,
|
|
176
|
+
"task_title": task.spec.title,
|
|
177
|
+
"task_description": task.spec.description or "(no further description)",
|
|
178
|
+
"acceptance_criteria": bullet_list(task.spec.acceptance_criteria),
|
|
179
|
+
"plan_steps": bullet_list(plan.steps),
|
|
180
|
+
"executor_report": clip(executor_report) or "(executor produced no report)",
|
|
181
|
+
"evidence": "\n\n".join(evidence_parts) or "(no evidence gathered)",
|
|
182
|
+
},
|
|
183
|
+
permission_mode="read_only",
|
|
184
|
+
)
|
|
185
|
+
if verdict.verdict not in ("pass", "revise"):
|
|
186
|
+
raise WorkerError(f"scrutinize returned invalid verdict {verdict.verdict!r}")
|
|
187
|
+
return verdict
|
|
188
|
+
|
|
189
|
+
def verify(self, task: TaskRecord, plan: PlanModel) -> tuple[bool, str]:
|
|
190
|
+
"""Run every verify command; returns (all_passed, failure_feedback)."""
|
|
191
|
+
commands = list(dict.fromkeys(task.spec.verify_commands + plan.verify_commands))
|
|
192
|
+
failures: list[str] = []
|
|
193
|
+
results: list[str] = []
|
|
194
|
+
for command in commands:
|
|
195
|
+
result = self.shell(command)
|
|
196
|
+
output = clip(result.output_text, 1_500)
|
|
197
|
+
results.append(f"$ {command}\n(exit {result.exit_code})\n{output}")
|
|
198
|
+
if result.exit_code != 0:
|
|
199
|
+
failures.append(
|
|
200
|
+
f"verify command failed: `{command}` (exit {result.exit_code})\n{output}"
|
|
201
|
+
)
|
|
202
|
+
self._last_verify_results = "\n\n".join(results) or "(no verify commands)"
|
|
203
|
+
return (not failures, "\n\n".join(failures))
|
|
204
|
+
|
|
205
|
+
_last_verify_results: str = "(verification not run)"
|
|
206
|
+
|
|
207
|
+
def validate(self, task: TaskRecord) -> Verdict:
|
|
208
|
+
verdict = self._agent_json(
|
|
209
|
+
Verdict,
|
|
210
|
+
"validate",
|
|
211
|
+
{
|
|
212
|
+
"outcome": self.outcome,
|
|
213
|
+
"task_id": task.spec.id,
|
|
214
|
+
"task_title": task.spec.title,
|
|
215
|
+
"task_description": task.spec.description or "(no further description)",
|
|
216
|
+
"acceptance_criteria": bullet_list(task.spec.acceptance_criteria),
|
|
217
|
+
"verify_results": self._last_verify_results,
|
|
218
|
+
},
|
|
219
|
+
permission_mode="read_only",
|
|
220
|
+
)
|
|
221
|
+
if verdict.verdict not in ("accept", "reject"):
|
|
222
|
+
raise WorkerError(f"validate returned invalid verdict {verdict.verdict!r}")
|
|
223
|
+
return verdict
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Prompt template loading and rendering.
|
|
2
|
+
|
|
3
|
+
Templates are Markdown files shipped in this package, rendered with
|
|
4
|
+
``string.Template`` ($-substitution) so JSON braces in the templates need no
|
|
5
|
+
escaping. Rendering fails loudly on missing variables — a silently empty
|
|
6
|
+
prompt section is a debugging nightmare.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from functools import cache
|
|
12
|
+
from importlib import resources
|
|
13
|
+
from string import Template
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@cache
|
|
17
|
+
def _template(name: str) -> Template:
|
|
18
|
+
text = (resources.files(__name__) / f"{name}.md").read_text()
|
|
19
|
+
return Template(text)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def render(name: str, **context: str) -> str:
|
|
23
|
+
"""Render prompt template ``name`` with strict substitution."""
|
|
24
|
+
context.setdefault("retry_context", "")
|
|
25
|
+
return _template(name).substitute(context)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def bullet_list(items: list[str], empty: str = "(none)") -> str:
|
|
29
|
+
if not items:
|
|
30
|
+
return empty
|
|
31
|
+
return "\n".join(f"- {item}" for item in items)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Decompose an outcome into a task graph
|
|
2
|
+
|
|
3
|
+
You are the planning stage of an automated engineering loop running inside an
|
|
4
|
+
isolated sandbox. Break the outcome below into a small dependency-ordered set
|
|
5
|
+
of concrete, independently verifiable tasks.
|
|
6
|
+
|
|
7
|
+
## Outcome
|
|
8
|
+
|
|
9
|
+
$outcome
|
|
10
|
+
|
|
11
|
+
## Rules
|
|
12
|
+
|
|
13
|
+
- At most $max_tasks tasks. Prefer fewer, larger, coherent tasks over many
|
|
14
|
+
fragments.
|
|
15
|
+
- Every task needs: a stable short `id` (t1, t2, ...), a `title`, a concrete
|
|
16
|
+
`description` of what to do and where, `depends_on` (ids of prerequisite
|
|
17
|
+
tasks, often empty), `acceptance_criteria` (specific, checkable statements),
|
|
18
|
+
and `verify_commands` (shell commands that exit 0 only when the task is
|
|
19
|
+
genuinely done — e.g. test runs, linters, greps; never `echo`).
|
|
20
|
+
- Tasks must form a DAG: no cycles, dependencies only on listed ids.
|
|
21
|
+
- Work happens in the current working directory of this sandbox.
|
|
22
|
+
|
|
23
|
+
## Response format
|
|
24
|
+
|
|
25
|
+
Respond with exactly one fenced JSON block:
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"tasks": [
|
|
30
|
+
{
|
|
31
|
+
"id": "t1",
|
|
32
|
+
"title": "...",
|
|
33
|
+
"description": "...",
|
|
34
|
+
"depends_on": [],
|
|
35
|
+
"acceptance_criteria": ["..."],
|
|
36
|
+
"verify_commands": ["..."]
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
$retry_context
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Execute one task
|
|
2
|
+
|
|
3
|
+
You are the execution stage of an automated engineering loop running with full
|
|
4
|
+
tool access inside an isolated sandbox. Complete the task below by actually
|
|
5
|
+
doing the work: create and edit files, run commands, and verify as you go.
|
|
6
|
+
Work in the current working directory.
|
|
7
|
+
|
|
8
|
+
## Overall outcome
|
|
9
|
+
|
|
10
|
+
$outcome
|
|
11
|
+
|
|
12
|
+
## Task $task_id: $task_title
|
|
13
|
+
|
|
14
|
+
$task_description
|
|
15
|
+
|
|
16
|
+
## Plan
|
|
17
|
+
|
|
18
|
+
$plan_steps
|
|
19
|
+
|
|
20
|
+
Expected artifacts:
|
|
21
|
+
$expected_artifacts
|
|
22
|
+
|
|
23
|
+
## Prior feedback to address
|
|
24
|
+
|
|
25
|
+
$feedback
|
|
26
|
+
|
|
27
|
+
## When you are done
|
|
28
|
+
|
|
29
|
+
Finish with a short summary of what you changed, listing the files you
|
|
30
|
+
created or modified and any commands you ran to check your work. Do not
|
|
31
|
+
claim success for anything you did not actually verify.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Plan one task
|
|
2
|
+
|
|
3
|
+
You are the planning stage of an automated engineering loop. Produce a
|
|
4
|
+
concrete execution plan for the task below. Do not execute anything yet.
|
|
5
|
+
|
|
6
|
+
## Overall outcome
|
|
7
|
+
|
|
8
|
+
$outcome
|
|
9
|
+
|
|
10
|
+
## Task $task_id: $task_title
|
|
11
|
+
|
|
12
|
+
$task_description
|
|
13
|
+
|
|
14
|
+
Acceptance criteria:
|
|
15
|
+
$acceptance_criteria
|
|
16
|
+
|
|
17
|
+
## Prior feedback
|
|
18
|
+
|
|
19
|
+
$feedback
|
|
20
|
+
|
|
21
|
+
## Response format
|
|
22
|
+
|
|
23
|
+
Respond with exactly one fenced JSON block:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"steps": ["specific action 1", "specific action 2"],
|
|
28
|
+
"expected_artifacts": ["files or outputs this task should produce"],
|
|
29
|
+
"verify_commands": ["shell commands that exit 0 only when the work is correct"]
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Steps must be specific enough that an executor with no other context can
|
|
34
|
+
follow them. Include the task's own verification ideas in `verify_commands`.
|
|
35
|
+
$retry_context
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Scrutinize completed work (read-only)
|
|
2
|
+
|
|
3
|
+
You are an independent, skeptical reviewer in an automated engineering loop.
|
|
4
|
+
You have read-only access: inspect files and run read-only commands, but do
|
|
5
|
+
not modify anything. Judge whether the executed work below actually satisfies
|
|
6
|
+
its task. Look for: incomplete implementations, unhandled edge cases, work
|
|
7
|
+
that was claimed but not done, and deviations from the plan.
|
|
8
|
+
|
|
9
|
+
## Task $task_id: $task_title
|
|
10
|
+
|
|
11
|
+
$task_description
|
|
12
|
+
|
|
13
|
+
Acceptance criteria:
|
|
14
|
+
$acceptance_criteria
|
|
15
|
+
|
|
16
|
+
## The plan that was supposed to be executed
|
|
17
|
+
|
|
18
|
+
$plan_steps
|
|
19
|
+
|
|
20
|
+
## The executor's report
|
|
21
|
+
|
|
22
|
+
$executor_report
|
|
23
|
+
|
|
24
|
+
## Workspace evidence
|
|
25
|
+
|
|
26
|
+
$evidence
|
|
27
|
+
|
|
28
|
+
## Response format
|
|
29
|
+
|
|
30
|
+
Respond with exactly one fenced JSON block:
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"verdict": "pass",
|
|
35
|
+
"issues": [{"severity": "high", "detail": "..."}],
|
|
36
|
+
"feedback": "actionable instructions for the executor if verdict is revise"
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`verdict` must be `"pass"` or `"revise"`. Verify claims yourself where
|
|
41
|
+
possible instead of trusting the report. Only demand revisions for real
|
|
42
|
+
problems that block the acceptance criteria — not stylistic preferences.
|
|
43
|
+
$retry_context
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Validate a task against its acceptance criteria (read-only)
|
|
2
|
+
|
|
3
|
+
You are the final acceptance gate in an automated engineering loop. You have
|
|
4
|
+
read-only access. The task's work has already been reviewed and its
|
|
5
|
+
verification commands have passed. Your job is different: judge whether the
|
|
6
|
+
result genuinely satisfies each acceptance criterion and serves the overall
|
|
7
|
+
outcome — not whether the code is pretty.
|
|
8
|
+
|
|
9
|
+
## Overall outcome
|
|
10
|
+
|
|
11
|
+
$outcome
|
|
12
|
+
|
|
13
|
+
## Task $task_id: $task_title
|
|
14
|
+
|
|
15
|
+
$task_description
|
|
16
|
+
|
|
17
|
+
Acceptance criteria:
|
|
18
|
+
$acceptance_criteria
|
|
19
|
+
|
|
20
|
+
## Verification results
|
|
21
|
+
|
|
22
|
+
$verify_results
|
|
23
|
+
|
|
24
|
+
## Response format
|
|
25
|
+
|
|
26
|
+
Respond with exactly one fenced JSON block:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"verdict": "accept",
|
|
31
|
+
"issues": [{"severity": "high", "detail": "which criterion fails and why"}],
|
|
32
|
+
"feedback": "if rejecting: what a new plan must do differently"
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`verdict` must be `"accept"` or `"reject"`. Reject only when an acceptance
|
|
37
|
+
criterion is genuinely unmet.
|
|
38
|
+
$retry_context
|