e2e_pipeline.py
18.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
"""U-014: end-to-end orchestration for architecture doc §11 MVP acceptance
criteria 1-3 (Planner produces validated BRD/PRD/SDD/Benchmark; Coder
implements against the SDD and self-tests pass; Validator runs
benchmark/tests and produces a root-cause-attributed verdict). Criteria 4-7
(Org-level Loop, breakpoint recovery, ...) are explicitly out of this Unit's
scope -- PH2 (U-017/U-021).
**Rewired 2026-08-01 (U-031 regression audit)**: `plan_documents` used to
call `org_node_trigger.trigger_org_node()` directly, bypassing
`work_graph_plan.plan_work_graph()`/`work_graph_execute.execute_work_graph()`
/`work_node_run.run_work_node()` entirely -- those three modules were each
fully unit-tested (and even real-API-validated) in isolation since PH1/PH2,
but nothing in production ever called any of them; this was the one real,
validated closed loop, and it used a completely separate, simpler mechanism.
REQ-02's own coverage line names `c_work_graph_plan`/`c_work_graph_execute`
explicitly, so that gap is now closed: `plan_documents` calls
`plan_work_graph()` for a real graph_manifest, then
`work_graph_orchestrate.run_graph()` executes each node for real (see that
module's docstring for why each node gets its own one-node dynamic-workflow
script rather than one big multi-node script -- REQ-06 breakpoint recovery
needs it). `implement_code` (Coder) goes through the same per-node
mechanism with a synthetic single-node manifest, since REQ-03's own
coverage only names `c_work_node_run`, not graph planning -- there is no
real "plan a Coder module graph" LLM capability built (a much bigger,
separate undertaking; planning docs and planning code modules are
different problems), so a one-node graph is the honest minimum that makes
`c_work_node_run` genuinely load-bearing for Coder too.
`validate_implementation` (Validator) is unchanged -- REQ-04's own
coverage never named the Work Graph controllers at all.
Status-transition bookkeeping (task.status, precondition guards) stays
U-010's job (org_node_lifecycle.trigger_org_node_full) -- called once per
phase with a lightweight "confirm ready" prompt.
**PRD consistency check (2026-08-02, PH4-closure audit)**: REQ-02's
boundary case (contradictory PRD scenario assumptions must be intercepted,
not silently pass validation) had no implementation anywhere -- found while
auditing REQ-01~14 for real coverage before starting PH5.
`_check_prd_consistency` is one extra real `agent()` call (reusing
`agentType='planner'` + a structured-output schema, same mechanism as every
other node) that reviews the PRD's own content for genuine internal
contradiction (not incompleteness/style) right after it's produced, wired
in as `plan_documents`'s `verify_fn` -- see `work_graph_orchestrate.
run_graph`'s docstring for what happens on a failed check (one regeneration
attempt, then `ConsistencyCheckFailedError` if it's still contradictory).
"""
import json
import os
import subprocess
import uuid
from src import (
artifact_write, org_node_lifecycle, org_node_trigger, paths, task_create,
work_graph_execute, work_graph_orchestrate, work_graph_plan, work_node_run,
)
_READY_PROMPT = "Confirm you're ready to begin. Take no other action yet."
_DOC_INPUT_SCHEMA = {"type": "object", "required": ["node_id"]}
_DOC_OUTPUT_SCHEMA = {"type": "object", "required": ["content"]}
_PRD_CONSISTENCY_SCHEMA = {
"type": "object",
"required": ["consistent", "issues"],
"properties": {
"consistent": {"type": "boolean"},
"issues": {"type": "array", "items": {"type": "string"}},
},
}
_PRD_CONSISTENCY_PROMPT = (
"Review the following PRD for internally CONTRADICTORY scenario assumptions -- e.g. two "
"requirements that cannot both hold at once, or the same scenario described two incompatible "
"ways. Do NOT flag mere incompleteness, ambiguity, or style issues, only genuine "
"self-contradiction. Reply via the required schema: consistent=false only if you found a real "
"contradiction, and list each one in issues.\n\nPRD:\n\n{prd_content}"
)
_DOC_PROMPTS = {
"brd": (
"Write a BRD (Business Requirement Document) for this goal, following your own "
"SOUL.md methodology: {goal_text}\n"
"Reply with the document content (Markdown) via the required schema -- no preamble, "
"no meta-commentary about what you're about to do."
),
"prd": (
"Here is the approved BRD for goal '{goal_text}':\n\n{brd_content}\n\n"
"Write the PRD (Product Requirement Document) based on this BRD. "
"Reply with the document content (Markdown) via the required schema."
),
"sdd": (
"Here is the approved PRD for goal '{goal_text}':\n\n{prd_content}\n\n"
"Write the SDD (System Design Document) based on this PRD, including a Development "
"Unit breakdown. Reply with the document content (Markdown) via the required schema."
),
"benchmark": (
"Here is the approved SDD for goal '{goal_text}':\n\n{sdd_content}\n\n"
"Write the Benchmark/acceptance-test document based on this SDD: concrete test "
"scenarios (happy/boundary/failure) with observable pass criteria. "
"Reply with the document content (Markdown) via the required schema."
),
}
_CODE_INPUT_SCHEMA = {"type": "object", "required": ["node_id"]}
_CODE_OUTPUT_SCHEMA = {"type": "object", "required": ["response"]}
_CODER_PROMPT = (
"Implement this goal per the approved SDD below. Write real code and real tests, run the "
"tests yourself, and only stop once they pass (or once you've exhausted reasonable attempts "
"and can explain why not). Reply via the required schema with a `response` field describing "
"what you did and the real test commands/output. Goal: {goal_text}\n\nSDD:\n\n{sdd_content}"
)
_VALIDATOR_PROMPT = (
"Independently verify the implementation for this goal against the Benchmark below: run "
"the tests yourself, don't just trust the Coder's claims. Reply with a verdict line "
"'VERDICT: PASS' or 'VERDICT: FAIL' or 'VERDICT: INCONCLUSIVE' followed by your reasoning "
"and, if FAIL, a root_cause_category (planning|coder|skill_defect) with detail. "
"Goal: {goal_text}\n\nBenchmark:\n\n{benchmark_content}"
)
def _planner_prompt_builder(goal_text, node_artifact_types):
"""node_artifact_types: dict[node_id] -> artifact_type, read off the
real graph_manifest before running it (build_prompt only ever sees one
node/outputs-so-far pair at a time, not the whole manifest)."""
def build_prompt(node, outputs):
node_id = node["node_id"]
artifact_type = node_artifact_types.get(node_id, node_id)
content_kwargs = {}
for dep_id in (node.get("depends_on") or []):
dep_output = outputs.get(dep_id)
if dep_output is not None:
dep_type = node_artifact_types.get(dep_id, dep_id)
content_kwargs[dep_type + "_content"] = dep_output["content"]
return _DOC_PROMPTS[artifact_type].format(goal_text=goal_text, **content_kwargs)
return build_prompt
def _coerce_bool(value):
"""A real call (2026-08-02) returned `"consistent": "false"` as a JSON
STRING, not a boolean, even with `"type": "boolean"` in the schema --
`if "false":` is truthy in Python, so this silently inverted the check
until caught by a real test (mocks always return real Python bools, so
this never showed up until RUN_REAL_AGENT_E2E=1). Don't trust the
schema's declared type any more than 05 §5.4.1.1 already tells us to
trust --json-schema itself."""
if isinstance(value, bool):
return value
if isinstance(value, str):
return value.strip().lower() == "true"
return bool(value)
def _check_prd_consistency(target_folder_path, prd_content, timeout=180):
"""REQ-02 boundary: one real agent() call reviewing the PRD's own
content for genuine internal contradiction. Returns (ok, issues).
**Self-target worktree isolation (2026-08-03, real self-hosting run)**:
this call uses `agentType='planner'` with `memory: project`, which
writes a real, uncommitted change to `.claude/agent-memory/planner/
MEMORY.md` in whatever `cwd` it runs with. Every prior real run of this
check only ever targeted a throwaway target-project clone, so calling
it directly against `target_folder_path` never caused a problem --
until a genuine self-hosting run hit it: the memory write landed as an
uncommitted local change on the *main* checkout (bypassing
`work_node_run.run_work_node`'s worktree isolation entirely, since this
call never goes through it), which then made the *next* node's own
worktree merge fail with "local changes would be overwritten by
merge". 05 §5.8's unconditional self-target worktree rule has to apply
here too, not just to `run_work_node`'s own callers."""
is_self = paths.is_self_target(target_folder_path)
node_exec_id = "prd-consistency-" + uuid.uuid4().hex[:12]
worktree_path = work_node_run.create_worktree(target_folder_path, node_exec_id) if is_self else None
cwd = worktree_path or target_folder_path
try:
script_name = "graphlemon-prd-consistency-" + uuid.uuid4().hex
prompt = _PRD_CONSISTENCY_PROMPT.format(prd_content=prd_content)
script_content = (
"export const meta = {{ name: {name}, description: 'graphlemon PRD consistency check' }}\n\n"
"const result = await agent({prompt}, {{ agentType: 'planner', schema: {schema} }})\n\n"
"return result\n"
).format(name=json.dumps(script_name), prompt=json.dumps(prompt), schema=json.dumps(_PRD_CONSISTENCY_SCHEMA))
work_graph_execute.write_workflow_script(cwd, script_name, script_content)
result = work_graph_execute.execute_work_graph(
cwd, script_name, {}, _PRD_CONSISTENCY_SCHEMA, timeout=timeout
)
if worktree_path:
work_node_run.ensure_worktree_committed(worktree_path, node_exec_id)
work_node_run.merge_worktree(target_folder_path, node_exec_id)
return _coerce_bool(result["consistent"]), result.get("issues", [])
finally:
if worktree_path:
work_node_run.remove_worktree(target_folder_path, worktree_path)
def _prd_consistency_verify_fn(node_artifact_types, target_folder_path):
"""work_graph_orchestrate.run_graph's verify_fn -- only the PRD node
gets checked, every other node passes through untouched."""
def verify_fn(node, output):
if node_artifact_types.get(node["node_id"]) != "prd":
return True, None
return _check_prd_consistency(target_folder_path, output["content"])
return verify_fn
def plan_documents(conn, task_id, goal_text, target_folder_path, timeout=300):
"""Architecture §11 criterion 1: a real Planner Work Graph
(c_work_graph_plan -> per-node c_work_graph_execute/c_work_node_run via
work_graph_orchestrate.run_graph) producing BRD->PRD->SDD->Benchmark,
each written via c_artifact_write with correct lineage.
Returns {"run_id": ..., "artifact_ids": {artifact_type: artifact_id}}.
"""
plan_result = work_graph_plan.plan_work_graph(
conn, "planner", task_id, goal_text, target_folder_path, timeout=timeout
)
run_id = plan_result["run_id"]
graph_manifest = plan_result["graph_manifest"]
node_artifact_types = {n["node_id"]: n.get("artifact_type", n["node_id"]) for n in graph_manifest["nodes"]}
outputs = work_graph_orchestrate.run_graph(
conn, run_id, "planner", "planner-doc-writing", target_folder_path, graph_manifest,
_planner_prompt_builder(goal_text, node_artifact_types), _DOC_INPUT_SCHEMA, _DOC_OUTPUT_SCHEMA,
verify_fn=_prd_consistency_verify_fn(node_artifact_types, target_folder_path),
)
artifact_ids = {}
for node in work_graph_orchestrate.topological_order(graph_manifest):
node_id = node["node_id"]
artifact_type = node_artifact_types[node_id]
deps = [
artifact_ids[node_artifact_types[dep_id]]
for dep_id in (node.get("depends_on") or [])
if node_artifact_types.get(dep_id) in artifact_ids
]
result = artifact_write.write_artifact(
conn, target_folder_path, artifact_type, task_id, run_id,
".graphlemon/runs/{}/{}.md".format(task_id, artifact_type), outputs[node_id]["content"],
depends_on_artifact_ids=deps,
)
artifact_ids[artifact_type] = result["artifact_id"]
return {"run_id": run_id, "artifact_ids": artifact_ids}
def implement_code(conn, task_id, sdd_content, goal_text, target_folder_path, timeout=1200):
"""Architecture §11 criterion 2: a real Coder call via c_work_node_run
(work_graph_orchestrate.run_graph with a synthetic single-node
manifest -- see module docstring for why there's no real Coder graph
planner), then registers whatever the Coder actually committed as
code_diff (the diff itself) and test_report (the Coder's own response
text, which per its migrated SOUL.md is required to include real test
commands + real output) artifacts.
"""
before_sha = _current_commit(target_folder_path)
run_id = str(uuid.uuid4())
manifest = {"nodes": [{"node_id": "implement", "depends_on": []}], "edges": []}
conn.execute(
"INSERT INTO work_graph_run (run_id, task_id, org_node, graph_manifest, status) "
"VALUES (?, ?, 'coder', ?, 'running')",
(run_id, task_id, json.dumps(manifest)),
)
conn.commit()
outputs = work_graph_orchestrate.run_graph(
conn, run_id, "coder", "coder-implementation", target_folder_path, manifest,
lambda node, outputs: _CODER_PROMPT.format(goal_text=goal_text, sdd_content=sdd_content),
_CODE_INPUT_SCHEMA, _CODE_OUTPUT_SCHEMA, timeout=timeout,
)
response = outputs["implement"]["response"]
diff = subprocess.run(
["git", "diff", before_sha, "HEAD"], cwd=target_folder_path, capture_output=True, text=True
).stdout
diff_result = artifact_write.write_artifact(
conn, target_folder_path, "code_diff", task_id, run_id,
".graphlemon/runs/{}/code_diff.patch".format(task_id), diff or "(no diff -- see test_report)",
)
report_result = artifact_write.write_artifact(
conn, target_folder_path, "test_report", task_id, run_id,
".graphlemon/runs/{}/test_report.md".format(task_id), response,
)
return {"code_diff": diff_result["artifact_id"], "test_report": report_result["artifact_id"]}
def validate_implementation(task_id, benchmark_content, goal_text, target_folder_path, timeout=1200):
"""Architecture §11 criterion 3: real Validator call, parses the
'VERDICT: PASS|FAIL|INCONCLUSIVE' line this Unit's prompt asks for.
Returns {"verdict", "raw_response"}. Unchanged by the 2026-08-01
rewiring -- REQ-04's own coverage never named the Work Graph
controllers (c_bug_record_create/p_validator_root_cause_trace only),
so the simpler flat trigger_org_node call is already faithful to what
this REQ actually requires.
"""
response = org_node_trigger.trigger_org_node(
"validator",
_VALIDATOR_PROMPT.format(goal_text=goal_text, benchmark_content=benchmark_content),
target_folder_path, timeout=timeout,
)
verdict = "INCONCLUSIVE"
for line in response.splitlines():
stripped = line.strip().upper()
if stripped.startswith("VERDICT:"):
for candidate in ("PASS", "FAIL", "INCONCLUSIVE"):
if candidate in stripped:
verdict = candidate
break
break
return {"verdict": verdict, "raw_response": response}
def _current_commit(repo_path):
return subprocess.run(
["git", "rev-parse", "HEAD"], cwd=repo_path, capture_output=True, text=True
).stdout.strip()
def run_task_lifecycle(conn, task_id, target_folder_path, goal_text):
"""The body of run_end_to_end() for an ALREADY-CREATED task -- split out
2026-08-02 so scheduler.py can create the task itself first and still
have a task_id in scope if this raises partway through (needed to route
CorruptPersistedStateError/ClassifierBlockedError/UsageLimitWaitTimeoutError
to task.status='blocked_pending_human', which is scheduler.py's job, not
this module's -- see that module's docstring). Behavior/return shape is
unchanged from before the split; run_end_to_end() below is now a 2-line
wrapper over this.
"""
org_node_lifecycle.trigger_org_node_full(conn, "planner", task_id, target_folder_path, _READY_PROMPT)
plan_result = plan_documents(conn, task_id, goal_text, target_folder_path)
artifact_ids = plan_result["artifact_ids"]
sdd_content = _read_artifact_content(conn, target_folder_path, artifact_ids["sdd"])
benchmark_content = _read_artifact_content(conn, target_folder_path, artifact_ids["benchmark"])
org_node_lifecycle.trigger_org_node_full(conn, "coder", task_id, target_folder_path, _READY_PROMPT)
code_artifact_ids = implement_code(conn, task_id, sdd_content, goal_text, target_folder_path)
org_node_lifecycle.trigger_org_node_full(conn, "validator", task_id, target_folder_path, _READY_PROMPT)
verdict = validate_implementation(task_id, benchmark_content, goal_text, target_folder_path)
return {
"task_id": task_id,
"doc_artifact_ids": artifact_ids,
"code_artifact_ids": code_artifact_ids,
"verdict": verdict,
}
def run_end_to_end(conn, target_folder_path, goal_text):
"""Full orchestration: create_task -> plan_documents -> implement_code ->
validate_implementation. Returns a dict with every intermediate result,
for inspection/debugging rather than hiding it behind a single verdict.
"""
project_id = conn.execute(
"SELECT project_id FROM project WHERE target_folder_path = ?", (target_folder_path,)
).fetchone()[0]
task_id = task_create.create_task(conn, project_id, goal_text)["task_id"]
return run_task_lifecycle(conn, task_id, target_folder_path, goal_text)
def _read_artifact_content(conn, target_folder_path, artifact_id):
file_path = conn.execute("SELECT file_path FROM artifact WHERE artifact_id = ?", (artifact_id,)).fetchone()[0]
with open(os.path.join(target_folder_path, file_path), encoding="utf-8") as f:
return f.read()