Branch of orca-workflow: same propose → gate → plan → dispatch, but deps is DATA (graph.json),
not guesswork in your head. Runtime: harness/scripts/orca-graph.py. Draw: fdk/tools/graph-viz.py
(1 graph) + fdk/tools/graph-atlas.py (2D atlas). Default store llmwiki/graph/.
The engine lives in a separate repo https://github.com/Rheinmir/orca-graph (tests + VT eval suite + separate history), installed into
~/.orca-graph/repo/. The three paths above in overstack are shims pointing to that engine — commands typed the same as before. The shim reports
"engine not installed" (rc 3) → run exactly the command it prints: curl -fsSL https://raw.githubusercontent.com/Rheinmir/orca-graph/main/install.sh | bash.
Without overstack: call directly ~/.orca-graph/bin/orca-graph.
WHAT
Purpose and context
- Purpose: break down a PLAN.md into a DEPENDENCY GRAPH (deps is DATA in graph.json), answer 5 questions with deterministic tools, dispatch by topo layer with locks + leases + generation, durable append-only state, and score every model answer against a rubric with sources.
- Trigger (when to use):
- There is a PLAN.md with ≥ 3 tasks and you need to know: what runs in parallel, what waits for what, how far along it is.
- Dispatch multiple agents and need locks so 2 agents don't grab the same task / write the same file.
- Work spans multiple sessions: state must survive crashes, no "remembering in your head".
- Non-goals: NOT for incidents (→
orca-issue), not when there's no PLAN yet (→/proposethen/plan). Outside PRD v1.1 scope (stated plainly): durable pipeline item with outbox/backpressure, token-based layered fan-in, anchor registry, model routing by eval floor, scale proposal, seen ledger for discovery — seeevals/vt-matrix.jsonof the engine repo: 13/28 VT scenarios recordout_of_scopewith reasons. Outside PRD scope (stated plainly, no pretending it exists): PostgreSQL ledger, secret gateway, money budget, LangGraph, integration queue/candidate hash, compensation for external effects, process-level sandbox (run agent commands in a separate container/VM). This tool is file-based for one machine; if you need the above, that's a different engine, not an orca-graph upgrade. (WRITE file allow-list — different from process sandbox — already exists, seerun --strictin the Reference section — Daemon & control-room below and GH#162.)
Mental model
PLAN.md → build → graph.json (cache) + events.jsonl (nguồn chân lý, append-only) → ask (5 câu hỏi) → next/lock/set/run theo lớp topo → verify (+ qc) → audit → graph-viz / graph-atlas / control-room.
State machine per node:
proposed → ready (mọi deps xong) → locked → dispatched → done | done_unverified | failed | unknown; blocked = HITL waiting on a human.
Three dimensions kept separate: state (lifecycle) · verified (verify rc 0?) · fresh (upstream redone after you finish → stale, warning only).
Rules borrowed from the Reprise PRD: idempotent op_key · CAS --if-rev · generation blocks stale results · lease expires → unknown not failed · no verify means no done.
qc(GH#163, optional — field**QC:**in PLAN.md, insidespec_hashasverify): the INDEPENDENT review command runs AFTERverifyrc=0, NOT run by the same call that self-verified (by != "reconcile"atemit()) — separating the reviewer role from the role of the person/agent that just self-verified. If it fails then: viarun/set donedirectly →emit()demotedone→done_unverified(same as verify fail); viareconcile→ conclusionready(reconcile decides definitively on its own, does not leave it hanging at done_unverified). If**QC:**is not declared → behavior is exactly as before (backward compatible).
Input and output contract
| Field | Required? | Meaning | |
|---|---|---|---|
| In | PLAN.md |
yes | ≥ 3 tasks; line **Depends:**, **Verify:**, optional **Kind:**, **QC:**, **Resources:**; not yet present → call skill plan |
| In | reason alongside | should have | **Depends:** Task 1 (data), Task 2 (preference) — reason ∈ data · contract · acceptance · effect_order · control (HARD edge) · preference (only by writing order) |
| In | resource claim | when shared | **Resources:** db-migration(exclusive), api-x(shared), llm(capacity:2) — missing mode = exclusive |
| In | store | no | default llmwiki/graph/ |
| In | --parent <gid>/<node> |
no | attach subgraph to parent graph node |
| In | agent + agent command | when dispatching | agent name for lock --by; command for run … -- |
| Out | <id>.graph.json + <id>.events.jsonl + .locks/ |
yes | graph + persistent state |
| Out | output build |
yes | cycle, inferred deps (gợi-ý), write conflict on the same file between 2 parallel nodes |
| Out | answer with label + source | when the model answers | score by rubric 1 / 0 / 0.3 / 0.5 / fabricated 0 |
| Out | HTML graph + 2D atlas + control-room.html |
yes | 2 python drawing files + control-room generator |
| Out | audit-log.jsonl |
when concluding | the result reopens each source, fabrication = 0 |
Rules and capabilities
- RULE-01 (MUST): Lock controls DISPATCH; file-writing side effects have a SOFT allow-list (GH#162) — see
run --strictabove. The allow-list does NOT replace real process isolation → a separate worktree per task remains an option when full isolation is needed (running commands, environment variables, side effects outside the filesystem). - RULE-02 (MUST): Do not dispatch node
blocked(HITL) to a headless CLI — it will guess on the person's behalf and then go silent (lesson 250626, delivered ~1/5). - RULE-03 (MUST): Every answer from the model about the graph must go through
answerwith an openable label + source. No source → choosekhông-biết(0.3) instead of fabricating (0). - RULE-04 (MUST): Deps
gợi-ý(inferred) must be confirmed by the user or declared**Depends:**before dispatching that layer. - RULE-05 (MUST): Do not delete/edit by hand
events.jsonl; if wrong, append a correction event.graph.jsonis only a cache — if corrupted,buildagain, state is folded from events. - RULE-06 (MUST): Generated HTML: light/dark toggle + full path + terms with explanations (fdk rule) — the 2 drawing files already handle it, do not write HTML by hand.
- RULE-08 (MUST): Resource contention is NOT a DAG edge (PRD v1.1 §23.2). Two independent tasks sharing an integration branch / DB schema / test port / API quota → declare
**Resources:**, so thatlockserializes at runtime; DO NOT add fake**Depends:**. A separate worktree does not exempt a claim. - RULE-09 (MUST):
audit-edgesonly READS and only SUGGESTS. An edge lacking a reason (EDGE_UNJUSTIFIED) must be KEPT and the user asked; "no dependency seen" does not equal "proven independent". Removing an edge = user agreement → fix PLAN → rebuild. - RULE-10 (MUST): User requests ADDING work to a running graph →
add-node(it modifies the original PLAN then rebuilds,plan_version + 1, history preserved). Forbidden to patch by handgraph.json; forbidden to create a new graph for the same goal. - RULE-07 (MUST): What it cannot do (say it plainly): hidden coupling not revealed in files; automatic rollback when an agent dies midway; two-way sync with the Orca ledger; atlas > ~500 nodes requires graphviz; allow-list only checks the filesystem, not network/process/secret access.
- Capabilities: read PLAN and project files; write graph store append-only + file lock; spawn and monitor agent processes (heartbeat by pid); run verify/qc commands; read git diff to check allow-list; generate static HTML. One-way mirror to an external coordination ledger is optional.
Failure boundaries
- No PLAN yet → blocked, call
plan(or/proposethen/plan); if it is an incident → switchorca-issue. - Cycle (
check-cyclesrc 2) or exceeding depth ≤ 6 / ≤ 20 nodes → tool refuses, blocked until scope is reduced. - Deps
gợi-ýnot yet confirmed → clarify with the user before dispatching that layer. - Node
blocked(HITL) → wait for a human, do NOT dispatch headless. - Agent silent beyond lease →
unknown(notfailed) →reconcile; over 3 attempts → failed. - Result of an old gen or a different plan_version → STALE, do not publish.
- No source for the answer → label
không-biết, do not fabricate.
HOW
Main workflow
| Step | Type | Inputs | Action | Outputs/exit | Failure/next |
|---|---|---|---|---|---|
| W01 | deterministic | repo has harness | Step 0: pre-work sweep pull-gate-sweep.sh |
sweep done | no harness → skip |
| W02 | judgment | PLAN | Step 1: PLAN exists yet; not yet → skill plan; fill in real Depends + Verify |
PLAN.md | missing Depends → tool infers, suggested label |
| W03 | deterministic | PLAN.md | Step 2: python3 harness/scripts/orca-graph.py build <PLAN.md> |
graph.json + cycle warning, file conflict | conflict → add Depends or accept with reason |
| W03b | deterministic | graph.json | Step 2b: audit-edges <id> — edges missing reason, preference edges can be dropped, critical path before/after |
finding table (read-only) | EDGE_UNJUSTIFIED → ask user for reason, fill into Depends |
| W04 | judgment | graph | Step 3: 5 questions using ask first, model later via answer with label + source |
answer has label | no source → unknown |
| W05 | effect | graph.json | Step 4: draw graph-viz.py → send HTML to user for approval (gate-create) |
HTML + gate | unknown kind → B06 |
| W06 | effect | graph approved | Step 5: loop next → lock → set dispatched → dispatch → set done until next reports complete |
node done | lease expired → B05 |
| W07 | deterministic | graph done | Step 6: audit <id> → graph-atlas.py llmwiki/graph/ → problem-tree if issues surface |
audit-log + atlas | fabricated > 0 → report |
Details of each step (source of truth for W01–W07; steps 0–6 correspond to W01–W07):
- Pre-work sweep as orca-workflow (
pull-gate-sweep.sh) if harness exists. - PLAN exists yet? Not yet → call skill
plan. Template/planhas lines**Depends:** Task 1, Task 3and**Verify:** <lệnh rc 0>— fill in real; if Depends is missing then tool INFERS from Consumes/Produces and attaches labelgợi-ý. - Build graph:
python3 harness/scripts/orca-graph.py build <PLAN.md>→ print cycle, inferred deps, and conflict writing the same file between 2 parallel nodes (⚠ → add Depends to force sequential, or accept with reason). - Answer 5 questions — using tool first, model later:
ask <id> needs·ask <id> parallel·ask <id> deps <n>·ask <id> why <n>·ask <id> related.- Model-added part (meaning, semantic link to old graph, risks) → write using
answerwith label + source:answer <id> <n> --q why --label chắc|gợi-ý|không-biết --score S --evidence file:path:line edge:a->b event:<op_key> absence:<lệnh> --text "...". - Rubric (user finalized 2026-09-12): correct 1 · wrong 0 · don't-know 0.3 · suggestion with real source 0.5 · fabricated source 0. Not sure → say
gợi-ý/không-biết, don't attachchắc.
- Draw + gate:
python3 fdk/tools/graph-viz.py llmwiki/graph/<id>.graph.json→ send HTML to user for approval (gate-createlike orca-workflow).- Nodes on the graph are drawn by the task's kind (
**Kind:** build|test|fix|research|docs|design|review|security|infra|data|migrate|deploy|release|cleanup|integration|wikiin PLAN.md, defaultbuild) — icon + shape + background color are looked up in theengine/kind-glyphs.jsonof the engine repo (~/.orca-graph/repo/engine/; the default ledger is SHARED across all graphs — edit/add via PR to that repo). Node border is still state (untouched). kindnot yet in the ledger → the tool auto-generates a temporary one (monogram + color/shape by hash, written to~/.orca-graph/kind-glyphs.local.json— machine-local, doesn't pollute the engine's git clone) and prints a warning to stderr. Seeing this warning → ask the user whether they want/raise-issueto submit a new glyph intokind-glyphs.json; don't add it to the default ledger on your own without asking.
- Nodes on the graph are drawn by the task's kind (
- Run loop (repeat until
nextreports complete):
Agent silent past lease →next <id> # node ready = chạy song song NGAY; blocked = HITL, KHÔNG dispatch headless lock <id> <n> --by <agent> # O_EXCL + lease (mặc định 90 phút); heartbeat <id> <n> khi chờ lâu set <id> <n> dispatched --op-key <uuid> # rồi orca orchestration dispatch --task … --inject (brief NGUYÊN VĂN từ PLAN) set <id> <n> done --gen <gen> --op-key <uuid> # gen lấy từ output lệnh dispatched; gen cũ → STALE, không publishnextauto-pushes the node back tounknown→reconcile <id> <n>(runs verify) before locking again. Max 3 attempts. Want to mirror to the Orca ledger:sync-orca <id> --run(one-way; the Orca ledger is runtime-global, stamps the project). - Result:
audit <id>(reopen each source; fabricating = 0; recordaudit-log.jsonl) →graph-atlas.py llmwiki/graph/regen atlas → update problem-tree if a process problem is exposed.
Branches
| ID | Kind | Guard | Behavior | Skip / failure | Rejoin |
|---|---|---|---|---|---|
| B01 | user_optional | work needs to split a subgraph under a node | build <PLAN> --parent <gid>/<node>; the parent node joins when EVERY child node is done |
exceeds depth 6 or 20 nodes → tool refuses | W03 |
| B02 | conditional_required | build again on a graph that already has |
plan_version + 1, node put into superseded[], node changes spec_hash already done → fresh=stale |
old plan_version result → STALE | W06 |
| B03 | user_optional | user wants to stop / continue / cancel | control <id> pause, resume, cancel, status |
cancelled no resume → build new plan |
W06 |
| B04 | capability_optional | want one command instead of 4 hand-typed commands | run <id> <node> -- <lệnh agent> + daemon watch + control-room |
node missing verify → refuse headless | W06 |
| B05 | recovery | agent silent too long lease | next push node back to unknown → reconcile <id> <n> run verify then lock again |
max 3 attempts then failed | W06 |
| B06 | conditional_required | kind strange not yet in kind-glyphs.json |
tool generates temporary glyph into kind-glyphs.local.json + stderr warning → ask user whether /raise-issue new glyph or not |
do not automatically add to the default registry | W05 |
| B07 | user_optional | want to mirror to Orca registry | sync-orca <id> --run (one-way) |
— | W06 |
| B09 | user_optional | user requests adding work / "draw more nodes" when the graph already exists | add-node <id> --title … [--depends t1:data] [--blocks t5] [--files …] [--verify …] [--kind …] [--resources …] → print + tN, rebuild, redraw HTML, give link to user |
generate cycle / > 20 nodes / strange dep → tool refuses, PLAN intact | W05 |
| B10 | recovery | lock report chờ RESOURCE |
another node currently holds a conflicting claim → do another ready node first, ask <id> waiting see who holds it; lock again when that node is done |
do not automatically unlock another agent's node |
W06 |
| B08 | conditional_required | node declares **QC:** |
run independent QC after verify rc 0; fail → done_unverified (run/set) or ready (reconcile) |
not declared → old behavior | W06 |
Validation and stopping
Deterministic: done only when verify rc 0 (and QC if declared); lease/generation/op_key/CAS decided by tool; check-cycles and lint/build --strict rc 2 on failure; audit reopen each source, fabricate = 0. Human needed: approve the HTML graph at the gate, confirm deps gợi-ý, handle node blocked. The run loop stops when next reports done; each node max 3 attempts.
Examples
- Positive: PLAN 5 tasks, t2 and t3 both Depends t1 →
buildreports no cycle, no file conflict →ask t2 parallelreturns t3 →nextgrants t1, when done grants t2 + t3 in parallel each with onelock --by,set done --gen→auditfabricate = 0 → atlas regen. - Boundary/failure: t2 and t3 in parallel both write
src/api.ts→buildprints ⚠ write conflict on the same file → add**Depends:** Task 2to t3 to force sequential then rebuild; agent t3 silent too long lease →unknown→reconcilerun verify before locking again. - Boundary: model wants to say "t4 exists to open the release lock" but there is no openable source →
answer … --label không-biết(0.3), do not attachchắc.
Reference — Parent → child graph hierarchy (PRD Reprise Graph Engine §4)
build <PLAN> --parent <gid>/<node>→ a graph attached to a node of the parent graph (containment tree, DIFFERENT from a dependency DAG). The parent node haschild_graph, automatic join: onlydone_unverifiedwhen EVERY child node is done (invariant 4); a child with nodeblocked→ parentblocked.- Hard limits: depth ≤ 6, ≤ 20 nodes/graph. Exceeding → the tool refuses, reduce scope instead of digging deeper.
- Cross-graph deps:
**Depends:** other-graph/t3— satisfied when that node is done in the other graph.check-cycles [dir]merges every graph, prints the SPECIFIC cycle PATH (a/t2 → b/t3 → a/t2), rc 2 (PRD §5.3). lint <id>— whether the leaf fully meets the contract (PRD §4.4): title · files · produces · verify · deps clear.build --strictrc 2 if verify/files are missing.
Reference — Justified topology & owned resources (PRD v1.1 §23)
- Depends parses STRICTLY: a token that cannot resolve (a note inserted after the
—,Task 1 và Task 2, a task that does not exist, a cross-graph dep in the wrong form<gid>/<tid>) is an ERRORbuild, not swallowed — an edge that silently disappears means a node is ready early with nobody noticing. Prevent it with,or;; put notes on another line. - Edge reason:
**Depends:** Task 1 (data), other-graph/t3 (control). The five hard classes (data · contract · acceptance · effect_order · control) cannot be dropped even if no artifact is passed — an approval gate carrying no payload is still a real edge.preference= only due to write order. Reason is NOT inspec_hash: attaching a reason to the running graph does not make an already-done node stale. audit-edges <id> [--json] [--strict]— dry-run. Finding codes:EDGE_UNJUSTIFIED(Depends declared without a reason → keep) ·EDGE_INFERRED(inferred deps) ·PREFERENCE_REMOVABLE(proposed removal) ·PREFERENCE_HAS_HIDDEN_CONSTRAINT(preference recorded but the downstream Consumes/Verify/QC mentions the upstream Task, file or output, or two nodes write the same file → keep; it compares STRINGS so coupling not exposed in words cannot be caught). Print the critical path counted by node (unit-weight, NOT duration), the number of layers and the ready-now sets before/after.--strictrc 2 whileEDGE_UNJUSTIFIED.- Resource claims: keys are normalized by the tool (lowercase + normpath) so aliases with different spellings are still one resource.
locktakes ALL claims or nothing (no holding A while waiting for B), scans every graph in the same store, runs under a single admission mutex.exclusiveblocks every claim with the same key;capacity:N= the POOL SIZE of the key — each node currently holding the key (includingshared) occupies 1 slot, nodes declaring different N take the smallest N (independent of lock order,sharedcannot bypass the quota);sharedpure is only blocked by exclusive or a full pool. A lease ending on a DIFFERENT graph is also swept by the reaper whenlock/next(a claim does not get stuck across graphs);unlockby hand on a running node returns it tounknown— reconcile before locking again.buildprintsℹ … cùng claimfor a parallel pair — information, not an error. ask <id> waiting— each node that has not run comes with ONE reason:data(waiting on upstream) ·gate(HITL, control/acceptance gate, verify not yet green, unknown awaiting reconcile) ·resource(claim is held, state who holds it) ·queue(max_parallel full) ·control(pause/cancel) ·retry(out of 3 attempts) ·none.nextalso print a line for node ready but not yet able to lock — ready ≠ admitted.- Topo layer (
ask parallel) is a projection for explanation, not a barrier: a node with enough deps is ready immediately even if another node in the same layer as its upstream has not finished.
Reference — Identity, dry run, call budget (PRD v1.1 §24–27)
reconcile-items --expected A,B,C --verdicts rows.jsonl [--universe N]— join verdict (supported|refuted|inconclusive) byitem_id, not by position.{A,A,B}with all 3 lines still MISSING C → rc 2. Lineverdict: nullorstatus≠ ok = outcome missing C WITH ID (errored_ids), not filtered out, not counted as refuted; the same item having both a verdict and an error line →CONFLICTING_OUTCOME; an empty manifest requires--allow-empty; a missing verdict file is an error, not "0 lines".coveragesplits two denominators: done/chosen and chosen/full set (unknownwhen unknown — do not report 100%). Use when fanning out a batch of items (e.g./tc-run) then collecting results.dry-streak --prev N --complete 0|1 --new N— a dry discovery round counts only when the round COMPLETES and there are no new candidates; an errored round resets to 0. Dry = stopping heuristic, not proof that everything has been scanned.cost-envelope <id> [--reviewers N] [--max-attempts N]— count model CALLS before dispatch: planner 1 + worker/node afk + reviewer × nodes with QC + synthesis 1;max_callsmultiply by the retry ceiling;expectedrecord unknown when there is no measurement yet. Verify shell = 0 tokens but still costs CPU, report separately. Do not say "N agents" instead of the total calls.
Reference — Replan does not break history (PRD §10)
buildagain on an existing graph =plan_version + 1; a dropped node goes intosuperseded[](keeping its last state), a node whose contract changes (spec_hash= title/files/deps/verify/produces) but has already finished →fresh=stale(warning only, do not roll back state).- Old result:
set done --plan-version Nwith N ≠ current → STALE, do not publish (invariant 2). runrememberplan_versionat dispatch: a replan happens WHILE the agent is running → the result is STALE, the node returns toreadyto redo according to the new spec (rc ≠ 0).add-nodeis a single-node replan: runs under the admission mutex (two parallel commands do not overwrite each other), sees PLAN exactly as the parser does (ignores code fences), every field must be ONE line,--blocksrejects a node that islocked|dispatched, PLAN must match the graph (for manual edits,buildfirst); a new id does not reuse the id of a dropped node;--blocks t5inserts a new node BEFORE t5 (wired into t5's Depends line, or add it if absent). Rebuilding a SUB graph but forgetting--parentstill keeps the old parent link.
Reference — Control (PRD §8.3, invariant 11)
control <id> pause|resume|cancel|status. "Request" ≠ "stopped":pause_requestedonly becomespausedwhen there are no nodes leftlocked|dispatched;next/lockdoes not grant new nodes when there is noactive.cancelleddoes not resume — build plan version new.build --max-parallel N(default 4):lockrejects when the number of running nodes is already enough (PRD §12.1).
Reference — Daemon & control-room (PRD §8.4: lease 60 s · heartbeat 15 s · reaper 15 s)
run <id> <node> [--hb 15] [--lease-sec 60] [--strict] -- <lệnh agent>— replaces 4 hand-typed commands: short lock → dispatched → spawn command → heartbeat by pid → exit 0 becomesdone(verify runs automatically), non-zero becomesfailed. A node withoutverifyis rejected headless (--allow-unverifiedto force). Automatically writes the directory into registry~/.orca-graph/registry.jsonand automatically spawns the daemon if not present (ORCA_GRAPH_NO_DAEMON=1to turn off,ORCA_GRAPH_HOMEchange home directory).- Write allow-list (GH#162): when in a git repo,
rundiff REAL files changed (tracked modified + new untracked, compared to RIGHT BEFORE spawn) againstfilesdeclared by the node. Out of scope → by default only WARN (written intonoteof eventdone);--strictonly then hard recovery (new untracked are deleted, tracked modified aregit checkoutback to HEAD). Not a git repo (e.g. store lies outside the repo) → automatically ignore, no error. Bookkeeping of orca-graph ITSELF (.graph.json/.events.jsonl/.locks/) is always excluded from the diff, not counted as "agent writes". watch [--once] [--interval 5] [--idle-sec 600]— one daemon for the whole machine, lockdaemon.lockby pid. Each pass only reads the dirs in the registry that still have running nodes (cost by number of running nodes, not by number of open projects; measured 84 ms/pass even including Python startup): reaper lease expires →unknown→ if there is verify then reconcile immediately; print a live/dead table; if the registry is empty longer than idle-sec then exit automatically; if the graph changes then regen control-room.fdk/tools/build-control-room.py→llmwiki/html/control-room.html— the first page that opens, data-first (only reads registry, graph.json, problem-tree, tokens.jsonl, audit-log): what is running on the whole machine + ceiling, progress of each graph + stuck nodes, open debt, today's cost + distance between self-scoring/audit. Auto refresh 5 s; daemon regen each pass and each timerunstarts/ends so the LIVE page needs no server. Tier 2 (server + click pause/cancel) and tier 3 (embed Orca) not done yet, only do when tier 1 used daily feels lacking.- Alive/dead ≠ right/wrong: the daemon only knows whether the process is still there or gone;
doneor redo is still decided byverify. A process alive but hung still has a green heartbeat — set**Verify:**idempotent and time ceiling per node.
Reference — Recap
/orca-graph = PLAN → graph.json has deps → ask answers 5 deterministic questions → dispatch by layer with lock/lease/gen → durable append-only state → answer/audit scores the model by rubric 1/0/0.3/0.5/fabricated=0 → HTML 1 graph + 2D atlas.
Unexpected use case: run build on an old PLAN already finished to recheck whether back then there were 2 tasks writing the same file but running in parallel; or related to see which file of the old PLAN the new PLAN touches first before touching.