Claude Code plugin + local console

Inside the console

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.

Claude Code session
Console or hook code
File on disk
Software Engineer
Zoom 100%
00

The pieces

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
Several runs at onceEverything a run owns lives on its RunSession, and every message that acts on a run names it by runId.
One boundaryOnly server/engine knows the binary, the hook vocabulary and the transcript format. Nothing above it imports node-pty.
Local onlyEvery request needs a Host naming the console, the WebSocket an Origin it served.
01

The path of a hook

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
What the guard refusesA workflow agent under its bare name, a Dev on a model other than Sonnet or Opus, a reviewer started while a task has no report, a session link in a commit or a publication, a publication that shows a credential, a git command that destroys work.
What the stop gate runsPer package the edited files belong to: the type-check, then the lint and the related tests of those files. A failure sends the agent back once. Every verdict is a line of gate-log.jsonl.
Lost hooksemit.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.
Fails openA guard that cannot read what it checks lets the call through, and so does a gate that cannot run a check. IMPL_STOP_GATE=off disables the gate.
02

Questions and decisions

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
Never retried, never spooledA question only makes sense while its session waits. Claude Code replays the hook with the answers; the engine ignores that second pass.
Refusing the trust dialogThe run ends stopped with its reason, not failed, and its worktree is settled like that of any stopped run. An unknown wording shows nothing.
03

The run worktree

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
What the worktree receivesThe ignored dependency directories (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.
What the session is toldIMPL_RUN_WORKTREE, IMPL_SOURCE_REPOSITORY, IMPL_SOURCE_BRANCH, how the dependencies came over, and IMPL_WORKTREE_DEPENDENCIES_STALE when they lag behind the lockfile.
If the worktree cannot be createdThe launch is aborted. The server never falls back on the main checkout.
Shutdown and restartA shutdown marks the worktree kept; the same rules run again on boot. Only a path directly under .claude/worktrees/ is ever removed, and the branch never is.
04

Run health

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
Five readingshealthy, waiting (on the engineer, dated from the question), suspected_stall (silence alone, never an incident), stalled, interrupted.
Four incidentsno_next_action, lost_session (the session exited before a result: the run is failed), missing_result, unresolvable_dependency.
ActionsAnswer, open the terminal, view the diagnostic, ask the pilot to continue, stop the run, dismiss.
File contractsAn agent that ends without the file its contract requires (planner-output.json, the QA and design reports with their plan and evidence, review-summary.md), with nobody taking over, raises an incident.
05

The evidence chain

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
Which status winsFailed over blocked, then unverified, then verified. Stale, unversioned or replaced evidence never counts as verified, and a fail is never read as a confirmation.
Break attemptsA QA item of kind attempt that fails counts against its criterion; any other is listed under it and verifies nothing.
QA verdict checkedA PASS over a criterion QA did not observe raises a warning in the tab and in the summary.
Review confidenceA note from 0 (a person reviews everything) to 5 (nothing observed stands against the automated review), computed by a pure function from the coverage, the diff, the stop gate's log, the findings and the incidents. No agent declares it, and what an agent declares can only lower it. Each rule that applied is listed with the note.
Coverage frozen at deliveryThe code is identified until the workflow's end is archived. A rebase or an edit made afterwards, outside a reopening, leaves the delivered coverage as it was.
06

What stays on disk

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.
Batch analysesIn 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 is not absentA persisted file that cannot be read is renamed *.unreadable-<time> and reported before the console starts from nothing.