Skip to content

Commit 79237d2

Browse files
loganjampagent
andcommitted
feat(penpal): CLI-based agent interface for external agent participation
Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-019d82bb-0087-761b-ab29-1acd2391048d
1 parent a2fb368 commit 79237d2

21 files changed

Lines changed: 1910 additions & 454 deletions

apps/penpal/.mcp.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"mcpServers": {
33
"penpal": {
44
"type": "http",
5-
"url": "http://localhost:18923/mcp"
5+
"url": "http://localhost:8080/mcp"
66
}
77
}
88
}

apps/penpal/ERD.md

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ see-also:
171171
- <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.
172172
[P-PENPAL-THREAD-PANEL](PRODUCT.md#P-PENPAL-THREAD-PANEL), [P-PENPAL-WORKING](PRODUCT.md#P-PENPAL-WORKING)
173173

174-
- <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.
174+
- <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`.
175175
[P-PENPAL-WAIT-CHANGES](PRODUCT.md#P-PENPAL-WAIT-CHANGES)
176176

177177
---
@@ -181,7 +181,7 @@ see-also:
181181
- <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.
182182
[P-PENPAL-WORKING](PRODUCT.md#P-PENPAL-WORKING)
183183

184-
- <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.
184+
- <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.
185185
[P-PENPAL-AGENT-PRESENCE](PRODUCT.md#P-PENPAL-AGENT-PRESENCE)
186186

187187
---
@@ -216,14 +216,14 @@ see-also:
216216
- <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.
217217
[P-PENPAL-AGENT-LAUNCH](PRODUCT.md#P-PENPAL-AGENT-LAUNCH)
218218

219-
- <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.
219+
- <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.
220220
[P-PENPAL-AGENT-LAUNCH](PRODUCT.md#P-PENPAL-AGENT-LAUNCH)
221221

222222
---
223223

224224
## HTTP API
225225

226-
- <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`.
226+
- <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`.
227227
[P-PENPAL-RENDER](PRODUCT.md#P-PENPAL-RENDER), [P-PENPAL-MCP](PRODUCT.md#P-PENPAL-MCP)
228228

229229
- <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.
@@ -407,6 +407,22 @@ see-also:
407407

408408
---
409409

410+
## CLI Agent Tools
411+
412+
- <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.
413+
[P-PENPAL-CLI-ATTACH](PRODUCT.md#P-PENPAL-CLI-ATTACH), [P-PENPAL-CLI-CONTENTION](PRODUCT.md#P-PENPAL-CLI-CONTENTION)
414+
415+
- <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.
416+
[P-PENPAL-CLI-AGENT-TOOLS](PRODUCT.md#P-PENPAL-CLI-AGENT-TOOLS)
417+
418+
- <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`.
419+
[P-PENPAL-CLI-CONTENTION](PRODUCT.md#P-PENPAL-CLI-CONTENTION), [P-PENPAL-CLI-ATTACH](PRODUCT.md#P-PENPAL-CLI-ATTACH)
420+
421+
- <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.
422+
[P-PENPAL-AGENT-PRESENCE](PRODUCT.md#P-PENPAL-AGENT-PRESENCE)
423+
424+
---
425+
410426
## Source Management
411427

412428
- <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.

apps/penpal/PRODUCT.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,12 @@ Global views aggregate content across all projects. They appear as top-level ite
302302

303303
- <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.
304304

305+
- <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.
306+
307+
- <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.
308+
309+
- <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.
310+
305311
---
306312

307313
## Real-Time Updates

apps/penpal/TESTING.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,13 @@ see-also:
8686
| Suggested Replies (P-PENPAL-SUGGESTED-REPLIES) || CommentsPanel.test.tsx || review-workflow.spec.ts |
8787
| MCP Tools (P-PENPAL-MCP) ||| mcpserver/tools_test.go, transport_test.go, worktree_test.go | review-workflow.spec.ts |
8888
| Wait for Changes (P-PENPAL-WAIT-CHANGES) ||| tools_test.go (TestWaitForChanges_Triggered) ||
89-
| Agent Management (P-PENPAL-AGENT-LAUNCH, STATUS) | stream_test.go | FilePage.test.tsx (auto-start) | api_agents_test.go ||
89+
| 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 ||
90+
| 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) ||
91+
| CLI Agent Tools (P-PENPAL-CLI-AGENT-TOOLS, E-PENPAL-CLI-AGENT-CMDS) ||| api_agents_test.go ||
9092
| Agent Detection (P-PENPAL-AGENT-PRESENCE) |||||
9193
| Review Workflow (P-PENPAL-PROJECT-IN-REVIEW, P-PENPAL-GLOBAL-IN-REVIEW) || InReviewPage.test.tsx | api_projects_test.go (TestAPIInReview) ||
9294
| Publishing (P-PENPAL-PUBLISH) | blockcell_test.go, render_test.go, state_test.go ||||
9395
| Tabs (P-PENPAL-TABS) || useTabs.test.ts, Layout.test.tsx || tab-navigation.spec.ts |
94-
| Search (P-PENPAL-SEARCH) || SearchPage.test.tsx || react-app.spec.ts |
9596
| Recent Files (P-PENPAL-PROJECT-RECENT, P-PENPAL-GLOBAL-RECENT, E-PENPAL-ACTIVITY-PERSIST) | activity_test.go | RecentPage.test.tsx | integration_test.go ||
9697
| CLI Open (P-PENPAL-CLI-OPEN) ||| api_manage_test.go | cli-open.spec.ts |
9798
| Manual Source Management (E-PENPAL-SRC-MANUAL, E-PENPAL-SRC-ALL-MD) ||| api_manage_test.go (TestAPISources_AddFileNotBlockedByAllMarkdown) ||

0 commit comments

Comments
 (0)