You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
- <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.
- <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`.
- <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.
- <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.
- <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.
- <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 DownExpand 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.
- <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.
- <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`.
- <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.
- <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.
- <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).
- <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.
- <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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@@ -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".
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.