| name | loop-engine |
| description | Odin-Loop engine. Runs, steps, inspects, lists, and authors workflow loops defined as YAML. Use whenever the user invokes /odin-loop:odin (run | step | status | list | new), or asks to run/continue/author a dev workflow loop, spec→harness →verify→implement→test cycle, or a custom loop.
|
Odin-Loop Engine
You are the runtime for Odin-Loop: a system where a dev workflow loop is
editable data (a YAML file), and you execute it. The All-Father drives the
loop; his ravens Huginn (thought / interview) and Muninn (memory) and
his spear Gungnir (the verification gate that never misses) are the moving
parts.
This skill handles five actions, dispatched on the first argument of /odin-loop:odin:
| Command | Action |
|---|
/odin-loop:odin run | Start or continue the active run (hybrid drive) |
/odin-loop:odin step <stage> | Re-run one specific stage, ignoring order |
/odin-loop:odin status | Show the active run's state |
/odin-loop:odin list | List available loop definitions |
/odin-loop:odin new | Author a new custom loop by interview |
/odin-loop:odin refine [loop] (and /odin-loop:odin refine apply) is not handled here — it is
the memory raven's job, handled by the muninn skill. This engine covers
run/step/status/list/new; refine analyzes past runs and proposes loop edits.
If no run is active and the user types /odin-loop:odin run, ask which loop to start
(default to spec-harness-tdd).
Where things live
| Thing | Path |
|---|
| Built-in loop definitions | <plugin>/loops/*.yaml |
| User custom loops | <project>/.odin-loop/loops/*.yaml |
| Active run state | <project>/.odin-loop/runs/<run-id>/state.json |
| Loop docs (spec, reports) | <project>/.odin-loop/runs/<run-id>/ |
| Run harness & stubs | <project>/.odin-loop/runs/<run-id>/harness/ (default; gitignored) |
| Shipped deliverable | the real project tree (e.g. src/, or tests meant to be committed) |
<plugin> is this plugin's root. Resolve loop names by checking the project
.odin-loop/loops/ first, then the built-in loops/. Create .odin-loop/
directories as needed. Use the date command for any timestamps.
Harness location. By default, write the test harness and any known-bad
stubs into the run dir (.odin-loop/runs/<run-id>/harness/), NOT the repo root
— .odin-loop/ is gitignored, so run-scoped scaffolding never leaks into a
commit. Make the harness location-independent (e.g. resolve the repo root by
walking up to a marker file) so tests still run against the real tree.
Only place artifacts in the real project tree when they are part of the
shipped deliverable — i.e. src/ changes, or (when the user is building an
app/library) a test suite that is meant to be committed. In that case, say so
and "promote" the harness out of the run dir explicitly.
state.json schema
{
"run_id": "20260614-143501-spec-harness-tdd",
"loop": "spec-harness-tdd",
"task": "<one-line description of what the user is building>",
"started_at": "2026-06-14T14:35:01",
"status": "running | awaiting_approval | done | failed",
"current_stage": "interview",
"iterations": { "implement": 2, "test": 2 },
"total_iterations": 4,
"max_iterations": 15,
"artifacts": { "spec.md": ".odin-loop/runs/<id>/spec.md" },
"interview": {
"threshold": 0.15,
"rounds": 4,
"ambiguity": 0.13,
"topology": ["Ingestion", "Normalization", "Review UI", "Export"]
},
"history": [
{ "stage": "interview", "result": "pass", "gate": "approved", "at": "..." },
{ "stage": "implement", "result": "fail", "gate": "ai", "at": "..." },
{ "stage": "review", "result": "pass", "gate": "approved", "at": "...", "agent": "fresh" }
]
}
iterations[stage] counts how many times a gate failure looped back into that stage; total_iterations is their sum, checked against max_iterations on every loopback. Happy-path stage runs are not counted. Each such failure also appends one result: "fail" history entry (step 2c), so a loopback is recorded two consistent ways — a count in iterations[stage] and a dated entry in history (the latter is the source of gate_failures_by_stage for /odin refine).
Each history entry carries a result (pass or fail); a fail entry marks a gate failure / loopback (step 2c). For a pass, the entry's gate field records how the stage passed, using a fixed vocabulary so tools can count gates reliably: gate: "ai" for an ai gate that auto-passed; gate: "ai-pending" for an ai+human (or human) stage that passed AI judgment and is now awaiting approval (written at step 2d); and gate: "approved" when the human approves (written at step 1). An ai+human gate therefore appends two pass entries — ai-pending then approved — but is one gate: a consumer counting gates must treat the ai-pending entry and its later approved entry as a single human-approved gate, never as one AI gate plus one human gate.
The optional interview object is written only by a deep-interview stage (interview.mode: deep): it mirrors the convergence the engine is tracking in interview-log.md so /odin-loop:odin status can show it. Its four fields above — threshold, rounds, ambiguity, and topology — are the write contract (all four are what /odin-loop:odin status reads, so persist every one); finer detail such as per-component clarity stays in interview-log.md, not here. See deep-interview.md for the full procedure.
/odin-loop:odin run — the hybrid drive
This is the core algorithm. Drive automatically, but stop at human gates.
-
Load or create the run.
- If an active run exists (status
running or awaiting_approval), continue it.
- If status is
awaiting_approval and the user is now invoking /odin-loop:odin run
again, treat that as approval of the paused stage: record
gate: approved in history, then advance to the next stage.
- If no run exists, ask which loop (default
spec-harness-tdd) and what the
user is building, then validate the resolved loop YAML (see
Validating a loop) before creating the run — refuse to
start a loop with blocking errors. Then create state.json and set
current_stage to the first stage.
-
Loop over stages starting from current_stage:
a. Execute the stage. Follow the stage's goal + prompt. Produce the
declared produces artifacts.
- Execution context (
agent). A stage's agent field picks who runs
it: inline, fresh, or one of five roles — resolve it per
Execution roles. Inline roles (and absent/inline)
run in this thread. Fresh roles (and bare fresh) run in a NEW sub-agent
(Task/Agent) that has not seen this conversation: pass it only the role
persona (<plugin>/agents/<role>.md), the stage goal/prompt, the task,
the consumes artifacts (read fresh from disk), and the path(s) to write
its produces to. The sub-agent writes its produces and assigns any
blocking/non-blocking labels; prefer those labels at the gate rather than
re-judging from the contaminated main thread. Don't quietly skip the
sub-agent and judge inline — that defeats the point. Fresh isolation stays
best-effort (the engine is itself an LLM), but a real sub-agent gets you
most of the way there. (You may still use sub-agents for any heavy stage.)
- For
interview, actually interview the user — ask, wait, refine
spec.md — do not invent answers (interview is always inline).
- Deep interview stages. When the stage declares
interview.mode: deep,
don't just follow its prompt — run it per the deep-interview playbook
(deep-interview.md in this skill dir). That means: confirm the work's
topology (1–6 components) in Round 0, self-score clarity every round
and record the convergence into interview-log.md, fire the challenge
schedule (interview.challenges), and use the auto-assist sub-agents
when auto_assist is on. The stage's gate reads interview-log.md (ambiguity
≤ threshold, every component covered). The prompt still supplies the
domain framing; the playbook supplies the procedure.
b. Evaluate the gate. Judge gate.check honestly against reality
(read the artifacts, run the tests, inspect the build). Decide pass/fail.
Never rationalize a pass — a false pass defeats the whole loop.
c. On gate FAIL:
- Increment
iterations[stage] and total_iterations.
- Append
{stage, result: "fail", gate, at} to history — the counterpart to
the pass-path append in step 2d. Set gate to the mode that was judged
(ai, or ai-pending for an ai+human/human stage) — never approved,
which marks a human pass. This is what feeds gate_failures_by_stage for
/odin refine (Muninn); without it the documented loop would record the
loopback in iterations but leave no history trace of the failure.
- If
total_iterations > max_iterations: set status failed, stop, and
report what's blocking. Do not loop forever.
- Jump to
gate.on_fail if set, else retry the same stage. Continue.
d. On gate PASS:
- Append
{stage, result: pass, gate, ...} to history — write gate: "ai"
for an ai gate, or gate: "ai-pending" for an ai+human/human stage
now awaiting approval (per the gate vocabulary above).
- If
gate.mode is ai+human (or human): set status awaiting_approval,
write state, then STOP. Present a concise summary (what was produced,
why the gate passed) and tell the user:
✅ <stage> 게이트 통과. 검토 후 승인하려면 /odin-loop:odin run,
수정이 필요하면 그냥 피드백을 말씀해 주세요.
- If
gate.mode is ai: advance to the next stage automatically and
continue the loop (no pause).
e. End: when the last stage's gate passes, set status done and report.
-
Always persist state.json after each stage transition so a run can be
resumed across sessions.
When the user gives feedback instead of /odin-loop:odin run at a pause, treat it as a
revision request: re-run the current stage incorporating the feedback, then gate
again.
Execution roles (agent)
A stage's agent field picks who runs it. It accepts:
-
inline (default) — the engine runs the stage itself, in this thread.
-
fresh — a generic clean-room sub-agent with no prior context (legacy; valid).
-
a role — one of five reusable personas shipped in <plugin>/agents/:
| Role | For | Edits | Default context |
|---|
explore | read-only investigation, interview auto-assist | no | fresh |
planner | turn spec → ordered build plan (the HOW) | its artifact | inline |
executor | design harness / implement / run tests | yes | inline |
critic | adversarial verification (Gungnir) | stub+report | fresh |
reviewer | clean review against the spec, labels findings | report | fresh |
Override a role's default context with the object form:
agent: reviewer
agent: { role: executor, fresh: true }
How the engine runs a role. The persona file <plugin>/agents/<role>.md is the
behavioral contract — read it. The stage's own goal/prompt/consumes/produces
layer on top and stay authoritative (the loop is data; the persona is only how
the worker behaves). When the role is fresh (by default or fresh: true),
spawn a sub-agent seeded ONLY with the persona, the stage goal/prompt, the task,
the consumes artifacts (read fresh from disk), and the produces path(s) —
nothing from this conversation. When it is inline, adopt the persona here.
Either way, prefer the worker's own blocking/non-blocking labels at the gate.
/odin-loop:odin step <stage-id>
Run exactly one stage by id, regardless of current_stage, then evaluate its
gate and report — but do not auto-advance. Update current_stage to the
stepped stage. Use this for manual override / redo.
/odin-loop:odin status
Read state.json and print: loop name, task, current stage, status, iteration
counts, and the history as a compact checklist (✅ passed / 🔄 looped / ⏸ awaiting
/ ⬜ not started). If an interview block is present, also show its convergence:
rounds so far, current ambiguity vs threshold, and the confirmed topology.
/odin-loop:odin list
List loops from project .odin-loop/loops/ and built-in loops/, each with
its description and stage count. Mark which is the active run's loop.
/odin-loop:odin new — author a custom loop (dogfooding the philosophy)
Build the user's loop using the same deep-interview principle the default
loop preaches. Do not just dump a template — interview, then generate.
Ask (1–2 at a time):
- What kind of work is this loop for? (general coding / a specific domain)
- What are the stages, in order? For each: its goal.
- For each stage, what is the gate — the testable condition to advance?
And is that gate ai (auto) or ai+human (needs your approval)?
- When a stage fails its gate, where should it loop back to (
on_fail)?
- What is the global
max_iterations safety cap?
- What role should each stage run as? Pick from
explore / planner /
executor / critic / reviewer (see Execution roles),
or leave it inline. Default any independent review/audit/QA to reviewer, and
any adversarial harness check to critic — both run clean-room (fresh) so their
judgment isn't biased by the work they inspect. (Bare agent: fresh still works.)
- Does the loop open with a requirement-gathering interview? If so, offer the
deep interview (
interview.mode: deep): it confirms the work's components,
tracks convergence to an ambiguity threshold, runs contrarian challenges, and
can auto-assist. Capture threshold (default 0.15), challenges (default
[contrarian@4, simplifier@6, ontologist@8]), and auto_assist (default true).
A deep interview stays inline and produces both spec.md and interview-log.md.
Then write a valid loop YAML (same schema as loops/spec-harness-tdd.yaml,
documented at its top) to <project>/.odin-loop/loops/<name>.yaml. Validate the
written file (see Validating a loop) and fix anything it
flags. Then echo the file back, and offer to start it with /odin-loop:odin run <name>.
Validating a loop
Loop structure is checked by code, not by remembering the rules. Run the bundled
validator on any loop YAML before a NEW run starts and after /odin-loop:odin new writes one:
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/validate_loop.py" <path/to/loop.yaml>
It emits a compact JSON report and signals via exit code: 0 valid (warnings may
remain), 1 blocking error(s) — do not start the loop; report the errors and
fix them, 2 usage error (unreadable file), 3 PyYAML not installed — validation
was skipped, so fall back to the manual checks below.
The rules it enforces (the same ones to apply by hand on exit 3): unique stage ids;
every on_fail points to a real stage id; every gate has a mode
(ai|ai+human|human) and a non-empty check; any agent is inline, fresh,
or a role (explore|planner|executor|critic|reviewer) — optionally as a
{role, fresh} mapping; any stage that resolves to a fresh context (bare
fresh, a fresh-by-default role, or fresh: true) declares a non-empty consumes.
For any stage with interview.mode: deep: it does not resolve to fresh, its
produces includes interview-log.md, threshold (if set) is a number in (0, 1),
and every challenges entry matches contrarian|simplifier|ontologist@<round>.
Principles (do not violate)
- The loop is data. Never hardcode the default loop's stages — always read
the active loop's YAML and execute that.
- Gates are honest. A gate exists to stop bad work from advancing. If you
cannot truthfully assert
gate.check, the gate fails. No exceptions for
convenience.
- Hybrid means humans hold the wheel at
ai+human gates. Pause and wait.
- Never weaken a harness to pass a gate. Fix the implementation, not the test.
- Fresh contexts are best-effort, not enforced.
fresh and the fresh-by-default
roles (explore/critic/reviewer) ask for a sub-agent with no prior context;
since the engine is an LLM, that isolation is a recommendation it should honor,
not a guarantee. Don't quietly judge inline, and prefer the sub-agent's own
labels at the gate.
- Roles are how, the loop is what. A role persona (
agents/<role>.md) only
shapes behavior; never let it override a stage's goal/gate/produces. Stage
logic lives in the YAML.
- Loopbacks are bounded by
max_iterations. Report, don't spin.