Commit ebbc162399232750edcd562d7a6ee83c3e8cde80

Authored by graphlemon
1 parent 98d86e47

graphlemon: auto-commit uncommitted changes for judgment-92eccdb1933c-main-checkout-drift

src/task_query.py
... ... @@ -206,9 +206,10 @@ def get_task(conn, task_id):
206 206 task["work_graph_runs"] = []
207 207 for w in runs:
208 208 nodes = _work_nodes_for_run(conn, w[0])
  209 + graph_manifest = json.loads(w[1]) if w[1] else None
209 210 task["work_graph_runs"].append({
210 211 "run_id": w[0],
211   - "graph_manifest": json.loads(w[1]) if w[1] else None,
  212 + "graph_manifest": _display_graph_manifest(graph_manifest, nodes),
212 213 "status": w[2], "started_at": w[3], "completed_at": w[4],
213 214 # D (2026-08-06): both None for a normal run; both set when this
214 215 # run was forked from a historical node (src/work_graph_fork.py).
... ...
src/work_graph_plan.py
... ... @@ -29,6 +29,13 @@ import json
29 29 import subprocess
30 30 import uuid
31 31  
  32 +from src import execution_log, paths
  33 +
  34 +# node_id reserved for the synthetic work_node_execution _record_planning_node
  35 +# creates below -- task_query.py matches on this exact string to recognize
  36 +# "this run has a real planning node to display", so keep the two in sync.
  37 +PLANNING_NODE_ID = "__planning__"
  38 +
32 39 GRAPH_MANIFEST_SCHEMA = {
33 40 "type": "object",
34 41 "required": ["nodes", "edges"],
... ... @@ -571,6 +578,71 @@ def _normalize_graph_manifest(manifest, additional_valid_loop_back_target_ids=()
571 578 return manifest
572 579  
573 580  
  581 +def _record_planning_node(conn, run_id, task_id, org_node, prompt, raw_response, graph_manifest):
  582 + """Persists the planning call itself (the `claude --agent <org_node> -p
  583 + <prompt>` subprocess call in plan_work_graph() below, NOT any node it
  584 + planned) as a real, clickable work_node_execution -- previously this
  585 + call's own raw output was discarded the moment _extract_graph_manifest
  586 + pulled the JSON back out of it, and the call itself left no trace
  587 + anywhere the console renders (v_task_detail's to-do tree/DAG only ever
  588 + show the nodes a planning call PRODUCED, never the act of planning
  589 + itself). Deliberately does NOT touch work_graph_run.graph_manifest --
  590 + that column stays the pure, LLM-authored manifest run_graph()/fork/
  591 + append-mode all still execute and merge against unchanged; task_query.py
  592 + synthesizes a display-only node from THIS row (matched by node_id ==
  593 + PLANNING_NODE_ID) when building the console's API response instead, so
  594 + execution/fork/append logic never has to know this row exists.
  595 +
  596 + Only called from the fresh (non-append) branch of plan_work_graph() --
  597 + one real "how was this run planned" entry per run is enough to answer
  598 + the question; Coder/Validator's own append_to_run_id planning calls
  599 + aren't given one (YAGNI until someone actually needs to inspect those
  600 + too)."""
  601 + project_id = conn.execute("SELECT project_id FROM task WHERE task_id = ?", (task_id,)).fetchone()[0]
  602 +
  603 + # work_node_execution.skill_ref is a required FK to a real skill row.
  604 + # design-dynamic-work-graph is the actual Skill documenting this
  605 + # self-planning mechanism (skills/design-dynamic-work-graph/SKILL.md) --
  606 + # reused here rather than inventing a placeholder skill_id nothing else
  607 + # would ever reference.
  608 + skill_id = "design-dynamic-work-graph"
  609 + if conn.execute("SELECT 1 FROM skill WHERE skill_id = ?", (skill_id,)).fetchone() is None:
  610 + conn.execute(
  611 + "INSERT INTO skill (skill_id, owner_org_node, file_path, current_git_commit_sha, source, status) "
  612 + "VALUES (?, ?, ?, 'unknown', 'new', 'active')",
  613 + (skill_id, org_node, "skills/" + skill_id + "/"),
  614 + )
  615 +
  616 + node_exec_id = str(uuid.uuid4())
  617 + conn.execute(
  618 + "INSERT INTO work_node_execution (node_exec_id, run_id, node_id, subagent_type, skill_ref, "
  619 + "input_schema_ref, output_schema_ref, input_payload, output_payload, status) "
  620 + "VALUES (?, ?, ?, ?, ?, 'work_graph_plan.goal_text', 'work_graph_plan.graph_manifest', ?, ?, 'done')",
  621 + (
  622 + node_exec_id, run_id, PLANNING_NODE_ID, org_node, skill_id,
  623 + json.dumps({"prompt": prompt}), json.dumps({"graph_manifest": graph_manifest}),
  624 + ),
  625 + )
  626 +
  627 + log_file_id = str(uuid.uuid4())
  628 + log_path = paths.log_file_path(project_id, task_id, run_id, node_exec_id)
  629 + conn.execute(
  630 + "INSERT INTO execution_log_file (log_file_id, node_exec_id, project_id, file_path, format, status) "
  631 + "VALUES (?, ?, ?, ?, 'jsonl', 'closed')",
  632 + (log_file_id, node_exec_id, project_id, log_path),
  633 + )
  634 + # execution_log.append_log does its own conn.commit() -- covers the two
  635 + # INSERTs above too, same connection/transaction.
  636 + execution_log.append_log(
  637 + conn, node_exec_id,
  638 + json.dumps({"event": "node_start", "org_node": org_node, "prompt": prompt}, ensure_ascii=False) + "\n"
  639 + + json.dumps(
  640 + {"event": "node_completed", "raw_response": raw_response, "graph_manifest": graph_manifest},
  641 + ensure_ascii=False,
  642 + ) + "\n",
  643 + )
  644 +
  645 +
574 646 def plan_work_graph(conn, org_node, task_id, goal_text, target_folder_path, timeout=300, append_to_run_id=None):
575 647 """Returns {"run_id", "graph_manifest", "new_node_ids"}. Creates one
576 648 `work_graph_run` row (status='planning') -- moving it to 'running'/
... ... @@ -672,6 +744,7 @@ def plan_work_graph(conn, org_node, task_id, goal_text, target_folder_path, time
672 744 (run_id, task_id, json.dumps(new_manifest)),
673 745 )
674 746 conn.commit()
  747 + _record_planning_node(conn, run_id, task_id, org_node, prompt, payload.get("result", ""), new_manifest)
675 748 return {"run_id": run_id, "graph_manifest": new_manifest, "new_node_ids": []}
676 749  
677 750 new_node_ids = [n["node_id"] for n in new_manifest["nodes"]]
... ...