Hierarchical Task Network
story · live
All of my agents coordinate over a shared knowledge graph of work items.
Steering a game studio's work graph from a Discord channel, with agents picking up the work.
Role: architect and sole engineer · Aug–Sep 2026 · Stack: Beads (Dolt-backed task graph), Gas City, Hermes agent harness, SvelteKit, Python · Status: shipped in the NEXUS kernel; live in Holodeck.
Read with Software Factories, the larger system this sits inside.
The problem
Holodeck is a game studio system where most implementation is done by AI agents. Agents are fast. What runs short is work that is shaped well enough for an agent to pick up, and human decisions that arrive in hours, not days.
Before this project the studio's plan lived in a markdown roadmap and in Discord threads. Agents couldn't read intent from either, and humans couldn't see what agents were waiting on. Two failures kept repeating:
- Open-ended human tasks stalled. "Design: the temple main floor" sat for days because there was no clear finish line.
- Decisions got lost. The design partner posted the finished Oracle Temple layout in Discord, an agent recorded it on the right task within the hour, and then nothing happened. The graph knew, but nobody was told.
What I built
One shared task graph, with a skill that lets humans and agents author into it at every level of the hierarchy.
milestone "A player can walk the temple floor, save, load, sleep"
│ (human-stated, player-observable acceptance)
├─ phase epic 1 ──[human gate: sign-off]
│ ├─ leaf: decision owner:human
│ ├─ leaf: content-request owner:human
│ └─ leaf: engineering delegate:city → harness gate
└─ phase epic 2 ──[human gate] (whole subtree hidden until released)
└─ ...
/roadmap, a skill in the NEXUS kernel. It turns goals a human states into a gated graph: milestones, phase epics, and leaf tasks that can be dispatched. The whole structure is written in one atomicbd create --graphcall. The skill proposes and the human confirms; it structures goals but never invents them.- Roadmap identity lives in the repo's linked-data graph. A
directory's
_.jsonldnames the root epic of its roadmap. The studio has one roadmap and Olympus has its own (any title can bind one), all in one database, each addressable from its place in the repo tree. The binding is the only thing committed to git. Every human-readable view is generated from the graph and never edited by hand, so there are never two sources of truth to keep in sync. - Phases are gated. A
humangate on a phase epic hides that phase's entire subtree from every "what's ready" query. Onebd gate resolvereleases it atomically. Sign-off is a single act in the graph, not a meeting note. - Every leaf has a kind with a mechanical finish line. The work
decomposition contract defines seven kinds:
decision,content-request,spec,engineering,transcription,discoveryandplaytest-feedback. Each must name what proves it done: a harness gate, a schema validator, or where a decision gets recorded. "Looks good" is not an acceptance criterion.
How it connects
#roadmap (Discord) holodeck-web
│ human states a goal SvelteFlow "living map"
▼ ▲ SSE push, read-only
holodeck-hermes ── /roadmap ──► beads graph ─┤
▲ (proposes, human confirms) │ ▲ └► roadmap-map → digest cards
│ │ │
roadmap-steward ◄── reads ──────────┘ │ ingest captures Discord
surfaces what waits on a human │ replies onto beads
the moment it exists │
holodeck-gascity
mayor slings routed leaves
to agent pools → PR → close
- holodeck-hermes is the studio's Discord agent. It loads
/roadmapin#roadmap, so goal-setting happens in conversation. - holodeck-gascity is the agent fleet. Pools only see leaves that are
ready, explicitly routed and not epics. The skill creates leaves
unrouted by default and marks intent as
owner:humanordelegate:city. Routing stays a deliberate act. - Ingest captures the substance of Discord discussion onto the right task, and only proposes status changes, never infers them. The steward tells humans what is waiting on them. It started as one daily digest and now surfaces each item the moment it exists. Together they close the loop: talk → graph → prompt → talk.
- roadmap-map is one pure function from the graph to a JSON map per goal. The Discord digest cards and holodeck-web's live graph view both render from it, so every picture of the roadmap agrees.
Decisions that mattered
The skill decomposes; the fleet doesn't. I read both forked
codebases and the live system. Gas City ships no decomposer, and epics
can't be dispatched by construction. So the skill owns decomposition
down to dispatchable leaves. Work that genuinely can't be known yet stays
one discovery leaf, never a fake subtree. A "decompose me" hand-off to
a planner agent is the designed upgrade, not a competitor.
Two agent runtimes, one skill source. The Discord agent is not Claude Code: Hermes discovers skills in a completely different way and ignores Claude plugin manifests. Rather than fork the skill, I ship one source directory exposed twice: a plugin manifest for Claude Code sessions, and a kernel entrypoint change for Hermes. Skill updates reach the gateway through an ordinary submodule pin bump, with no image rebuild.
Hermes and Gas City stay separate planes. Merging the containers would have been simpler and wrong. The fleet assumes it can crash and reconcile, carries its own scoped credentials (so it can't burn the gateway's budget), and changes its checkouts in ways that must not share a working tree. They coordinate through git and the shared graph.
Cycle time is a design property. The contract names three loops:
| Loop | Feedback | Target |
|---|---|---|
| L1, agent self-check | headless harness gates | minutes, closes unattended |
| L2, human steering | steward → reply → ingest | hours, never a day |
| L3, playtest | session-service feedback artifact → task | hours to days |
The bottleneck was L2. A request filed at 9am surfaced in the 4pm digest
and was captured the next day, two orders of magnitude slower than the
agents. The fix: I retired the daily digest (2026-09-01). Human-facing leaves
are now posted the moment they exist,
and a context-advantage test decides between asking a human and
researching first. If the human's edge is context only they have, ask.
If the gap can be closed, file a discovery first, so the eventual
decision takes minutes.
What went wrong, and what I learned
- Several traps fake success.
bd initwithout an explicit database silently creates a private graph where every command "works". Applied graph nodes inherit no labels. Closing a task with children quietly orphans them. The skill now carries a trap list, because each of these looked like success in testing. - A dependency edge on a milestone hides everything beneath it from
bd ready. Milestone prerequisites moved into acceptance text instead of edges. - Structure is not state. An early version answered "what's blocking?" from the graph's shape alone, and confidently asked the design partner for a decision already recorded on the task. The skill now has to read a task's comments before naming it as a blocker.
- Principles meet operations. I set the rule that a graph's client belongs where its work's context lives, and moved NEXUS tasks off Holodeck's server. When the NEXUS-side runtime wasn't ready, I parked them back on Holodeck's graph as a dated, recorded exception rather than quietly bending the rule.
Outcome
/roadmapshipped in the NEXUS kernel and is used by both Claude Code sessions and Hermes agents in Discord.- Holodeck's roadmap runs end to end: goal in Discord → gated graph → ingest capture → steward surfacing, with leaves shaped for fleet dispatch. Milestones were backfilled and every open leaf is labelled with its kind.
- The same graph feeds holodeck-web's live map. That view is the basis for this portfolio's own graph navigation.
Connected
NEXUS Graph
Graph in Development
Tech: Beads, Dolt, Gas City, Hermes, SvelteKit, Python.
People: Justin Hromalik, Creative director.
Outputs: Hierarchical Task Network.