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
14 changes: 7 additions & 7 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,13 +135,13 @@ This roadmap translates `REQUIREMENTS.md` into phased, atomic tasks for building
- [x] Give each subagent access to the same tool catalog as the main agent.
- [x] Collect subagent outputs and return a merged parent summary.
- [x] Keep subagent tool-call traces out of main chat transcript by default.
- [ ] Add a plan-mode switch that disables mutating tools.
- [ ] Use a planner-oriented system prompt while in plan mode.
- [ ] Add explicit user approval flow to exit plan mode into execution mode.
- [ ] Execute approved plans as ordered actionable steps.
- [ ] Add a manual test where plan mode produces a plan without edits.
- [ ] Add a manual test where plan approval leads to controlled implementation.
- [ ] Add a manual test where plan rejection or modification revises the plan without execution.
- [x] Add a plan-mode switch that disables mutating tools.
- [x] Use a planner-oriented system prompt while in plan mode.
- [x] Add explicit user approval flow to exit plan mode into execution mode.
- [x] Execute approved plans as ordered actionable steps.
- [x] Add a manual test where plan mode produces a plan without edits.
- [x] Add a manual test where plan approval leads to controlled implementation.
- [x] Add a manual test where plan rejection or modification revises the plan without execution.

## Going Further - Stretch Phases

Expand Down
54 changes: 54 additions & 0 deletions docs/FOR-Rob-Simpson.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,3 +280,57 @@ Good engineers do not just ask, "Can I persist this?" They ask, "What kind of th
Durable memories are curated facts. Session transcripts are exact conversation state. Session summaries are compressed breadcrumbs for future work. Each one exists because it serves a different operational need.

This is also a nice example of test-driven architecture. We wrote one failing anchor test for every Step 7 spec scenario first. That forced the implementation to grow along stable seams instead of becoming one giant `repl.ts` blob with filesystem calls sprinkled everywhere.

---

# Step 8: Subagents and Plan Mode

Step 8 adds two major features to the agent. First, subagents — separate agent instances that work on focused tasks in isolation and report back. Second, plan mode — a way to tell the agent "think before you act" by locking it into a read-only architect role until you approve its plan.

Subagents were already done. This entry covers the plan mode work, which is the more architecturally interesting part.

## Technical Architecture

Plan mode adds a second operational state to the REPL. When active, the agent can read files, search code, and ask questions, but it cannot make any changes. The model produces a plan, the user reviews it, and only after approval does the agent switch back to execution mode.

The implementation sits across three key integration points:

**The agent loop** (`src/agent.ts:100`) gets an `isToolDenied` callback. This is checked *before* the existing permission system. If the callback says deny, the tool is rejected immediately — regardless of whether it would normally be allowed or prompt. This is the hard enforcement layer.

**The REPL** (`src/repl.ts:125`) owns the `planMode: boolean` state. When true, it passes three things to the agent loop: the `isToolDenied` callback (denying `write_file`, `edit_file`, `bash`), a modified system prompt (appending `PLAN_MODE_PROMPT`), and after the loop completes, it runs the approval flow prompt.

**The slash command handler** (`src/repl/commands.ts:56-69`) handles `/plan` (activate) and `/plan off` (deactivate). The `/status` command also shows whether plan mode is on.

The approval flow is deliberately simple: after the agent responds in plan mode, the user sees a prompt asking to approve (`y`), reject (`n`), or provide modifications (any other text). Approval deactivates plan mode and appends a "proceed with implementation" message to history. Rejection and modifications keep plan mode active and add the feedback as a new user message.

## Codebase Structure

Plan mode touches existing files, not new ones:

* `src/agent.ts:100,165-169` — the `isToolDenied` callback option and its check in the tool execution loop
* `src/repl.ts:27-35,125,153,193-229` — plan mode constants, state variable, dynamic prompt, system prompt construction, tool denial callback, and approval flow
* `src/repl/commands.ts:18-19,52-69` — new `getPlanMode`/`setPlanMode` options and `/plan`/`/plan off` command handling
* `src/__tests__/agent-plan-mode.test.ts` — unit tests for the `isToolDenied` callback
* `src/__tests__/commands-plan.test.ts` — unit tests for the slash commands

## Technologies and Why

We used a callback pattern (`isToolDenied`) instead of modifying the tool registry. The registry is created once at startup and shared with subagents. Mutating it for mode switches would be fragile and could leak state. A callback is cleaner — it's scoped to a single agent loop invocation and disappears when the loop returns.

