dagr producer — write the run, prove the run
You are the
producer: the single writer of a run file that
renders live. dagr is a
representation kernel: you assert task truth, and
it derives only defined view signals from those facts. Missing or wrong facts
still produce a missing or wrong graph; there is no workflow engine to repair it.
Contract version:
(v1/v2 files remain readable; write v3 for new runs).
Find your validator (before you write anything)
The check loop below is the only feedback you get, so resolve the
binary FIRST and stop if you can't:
- if set;
- (on PATH);
- the plugin/repo build:
<dagr repo>/target/release/dagr
— inside a
herdr pane the runtime injects , which IS the dagr
repo root;
- fallback:
cargo run --manifest-path <dagr repo>/Cargo.toml -- check …
.
No validator available → do not start writing run files; say so and
stop. An unvalidated run file is exactly the silent wrongness this whole
system exists to prevent.
The loop (non-negotiable)
write run.json.tmp → dagr check run.json.tmp --strict --json → fix → repeat until []
→ THEN rename over run.json
Validate the candidate, then publish it — never the other way around.
The pane renders whatever
holds, immediately; renaming an
invalid candidate over it shows your error state to every viewer while
you iterate. The safe transaction:
- Write the complete next document to (same directory —
rename must be atomic, so same filesystem).
dagr check run.json.tmp --strict --json
. Exit 0 with is clean;
exit 1 means findings (E-codes are errors; W-codes mean representable-
but-suspect — fix them unless you can say why not; treats
them as failures, prefer it). Exit 2 means the validator could not
read the file at all — a path or tooling problem, never a document
problem; stdout is empty on that path, so never treat empty output as
clean. Stop, re-run the preflight, and never publish on a non-zero
exit.
- Only when clean: (atomic rename — the pane
reloads on mtime and never sees a half-written or invalid file).
- On failure: fix the temp file and re-check. The previous live file
stays untouched — never leave itself in an error state.
Where to write it: the pane looks for
first, then
, then
under the workspace cwd — and waits on
when none exists yet. Default to
in
the workspace root (gitignore it), and one run per file: parallel lanes
inside a run are just tasks with disjoint
, but a separate
workflow gets its own run file and its own pane.
Object model in one breath
- Project = the recursive visual scope. The run is the implicit root;
may have . A phase or workstream is just a project,
not another entity. Each task has one optional home, while its
dependency edges may cross any project boundary.
- Task = the work item. Stable you choose (never a pane id).
is an open set —
impl · review · test · gate · question · docs · ship · …
— pick the honest one ( for a task that exists to be
answered by a human, for fan-ins; both change how dagr draws it).
States: queued · working · review · blocked · done · failed · rejected · canceled · settled_unverified
.
- Attempt = one try at a task. styled , 1-based .
States:
queued · working · done · failed · rejected · settled_unverified · lost
. A retry never rewrites an attempt — it
appends a new one with a . A task's history never moves
backward — you never rewrite or delete an attempt. A task's state
follows its latest attempt, so a send-back moves a task back to
as its new attempt opens.
- Event = append-only provenance:
attempt_started · attempt_settled · promoted · directive · message_resolved · note
, ascending timestamps. Never rewrite
or reorder events.
- Evidence tier on every terminal outcome: (mechanically
checked — test run, commit receipt) · (typed envelope from
the actor) · (inferred) · (bare claim). "The
agent said done" is at best . Missing envelope? That's
— a real terminal state, not a soft .
Invariants dagr check will hold you to
- Task state is a projection over attempts: needs a
working attempt; // need the latest
attempt to match; accepts a latest attempt of or
; forbids a working attempt, and forbids a latest
attempt of or — a task re-queued after a
or attempt is correct and expected; needs
at least one attempt. is task-only: it withdraws planned work
without rewriting or inventing an attempt.
- Causes point backward in time: attempt n>1 carries
(
sent_back · gate_failed · followup · superseded
) whose
names an earlier attempt. only for n=1.
- The run is a DAG: no cycles through or gate
. A gate's fan-in IS its ; exists only to
override when the fan-in set differs from the dependency set.
Encode true sequential work as dependencies; task declaration order is
only the attempt-less sibling tiebreak.
- Terminal attempts carry with ==
and a real evidence tier; timestamps are real ISO-8601
and attempts end after they start.
- Ids are yours and unique: task ids never collide
with attempt ids. herdr pane ids go in , never in .
- Live attempts are locatable and alive: working attempts want a
() and a populated
— (bool), (timestamp
string), (count of composer lines typed but
unsubmitted, when none — a number, not a bool; a bool rejects the
whole document). Update when your agents
produce output; staleness is rendered, silence is the enemy.
- Blocked names its unblocker; promotion is an event, not
an inference — emit
{"type": "promoted", "task": ...}
when a fan-in
completes.
- Project containment is not dependency. Give each task one truthful
visual home; keep every blocker in , including cross-project edges.
Never duplicate a task into two projects to make both impacts visible.
- Operator messages retain their authority and id. The pane delivers a
envelope to you. Respect
versus ; preserve in the resolution
event. dagr transports the request but does not act on it for you.
Recipes
Every recipe below has a complete, strict-clean companion document under
in this skill directory — held clean by the dagr test suite.
The fragments here show the
shape; when you assemble a real file, crib
from the example, because the fragments alone omit cross-references (a
needs its referent declared, a working attempt needs
locator + liveness) that
will hold you to.
Initialize a run
json
{
"dagr": 3,
"run": {
"id": "run-myjob-v01", "title": "what this run is",
"started_at": "2026-02-01T09:00:00Z",
"orchestrator": {"pane": "wX:p1"}
},
"generated_at": "2026-02-01T09:00:00Z",
"projects": [],
"tasks": [],
"events": []
}
Refresh
on every write — it anchors every "Nm ago" on
screen, and a stale value renders a staleness banner.
Set
automatically from your own
when
available (or a stable Herdr agent target otherwise). This is where
queues operator messages; do not point it at a worker. No user onboarding
step or extra controller is required.
Shape projects before tasks
Use the smallest hierarchy that provides honest visual homes. Do not create
separate
or
arrays:
json
"projects": [
{"id": "APP", "title": "Application"},
{"id": "API", "title": "API stream", "parent": "APP", "owner": "api-lead"},
{"id": "UI", "title": "UI stream", "parent": "APP", "owner": "ui-lead"}
]
A task in
uses
. If a UI task depends on it, keep
the UI task in
and put the API task id in its
; the renderer shows
the cross-project edge. Do not duplicate the task or force it into the
common parent. Omit
only for genuinely run-level work.
Start coarse: declare the useful project skeleton, immediate work, and
meaningful gates. Add discovered tasks as they become relevant; publish
operator-visible work, not every internal agent, tool, or runtime step.
Dagr derives queued-row
,
,
, and
from
,
/
, and
; never encode those as extra fields.
Open a task and start its first attempt
json
{
"id": "L1", "title": "impl: core lane", "kind": "impl",
"owner": "l1-dev", "state": "working", "deps": [],
"attempts": [{
"id": "L1·a1", "n": 1, "cause": {"type": "initial"},
"actor": "l1-dev", "model": "fable",
"locator": {"pane": "wX:p3"},
"state": "working", "started_at": "2026-02-01T09:05:00Z",
"liveness": {"prompt_acknowledged": true, "last_output_at": "2026-02-01T09:05:00Z"}
}]
}
Append
{"at": ..., "type": "attempt_started", "task": "L1", "attempt": "L1·a1", "actor": "l1-dev"}
to
.
is a free string rendered verbatim — dagr never rewrites it, so
YOU pick the display form. Use a short
chip:
,
,
,
. The pane gives the column 12
cells (
fills it exactly); longer strings get truncated with
an ellipsis, and narrow layouts drop the chip before they clip your title.
Settle an attempt (with proof)
Set attempt
,
, and
(
must equal the
state); mirror the task
; append an
event.
(
is when the
work stopped; the settled event's
is when
the
verdict landed — they may differ, and the gap is real information:
a pane that stopped at 09:50 whose rejection landed at 10:12 records
both.) Prefer the strongest evidence you have.
The decision rule: the tier describes your evidence for the
settlement claim.
requires that
someone claimed completion —
however weakly; the tier grades the claim's evidence. When
nobody
claimed anything and you are inferring from a runtime signal (pane
exit, silence, a green prompt), the state is
— that
is what the state is for. The four honest settlements, spelled out
completely in
examples/06-evidence-tiers.json
:
- verified — mechanical receipt:
{"result": "done", "evidence": "verified", "receipt": "cargo test 40/40 ✓ @ a1b2c3d"}
- reported — a typed envelope from the actor (structured result
data, not chat prose):
{"result": "done", "evidence": "reported", "receipt": "result envelope: {files: 4, status: complete}"}
- asserted — the actor claimed completion, but only as prose:
{"result": "done", "evidence": "asserted", "reason": "actor asserted completion in chat; no typed envelope to verify against"}
- nobody claimed anything — that is not a soft , it is the
distinct terminal state , and it still needs
and a matching outcome:
"state": "settled_unverified", "ended_at": ..., "outcome": {"result": "settled_unverified", "evidence": "heuristic", "reason": "no claim of completion from the actor; inferring from clean pane exit"}
.
It never upgrades unproven work to success.
Send back and re-enter
A reviewer rejecting work touches THREE records — the review attempt
settles
(the review itself succeeded), the reviewed attempt
settles
, and the fix round is a
new attempt whose
points at the review attempt:
json
{"id": "L1·a2", "n": 2,
"cause": {"type": "sent_back", "by": "rev-1", "ref": "R1·a1", "reason": "error paths untested"},
"locator": {"pane": "wX:p3"}, "state": "working", "started_at": "...",
"liveness": {"prompt_acknowledged": true, "last_output_at": "..."}}
must name a
declared, earlier attempt — if
isn't
in the file, the check fails; a working retry still needs locator +
liveness. Task back to
; dagr draws the ↩ re-entry
from exactly this cause. Complete document:
examples/03-send-back.json
.
Gate a fan-in
A gate's fan-in
is its — that keeps every gate edge inside the
cycle check. Use
only when the fan-in set genuinely
differs from the dependency set:
json
{"id": "G1", "title": "gate: merge lanes", "kind": "gate", "owner": "orchestrator",
"project": "APP", "criteria": "API and UI reviews are clean",
"state": "queued", "deps": ["L1", "L2", "L3"], "attempts": []}
Declare gate inputs in the intentional human reading order, and keep the
whole
array intentional too: dagr preserves declaration order for
attempt-less siblings and for the gate's state-bearing join strip. Do not
rename ids for sorting, attach the gate to one lane as a layout workaround,
or add a synthetic "join" task. Declare the truthful fan-in. A gate with
is a milestone in that project; without one, dagr places it at the
nearest project shared by all inputs. Therefore a gate local to
stays
inside
, a gate joining
and
lives in their parent
, and a
gate joining unrelated top-level projects is a run-level milestone. Input
attempt timestamps never choose its parent. Each direct input renders as
waiting,
working, or
satisfied (plus the normal
blocked/review/failure marks). On narrow panes it aggregates those marks; a
selected gate still reveals the exact input ids.
Prefer an explicit
when the organizational ownership is known;
omit it when inference from input homes is the truthful answer. Never add a
fake dependency solely to move a gate on screen.
When the last input lands, append a
event —
with its
timestamp, like every event:
json
{"at": "...", "type": "promoted", "task": "G1", "detail": "fan-in complete: L1 ✓, L2 ✓"}
"Moving the gate forward" means the projection rules still apply: a gate
at
/
needs its
own attempt in that state — open
(with locator + liveness if live) when the gate's work starts,
and settle it with evidence like any other attempt. A gate with
can be
, or
when the gate was withdrawn.
Complete document:
examples/04-gate-promotion.json
.
Cancel planned work
Set the task to
and give a short
; keep all prior
attempts and events unchanged. There is no canceled attempt outcome. Because
cancellation is not success, update or cancel any task that still depends on it.
Declare loop policy (futures), don't imply it
json
"policy": {"rounds_max": 3, "futures": [
{"on": "pass", "ref": "G1"},
{"on": "fail", "node": {"id": "L1·a2", "title": "fix round", "actor": "l1-dev"}, "loop_back": true},
{"on": "fail", "streak": 2,
"node": {"id": "L1x·a1", "title": "escalate: fresh approach", "attribution": "predicted"},
"after": "L1·a2", "source": "two-strikes rule"}
]}
points at an existing task (rendered
);
declares a
not-yet-real one (
,
with
,
when
is
);
chains onto a sibling future node of the same
policy. dagr renders futures
only from this block, and only for
working or blocked nodes. Complete document:
examples/05a-policy-declared.json
.
Materialize a future (when the predicted round actually starts)
is
current intent, not history — it is the one block you
edit in place. History lives in
and
; those are
append-only, the policy is a forecast you keep truthful. When a declared
future comes real, do all of this in ONE candidate document (then
check → rename):
- Remove the consumed future node from . Leaving it
would collide with the real attempt's id, which the check rejects.
- Repair chains that targeted the removed node: a sibling
future that chained now hangs off the task
directly (drop its ) or off another still-future sibling.
- Append the real attempt with the id the future predicted, a
pointing at the trigger ( ref'ing the failed
attempt for a loop-back fix round; when a gate bounced
it), locator + liveness if live.
- Append the event. Never touch prior attempts or
events.
Before/after pair, both strict-clean:
examples/05a-policy-declared.json
→
examples/05b-policy-materialized.json
.
Record a human decision
json
{"at": "...", "type": "directive", "verb": "reject", "by": "operator",
"task": "L1", "detail": "error paths untested"}
Verbs:
reject · unblock · answer · rule
. Directives are the decisions
log; chat prose is not.
Answer a question (settle a task by directive)
A queued
whose dependencies are done appears as
in
the attention queue; the producer declares no separate readiness field.
A directive event alone cannot settle a task — task state is a
projection over
attempts, so the human's answer needs an
attempt whose actor is the human. Write both in the same candidate:
json
{"id": "Q1", "title": "question: retry budget?", "kind": "question",
"owner": "operator", "state": "done", "deps": [],
"attempts": [{
"id": "Q1·a1", "n": 1, "cause": {"type": "initial"},
"actor": "operator", "state": "done",
"started_at": "2026-02-01T09:40:00Z", "ended_at": "2026-02-01T09:42:00Z",
"outcome": {"result": "done", "evidence": "reported",
"receipt": "directive answer: retry budget 2 rounds"}}]}
plus the event:
json
{"at": "2026-02-01T09:42:00Z", "type": "directive", "verb": "answer",
"by": "operator", "task": "Q1", "detail": "retry budget 2 rounds"}
The receipt quotes the answer; the directive is the decision-log entry.
The same pattern settles any human-resolved task (an unblock that closes
a
, a rule that retires a task). Complete document:
examples/07-answer-question.json
.
Handle an operator message
The pane's default action is one contextual message composer. Herdr queues
the finished message directly to the
locator, so do not
build another inbox daemon. You receive an envelope like:
text
[DAGR OPERATOR MESSAGE]
message_id: msg-0123456789abcdef
run: run-myjob-v01
revision: 2026-02-01T09:42:00Z
target: G1
starter: get-guidance
authority: recommend_and_return
Ask sol5.6·max and fable·xhigh independently, then combine their opinions.
Do this:
- Acknowledge the message. Treat the raw prose as instructions about the
named target, bounded by the existing run scope.
- Obey authority independently of prose:
means do the analysis and return the choice;
lets you decide and proceed. Never infer the
second from wording such as “best guess”.
- Treat model, reasoning, and multi-agent requests as ordinary editable
instructions and use your normal orchestration tools. dagr runs none of it.
- Keep the through follow-ups. On resolution, append an event:
json
{"at":"2026-02-01T09:50:00Z","type":"message_resolved","task":"G1",
"message_id":"msg-0123456789abcdef",
"detail":"recommended option B after two independent reviews; awaiting operator"}
If several messages informed one decision, set
on the
directive/resolution event. Do not edit
; dagr owns that
append-only delivery journal. Your event is the durable project-memory link
back to it.
Customize the three prompt starters when asked
There is no onboarding step. The built-ins (Use judgment, Get guidance,
Snooze) are available by default. If the user asks to add or change an action, atomically
write
beside the run file:
json
{
"version": 1,
"include_defaults": true,
"actions": [
{"id":"architecture-council", "label":"Architecture council",
"prompt":"Ask two independent architecture reviewers and synthesize.",
"authority":"recommend"}
]
}
Each action is only a prefilled editable prompt plus
authority. Keep the list small; prefer one flexible starter over many rigid
buttons. An id matching a built-in overrides it.
replaces the built-ins. Config version
is the supported shape; at most
nine starters are shown, labels are capped at 80 bytes, and prompts at 32 KiB.
The pane reloads this file automatically and shows a banner for invalid or
unsupported configuration.
Old top-level run
values are readable but inert. Do not add them.
Lose a runtime
A pane died mid-work? The attempt is
(no outcome needed), the task
projects to
or
. Do not delete the attempt; the trace is
the record.
What NOT to do
- Don't renumber, rewrite, or delete attempts or events. Append.
- Don't put herdr pane/agent ids in task or attempt ids — they're
data, volatile by design.
- Don't claim without a mechanical receipt. Don't upgrade
evidence after the fact without a new event explaining why.
- Don't encode task state from herdr's view of the world (pane alive ≠
work done). herdr is where, the contract is what is true.
- Don't mirror every internal agent/tool step; include work an operator needs
to understand, steer, or verify.
- Don't hand-compute "% complete" — write per-attempt
(
{"done": 3, "total": 7, "note": "..."}
) and timestamps; analytics are
queries over those.
Field reference
The full schema is
(Schema v3 section) in the dagr repo;
findings codes are listed there too. When
names a code
you don't recognize, read its message — every finding carries the JSON
path of the offending field.