# Fido Todo/Plan and Subagent Plan **Status:** Proposed **Scope:** Add a small coordination layer without turning Fido into a general workflow engine. ## Goals - Preserve a concise, durable task plan across turns and restarts. - Make progress and blockers visible to the user in a persistent, full todo panel. - Make the complete current todo state available to the model in the conversation, as a tool response rather than as mutable initial prompt text. - Keep the system-prompt prefix stable so plan updates do not unnecessarily bust prompt caching. - Allow bounded delegation for genuinely independent investigation or review work, with an explicit subagent identity and a clearly isolated message fork. - Keep the advertised tool catalog stable for prompt caching; enforce read-only and no-recursion policy at invocation time rather than changing schemas or tool lists when entering a subagent. - Show live subagent progress in a separate, accent-colored console activity bar. - Keep the tool surface small: no separate task database UI, queue service, or shell wrappers. - Keep the parent agent responsible for integration, safety, and final verification. ## Non-goals - A general-purpose project-management system. - Autonomous recursive agents. - Background workers, job queues, or a persistent multi-agent scheduler in the first version. - Letting subagents silently modify the user's working tree. - Replacing `bash_exec`; shell remains the general escape hatch for tests, builds, Git, and unusual operations. ## Design principles 1. **State is not shell state.** A plan is structured session/workspace state and should not be implemented by parsing files or maintaining a second conversation transcript. 2. **One tool per capability.** Use one `update_plan` tool and one optional `delegate` tool. Do not add `get_plan`, `complete_task`, `list_agents`, or `join_subagent` initially. 3. **Short-lived, explicit delegation.** A delegated task has a goal, scope, budget, and output contract. Results return to the parent before the parent continues. 4. **Read-only by runtime policy.** Subagents see the same stable advertised tool catalog as the parent, but invocation guards reject mutating tools and recursive delegation. Write-capable delegation is a later, explicitly approved feature. 5. **Evidence over confidence.** A plan item is complete only after the relevant verification is recorded; a subagent finding is a hypothesis until the parent checks it. 6. **No repository litter.** Runtime plan and delegation state should use the existing `.fido-*` ignore convention or the per-workspace state location, not a tracked `TODO.md`. 7. **Cache-stable system context.** The system message contains stable instructions and tool contracts only. The current todo state is delivered through the `update_plan` tool result (and a restored tool-result event after a restart), so changing a plan does not rewrite the cached system-prompt prefix. 8. **Explicit delegation boundary.** A subagent receives an independent snapshot of the parent message array plus a bootstrap result that states its role and responsibilities. The parent receives the subagent's final output, not its private transcript. 9. **Stable tool advertisements.** The tool set, schemas, descriptions, and ordering must not change when a main agent spawns a subagent. `delegate` remains advertised inside the subagent; a runtime identity/depth guard converts a nested call into an explicit failure tool result. 10. **Observable delegation.** While a subagent is working, the console displays a second activity bar, distinct from the main status bar and visually marked with an accent color. ## 1. Todo/plan capability ### Tool Add one model-facing tool: ```text update_plan({ reason: string, items: [ { id: string, title: string, status: "pending" | "in_progress" | "blocked" | "done", details?: string, dependencies?: string[], next_action?: string, evidence?: string[] } ] }) ``` The tool replaces the current plan atomically. The model must include existing IDs when changing status; unknown IDs, invalid dependencies, and more than one `in_progress` item should produce a clear `ToolError`. A single replacement operation is intentional: it is easier for the model to reason about and keeps the tool schema small. On every successful call, the tool result is the **complete canonical plan state** for the next model request. The result must include the version, update timestamp, reason, and every normalized item, including all optional fields. Do not replace it with a count, a summary, a partial list, or an ellipsis. The model should treat the latest `update_plan` result—not memory of an earlier call—as the source of truth. ### Model-facing tool description The description for `update_plan` must explicitly say: > Replace the workspace todo list and return its complete normalized state. The returned tool result is the model-visible source of truth for the current plan; it contains the full `version`, `updatedAt`, `reason`, and every item with its ID, title, status, details, dependencies, next action, and evidence. The plan is delivered in the tool result, not interpolated into the initial system prompt, so updating it does not change the cached system-prompt prefix. Use it at milestones, preserve existing IDs, and keep exactly zero or one `in_progress` item. ### Message placement and cache rule - Build the system message once per session from the stable workspace/tool instructions. It must never contain the current plan JSON, a plan timestamp, or a plan-dependent status. - After a successful replacement, the normal provider `tool` message is already part of the model's message history. The next request therefore sees the full state without a second prompt or a `get_plan` call. - If a plan is restored from disk after a restart, load it into the state module and restore it as a provider-compatible `tool` result (with an internal matching call/event if the provider requires one), not by rewriting the system message. This restore result must also contain the complete plan state. - Compaction must preserve the latest plan-state tool result. If older results are elided, carry the latest complete result forward as the newest tool result rather than replacing it with a summary. - The plan UI consumes the same normalized object returned by the tool, so the model-visible state and the floating panel cannot silently diverge. ### State model Persist a versioned JSON document, initially at `.fido-plan.json` (covered by the existing `.fido-*` ignore rule), with this shape: ```json { "version": 1, "updatedAt": "2025-01-01T00:00:00.000Z", "reason": "Current task plan", "items": [] } ``` Recommended rules: - Keep IDs stable and human-readable (`inspect`, `implement`, `verify`). - Allow at most 20 items and keep titles/details short. - Permit zero or one `in_progress` item; represent waiting on a person or external service as `blocked`. - Require `next_action` for `pending`, `in_progress`, and `blocked` items. - Record concise evidence such as `npm test` or `test/ui.test.js:42`; do not store full command output in the plan. - Never treat a plan update as an undoable source-code edit. ### Runtime integration 1. Add a small `src/plan.js` state module with `loadPlan`, `getPlan`, and `setPlan`/validation helpers. 2. Add `PLAN_FILE` to `src/config.js`, defaulting to `.fido-plan.json`. 3. Load the plan during startup for the UI and local commands, but do **not** interpolate it into the system prompt. When a persisted plan exists, arrange for the initial model-visible message history to contain a full plan-state tool result (or the equivalent provider-compatible restore event). 4. Register `update_plan` in `src/tools/index.js`; its description and full-result contract must be available to the model. 5. Add `plan` to the UI store and call `ui.setPlan(...)` after startup load, every successful `update_plan`, and `/plan clear`. The panel must update atomically from the canonical normalized result. 6. Add a `PlanPanel` floating box anchored to the right side of the full-screen Ink layout. It is a persistent overlay/side rail, not a transcript entry, and it must show the **full todo list**, not only the active item or a count. Render every item and every bounded item field (ID, title, status, details, dependencies, next action, and evidence), with an empty-state message when there is no plan. At narrow widths, use a responsive overlay/fallback, but do not discard the state or silently reduce the model-facing result. 7. Keep a concise tool-call notification for the transcript, but do not dump the full plan there; the full state belongs in the right-side panel and in the tool result. 8. Add `/plan` to display the current plan and `/plan clear` to start a new plan. Keep these as local commands; they do not require an LLM turn. 9. Decide explicitly whether `/recall` restores the plan. Recommended default: recall restores conversation messages but keeps the workspace plan unchanged, because the plan describes the current workspace rather than a historical transcript. ### Usage policy The stable system prompt should tell the model to: - create a plan when a request has multiple meaningful steps, spans several files, or is likely to be interrupted; - skip a formal plan for a small, obvious change; - call `update_plan` when the current state needs to be established, after a milestone, or when a blocker changes the next action; - read the complete state from the latest `update_plan` tool result instead of assuming that an earlier result or the initial prompt is still current; - keep exactly zero or one current item in progress; - preserve stable IDs and record concise blockers, next actions, and evidence; - update the plan at milestones, not after every tool call; and - never place the plan in the system prompt or use a shell command to maintain it. A full plan snapshot in a tool result is model-visible prompt context, even though it is not the initial system prompt. It must survive normal turn handoff and compaction. ## 2. Subagent capability ### Tool Add one optional model-facing tool: ```text delegate({ task: string, context?: string, scope?: { paths?: string[], symbols?: string[] }, mode?: "read_only", output_contract?: "findings", timeout_ms?: number }) ``` The first version should run one bounded task synchronously and return a concise result. There should be no background job ID and no separate status/join tool. A later background implementation may add those only after demonstrating a real need. ### Required model-facing tool description The `delegate` description must not merely say “spawn a helper.” It must tell the parent model all of the following: > Run one bounded, synchronous, read-only subagent and return only its final normalized result. At invocation, Fido creates a private fork (an independent snapshot) of the parent's current `messages` array; the subagent works from that fork, not from a live reference to the parent array. The subagent's forked messages include a bootstrap tool result that explicitly gives it its current identity (`role: "subagent"`, a unique `agent_id`, and `parent_role: "main"`) and explains that its final output is for the parent. When the subagent finishes, its final output becomes the `delegate` tool result in the **parent's** messages. The parent does not receive the subagent's private tool calls or full transcript. The tool catalog remains unchanged in the subagent: if a subagent calls `delegate` again, that invocation must immediately return a normal `status: "failed"` tool result explaining that nested delegation is forbidden. No further subagent is created. The subagent is read-only, and the parent remains responsible for verification and integration. The same identity/fork/return-path facts must be enforced in the tool result payload and covered by tests, so the model is not relying only on prose in the tool description. ### Subagent prompt and tools Create a separate LLM context by forking the parent's current `messages` array at the instant `delegate` is called. The fork must be isolated: later parent messages and mutations must not appear in or mutate the subagent context, and a subagent must not mutate the parent's array. The forked array is augmented with a bootstrap **tool result** before it is sent to the subagent. That result, not an unstated system-prompt convention, tells the subagent what identity it now has and how to return work. At minimum it has this shape: ```json { "type": "subagent_bootstrap", "identity": { "role": "subagent", "agent_id": "subagent-123", "parent_role": "main" }, "context_mode": "forked_parent_messages", "forked_message_count": 42, "task": "the delegated task", "responsibility": "Inspect only; return the final result to the parent. You are not the main agent, you may not delegate, and the parent will verify your claims." } ``` The bootstrap result is a `role: "tool"` message in the messages array the subagent takes with it. If the provider requires a matching assistant tool call for every tool result, create an internal, non-model-facing matching call/event; do not add another model-facing coordination tool. In addition to that fork/bootstrap result, the separate subagent system/developer instructions must include: - the parent task and why delegation was chosen; - the relevant workspace-relative paths and symbols; - a strict read-only instruction; - a request to inspect rather than implement; and - the required final output format below. The `scope` fields constrain investigation, but they do not replace or truncate the message-array fork. Pass the fork directly to the isolated runner rather than re-encoding the entire parent transcript as one prompt string. #### Stable advertised catalog and runtime guards Do **not** construct a reduced subagent tool array. Parent and subagent runs must receive the same immutable tool objects, names, JSON schemas, descriptions, and ordering. In particular, do not remove `delegate` from the subagent's advertised tools: doing so changes the cached tool prefix and leaves the attempted nested call without a normal tool-result turn. Instead, pass an invocation context containing the current `identity` and `delegation_depth`. Enforce policy in the tool invocation guard: - Allowed read-only operations may include `list_files`, `read_file`, `ripgrep`, and explicitly approved `browse_url`/`websearch` calls. - `patch`, `mkdir`, `rm`, `rmdir`, unrestricted `bash_exec`, `ask_user`, and other parent-only or mutating tools return a normal `permission_denied` tool result for a subagent. They must not execute their side effects. - A subagent call to `delegate` returns an immediate `status: "failed"` tool result with a stable error code. It does not start a provider request or create another fork. The nested-call result must be returned through the normal provider tool-result path, not thrown as a runner exception, so the subagent receives a valid response for the tool call it made. Its minimum shape is: ```json { "type": "delegate_result", "identity": { "role": "subagent", "agent_id": "subagent-123", "parent_role": "main" }, "status": "failed", "summary": "Nested delegation is not allowed.", "findings": [], "recommended_next_steps": [ "Continue the assigned task without calling delegate." ], "tests_to_run": [], "uncertainties": [], "error": { "code": "nested_delegation_forbidden", "message": "A subagent cannot call delegate; no nested subagent was created." } } ``` The advertised `delegate` description must tell the model that this failure result is the expected runtime behavior and that the tool remains visible solely to preserve a stable cacheable tool catalog. The same guard remains in place even if the bootstrap result is ignored or the subagent attempts to claim the main-agent identity. ### Console subagent activity bar The full-screen console must provide a second activity bar dedicated to subagents; do not overload the main command/status bar and do not inject subagent chatter into the normal transcript. Place the bar directly below the header (or above the command bar) as a distinct fixed row, separate from the right-side todo panel. - Use a dedicated accent color, specified by the UI design (for example violet/blue such as `#a78bfa`) with contrasting text, rather than the cyan main-chrome or default transcript colors. - Show `SUBAGENT`, the short `agent_id`, and the current phase/action such as `starting`, `reasoning`, `read_file`, `reviewing`, `completed`, `partial`, `failed`, or `cancelled`. - Show elapsed time and a concise, sanitized activity detail. Never print the private message array, raw reasoning, or full subagent transcript. - Update the bar for subagent provider status, tool-call, tool-result, and completion callbacks. A nested `delegate` attempt should briefly show `nested delegation rejected` and then remain failed/rejected rather than creating a child. - The bar is visible while the subagent is active and remains available for the final outcome until the parent turn advances. At narrow widths it may become full-width and wrap/truncate details, but it must still identify the active subagent and status. - UI state should be driven by the same store/bridge used by the main app, so nested activity does not steal the input prompt or corrupt the main status line. The parent retains its own full messages array. The subagent gets a private fork of that array as specified above; “minimum relevant context” applies to `task`, `context`, `scope`, and external data, not to silently replacing the required messages-array fork with an empty context. Neither the parent UI nor the returned tool result may expose the subagent's private transcript. ### Result contract Normalize the subagent's final output to one small object before placing it in the parent conversation as the `delegate` tool response: ```json { "type": "delegate_result", "identity": { "role": "subagent", "agent_id": "subagent-123", "parent_role": "main" }, "status": "completed" | "partial" | "failed", "summary": "one-paragraph result", "findings": [ { "file": "src/example.js", "line": 42, "claim": "what was observed", "confidence": "high" | "medium" | "low" } ], "recommended_next_steps": ["..."], "tests_to_run": ["..."], "uncertainties": ["..."] } ``` There are two normal identity-bearing result paths, plus the nested-call failure path: 1. The `subagent_bootstrap` tool result tells the **subagent** that its current identity is the named, read-only fork shown above and that it must finish with a result for `main`. 2. The `delegate_result` tool response tells the **parent** which subagent identity produced the final output. It contains the normalized final output and provenance, not the subagent's intermediate messages. 3. If that subagent calls `delegate`, the nested invocation returns the `nested_delegation_forbidden` `status: "failed"` tool result to the subagent. It is a valid tool response, not a second successful delegation and not a private transcript sent to the parent. The parent must verify claims against current files and own integration and the final user-facing response. A subagent must never report a change as complete when it was only asked to investigate. ### Safety and lifecycle - Default to read-only operation. - Deep-fork (or otherwise structurally isolate) the parent message array at delegation time; never share a mutable live message reference between parent and subagent. - Generate a unique subagent identity in the runner, include it in both the bootstrap tool result and the final parent-facing result, and do not accept identity text supplied by the model as authoritative. - Limit concurrency to one for the first implementation; add parallelism only after rate limits and UI behavior are tested. - Enforce a timeout and a result-size limit. - Propagate Ctrl+C/cancellation to the subagent. - Prevent recursive delegation without changing the advertised tool catalog: keep `delegate` available, enforce a maximum depth of one in the invocation guard, and return a normal failed tool result when a subagent attempts it. - Return only the normalized final output to the parent. Never append the subagent's private message/tool transcript to the parent array or render it as ordinary parent history. - Treat subagent output as untrusted input: it may be wrong, stale, or influenced by repository content. - Do not expose provider keys, private subagent messages, or full parent transcripts in UI messages; the activity bar may expose only sanitized lifecycle metadata. - If write-capable agents are added later, require explicit user approval, use isolated Git worktrees, and require the parent to review and test the diff before integration. ## 3. Minimal tool surface After this work, the coordination-related surface should be only: ```text update_plan # durable task state delegate # optional, bounded read-only delegation ``` Existing filesystem and execution tools remain unchanged. `bash_exec` already covers tests, builds, Git, and other commands; do not add `run_tests`, `git_status`, or similar wrappers. ## 4. Implementation order ### Phase 1: Plan state and todo UI - [ ] Add plan configuration, validation, and atomic state helpers. - [ ] Add `update_plan` and its unit tests; assert that every successful tool response contains the complete normalized plan with no truncation or summary-only payload. - [ ] Keep the system prompt stable and add a post-system plan-state tool-result restore path for persisted plans; test that updates do not rewrite `messages[0]`. - [ ] Add plan state to the UI store and implement the persistent right-side floating full-plan panel with responsive narrow-terminal behavior. - [ ] Add `/plan` and `/plan clear` local UI commands. - [ ] Verify restart persistence, compaction preservation of the latest full plan result, and malformed-input errors. ### Phase 2: Read-only delegation MVP - [ ] Extract a subagent runner that deep-forks the parent's current `messages` array into an isolated model context. - [ ] Add the identity-bearing `subagent_bootstrap` tool result to that fork and make it an explicit part of the `delegate` tool contract. - [ ] Add the stable advertised tool catalog and identity/depth-aware invocation guards; do not remove `delegate` from the subagent's tool list. - [ ] Add `delegate` with timeout, cancellation, and final result normalization; place only the final `delegate_result` in the parent array. - [ ] Add the second accent-colored subagent activity bar and wire it to sanitized nested-runner lifecycle events. - [ ] Add fake-runner tests; do not require a live provider for unit tests. - [ ] Assert that the subagent receives a fork plus its identity, the parent receives the named subagent's final output as a tool response, neither side shares a live mutable messages reference, and the parent receives no full subagent transcript. - [ ] Assert that a subagent's `delegate` call returns a normal `status: "failed"` tool result with `error.code: "nested_delegation_forbidden"`, starts no nested provider run, and does not mutate the advertised tools or tool schemas. - [ ] Assert that the console renders a separate accent-colored activity bar during a subagent run and shows the final outcome without exposing the private transcript. - [ ] Verify the parent remains responsible for final tests, file inspection, and integration. ### Phase 3: UX and safety hardening - [ ] Add status/cancellation rendering for delegation. - [ ] Add usage/cost reporting for subagent calls. - [ ] Test provider errors, timeouts, oversized results, and partial findings. - [ ] Document when to delegate and when not to. ### Phase 4: Optional write-capable delegation Only begin this phase if read-only delegation proves insufficient. - [ ] Define explicit approval and worktree isolation. - [ ] Define patch review/merge behavior. - [ ] Prevent two agents from editing the same files. - [ ] Add integration tests for applying and reverting agent changes. ## Acceptance criteria ### Plan - A multi-step request can create, update, reload, and clear a plan. - The current plan is available after restarting Fido, and the restored snapshot is delivered to the model as a full tool result rather than system-prompt text. - Every successful `update_plan` response contains the complete normalized state (version, timestamp, reason, all items, and all present optional fields) with no truncation, ellipsis, or count-only replacement. - Updating the plan does not mutate the system message or force a rebuilt cached system-prompt prefix; a test can snapshot `messages[0]` before and after many updates and prove it is identical. - The latest full plan-state tool result remains available after turn handoff and compaction. - Invalid IDs, dependencies, and status combinations fail without corrupting saved state. - A plan update does not modify source files or appear in the undo log. - The UI renders every current todo item in a persistent floating box anchored to the right side of the screen; it does not merely append a compact plan to transcript history. ### Subagent - A bounded read-only task returns the documented structured result. - At call time the subagent receives an isolated fork of the parent `messages` array, not a shared live reference. - The fork contains a tool result that tells the subagent its current identity (`role: "subagent"`, unique `agent_id`, `parent_role: "main"`) and return responsibility. - The parent receives only the subagent's normalized final output, as a `delegate` tool response carrying the producing subagent's identity; it does not receive the subagent's private tool/message transcript. - The `delegate` tool description explicitly states the fork, identity, read-only restrictions, stable advertised catalog, nested-call failure behavior, return path, and parent ownership responsibilities. - The same tool names, schemas, descriptions, and ordering are advertised to parent and subagent; policy is enforced at invocation time rather than by mutating the tool list. - A subagent that calls `delegate` receives a normal failed tool result with `nested_delegation_forbidden`; no second subagent is created and no nested provider request is made. - The console displays a separate accent-colored subagent activity bar with agent ID, sanitized activity, and final status while delegating. - Timeout, cancellation, provider failure, and malformed output are handled gracefully. - The parent can continue normally after a partial or failed delegation. - Unit tests pass without making a live model/API request. ### Regression - Existing `npm test` tests remain green. - Existing `/undo`, `/suspend`, `/recall`, and tool notification behavior remain intact. - No new tool duplicates an existing shell operation unless it provides a clear safety or state-management benefit. ## Recommended first release Ship the plan capability first. Add read-only `delegate` only after the plan state is stable. This gives the agent a reliable way to decompose work without introducing concurrency, isolation, and model-context bugs all at once.