LLM output extends the running workflow: dynamic DAG growth and spawned child work
State: open/active · Genre: analysis
Use case: dynamic-workflow-growth RFC mapping: new (extends UC7) Source: cross-repo
survey of sksizer/* (2026-07-05), companion to the workflow-constellation RFC (PR #682).
The need
Section titled “The need”A recurring shape the static-DAG framing of UC7 misses: an LLM step’s output adds nodes to the workflow that is currently executing — a planning agent decomposes an epic into child tasks that must join the same traversal, an implementation run spawns one follow-up PR per friction finding, an orchestrator re-derives its ready set every tick because agents keep growing it. Today this is supported only by reload-and-repick hacks.
Evidence
Section titled “Evidence”DarkFactory
Section titled “DarkFactory”The planning workflow pins an opus agent to write child PRD files, and the graph executor explicitly supports ‘mid-run DAG growth’ by re-loading all PRDs between runs so agent-created children join the same invocation; PRD-551 specifies reload-between-batches for the parallel case.
Where: python/darkfactory/workflow/definitions/prd/planning/workflow.py ·
python/darkfactory/graph/_execution.py ·
.darkfactory/data/prds/PRD-551-parallel-graph-execution.md
Pain: The executor’s reload-and-repick loop exists largely to support agent-grown graphs — a hand-rolled workaround for a missing primitive.
task-work Step 8 fans out one /sdlc:spawn-task-pr invocation per classified friction bullet from the post-mortem (each landing as its own PR, potentially on foreign repos); the orchestrator re-derives ‘what is ready’ every tick because the task graph grows between ticks.
Where: plugin/skills/task-work/SKILL.md · plugin/skills/spawn-task-pr/SKILL.md ·
plugin/skills/orchestrate/SKILL.md
Pain: Child-work spawning is a prose-coordinated sub-agent dispatch with its own marker contract per spawn.
Next-work selection is re-derived by the LLM every iteration (‘pick the highest-priority story where passes:false’) from a ledger the previous iteration’s agent may have rewritten — the degenerate form of a dynamically re-evaluated work graph.
Where: CLAUDE.md · skills/ralph/SKILL.md
Pain: ‘What runs next’ is recomputed by an LLM per iteration because no scheduler owns a mutable graph.
Flowline fit
Section titled “Flowline fit”Gap in @flow/dag as designed: it needs either first-class node append on a running graph (with re-planning of the ready frontier) or a child-workflow spawn primitive whose children join the parent’s scheduling, caps, and reporting. Without it, every planning-agent pattern falls back to reload hacks outside the library.
Proposed API
Section titled “Proposed API”Builds on: 11–12 Introduces: extension: graph.append() on a running DAG
Nodes appended to a running graph from a step’s typed output — the primitive whose absence forces reload-and-repick hacks:
const g = dag("epic") .task("plan", planAgent, { deps: [] }) // agent -> { children: ChildSpec[] } .build();
await runDag(g, { concurrency: 4, onOutput: (step, out, graph) => { if (step !== "plan") return; for (const c of out.children) { graph.append(c.id, taskFor(c), { deps: ["plan"] }); // appended nodes join THIS run's scheduling, caps, journal, and events } },});Entirely an extension: the RFC’s UC7 frames the DAG as static. The scheduler re-derives the
ready frontier after each append — which is also exactly the incremental g.ready() form the
queue-supervision evidence wants (11).