Skip to content
Open
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
1 change: 1 addition & 0 deletions apps/penpal/.gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Binary
/penpal
/penpal-cli
/penpal-server
/birdseye
.penpal.pid
Expand Down
2 changes: 1 addition & 1 deletion apps/penpal/.mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"mcpServers": {
"penpal": {
"type": "http",
"url": "http://localhost:18923/mcp"
"url": "http://localhost:8080/mcp"
}
}
}
33 changes: 29 additions & 4 deletions apps/penpal/ERD.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ see-also:
- <a id="E-PENPAL-COMMENT-ORDER"></a>**E-PENPAL-COMMENT-ORDER**: `OrderComments()` arranges comments in tree order: root comments sorted by time, replies grouped under their parents, siblings sorted by effective time. A comment's effective time is `WorkingStartedAt` when present, otherwise `CreatedAt`. This ensures agent replies are ordered by when the agent started working, not when the reply was posted. Missing parents fall back to root level.
← [P-PENPAL-THREAD-PANEL](PRODUCT.md#P-PENPAL-THREAD-PANEL), [P-PENPAL-WORKING](PRODUCT.md#P-PENPAL-WORKING)

- <a id="E-PENPAL-CHANGE-SEQ"></a>**E-PENPAL-CHANGE-SEQ**: A global monotonic sequence number increments on every comment change. `WaitForChangeSince(ctx, sinceSeq)` blocks on a channel until `changeSeq` advances or context cancels.
- <a id="E-PENPAL-CHANGE-SEQ"></a>**E-PENPAL-CHANGE-SEQ**: A global monotonic sequence number increments on every comment change. `WaitForChangeSince(ctx, sinceSeq)` blocks on a channel until `changeSeq` advances or context cancels. `WaitAndEnrich(ctx, project, worktree, sinceSeq)` combines the wait with enrichment — on change, it loads pending threads and sets working indicators; on timeout, it refreshes working timestamps. Both the MCP `penpal_wait_for_changes` tool and the REST `handleAgentWait` handler delegate to `WaitAndEnrich`.
← [P-PENPAL-WAIT-CHANGES](PRODUCT.md#P-PENPAL-WAIT-CHANGES)

---
Expand All @@ -181,7 +181,7 @@ see-also:
- <a id="E-PENPAL-WORKING"></a>**E-PENPAL-WORKING**: An in-memory `working` map (keyed by `"project:path:threadID"`) tracks which threads an agent is actively processing. Each entry stores `startedAt` (time) and `afterCommentID` (the last comment ID when the agent started working). Entries expire after 60s. `SetWorking()`/`ClearWorking()` trigger SSE `comments` events. The API thread response includes `workingAfterCommentId` so the frontend renders the indicator after the correct comment.
← [P-PENPAL-WORKING](PRODUCT.md#P-PENPAL-WORKING)

- <a id="E-PENPAL-HEARTBEAT"></a>**E-PENPAL-HEARTBEAT**: An in-memory `heartbeats` map (keyed by `"project:filePath"`) records agent activity. `IsAgentActive()` returns true if heartbeat is <60s old. MCP tool calls record heartbeats.
- <a id="E-PENPAL-HEARTBEAT"></a>**E-PENPAL-HEARTBEAT**: Agent presence is determined solely by `agents.Manager.HasActiveAgent()`, which checks both spawned processes (via the process map) and CLI sessions (via the session manager). The previous heartbeat-based tracking in `comments.Store` has been removed — heartbeats are no longer needed because the agent management system is the single source of truth for agent presence.
← [P-PENPAL-AGENT-PRESENCE](PRODUCT.md#P-PENPAL-AGENT-PRESENCE)

---
Expand Down Expand Up @@ -216,14 +216,14 @@ see-also:
- <a id="E-PENPAL-AGENT-AUTOSTART"></a>**E-PENPAL-AGENT-AUTOSTART**: `maybeStartAgent()` is called after `handleCreateThread` and `handleAddComment`. If the new comment's Role is `"human"` and no agent is running, one is started.
← [P-PENPAL-AGENT-LAUNCH](PRODUCT.md#P-PENPAL-AGENT-LAUNCH)

- <a id="E-PENPAL-AGENT-CLEANUP"></a>**E-PENPAL-AGENT-CLEANUP**: On agent exit: temp MCP config is removed, project heartbeats and working indicators are cleared, and the `onChange` callback fires an SSE event.
- <a id="E-PENPAL-AGENT-CLEANUP"></a>**E-PENPAL-AGENT-CLEANUP**: On agent exit: temp MCP config is removed, working indicators are cleared, and the `onChange` callback fires an SSE event.
← [P-PENPAL-AGENT-LAUNCH](PRODUCT.md#P-PENPAL-AGENT-LAUNCH)

---

## HTTP API

- <a id="E-PENPAL-API-ROUTES"></a>**E-PENPAL-API-ROUTES**: The server exposes REST endpoints: projects CRUD, project files (grouped), recent files, in-review, search, workspaces, sources, open/navigate, threads CRUD, reviews, focus, agents start/stop/status, raw file, view tracking, publish, publish-state, ready, install-tools, claude-path. SPA served from `/app/`. MCP at `/mcp`.
- <a id="E-PENPAL-API-ROUTES"></a>**E-PENPAL-API-ROUTES**: The server exposes REST endpoints: projects CRUD, project files (grouped), recent files, in-review, search, workspaces, sources, open/navigate, threads CRUD, reviews, focus, agents start/stop/status/attach/wait, raw file, view tracking, publish, publish-state, ready, install-tools, claude-path. SPA served from `/app/`. MCP at `/mcp`.
← [P-PENPAL-RENDER](PRODUCT.md#P-PENPAL-RENDER), [P-PENPAL-MCP](PRODUCT.md#P-PENPAL-MCP)

- <a id="E-PENPAL-LAZY-INIT"></a>**E-PENPAL-LAZY-INIT**: First HTTP request triggers `sync.Once` that discovers projects, starts the watcher, then runs `populateProjects()` in a background goroutine. `populateProjects()` refreshes the cache, seeds activity, closes `readyCh`, broadcasts events, then enriches git info.
Expand Down Expand Up @@ -407,6 +407,31 @@ see-also:

---

## CLI Agent Tools

- <a id="E-PENPAL-CLI-ATTACH"></a>**E-PENPAL-CLI-ATTACH**: `penpal attach <path>` reads the port file (`ReadPortFile()`), launches the app if not running (same as `penpal open`), resolves the path to an absolute path, and calls `POST /api/agents/attach` with `{"path": "<abs-path>", "force": false}`. The server resolves the path to a project (via `FindProjectByPathWithWorktree`), checks for existing agents (Manager-spawned or external sessions), and either returns `{"project", "worktree", "sessionToken"}` or 409 Conflict. `--force` sends `force: true`, which kills/evicts the existing agent first. The session token is a random UUID generated server-side.
← [P-PENPAL-CLI-ATTACH](PRODUCT.md#P-PENPAL-CLI-ATTACH), [P-PENPAL-CLI-CONTENTION](PRODUCT.md#P-PENPAL-CLI-CONTENTION)

- <a id="E-PENPAL-CLI-AGENT-CMDS"></a>**E-PENPAL-CLI-AGENT-CMDS**: CLI subcommands (`files-in-review`, `list-threads`, `read-thread`, `reply`, `create-thread`, `wait`) each read the port file, call the corresponding REST API endpoint with `?session=<token>` query parameter, and print the JSON response to stdout. The server validates the session token on each request — invalid or evicted tokens return 401. All endpoints record heartbeats for the session's project. `penpal reply` accepts `--body` flag or reads from stdin. `penpal wait` calls `GET /api/agents/wait?project=X&session=T&sinceSeq=N` which uses the same `WaitForChangeSince` mechanism as the MCP `penpal_wait_for_changes` tool.
← [P-PENPAL-CLI-AGENT-TOOLS](PRODUCT.md#P-PENPAL-CLI-AGENT-TOOLS)

- <a id="E-PENPAL-SESSION-MGMT"></a>**E-PENPAL-SESSION-MGMT**: The server maintains an in-memory `sessions` map (keyed by token) storing project name, worktree, created-at, and last-heartbeat. Sessions expire after 90 seconds without a heartbeat. `POST /api/agents/attach` creates a session after contention checks. Agent contention checks both `agents.Manager.Status(project).Running` and active sessions. `--force` calls `Manager.Stop()` for spawned agents or marks the existing session as evicted. Evicted sessions return 401 on subsequent use. `Manager.Start()` also checks for active CLI sessions and refuses to spawn if one is attached. `Manager.StopAny()` provides a unified stop that terminates spawned agents and evicts CLI sessions in a single call — used by `POST /api/agents/stop`.
← [P-PENPAL-CLI-CONTENTION](PRODUCT.md#P-PENPAL-CLI-CONTENTION), [P-PENPAL-CLI-ATTACH](PRODUCT.md#P-PENPAL-CLI-ATTACH)

- <a id="E-PENPAL-AGENT-ACTIVE-UNIFIED"></a>**E-PENPAL-AGENT-ACTIVE-UNIFIED**: `AgentActive` in API responses checks both `agents.Manager.Status(project).Running` AND whether an active (non-expired) session exists for the project. This ensures external CLI-attached agents show the same presence indicators as internally-spawned agents.
← [P-PENPAL-AGENT-PRESENCE](PRODUCT.md#P-PENPAL-AGENT-PRESENCE)

- <a id="E-PENPAL-AGENT-PARITY"></a>**E-PENPAL-AGENT-PARITY**: Both launched and external agents go through the same `claimSession()` contention path in `agents.Manager`. Spawned agents claim a `SessionSpawned` session before launching; CLI agents claim a `SessionCLI` session via `Attach()`. Session ownership is the single source of truth for "who owns this project." The `agents` map is purely a process handle — it does not determine ownership.
← [P-PENPAL-AGENT-PARITY](PRODUCT.md#P-PENPAL-AGENT-PARITY), [P-PENPAL-CLI-CONTENTION](PRODUCT.md#P-PENPAL-CLI-CONTENTION)

- <a id="E-PENPAL-SHARED-CODEPATHS"></a>**E-PENPAL-SHARED-CODEPATHS**: Shared codepaths are strongly preferred over branching or special-casing by agent type. When launched and external agents need the same behavior (contention, working indicators, comment threading, presence), a single implementation serves both. `SessionKind` (`SessionSpawned` vs `SessionCLI`) is used only where behavior genuinely differs (e.g., heartbeat expiry applies to CLI sessions but not spawned sessions).
← [P-PENPAL-AGENT-PARITY](PRODUCT.md#P-PENPAL-AGENT-PARITY)

- <a id="E-PENPAL-AGENT-SELF-ID"></a>**E-PENPAL-AGENT-SELF-ID**: `Session.AgentName` stores the agent's self-reported name. `Attach(projectName, worktree, agentName, force)` and `claimSession()` accept an `agentName` parameter. `Manager.AgentName(project)` returns the agent name from the active session, defaulting to `"agent"`. The REST `POST /api/agents/attach` reads `"agent"` from the JSON body (default `"agent"`); the attach response includes `"agentName"`. The CLI `penpal attach --agent NAME` sends the name. `handleAddComment` and `handleCreateThread` override the `author` field from the session's `AgentName` when `role == "agent"`. MCP tools receive an `agentNameFunc` parameter and use it to derive the author for `penpal_reply` and `penpal_create_thread`. `Manager.Start()` passes `"claude"` as the agent name for spawned agents.
← [P-PENPAL-AGENT-SELF-ID](PRODUCT.md#P-PENPAL-AGENT-SELF-ID)

---

## Source Management

- <a id="E-PENPAL-ADD-SOURCE"></a>**E-PENPAL-ADD-SOURCE**: `POST /api/sources` accepts a relative path. Directories create "tree" sources; `.md` files are added to a "files" source. Duplicate detection refuses paths already covered by an existing source. Only `.md` files accepted for individual file sources. Used internally by `penpal open` CLI to auto-add files to their containing project.
Expand Down
10 changes: 10 additions & 0 deletions apps/penpal/PRODUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,16 @@ Global views aggregate content across all projects. They appear as top-level ite

- <a id="P-PENPAL-CLI-OPEN"></a>**P-PENPAL-CLI-OPEN**: The `penpal open <path>...` command opens one or more files or directories in the Penpal app, launching the app if it's not running. Directories are resolved to their project; `.md` files are auto-added to their containing project if not already tracked (or a new standalone project is created). Non-`.md` files are rejected.

- <a id="P-PENPAL-CLI-ATTACH"></a>**P-PENPAL-CLI-ATTACH**: `penpal attach <path>` registers the calling agent as the active agent for a project. The command discovers the running server, resolves the path to a project, opens the file in the app, and claims agent ownership. If another agent is already attached, the command fails with an error. `--force` evicts the existing agent and takes over. On success, prints JSON with the project name, worktree (if any), and a session token. The session token must be passed to all subsequent CLI commands.

- <a id="P-PENPAL-CLI-AGENT-TOOLS"></a>**P-PENPAL-CLI-AGENT-TOOLS**: Agents interact with penpal via CLI subcommands that mirror the MCP tools: `penpal files-in-review`, `penpal list-threads`, `penpal read-thread`, `penpal reply`, `penpal create-thread`, and `penpal wait`. Each command requires `--session <token>` for authentication and prints JSON to stdout. `penpal wait` blocks up to 30 seconds and returns when comments change or on timeout — agents call it in a loop. All commands record agent heartbeats. An invalid or evicted session token causes the command to exit with an error.

- <a id="P-PENPAL-CLI-CONTENTION"></a>**P-PENPAL-CLI-CONTENTION**: At most one agent (internal or external) can be active for a project at a time. `penpal attach` fails if an agent is already active. `penpal attach --force` terminates the existing agent (kills an internally-spawned process, or evicts an external session). When an external agent's session is evicted, its next CLI command fails with an "evicted" error. Internally-launched agents and CLI-attached agents use the same contention system.

- <a id="P-PENPAL-AGENT-PARITY"></a>**P-PENPAL-AGENT-PARITY**: Launched (internally-spawned) and external (CLI-attached) agents have the same capabilities and Penpal exhibits the same behavior for both. Working indicators, presence dots, auto-start suppression, stop button behavior, and comment threading all work identically regardless of how the agent connected.

- <a id="P-PENPAL-AGENT-SELF-ID"></a>**P-PENPAL-AGENT-SELF-ID**: Agents identify themselves by name when attaching. `penpal attach --agent amp` records "amp" as the agent name. The agent name is stored on the session and used as the comment author for all agent-role comments, so comments show the actual agent that wrote them (e.g., "amp" or "claude") rather than a generic label. Internally-spawned agents are always named "claude". If no agent name is provided, it defaults to "agent".

---

## Real-Time Updates
Expand Down
7 changes: 5 additions & 2 deletions apps/penpal/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,15 @@ see-also:
| Suggested Replies (P-PENPAL-SUGGESTED-REPLIES) | — | CommentsPanel.test.tsx | — | review-workflow.spec.ts |
| MCP Tools (P-PENPAL-MCP) | — | — | mcpserver/tools_test.go, transport_test.go, worktree_test.go | review-workflow.spec.ts |
| Wait for Changes (P-PENPAL-WAIT-CHANGES) | — | — | tools_test.go (TestWaitForChanges_Triggered) | — |
| Agent Management (P-PENPAL-AGENT-LAUNCH, STATUS) | stream_test.go | FilePage.test.tsx (auto-start) | api_agents_test.go | — |
| Agent Management (P-PENPAL-AGENT-LAUNCH, STATUS, P-PENPAL-CLI-CONTENTION) | stream_test.go, session_test.go | FilePage.test.tsx (auto-start) | api_agents_test.go | — |
| CLI Attach & Sessions (P-PENPAL-CLI-ATTACH, P-PENPAL-CLI-CONTENTION, E-PENPAL-CLI-ATTACH, E-PENPAL-SESSION-MGMT, E-PENPAL-AGENT-ACTIVE-UNIFIED) | session_test.go (Attach, ValidateSession, HasActiveAgent, StopAny, Detach, RecordSessionHeartbeat) | — | api_agents_test.go (attach success/conflict/force/missing/noManager, wait invalid/missing, stop ok, maybeStartAgent skip) | — |
| Agent Parity (P-PENPAL-AGENT-PARITY, E-PENPAL-AGENT-PARITY, E-PENPAL-SHARED-CODEPATHS) | session_test.go (claimSession for both SessionSpawned and SessionCLI) | — | api_agents_test.go | — |
| Agent Self-ID (P-PENPAL-AGENT-SELF-ID, E-PENPAL-AGENT-SELF-ID) | session_test.go (Attach stores AgentName) | — | api_agents_test.go (attach with agent name, author override) | — |
| CLI Agent Tools (P-PENPAL-CLI-AGENT-TOOLS, E-PENPAL-CLI-AGENT-CMDS) | — | — | api_agents_test.go | — |
| Agent Detection (P-PENPAL-AGENT-PRESENCE) | — | — | — | — |
| Review Workflow (P-PENPAL-PROJECT-IN-REVIEW, P-PENPAL-GLOBAL-IN-REVIEW) | — | InReviewPage.test.tsx | api_projects_test.go (TestAPIInReview) | — |
| Publishing (P-PENPAL-PUBLISH) | blockcell_test.go, render_test.go, state_test.go | — | — | — |
| Tabs (P-PENPAL-TABS) | — | useTabs.test.ts, Layout.test.tsx | — | tab-navigation.spec.ts |
| Search (P-PENPAL-SEARCH) | — | SearchPage.test.tsx | — | react-app.spec.ts |
| Recent Files (P-PENPAL-PROJECT-RECENT, P-PENPAL-GLOBAL-RECENT, E-PENPAL-ACTIVITY-PERSIST) | activity_test.go | RecentPage.test.tsx | integration_test.go | — |
| CLI Open (P-PENPAL-CLI-OPEN) | — | — | api_manage_test.go | cli-open.spec.ts |
| Manual Source Management (E-PENPAL-SRC-MANUAL, E-PENPAL-SRC-ALL-MD) | — | — | api_manage_test.go (TestAPISources_AddFileNotBlockedByAllMarkdown) | — |
Expand Down
Loading
Loading