We used a conversational approval flow instead of a structured plan schema. The LLM naturally produces numbered plans. Conversation history already captures everything. Adding a JSON plan format would add complexity without adding capability — the human reads the plan, not a parser.

The plan mode prompt indicator (`[plan] > `) is a small but important UX detail. You should always be able to glance at the terminal and know what mode you're in without running a command.

## Lessons Learned

The biggest lesson is the callback-as-lifecycle-hook pattern. The agent loop doesn't need to know *why* a tool is denied. It just needs to know *that* it's denied. This keeps the agent loop generic — you could reuse the same `isToolDenied` mechanism for other modes (a dry-run mode, a restricted mode for untrusted inputs, etc.) without touching the loop's internals.

Another lesson: separation of mode state from execution state. The REPL owns mode state and passes the consequences down. The agent loop remains a pure execution engine. This makes both layers independently testable — you can test tool denial by passing a callback, and you can test slash commands by passing mock callbacks, without needing to simulate the other layer.

A practical lesson: when adding new required fields to an options type (`HandleSlashCommandOptions`), check all existing callers immediately. The typechecker caught the missing fields in test files, but only because we ran `npm run typecheck` right away. If you wait until the end, you accumulate a pile of type errors that are harder to untangle.

## How Good Engineers Think

Good engineers ask, "Where does this state belong?" before they start coding. Plan mode state belongs in the REPL because the REPL is the orchestrator — it manages user interaction, mode switches, and the flow between planning and execution. The agent loop is a worker. Workers don't decide what mode they're in.

Good engineers also ask, "What's the minimum interface I need?" The `isToolDenied` callback is a single function that takes a string and returns a boolean. That's the minimum contract for "should this tool be blocked?" Any extra complexity — knowing why, knowing the mode, knowing the user — would leak concerns across boundaries.
71 changes: 70 additions & 1 deletion docs/MANUAL_TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,14 @@ The following commands are available in the REPL:

| Command | Description |
|---------|-------------|
| `/status` | Display current context window usage (tokens, percentage, message count) |
| `/status` | Display current context window usage (tokens, percentage, message count) and mode |
| `/plan` | Activate plan mode (read-only architect, mutating tools disabled) |
| `/plan off` | Deactivate plan mode (return to normal execution) |
| `/model` | Show current model |
| `/model <id>` | Switch to a different model |
| `/remember <fact>` | Store a durable memory |
| `/recall [query]` | List or search memories |
| `/forget <id>` | Remove a stored memory |
| `exit` or `quit` | Exit the REPL |

### `/status` Command
Expand Down Expand Up @@ -645,3 +652,65 @@ rm -rf .ai-agent
**Expected:** The CLI prints an error explaining that the saved session was not found and exits without showing the REPL prompt.

**Pass criteria:** No interactive prompt appears. The command exits immediately with an error message.

---

## Step 8 - Subagents and Plan Mode

### Test 8.1: Plan mode produces a plan without edits

**Goal:** Verify that activating plan mode disables mutating tools and the agent produces a plan instead of making changes.

**Steps:**

1. Start the agent with `npm run dev`
2. Type: `/plan`
3. Verify the prompt changes from `> ` to `[plan] > `
4. Verify a confirmation message appears ("Plan mode activated")
5. Type: `Add a hello() function to src/tools/readFileTool.ts that logs "hello"`
6. Wait for the response

**Expected:** The agent reads the file (using `read_file`), analyzes it, and produces a plan describing the steps it would take. It should NOT call `write_file`, `edit_file`, or `bash`. If it tries, those tools should be denied with a plan-mode error message.

**Pass criteria:** The agent produces a numbered plan. No file edits occur. Mutating tool calls are denied. The prompt stays as `[plan] > `.

---

### Test 8.2: Plan approval leads to controlled implementation

**Goal:** Verify that approving a plan exits plan mode and triggers implementation.

**Steps:**

1. Start the agent with `npm run dev`
2. Type: `/plan`
3. Ask the agent to do something specific (e.g., `What changes would you make to add a trailing newline to the end of the read_file tool output?`)
4. Wait for the plan response
5. When the approval prompt appears, type: `y`
6. Verify the prompt changes back to `> `
7. Observe the agent implementing the approved plan

**Expected:** After typing `y`, plan mode deactivates, the prompt returns to `> `, and the agent proceeds to implement the plan using mutating tools (which are now re-enabled).

**Pass criteria:** Plan mode deactivates on approval. The prompt reverts to `> `. The agent implements the plan using tools like `edit_file` or `write_file`. The changes match what was planned.

---

### Test 8.3: Plan rejection or modification revises the plan

