Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ Always add tests when implementing new features or fixing bugs. Conventions:
- Same package tests (access to unexported functions)
- `setupTestStore(t)` helper in `store_test.go` for tests needing a Store with project + tasks
- Invariant tests (links merge, Log append, Spec rewrite, brief lifecycle) live in `tools_test.go`; the `ApplySpec`/`ExtractSpec` unit tests live in `internal/storage/spec_test.go`
- **Executor paths**: test through a FAKE `claude` binary (`fakeClaude` in `internal/cmd/work_exec_test.go` - a PATH-prepended script emitting a canned result envelope), never by spending tokens; covers verified/blocked/crash/garbage paths of `executeWork` (the envelope's green word was `merged` before 0.34.0 and is still accepted - `normalizeWorkerStatus`)
- **Executor paths**: test through a FAKE `claude` binary (`fakeClaude` in `internal/executor/work_exec_test.go` - a PATH-prepended script emitting a canned result envelope), never by spending tokens; covers verified/blocked/crash/garbage paths of `executeWork` (the envelope's green word was `merged` before 0.34.0 and is still accepted - `normalizeWorkerStatus`)
- **MCP handlers**: test END-TO-END via the SDK in-memory transport (`startMCP` in `internal/mcpserver/e2e_test.go`) so the real registered closures run - do NOT re-simulate handler logic in the test body (the pre-0.23 tests did, which is why coverage sat at 12%)
- **Concurrency**: lock/race invariants live in `internal/storage/lock_test.go` + the `TestAcquireWorktreeLockAtomicRace` two-acquirer hammer in `worktree_test.go`; run suspects under `-race`

Expand Down Expand Up @@ -188,11 +188,11 @@ A **journal** is a per-project append-only record of how ONE declared, repeatedl

## Executor (`pm work` / `pm run-epic` / `pm finish`)

Runs pm tasks/epics via isolated headless `claude -p` workers (implement -> test -> review -> verify). **The full internals live in `docs/executor.md`** (worktree slots, review policy, run-state/journal/heartbeat, kill + crash forensics, acceptance claims, baseline, independent mode, telemetry) - read it before touching `internal/cmd/work*.go`, `run_epic*.go`, `finish*.go`, `worker_guard.go` or `internal/storage/executor*.go`. The rules that bite regardless of what you are working on:
Runs pm tasks/epics via isolated headless `claude -p` workers (implement -> test -> review -> verify). **The full internals live in `docs/executor.md`** (worktree slots, review policy, run-state/journal/heartbeat, kill + crash forensics, acceptance claims, baseline, independent mode, telemetry) - read it before touching `internal/executor/` (`work*.go`, `run_epic*.go`, `finish*.go`, `worker_guard.go`, `review_*.go`, `executor_*.go`) or `internal/storage/executor*.go`. `internal/cmd` only registers its commands (`root.go`), and the executor never imports `internal/cmd`. The rules that bite regardless of what you are working on:

- **Headless workers cannot write under `~/.claude`** (CC sensitive-path guard, a hard refusal with nobody to approve) - source lives in an ordinary checkout, data in `~/.claude/pm/`.
- **Review is decided by pm, not the worker** (0.38.0): reviewer count, model (`executor.review_model`, default opus), packet (the whole diff) and rounds (`executor.fix_rounds`) are enforced by the `pm worker-guard` PreToolUse hook, which also blocks hook bypasses and `.git/` writes - in `--yolo` too. Since pm-cli-130 there is a FLOOR as well: a `verified` on a non-empty production diff with zero reviewer spawns that ran is refused at `StructuredOutput` time and, if it still arrives, demoted to `blocked` + `REVIEW-SKIPPED:` by `demoteUnreviewed` (`review_floor.go`) before anything merges or pushes.
- **Test executor paths through the fake `claude` binary** (`fakeClaude` in `internal/cmd/work_exec_test.go`), never by spending tokens; the envelope has two shapes (object, or a message array whose last element is the object - `decodeClaudeOutput`).
- **Test executor paths through the fake `claude` binary** (`fakeClaude` in `internal/executor/work_exec_test.go`), never by spending tokens; the envelope has two shapes (object, or a message array whose last element is the object - `decodeClaudeOutput`).
- **Every run journals itself** (`<project>/.executor/journal.jsonl`, append-only; `pm executor stats` is the rollup, `/executor-retro` the reader) - a new run-time fact belongs in the run-state + journal, not in a log line.
- **The web can start and stop runs too** (pm-cli-118-21): `pm serve`'s `POST /api/runs/{p}/{id}/{action}` goes through `internal/runctl`, which mirrors the board's launch/kill/claim rules (argv preview = the argv that runs, never `--sim`, `PM_LAUNCH_SOURCE=cockpit` -> journal `source`, a claim session `cockpit-<host>` with a 3-min heartbeat for at most 1 h). A change to how the board launches or kills has a twin in `runctl` to keep in step.
- **Tokens, not dollars, are what the subscription window meters** (0.55.0): per-sub `tokens` off the envelope's `usage`; workers run under `executor.worker_claude_config_dir` when set (a slim config dir without the user-scope skill catalogue / CLAUDE.md), and the worker prompt carries a call discipline (no prose between tools, batch independent calls) - see pm-cli-119.
Expand Down
6 changes: 3 additions & 3 deletions cmd/pm/main.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// Command pm is the pm binary: the CLI, the kanban TUI (`pm board`, or a bare
// `pm`), the MCP server Claude Code registers (`pm mcp`), the cockpit's HTTP
// server (`pm serve`) and the unattended executor (`pm work`, `pm run-epic`,
// `pm finish`). Every subcommand lives in internal/cmd; this file builds the
// root command and stamps the process start time the board measures its own
// startup against.
// `pm finish`). Every subcommand is registered in internal/cmd (the executor's
// come from internal/executor); this file builds the root command and stamps
// the process start time the board measures its own startup against.
package main

import (
Expand Down
2 changes: 1 addition & 1 deletion docs/cli.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# CLI reference

Every command the `pm` binary exposes, with its flags and defaults, derived from `internal/cmd`. This is for people running pm from a shell or a script; an agent normally goes through the MCP server instead ([mcp.md](mcp.md)). `pm help <command>` and `pm completion <shell>` come from cobra, and `--help` prints any command's flags. A bare `pm` opens the board.
Every command the `pm` binary exposes, with its flags and defaults, derived from `internal/cmd` (and `internal/executor` for `pm work`, `pm run-epic`, `pm finish` and `pm executor`). This is for people running pm from a shell or a script; an agent normally goes through the MCP server instead ([mcp.md](mcp.md)). `pm help <command>` and `pm completion <shell>` come from cobra, and `--help` prints any command's flags. A bare `pm` opens the board.

Argument conventions: `<project>` is a project slug (a case-folded prefix works when it is unique), `<task-id>` is a full task id such as `acme-12` unless the command says it searches. `[project]` in brackets means the project is optional and is detected from the current directory when omitted, by matching cwd against each project's `path`.

Expand Down
2 changes: 1 addition & 1 deletion docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ go test ./internal/storage/ -run TestSlugify -v # one test
- Go stdlib `testing`, table-driven, `t.Run("case", ...)` subtests, `t.TempDir()` for anything that touches disk.
- Tests live in the same package as the code so they reach unexported functions. `setupTestStore(t)` in `internal/storage/store_test.go` builds a store with a project and tasks.
- The write-rule invariants (links merge, Log appends, Spec rewrites, brief lifecycle) live in `internal/mcpserver/tools_test.go`; `ApplySpec` and `ExtractSpec` have unit tests in `internal/storage/spec_test.go`.
- Executor paths run through the fake `claude` binary (`fakeClaude` in `internal/cmd/work_exec_test.go`), covering the verified, blocked, crash and garbage-output paths of a worker. The envelope has two shapes, an object or a message array whose last element is the object.
- Executor paths run through the fake `claude` binary (`fakeClaude` in `internal/executor/work_exec_test.go`), covering the verified, blocked, crash and garbage-output paths of a worker. The envelope has two shapes, an object or a message array whose last element is the object.
- MCP handlers are tested end to end through the SDK's in-memory transport (`startMCP` in `internal/mcpserver/e2e_test.go`) so the real registered closures run; the test body never re-simulates handler logic.
- Lock and race invariants live in `internal/storage/lock_test.go` and the two-acquirer hammer in `internal/storage/worktree_test.go`. Run suspects under `-race`; `make test-race` runs the whole suite that way before anything touching locking is pushed.
- HTTP routes are tested in `internal/server/server_test.go` with `httptest` over the same temp-store fixture, including the SSE feed and the remote-runs hook through a fake, never ssh.
Expand Down
Loading
Loading