Architecture diagrams¶
Three views of the system as it exists now, drawn from the code rather than the design that preceded it: a component view (how the packages layer), a sequence for one task's life from spawn to landing, and the task lifecycle state machine.
Each diagram is checked against the code it depicts. Every edge is backed by a file:line citation in the table beneath its diagram — that is what keeps the picture from drifting silently from the system, and it is why the diagrams live here as Mermaid text (they diff in review) rather than as an exported image.
Normative alignment. ARCHITECTURE.md § 2b defines the feature-slice rules and this diagram enumerates the current slices.
briefis an I/O assembly service, not an L4 pure engine: it reads workspace entities and renders the brief. The diagram also includes the entities that own serialized state transitions; append-only events avoid most contention, while scoped locks protect shared transitions.internal/cli/arch_test.goenforces slice isolation andinternal/cli/diagrams_test.gokeeps this inventory complete.
1. Component view — the feature-sliced layers¶
Dependencies point downward only: app → features → entities → shared. Two rules are enforced as tests, not honored by convention (internal/cli/arch_test.go): slices never import each other, and the app layer never reaches past the kernel into entities.
graph TD
subgraph APP["app — aggregation & protocol (no feature logic)"]
cli["cli<br/>aggregate + dispatch + MCP executor"]
mcp["mcp<br/>JSON-RPC 2.0 over stdio"]
end
subgraph FEAT["features — one slice per capability · slices NEVER import each other"]
direction LR
wscore & onboard & planning & briefing & knowledge
collab & insight & teamops & shortcuts & queues
execution & stagegate & ghmirror & skillforge & vcs
selfreport & acceptance & ship & catalog & orchestration & dashboard
reconciliation & cleanup & journal & slices & releasetrain
end
subgraph ENT["entities — domain objects, I/O assembly service, and serialized state transitions"]
model & workspace & store & eventlog & agentid & brief
gates & gitx & procmon & agentstate & skills
end
subgraph SHARED["shared — kernel + pure engines (no upward imports)"]
clikit & mdstore & prompts & ulid
spm & shortcut & team
end
cli -->|aggregate 26 Commands tables| FEAT
cli -->|executor closure| mcp
mcp -.->|argv into same dispatch| cli
execution -->|spawn / worktrees / brief| brief
execution --> gitx
execution --> procmon
briefing -->|assembles the product| brief
planning --> store
planning --> eventlog
FEAT ==>|every slice| store
store --> mdstore
eventlog --> store
brief --> store
ENT --> SHARED
Edges → code¶
| Edge | Where it lives |
|---|---|
cli imports & aggregates all 26 slice Commands tables + mcp serve |
internal/cli/cli.go |
Dispatch: Main → match (longest-path-first) → invoke (gates + handler) |
internal/cli/cli.go:101-163, :266-279 |
mcp never imports cli; cli hands it an Executor closure over the same dispatch |
internal/mcp/mcp.go:22-25, internal/cli/cli.go:316-352 |
20 core MCP tools + cli escape hatch, argv into the same table (no drift) |
internal/mcp/tools.go |
| Slice isolation enforced (feature→feature import fails the build) | internal/cli/arch_test.go:15-44 |
App-layer thinness enforced (cli.go may not import store/eventlog/brief/spm) |
internal/cli/arch_test.go:49-61 |
briefing and execution and vcs are the only slices that import brief |
internal/features/briefing/briefing.go:343, execution/execution.go:364, vcs/lifecycle.go:120 |
store over mdstore (parse/render, atomic writes); eventlog append-only over store |
internal/store/store.go, internal/eventlog/eventlog.go:87 |
| Serialized state transitions: task/sequence, queue/stage, GitHub push, and worktree reclaim use scoped locks | internal/store/store.go:878-927, internal/features/queues/transitions.go:33, internal/features/stagegate/transitions.go:33, internal/features/ghmirror/ghmirror.go:255, internal/features/vcs/vcs.go:294 |
brief is the I/O assembly service over store, eventlog, prompts, risks, glossary, and notes |
internal/brief/brief.go:62-160 |
The 26 feature slices, one line each:
| Slice | Capability | Slice | Capability |
|---|---|---|---|
wscore |
init + workspace identity | stagegate |
project templates + stage gates |
onboard |
adopt dacli into an existing repo | ghmirror |
GitHub projection (local md is source) |
planning |
projects, tasks, risks, glossary | skillforge |
compile skills per runtime |
briefing |
the context/brief product | vcs |
commit/blame/pr/merge/integrate |
knowledge |
notes, retros, prompt audit | selfreport |
file upstream bugs against dacli |
collab |
sync, ask/answer, escalate | acceptance |
dacli accept (verify + close) |
insight |
status, lint, doctor, standup, SPM | ship |
one-command wave tail |
teamops |
identities, roles, routing | catalog |
render role/skill roster |
shortcuts |
memoized guarded commands | orchestration |
the governed perpetual loop |
queues |
owned-cursor checklists | dashboard |
read-only local web UI |
execution |
adapters, spawn, supervise, runs | reconciliation |
canonical read-only delivery-state projection |
cleanup |
immutable safe repository-cleanup plans | journal |
append-only event reconciliation + recoverable archival |
slices |
independently landable child-task delivery slices | releasetrain |
resumable checks-gated branch promotion |
2. Sequence — spawn through landing¶
One task, from the loop launching an agent to the branch reaching trunk. The gates all refuse with exit 3 (a policy "no", never retried); the child's writes reach the owner because a worktree agent's dacli state resolves to the shared root, not its worktree's stale .dacli.
sequenceDiagram
participant Loop as loop / root
participant D as dacli (spawn)
participant G as launchGates
participant RT as runtime
participant Ch as child agent
participant Git as git / remote (gh)
participant St as eventlog + store
Loop->>D: spawn --task --role --detach --worktree --claim
D->>G: resolveLaunch runs launchGates in order
Note over G: role-wip · seniority · phase ·<br/>token-budget · taint · claim-overlap<br/>(any fail → exit 3)
G-->>D: cleared to launch
D->>Git: gitx.AddWorktree → branch dacli/NNN-slug
D->>St: mint identity · stamp claim · freeze brief
D->>RT: execRuntime (sandbox + env allowlist)
RT->>Ch: run with frozen brief + worktree preamble
Ch->>Git: dacli commit (author = agent, on its branch)
Ch->>St: EventCommit crumb
Ch->>St: task done → EventProposeStatus "propose: done"
Loop->>D: wait (block until wave finalizes)
Loop->>D: sync (owner applies pending events)
D->>St: apply propose:done → verify boxes → CloseTask
alt --pr (controller-owned PR)
Loop->>Git: push canonical branch + create/reuse PR
opt persisted auto_merge=true
Loop->>Git: queue auto-merge behind required checks
end
Loop->>D: recordSelfPR (holds push while a PR is in flight)
Note over Loop,Git: next cycle: reconcilePendingAccepts
Loop->>Git: prLandStatus == merged?
Loop->>D: accept --force → close record (only after merge)
else --no-pr (local)
Loop->>D: ship
D->>Git: integrate / merge branch into trunk
end
Edges → code¶
| Step | Where it lives |
|---|---|
cmdSpawn → resolveLaunch builds the fully-gated plan |
internal/features/execution/execution.go |
Provider process/stream lifecycle behind runtimeLauncher |
internal/features/execution/provider_runtime.go |
| Runs, agents, logs, and sourced progress projection | internal/features/execution/observability.go |
| Detached wait/finalization and durable lifecycle evidence | internal/features/execution/lifecycle.go |
launchGates — the single ordered gate list (WIP, seniority, phase, token-budget, taint, claim-overlap) |
internal/features/execution/execution.go |
gateRoleWIP / gateTaint / gateClaimOverlap refusals (exit 3) |
internal/features/execution/execution.go |
sandboxFor grant check (rw needs a write-capable runtime; ro needs an enforcer) |
internal/features/execution/execution.go |
preflightIssues — grant/binary-allowlist/prompt-tools, all in one pass |
internal/features/execution/preflight.go:58-88, called at execution.go:487 |
Worktree creation + worktreePreamble (state resolves to shared root) |
execution.go:703-726, :891 |
dacli commit (rw-gated, refuses main, claim-scoped) + EventCommit crumb |
internal/features/vcs/vcs.go:69-127, :178 |
task done (non-owner) files EventProposeStatus "propose: done" |
internal/features/planning/planning.go:433 |
wait → sync → owner applies proposal, verifies boxes, CloseTask |
orchestration.go:712, :725; internal/eventlog/sync.go:179-234 |
accept-propose: comment is left pending by sync; consumed only by dacli accept |
internal/eventlog/sync.go:246-256; acceptance.go:163,189 |
LAND: --pr parks in pendingAccept + recordSelfPR; --no-pr runs ship |
internal/features/orchestration/delivery_tail.go |
reconcilePendingAccepts closes only on confirmed merge (prLandStatus) |
internal/features/orchestration/delivery_tail.go |
ship pipeline: accept → integrate → record → push → optional release |
internal/features/ship/ship.go:72, :150-251 |
record commit to a separate ref (code-only trunk) or staged .dacli on trunk |
internal/features/ship/ship.go:294-349 |
The loop's cycle (the caller of the sequence above)¶
runCycle walks one sprint; the phases are all real dacli subcommand invocations, sequenced by the driver:
graph LR
A["syncTrunk"] --> B["reconcilePendingAccepts"]
B --> C["reapWorktrees"] --> D["advanceStages"]
D --> E["governor: Before?"]
E -->|halt / sleep / idle| E
E -->|run| F["BUILD: spawn wave (per-task --claim)"]
F --> G["wait"] --> H["sync"]
H --> I{"--pr?"}
I -->|yes| J["park pendingAccept + recordSelfPR"]
I -->|no| K["ship"]
J --> L["classifyBatch rollup"]
K --> L
L --> M["review (regenerate backlog)"] --> N["retro + workspace health"]
| Phase | Call site |
|---|---|
syncTrunk, reconcilePendingAccepts, reapWorktrees; ready-frontier scheduling; governor Before |
delivery_tail.go; scheduling.go; orchestration.go |
| BUILD (spawn per task) · wait · sync | orchestration.go:703, :712, :725 |
LAND (recordSelfPR or ship) · rollup · review · retro |
delivery_tail.go; orchestration.go |
3. Task lifecycle — the states a task moves through¶
Status is folder-derived (tasks/open|active|blocked|done/), never frontmatter — the folder wins. There are exactly four states; "accepted" is not a state, it is a close path into done. A non-owner can only propose a transition (an append-only event); the owner applies it on sync.
stateDiagram-v2
[*] --> open: task add<br/>(near-dup → exit 3 unless --force)
open --> active: task claim (owner)
active --> blocked: ask / task block (owner)
blocked --> active: answer (owner) · sync unblock
active --> active: task check (owner only)<br/>(non-owner → exit 3)
active --> done: task done / accept (owner)<br/>[all boxes checked; else exit 3]
done --> [*]
note right of active
Non-owner path: append an event, do NOT move.
task done → EventProposeStatus "propose: done"
accept → EventComment "accept-propose:"
Owner applies on sync / accept.
end note
note right of done
--pr: the record is held OPEN in pendingAccept
until the PR MERGES; close (accept --force) never
precedes trunk landing (issue #74 / #382).
A PR that closes unmerged → back to the ready pool.
end note
Transitions → code¶
| Transition | Command · guard | Where it lives |
|---|---|---|
∅ → open |
task add; near-duplicate refused unless --force (exit 3) |
planning.go:164, :195; store.CreateTask store.go:396 |
open → active |
task claim (owner sets owner + MoveTask); owned+active → exit 3 |
planning.go:319-359, :338-340 |
| non-owner claim | appends EventClaim; "owner applies it on sync" |
planning.go:341-348 |
active → blocked |
ask / task block (owner) → MoveTask(StatusBlocked) |
collab.go:86-123; planning.go:514-554 |
blocked → active |
answer (owner); EventBlock/unblock applied on sync |
collab.go:195-203; internal/eventlog/sync.go:263-271 |
active → active |
task check — owner only checks boxes (non-owner → exit 3) |
planning.go:364-382 |
active → done (owner) |
task done: empty-acceptance → exit 3; any unmet box → exit 3; else CloseTask |
planning.go:403-465, :428-430, :448-450; store.CloseTask store.go:1249 |
active → done (owner) |
accept: empty/require-verify/require-independent/unlanded guards, then CloseTask |
acceptance.go:116-193; internal/features/vcs/lifecycle.go:395 (landing check) |
| non-owner done / accept | EventProposeStatus "propose: done" / EventComment "accept-propose:" |
planning.go:433; acceptance.go:98-111 |
owner applies propose: done |
sync mirrors the owner close (verifies boxes, else stays pending) | internal/eventlog/sync.go:179-234 |
| root reconcile | accept --force adopts an orphaned task, then closes |
acceptance.go:80-91 |
PR merged / unmerged (loop, --pr) |
merged → accept --force close; unmerged → drop back to ready pool |
orchestration.go:897-920 |
The exit-code contract these guards obey: 2 usage, 3 refused-by-policy (never retry), 4 not found, 1 everything else — internal/clikit/clikit.go:67-110.