**Goal:** Verify that rejecting or modifying a plan keeps plan mode active and the agent revises without executing.

**Steps:**

1. Start the agent with `npm run dev`
2. Type: `/plan`
3. Ask: `Plan how to refactor the tool registry to support async tool definitions`
4. Wait for the plan response
5. When the approval prompt appears, type: `I don't want async definitions. Just add a way to list tool names.`
6. Wait for the revised response
7. Verify the prompt is still `[plan] > `
8. Type: `n` at the next approval prompt

**Expected:** After typing the modification text (step 5), the agent stays in plan mode and produces a revised plan that addresses the feedback. After typing `n` (step 8), the agent stays in plan mode and acknowledges the rejection. No code changes are made at any point.

**Pass criteria:** Plan mode stays active throughout. The agent revises its plan based on feedback. After rejection, the agent acknowledges it and waits for further input. No mutating tools are called. The prompt remains `[plan] > `.
41 changes: 41 additions & 0 deletions docs/SOCRATIC_JOURNAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,3 +294,44 @@ That is easier to reason about, easier to test, and safer than letting a live RE
- `src/repl.ts:108` — `let model` (mutable) replaces `const model`
- `src/repl/commands.ts:16-17` — `getModel`/`setModel` callbacks in `HandleSlashCommandOptions`
- `src/repl/commands.ts:53-62` — `/model` command shows current model or switches to a new one

---

## Step 8 - Subagents and Plan Mode

### Q1: Why use a callback (`isToolDenied`) instead of modifying the tool registry for plan mode?

**Why it matters:** Plan mode needs to dynamically deny mutating tools at runtime. The tool registry is a static structure created once at startup. Mutating it for mode switches would couple mode state to the registry and risk leaking state between toggles.

**What we learned:** The callback pattern mirrors the existing `promptForApproval` callback on `AgentLoopOptions`. By adding `isToolDenied?: (toolName: string) => boolean`, the agent loop checks it before the permission check — no registry mutation needed. When plan mode is off, the callback returns `false` (or is absent), and the existing permission logic runs unchanged. This keeps the agent loop generic and testable: it doesn't need to know *why* a tool is denied, just *that* it is.

**Demonstrated in:**
- `src/agent.ts:100` — `isToolDenied` optional callback on `AgentLoopOptions`
- `src/agent.ts:165-169` — denial check runs before the existing permission flow
- `src/repl.ts:195-197` — REPL passes a closure that checks `planMode` and `MUTATING_TOOLS`

---

### Q2: Why does plan mode state live in the REPL instead of the agent loop?

**Why it matters:** Plan mode is a REPL-level concern — it's toggled by slash commands, displayed in the prompt and status, and controls the approval flow after the agent loop returns. The agent loop is a pure execution engine that processes messages and tools; it shouldn't manage UI state.

**What we learned:** Separation of concerns. The REPL owns mode state (`planMode: boolean`), prompt construction (appending `PLAN_MODE_PROMPT`), and the approval interaction. It passes the consequences of that state down to the agent loop via options (`isToolDenied`, `system`). This means the agent loop remains testable in isolation — you can verify tool denial by passing a callback, without simulating slash commands or REPL interactions.

**Demonstrated in:**
- `src/repl.ts:125` — `let planMode = false` in REPL scope
- `src/repl.ts:153` — dynamic prompt: `planMode ? PLAN_PROMPT : NORMAL_PROMPT`
- `src/repl.ts:193-197` — system prompt and `isToolDenied` derived from `planMode`
- `src/repl.ts:204-229` — approval flow runs after agent loop returns, only when `planMode` is true

---

### Q3: Why is the plan approval flow conversational instead of structured?

**Why it matters:** Plans could be stored in a JSON schema with explicit steps, statuses, and dependencies. Instead, we keep plans as freeform text in conversation history and use a simple `y/n/text` prompt.

**What we learned:** The conversational approach leverages what already exists — the LLM naturally produces numbered plans, and conversation history preserves full context. When the user approves, their approval message is appended to history, giving the model everything it needs to execute. No extra data structures, no serialization, no risk of schema mismatch with what the model actually generated. The trade-off is less programmatic control over individual plan steps, but for a coding agent this is the right trade-off: the human is the orchestrator, not a script.

**Demonstrated in:**
- `src/repl.ts:206-229` — three-way approval prompt: `y` (approve and exit plan mode), `n` (reject and stay), or freeform text (feedback that revises the plan)
- `src/repl.ts:211-213` — approval appends "Plan approved. Please proceed with implementation." to conversation history
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-18
Loading
Loading