How the local console runs a ticket: the path of a hook, the questions, the run worktree, run health, the evidence chain and what stays on disk. Who does what and on which model is in the agent map; this page shows the machinery underneath.
A Next.js page and a Node server on 127.0.0.1:3210, started by impl. The server spawns the real claude binary in a PTY, one session per run, and learns what it does through its hooks. No direct API call and no key. The forge is read with glab or gh by the server itself, without a Claude session. In every diagram, hover a node to see what it does.
flowchart LR
swe("Software Engineer
the console page"):::swe
page{"Next.js page
run view, terminal,
tracking, evidence"}:::script
server{"Node server
registry, RunSession,
monitor, watchers"}:::script
engine{"server/engine
the only code that
knows Claude Code"}:::script
session["Claude Code session
in a PTY,
in the run worktree"]:::agent
data[("console/data
runs, queue,
repositories")]:::file
forge{"glab / gh
merge request state,
blocking links"}:::script
swe --> page
page -- "WebSocket" --> server
server -- "run state" --> page
server --> engine -- "spawn, keys" --> session
session -- "hooks" --> server
server --> data
server -. "no token" .-> forge
classDef agent fill:#3a2210,stroke:#ff9d3c,stroke-width:1.6px,color:#c9cddb
classDef script fill:#12301f,stroke:#4ade80,stroke-width:1.6px,color:#c9cddb
classDef file fill:#0f2236,stroke:#60a5fa,stroke-width:1.6px,color:#c9cddb
classDef swe fill:#3a310a,stroke:#facc15,stroke-width:1.6px,color:#c9cddb
RunSession, and every message that acts on a run names it by runId.server/engine knows the binary, the hook vocabulary and the transcript format. Nothing above it imports node-pty.Host naming the console, the WebSocket an Origin it served.Each hook of the session runs emit.mjs. The guard and the stop gate decide first, inside the session, then the event is posted to the server, which turns it into an engine event, applies it to the run and pushes the new state to the pages that opened that run.
flowchart LR session["Pilot or agent
a tool call,
an agent stop"]:::agent emit{"emit.mjs
one call
per hook"}:::script guard{"guard.mjs
refuses a
dangerous call"}:::script gate{"gate.mjs
checks at the stop
of Dev and Senior"}:::script session --> emit --> guard guard -- "refused" --> session guard --> gate gate -- "fails: back
to work once" --> session classDef agent fill:#3a2210,stroke:#ff9d3c,stroke-width:1.6px,color:#c9cddb classDef script fill:#12301f,stroke:#4ade80,stroke-width:1.6px,color:#c9cddb classDef file fill:#0f2236,stroke:#60a5fa,stroke-width:1.6px,color:#c9cddb classDef swe fill:#3a310a,stroke:#facc15,stroke-width:1.6px,color:#c9cddb
flowchart LR
spool[("hooks-spool.jsonl
kept when the
server is away")]:::file
access{"access.ts
checks the secret
of the hook URL"}:::script
engine{"server/engine
raw hook into
EngineEvent"}:::script
hooks{"hooks.ts
applies it to
the RunSession"}:::script
runjson[("run.json
one write
at a time")]:::file
ws{"context.ts
WebSocket to the
pages of that run"}:::script
spool -. "replayed" .-> access
access --> engine --> hooks
hooks --> runjson
hooks --> ws
classDef agent fill:#3a2210,stroke:#ff9d3c,stroke-width:1.6px,color:#c9cddb
classDef script fill:#12301f,stroke:#4ade80,stroke-width:1.6px,color:#c9cddb
classDef file fill:#0f2236,stroke:#60a5fa,stroke-width:1.6px,color:#c9cddb
classDef swe fill:#3a310a,stroke:#facc15,stroke-width:1.6px,color:#c9cddb
gate-log.jsonl.emit.mjs retries once, then spools the event. The spool is replayed before the next hook and on every monitor tick, and hookId drops the duplicates.IMPL_STOP_GATE=off disables the gate.A question from the workflow blocks the session until the engineer answers in the console. The folder trust dialog Claude Code draws before any hook is read off the terminal and shown the same way, but it is never answered on the engineer's behalf.
flowchart LR pilot["Pilot
AskUserQuestion"]:::agent hook{"PreToolUse hook
waits for the answer,
3600 s at most"}:::script run{"RunSession
pending question,
dated when asked"}:::script swe("Software Engineer
answers in the
conversation"):::swe pty["Claude Code
folder trust dialog,
before any hook"]:::agent trust{"trust-prompt.ts
reads the dialog
off the PTY"}:::script pilot --> hook -- "question" --> run -- "question card" --> swe swe -- "answer" --> run -- "updatedInput" --> hook --> pilot pty -- "screen text" --> trust -- "decision" --> swe swe -. "accept or refuse:
the engine types the keys" .-> pty classDef agent fill:#3a2210,stroke:#ff9d3c,stroke-width:1.6px,color:#c9cddb classDef script fill:#12301f,stroke:#4ade80,stroke-width:1.6px,color:#c9cddb classDef file fill:#0f2236,stroke:#60a5fa,stroke-width:1.6px,color:#c9cddb classDef swe fill:#3a310a,stroke:#facc15,stroke-width:1.6px,color:#c9cddb
stopped with its reason, not failed, and its worktree is settled like that of any stopped run. An unknown wording shows nothing.Every ticket run works in a git worktree of its own, created before the session and settled after it. The main checkout is never touched, and a worktree that may still hold work is kept with its reasons.
flowchart LR
registry{"registry.ts
main checkout,
one run per ticket"}:::script
add{"git worktree add
.claude/worktrees/#lt;run-id#gt;
detached HEAD"}:::script
nope(("no run"))
provision{"provisionWorktree
dependencies, ignored
config, git hooks"}:::script
stale{"staleDependencies
node_modules against
package-lock.json"}:::script
session["Session
in the worktree,
reinstalls when stale"]:::agent
registry --> add
add -. "fails" .-> nope
add --> provision --> stale --> session
classDef agent fill:#3a2210,stroke:#ff9d3c,stroke-width:1.6px,color:#c9cddb
classDef script fill:#12301f,stroke:#4ade80,stroke-width:1.6px,color:#c9cddb
classDef file fill:#0f2236,stroke:#60a5fa,stroke-width:1.6px,color:#c9cddb
classDef swe fill:#3a310a,stroke:#facc15,stroke-width:1.6px,color:#c9cddb
flowchart LR
settle{"settleRunWorktree
applies the
removal rules"}:::script
removed(("removed"))
kept[("kept
with its reasons")]:::file
swe("Software Engineer
removes it after
seeing the risks"):::swe
settle -- "MR open and not a draft,
archive synced, tree clean,
HEAD pushed" --> removed
settle -- "otherwise" --> kept --> swe
classDef agent fill:#3a2210,stroke:#ff9d3c,stroke-width:1.6px,color:#c9cddb
classDef script fill:#12301f,stroke:#4ade80,stroke-width:1.6px,color:#c9cddb
classDef file fill:#0f2236,stroke:#60a5fa,stroke-width:1.6px,color:#c9cddb
classDef swe fill:#3a310a,stroke:#facc15,stroke-width:1.6px,color:#c9cddb
node_modules by default), copied copy-on-write or linked when the copy fails, the .env* files and .claude/settings.local.json, and the ignored git hooks helper (husky's .husky/_). Build outputs are not provided.IMPL_RUN_WORKTREE, IMPL_SOURCE_REPOSITORY, IMPL_SOURCE_BRANCH, how the dependencies came over, and IMPL_WORKTREE_DEPENDENCIES_STALE when they lag behind the lockfile..claude/worktrees/ is ever removed, and the branch never is.The server does not read silence as a failure. It pairs the tool calls, tells the pilot from its subagents and reads the step the workflow declares, evaluates all of it at each tick and opens an incident only when a decision is needed. The decision on an incident is written before it takes effect.
flowchart LR
signals[("RunSession.signals
turn ends, paired calls,
background calls")]:::file
state[("workflow-state.json
the declared step")]:::file
monitor{"run-monitor.ts
one tick per run,
serialised"}:::script
health{"run-health.ts
evaluateRunHealth,
pure, injected clock"}:::script
incidents{"run-incidents.ts
deduplicated,
lifted by its event"}:::script
swe("Software Engineer
picks an action
on the banner"):::swe
action{"incident.action
decision persisted
before the effect"}:::script
archive[("/api/archive
runs left with
an open incident")]:::file
signals --> monitor
state --> monitor
monitor --> health -- "stalled, interrupted" --> incidents -- "banner" --> swe
swe --> action --> incidents
incidents -. "after a restart" .-> archive
classDef agent fill:#3a2210,stroke:#ff9d3c,stroke-width:1.6px,color:#c9cddb
classDef script fill:#12301f,stroke:#4ade80,stroke-width:1.6px,color:#c9cddb
classDef file fill:#0f2236,stroke:#60a5fa,stroke-width:1.6px,color:#c9cddb
classDef swe fill:#3a310a,stroke:#facc15,stroke-width:1.6px,color:#c9cddb
healthy, waiting (on the engineer, dated from the question), suspected_stall (silence alone, never an incident), stalled, interrupted.no_next_action, lost_session (the session exited before a result: the run is failed), missing_result, unresolvable_dependency.planner-output.json, the QA and design reports with their plan and evidence, review-summary.md), with nobody taking over, raises an incident.Each acceptance criterion gets a status the console derives itself from the evidence files, never from what an agent says about its own work. The same derivation feeds the "Evidence" tab and the summary the merge request quotes.
flowchart LR pilot["Pilot
acceptance-criteria.json
AC1 … ACn"]:::agent agents["Dev, Senior,
Designer, QA
evidence files"]:::agent ingest{"acceptance-runtime.ts
ingests, identifies
the code version"}:::script versions[("data/runs/#lt;id#gt;/evidence
every version
and capture")]:::file derive{"acceptance.ts
failed, blocked,
unverified, verified"}:::script qa{"qaVerdictConsistency
QA's verdict against
what QA observed"}:::script facts[("Observed facts
diff, gate log, findings,
incidents, workflow state")]:::file confidence{"review-confidence.ts
note from 0 to 5,
with what lowered it"}:::script tab{"Evidence tab
in the console,
in English"}:::script summary[("acceptance-summary.md
in IMPL_LANGUAGE")]:::file mr["Pilot
merge request
description"]:::agent pilot --> ingest agents --> ingest --> versions ingest --> derive derive --> qa derive --> tab qa --> summary --> mr derive --> summary derive --> confidence qa --> confidence facts --> confidence confidence --> tab confidence --> summary classDef agent fill:#3a2210,stroke:#ff9d3c,stroke-width:1.6px,color:#c9cddb classDef script fill:#12301f,stroke:#4ade80,stroke-width:1.6px,color:#c9cddb classDef file fill:#0f2236,stroke:#60a5fa,stroke-width:1.6px,color:#c9cddb classDef swe fill:#3a310a,stroke:#facc15,stroke-width:1.6px,color:#c9cddb
fail is never read as a confirmation.attempt that fails counts against its criterion; any other is listed under it and verifies nothing.PASS over a criterion QA did not observe raises a warning in the tab and in the summary.Everything the console keeps is under console/data/, ignored by git. It holds ticket content: it never goes into tracked files, commits or docs.
data/runs/<run-id>/run.json (the state, one write at a time), terminal.log, artifacts/ (the workflow documents), evidence/, metrics.json (tokens, durations, diff), gate-log.jsonl, hooks-spool.jsonl while hooks wait.data/queue.jsonThe launch queue and the schedule: tickets, conflict edges and merge request watches. Written whole, then renamed.data/repositories/<checkout>-<digest>/Per repository: runtime-recipe.md (how the app is started and driven) and review-findings.json (the defects the reviews keep finding), both handed to the next run.data/ticket-proposals.jsonThe file the outside watcher writes, read every 5 seconds, and ticket-proposals-handled.json for the tickets already queued or dismissed.implementation-factory-<user>/schedule/ of the system's temporary directory, removed once read, kept with session.log on a failure, wiped on boot.data/feedback/ and data/self-improvement-decisions.jsonlThe feedback and self-audits the improvement loop reads, its plans and reports, and one line per decision of the automatic merge (merged, rejected with its reasons, reverted), with the duration and tokens of each judge session on the branch. The judge's files sit beside the batch analyses, in improvement-judge/, removed once read and kept on a failure.*.unreadable-<time> and reported before the console starts from nothing.