From 739f46fd2b11daf56cd79bb29914bf0865cb531e Mon Sep 17 00:00:00 2001 From: Marcin Balazy
Date: Sat, 10 Oct 2026 18:15:42 +0200
Subject: [PATCH 1/4] docs: add an architecture page with the package layers
drawn from the import graph
---
docs/README.md | 1 +
docs/architecture.md | 92 ++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 93 insertions(+)
create mode 100644 docs/architecture.md
diff --git a/docs/README.md b/docs/README.md
index 31ec96b..1d09129 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -20,6 +20,7 @@ One file per surface, each readable cold. The README is the two-minute landing p
**Working on pm**
+- [architecture.md](architecture.md) - one diagram of how the packages fit: the storage layer, the service layer, the four front ends (CLI, board, MCP server, web cockpit) and the executor beside them, drawn from the real import graph.
- [development.md](development.md) - build, test, the web toolchain, CI, releasing, the deploy target, and the one rule about names in a public repo.
- [design-log.md](design-log.md) - the rationale, the bug stories and the measurements behind the rules in `CLAUDE.md`: what was tried, what it cost, why the current shape won. A rule ending `(history: docs/design-log.md)` has its story here.
diff --git a/docs/architecture.md b/docs/architecture.md
new file mode 100644
index 0000000..b7dbf3d
--- /dev/null
+++ b/docs/architecture.md
@@ -0,0 +1,92 @@
+# Architecture
+
+pm is one data layer with several front ends on top of it, plus the executor as a module beside them. Everything lives in one binary, `cmd/pm`. The diagram is drawn from the real import graph (`go list -f '{{.ImportPath}}: {{join .Imports " "}}' ./...`): a solid arrow means "imports", and every one of them is an import in the code. A dashed arrow is not an import. It marks a process boundary: the package runs `pm` as a child process.
+
+```mermaid
+flowchart TB
+ bin["cmd/pm
Date: Sat, 10 Oct 2026 18:17:33 +0200
Subject: [PATCH 2/4] docs: use mermaid's pipe form for the process-boundary
edge labels
---
docs/architecture.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/architecture.md b/docs/architecture.md
index b7dbf3d..a9f1064 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -54,8 +54,8 @@ flowchart TB
runctl --> storage
solo --> storage
- tui -. "starts pm work / run-epic / finish" .-> bin
- runctl -. "starts pm work / run-epic / finish" .-> bin
+ tui -.->|starts pm work, run-epic, finish| bin
+ runctl -.->|starts pm work, run-epic, finish| bin
```
## The boxes
From 0d6880c5098c325f0ab4ea844f170318ce74173d Mon Sep 17 00:00:00 2001
From: Marcin Balazy
Date: Sat, 10 Oct 2026 18:25:45 +0200
Subject: [PATCH 3/4] docs: correct the architecture page on shared state,
helper imports and child processes
---
docs/architecture.md | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/docs/architecture.md b/docs/architecture.md
index a9f1064..c3448ad 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -1,6 +1,6 @@
# Architecture
-pm is one data layer with several front ends on top of it, plus the executor as a module beside them. Everything lives in one binary, `cmd/pm`. The diagram is drawn from the real import graph (`go list -f '{{.ImportPath}}: {{join .Imports " "}}' ./...`): a solid arrow means "imports", and every one of them is an import in the code. A dashed arrow is not an import. It marks a process boundary: the package runs `pm` as a child process.
+pm is one data layer with several front ends on top of it, plus the executor as a module beside them. Everything lives in one binary, `cmd/pm`. The diagram is drawn from the real import graph (`go list -f '{{.ImportPath}}: {{join .Imports " "}}' ./...`): a solid arrow means "imports", and every one of them is an import in the code. A dashed arrow is not an import. It marks a process boundary: the package starts `pm` as a child process to run the executor. These are not the only `pm` child processes. The board also runs `pm runs --json`, and the executor runs `pm finish` after `run-epic --then-finish` and registers `pm worker-guard` as a hook.
```mermaid
flowchart TB
@@ -73,7 +73,7 @@ flowchart TB
- **`internal/solo`** - starting a solo Claude Code session.
- **`internal/executor`** - runs pm tasks through headless `claude -p` workers: the `pm work`, `pm run-epic`, `pm finish` and `pm executor` commands, and the `pm worker-guard` hook. It depends on `internal/storage` and the leaf helpers below, never on `internal/cmd`. Its internals are in [executor.md](executor.md).
-The leaf helpers are left out of the diagram, since nearly every box imports one of them:
+The leaf helpers are left out of the diagram to keep it on one screen. Only `internal/cmd`, `internal/executor`, `internal/mcpserver` and `internal/tui/board` import them:
- **`internal/proc`** - process groups.
- **`internal/exitcode`** - exit codes.
@@ -89,4 +89,4 @@ None of them imports another pm package, except `projectarg`, which imports `exi
- **Nothing imports `internal/cmd`** except the binary. `go list -f '{{.ImportPath}}: {{join .Imports " "}}' ./internal/... | grep pm-cli/internal/cmd` lists only `internal/cmd`'s own line.
- **The board and the cockpit never import the executor.** They start it as a process (`exec.Command` with `pm work ...`). Their contract with it is the command names, the flags, the run-state files in `
Date: Sat, 10 Oct 2026 18:36:43 +0200
Subject: [PATCH 4/4] docs: correct three sentences on the architecture page
against the import graph
---
docs/architecture.md | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/docs/architecture.md b/docs/architecture.md
index c3448ad..03bd2ac 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -1,6 +1,6 @@
# Architecture
-pm is one data layer with several front ends on top of it, plus the executor as a module beside them. Everything lives in one binary, `cmd/pm`. The diagram is drawn from the real import graph (`go list -f '{{.ImportPath}}: {{join .Imports " "}}' ./...`): a solid arrow means "imports", and every one of them is an import in the code. A dashed arrow is not an import. It marks a process boundary: the package starts `pm` as a child process to run the executor. These are not the only `pm` child processes. The board also runs `pm runs --json`, and the executor runs `pm finish` after `run-epic --then-finish` and registers `pm worker-guard` as a hook.
+pm is one data layer with several front ends on top of it, plus the executor as a module beside them. Everything lives in one binary, `cmd/pm`. The diagram is drawn from the real import graph (`go list -f '{{.ImportPath}}: {{join .Imports " "}}' ./...`): a solid arrow means "imports", and every one of them is an import in the code. A dashed arrow is not an import. It marks a process boundary: the package starts `pm` as a child process to run the executor. These are not the only `pm` child processes. The board also runs `pm runs --json`, and the executor runs `pm finish` after `run-epic --then-finish` (or on a tracker with `finish_mode: auto`) and registers `pm worker-guard` as a hook.
```mermaid
flowchart TB
@@ -62,7 +62,7 @@ flowchart TB
- **`internal/storage`** - the data layer. Tasks are markdown files with YAML frontmatter under `~/.claude/pm/
the binary"]
+
+ subgraph fronts["Front ends"]
+ cli["internal/cmd
CLI commands (cobra)"]
+ tui["internal/tui/board
terminal board (pm board)"]
+ mcp["internal/mcpserver
MCP server (pm mcp)"]
+ web["internal/server
web cockpit API + SPA (pm serve)"]
+ end
+
+ exec["internal/executor
pm work, pm run-epic, pm finish,
pm executor, the worker guard"]
+
+ subgraph cockpit["Cockpit back end"]
+ feed["internal/feed"]
+ report["internal/report"]
+ review["internal/review"]
+ runctl["internal/runctl"]
+ solo["internal/solo"]
+ end
+
+ service["internal/service
tool semantics shared by MCP and the web API"]
+ storage["internal/storage
task files, project.yaml, run state, journals, timeline"]
+
+ bin --> cli
+ bin --> tui
+ cli --> tui
+ cli --> mcp
+ cli --> web
+ cli --> exec
+ cli --> service
+ cli --> storage
+ cli --> feed
+ mcp --> service
+ mcp --> storage
+ web --> service
+ web --> storage
+ web --> cockpit
+ service --> feed
+ service --> storage
+ tui --> storage
+ exec --> storage
+ feed --> storage
+ report --> feed
+ report --> storage
+ review --> feed
+ review --> report
+ review --> solo
+ review --> storage
+ runctl --> storage
+ solo --> storage
+
+ tui -. "starts pm work / run-epic / finish" .-> bin
+ runctl -. "starts pm work / run-epic / finish" .-> bin
+```
+
+## The boxes
+
+- **`internal/storage`** - the data layer. Tasks are markdown files with YAML frontmatter under `~/.claude/pm/