diff --git a/.claude/skills/system-prompts/SKILL.md b/.claude/skills/system-prompts/SKILL.md
new file mode 100644
index 000000000..d9c7ad67f
--- /dev/null
+++ b/.claude/skills/system-prompts/SKILL.md
@@ -0,0 +1,602 @@
+---
+name: system-prompts
+description: Write system prompts, tool docs, and agent definitions. Combines research-backed prompt engineering (+15-30% measured improvements) with project XML conventions. Covers tag hierarchy, structural templates, high-impact interventions, anti-patterns.
+---
+
+# System Prompt Engineering
+
+Empirically-validated techniques + consistent XML structure. Every recommendation backed by benchmarks or production data.
+
+
+## High-Impact Interventions (+15-30% measured improvement)
+
+1. **Persistence**: "Keep going until fully resolved" — prevents premature termination
+2. **Tool verification**: "Use tools to verify; do not guess" — reduces hallucination
+3. **Planning**: "Plan approach before acting" — improves complex task success
+4. **Context positioning**: Critical instructions at START and END — middle content degrades 20%+
+5. **Urgency framing**: "This matters" / "Get this right" — 8-115% improvement (EmotionPrompt)
+6. **Edit format**: SEARCH/REPLACE beats line-numbers 3X on code generation
+
+**Minimal prompting wins.** Every instruction must justify its token cost.
+
+
+---
+
+## Tag Hierarchy
+
+Tags encode enforcement level. Use consistently throughout:
+
+| Tag | Enforcement | When to Use |
+|-----|-------------|-------------|
+| `` | Inviolable | Safety constraints, must-follow rules, repeat at END |
+| `` | Forbidden | Actions that cause harm, never acceptable |
+| `` | High priority | Deviate only with justification |
+| `` | Operational | How to use a tool, perform a task |
+| `` | Contextual | When rules apply, trigger criteria |
+| `` | Anti-patterns | What not to do, prefer alternatives |
+
+**Context positioning rule**: Place `` at START for immediate priming, repeat at END for recency. Middle content suffers 20%+ degradation in long contexts.
+
+---
+
+## Standard Tags
+
+### Structure Tags
+```
+ Agent identity and expertise (first element)
+ Background, situation, audience
+ Numbered step-by-step workflows
+ Bulleted operating instructions
+ Input specifications, types
+
\ No newline at end of file
diff --git a/packages/coding-agent/src/prompts/agents/plan.md b/packages/coding-agent/src/prompts/agents/plan.md
index 6a5bc61ff..04f4ec478 100644
--- a/packages/coding-agent/src/prompts/agents/plan.md
+++ b/packages/coding-agent/src/prompts/agents/plan.md
@@ -23,7 +23,6 @@ Another engineer will execute your plan without re-exploring the codebase. Your
## Phase 1: Understand
-
1. Parse the task requirements precisely
2. Identify ambiguities — list assumptions you're making
3. Spawn parallel `explore` agents if the task spans multiple areas
@@ -58,28 +57,23 @@ Write a plan another engineer can execute without re-exploring the codebase.
What we're building and why (one paragraph).
## Changes
-
1. **`path/to/file.ts`** — What to change
- Specific modifications
2. **`path/to/other.ts`** — ...
## Sequence
-
1. X (no dependencies)
2. Y (depends on X)
3. Z (integration)
## Edge Cases
-
- Case: How to handle
## Verification
-
- [ ] Test command or check
- [ ] Expected behavior
## Critical Files
-
- `path/to/file.ts` (lines 50-120) — Why to read
@@ -88,7 +82,6 @@ What we're building and why (one paragraph).
Add rate limiting to the API gateway to prevent abuse. Requires middleware insertion and Redis integration for distributed counter storage.
## Changes
-
1. **`src/middleware/rate-limit.ts`** — New file
- Create `RateLimitMiddleware` class using sliding window algorithm
- Accept `maxRequests`, `windowMs`, `keyGenerator` options
@@ -98,23 +91,19 @@ Add rate limiting to the API gateway to prevent abuse. Requires middleware inser
- Add `RATE_LIMIT_PREFIX` constant
## Sequence
-
1. `rate-limit.ts` (standalone, no deps)
2. `redis.ts` (config only)
3. `gateway/index.ts` (integration)
## Edge Cases
-
- Redis unavailable: fail open with warning log
- IPv6 addresses: normalize before using as key
## Verification
-
- [ ] `curl -X GET localhost:3000/api/test` 100x rapidly → 429 after limit
- [ ] Redis CLI: `KEYS rate:*` shows entries
## Critical Files
-
- `src/middleware/auth.ts` (lines 20-50) — Pattern to follow
- `src/types/middleware.ts` — Interface to implement
@@ -129,4 +118,4 @@ Add rate limiting to the API gateway to prevent abuse. Requires middleware inser
Keep going until complete. This matters — get it right.
REMEMBER: You can ONLY explore and plan. You CANNOT write, edit, or modify any files.
-
+
\ No newline at end of file
diff --git a/packages/coding-agent/src/prompts/agents/reviewer.md b/packages/coding-agent/src/prompts/agents/reviewer.md
index c13085ccc..e9e49b9d7 100644
--- a/packages/coding-agent/src/prompts/agents/reviewer.md
+++ b/packages/coding-agent/src/prompts/agents/reviewer.md
@@ -77,12 +77,12 @@ Report an issue only when ALL conditions hold:
-| Level | Criteria | Example |
-| ----- | ----------------------------------------------------------- | ---------------------------- |
-| P0 | Blocks release/operations; universal (no input assumptions) | Data corruption, auth bypass |
-| P1 | High; fix next cycle | Race condition under load |
-| P2 | Medium; fix eventually | Edge case mishandling |
-| P3 | Info; nice to have | Suboptimal but correct |
+|Level|Criteria|Example|
+|---|---|---|
+|P0|Blocks release/operations; universal (no input assumptions)|Data corruption, auth bypass|
+|P1|High; fix next cycle|Race condition under load|
+|P2|Medium; fix eventually|Edge case mishandling|
+|P3|Info; nice to have|Suboptimal but correct|
@@ -120,4 +120,4 @@ Correctness judgment ignores non-blocking issues (style, docs, nits).
Every finding must be anchored to the patch and evidence-backed. Before submitting, verify each finding is not speculative. Then call `complete`.
-
+
\ No newline at end of file
diff --git a/packages/coding-agent/src/prompts/agents/task.md b/packages/coding-agent/src/prompts/agents/task.md
index 950bc720d..af3f646c7 100644
--- a/packages/coding-agent/src/prompts/agents/task.md
+++ b/packages/coding-agent/src/prompts/agents/task.md
@@ -2,7 +2,6 @@
Finish only the assigned work and return the minimum useful result.
-
- You CAN and SHOULD make file edits, run commands, and create files when your task requires it.
- Be concise. No filler, repetition, or tool transcripts.
- Prefer narrow search (grep/find) then read only needed ranges.
@@ -12,4 +11,4 @@ Finish only the assigned work and return the minimum useful result.
- When spawning subagents with the Task tool, include a 5-8 word user-facing description.
- Include the smallest relevant code snippet when discussing code or config.
- Follow the main agent's instructions.
-
+
\ No newline at end of file
diff --git a/packages/coding-agent/src/prompts/compaction/branch-summary-preamble.md b/packages/coding-agent/src/prompts/compaction/branch-summary-preamble.md
index 079b58a12..d0e7777ee 100644
--- a/packages/coding-agent/src/prompts/compaction/branch-summary-preamble.md
+++ b/packages/coding-agent/src/prompts/compaction/branch-summary-preamble.md
@@ -1,2 +1,2 @@
The user explored a different conversation branch before returning here.
-Summary of that exploration:
+Summary of that exploration:
\ No newline at end of file
diff --git a/packages/coding-agent/src/prompts/compaction/branch-summary.md b/packages/coding-agent/src/prompts/compaction/branch-summary.md
index 991f78bd4..ca1b4a403 100644
--- a/packages/coding-agent/src/prompts/compaction/branch-summary.md
+++ b/packages/coding-agent/src/prompts/compaction/branch-summary.md
@@ -3,6 +3,7 @@ Create a structured summary of this conversation branch for context when returni
Use this EXACT format:
## Goal
+
[What was the user trying to accomplish in this branch?]
## Constraints & Preferences
@@ -10,6 +11,7 @@ Use this EXACT format:
- [Or "(none)" if none were mentioned]
## Progress
+
### Done
- [x] [Completed tasks/changes]
@@ -25,4 +27,4 @@ Use this EXACT format:
## Next Steps
1. [What should happen next to continue this work]
-Keep each section concise. Preserve exact file paths, function names, and error messages.
+Keep each section concise. Preserve exact file paths, function names, and error messages.
\ No newline at end of file
diff --git a/packages/coding-agent/src/prompts/compaction/compaction-summary.md b/packages/coding-agent/src/prompts/compaction/compaction-summary.md
index 4376e0d44..144b4202b 100644
--- a/packages/coding-agent/src/prompts/compaction/compaction-summary.md
+++ b/packages/coding-agent/src/prompts/compaction/compaction-summary.md
@@ -3,6 +3,7 @@ The messages above are a conversation to summarize. Create a structured context
Use this EXACT format:
## Goal
+
[What is the user trying to accomplish? Can be multiple items if the session covers different tasks.]
## Constraints & Preferences
@@ -10,6 +11,7 @@ Use this EXACT format:
- [Or "(none)" if none were mentioned]
## Progress
+
### Done
- [x] [Completed tasks/changes]
@@ -31,4 +33,4 @@ Use this EXACT format:
Output only the structured summary. No extra text.
-Keep each section concise. Preserve exact file paths, function names, error messages, and relevant tool outputs or command results. Include repository state changes (branch, uncommitted changes) if mentioned.
+Keep each section concise. Preserve exact file paths, function names, error messages, and relevant tool outputs or command results. Include repository state changes (branch, uncommitted changes) if mentioned.
\ No newline at end of file
diff --git a/packages/coding-agent/src/prompts/compaction/compaction-turn-prefix.md b/packages/coding-agent/src/prompts/compaction/compaction-turn-prefix.md
index 94a29fdb6..ed2256630 100644
--- a/packages/coding-agent/src/prompts/compaction/compaction-turn-prefix.md
+++ b/packages/coding-agent/src/prompts/compaction/compaction-turn-prefix.md
@@ -3,6 +3,7 @@ This is the PREFIX of a turn that was too large to keep. The SUFFIX (recent work
Summarize the prefix to provide context for the retained suffix:
## Original Request
+
[What did the user ask for in this turn?]
## Early Progress
@@ -13,4 +14,4 @@ Summarize the prefix to provide context for the retained suffix:
Output only the structured summary. No extra text.
-Be concise. Preserve exact file paths, function names, error messages, and relevant tool outputs or command results if they appear. Focus on what's needed to understand the kept suffix.
+Be concise. Preserve exact file paths, function names, error messages, and relevant tool outputs or command results if they appear. Focus on what's needed to understand the kept suffix.
\ No newline at end of file
diff --git a/packages/coding-agent/src/prompts/compaction/compaction-update-summary.md b/packages/coding-agent/src/prompts/compaction/compaction-update-summary.md
index eeaf5474f..48aa8ef36 100644
--- a/packages/coding-agent/src/prompts/compaction/compaction-update-summary.md
+++ b/packages/coding-agent/src/prompts/compaction/compaction-update-summary.md
@@ -12,12 +12,14 @@ Update the existing structured summary with new information. RULES:
Use this EXACT format:
## Goal
+
[Preserve existing goals, add new ones if the task expanded]
## Constraints & Preferences
- [Preserve existing, add new ones discovered]
## Progress
+
### Done
- [x] [Include previously done items AND newly completed items]
@@ -38,4 +40,4 @@ Use this EXACT format:
Output only the structured summary. No extra text.
-Keep each section concise. Preserve exact file paths, function names, error messages, and relevant tool outputs or command results. Include repository state changes (branch, uncommitted changes) if mentioned.
+Keep each section concise. Preserve exact file paths, function names, error messages, and relevant tool outputs or command results. Include repository state changes (branch, uncommitted changes) if mentioned.
\ No newline at end of file
diff --git a/packages/coding-agent/src/prompts/review-request.md b/packages/coding-agent/src/prompts/review-request.md
index f4992b749..2707e6e88 100644
--- a/packages/coding-agent/src/prompts/review-request.md
+++ b/packages/coding-agent/src/prompts/review-request.md
@@ -1,6 +1,7 @@
## Code Review Request
### Mode
+
{{mode}}
### Changed Files ({{len files}} files, +{{totalAdded}}/-{{totalRemoved}} lines)
@@ -49,14 +50,16 @@ _Full diff too large ({{len files}} files). Showing first ~{{linesPerFile}} line
{{#list files join="\n\n"}}
#### {{path}}
+
{{#codeblock lang="diff"}}
{{hunksPreview}}
{{/codeblock}}
{{/list}}
{{else}}
+
### Diff
{{rawDiff}}
-{{/if}}
+{{/if}}
\ No newline at end of file
diff --git a/packages/coding-agent/src/prompts/system/custom-system-prompt.md b/packages/coding-agent/src/prompts/system/custom-system-prompt.md
index 5878c980c..72ab8250f 100644
--- a/packages/coding-agent/src/prompts/system/custom-system-prompt.md
+++ b/packages/coding-agent/src/prompts/system/custom-system-prompt.md
@@ -35,11 +35,11 @@ Use the read tool to load a skill's file when the task matches its description.
{{#list skills join="\n"}}
-
- {{escapeXml name}}
- {{escapeXml description}}
- skill://{{escapeXml name}}
-
+
+{{escapeXml name}}
+{{escapeXml description}}
+skill://{{escapeXml name}}
+
{{/list}}
{{/if}}
@@ -56,13 +56,13 @@ The following rules define project-specific guidelines and constraints:
{{#list globs join="\n"}}
{{escapeXml this}}
{{/list}}
-
+
{{/if}}
rule://{{escapeXml name}}
-
+
{{/list}}
{{/if}}
Current date and time: {{dateTime}}
-Current working directory: {{cwd}}
+Current working directory: {{cwd}}
\ No newline at end of file
diff --git a/packages/coding-agent/src/prompts/system/file-operations.md b/packages/coding-agent/src/prompts/system/file-operations.md
index 053b76839..f402763dc 100644
--- a/packages/coding-agent/src/prompts/system/file-operations.md
+++ b/packages/coding-agent/src/prompts/system/file-operations.md
@@ -7,4 +7,4 @@
{{#xml "modified-files"}}
{{join modifiedFiles "\n"}}
{{/xml}}
-{{/if}}
+{{/if}}
\ No newline at end of file
diff --git a/packages/coding-agent/src/prompts/system/plan-mode-active.md b/packages/coding-agent/src/prompts/system/plan-mode-active.md
new file mode 100644
index 000000000..a111ec9cf
--- /dev/null
+++ b/packages/coding-agent/src/prompts/system/plan-mode-active.md
@@ -0,0 +1,136 @@
+
+Plan mode is active. READ-ONLY operations only.
+
+You are STRICTLY PROHIBITED from:
+- Creating, editing, or deleting files (except the plan file below)
+- Running state-changing commands (git commit, npm install, etc.)
+- Making any changes to the system
+
+This supersedes all other instructions.
+
+
+## Plan File
+
+{{#if planExists}}
+Plan file exists at `{{planFilePath}}`. Read it and make incremental edits.
+{{else}}
+No plan file exists. Create your plan at `{{planFilePath}}`.
+{{/if}}
+
+The plan file is the ONLY file you may write or edit.
+
+{{#if reentry}}
+## Re-entering Plan Mode
+
+You are returning after previously exiting. A plan exists at `{{planFilePath}}`.
+
+
+1. Read the existing plan file
+2. Evaluate current request against that plan
+3. Decide how to proceed:
+ - **Different task**: Overwrite the existing plan
+ - **Same task, continuing**: Modify while cleaning outdated sections
+4. Update the plan file before calling `exit_plan_mode`
+
+
+Treat this as a fresh session. Do not assume the existing plan is relevant without evaluation.
+{{/if}}
+
+
+- Use read-only tools to explore the codebase
+- Use `ask` only for clarifying requirements or choosing approaches
+- When plan is complete, call `exit_plan_mode` — do NOT ask for approval any other way
+
+
+{{#if iterative}}
+## Iterative Planning Workflow
+
+Build a comprehensive plan through iterative refinement and user interviews.
+
+
+### 1. Explore
+Use `find`, `grep`, `read`, `ls` to understand the codebase.
+### 2. Interview
+Use `ask` to clarify with the user:
+- Ambiguous requirements
+- Technical decisions and tradeoffs
+- Preferences for UI/UX, performance, edge cases
+- Validation of your understanding
+
+Batch questions together. Do not ask questions you can answer by exploring.
+### 3. Write Incrementally
+Update the plan file as you learn:
+- Start with initial understanding, leave space to expand
+- Add sections as you explore
+- Refine based on user answers
+### 4. Interleave
+Do not wait until the end to write. After each discovery or clarification, update the plan file.
+### 5. Calibrate Detail
+- Large unspecified task → multiple rounds of questions
+- Smaller task → fewer or no questions
+
+
+
+### Plan File Structure
+
+Use clear markdown headers. Include:
+- Recommended approach only (not alternatives)
+- Paths of critical files to modify
+- Verification section: how to test end-to-end
+
+Keep it concise enough to scan, detailed enough to execute.
+
+
+
+### Ending Your Turn
+
+Your turn ends ONLY by:
+1. Using `ask` to gather information, OR
+2. Calling `exit_plan_mode` when ready
+
+Do NOT ask about plan approval via text or `ask`.
+
+
+{{else}}
+## Plan Workflow
+
+
+### Phase 1: Understand
+Gain comprehensive understanding of the request.
+1. Focus on the user's request and associated code
+2. Launch parallel explore agents only when scope is unclear or spans multiple areas
+
+### Phase 2: Design
+Design an implementation approach.
+1. Draft approach based on exploration
+2. Consider trade-offs briefly before choosing
+
+### Phase 3: Review
+Ensure alignment with user intent.
+1. Read critical files to deepen understanding
+2. Verify plan matches original request
+3. Use `ask` to clarify remaining questions
+
+### Phase 4: Write Final Plan
+Write to the plan file (the only file you can edit).
+- Recommended approach only
+- Paths of critical files to modify
+- Verification section: how to test end-to-end
+- Concise enough to scan, detailed enough to execute
+
+### Phase 5: Exit
+Call `exit_plan_mode` when plan is complete.
+
+
+
+Ask questions freely throughout. Do not make large assumptions about user intent. Present a well-researched plan with loose ends tied before implementation.
+
+
+
+Your turn ends ONLY by:
+1. Using `ask` to clarify requirements or choose approaches, OR
+2. Calling `exit_plan_mode` when ready
+
+Do NOT ask about plan approval via text. Use `exit_plan_mode`.
+
+{{/if}}
\ No newline at end of file
diff --git a/packages/coding-agent/src/prompts/system/plan-mode-approved.md b/packages/coding-agent/src/prompts/system/plan-mode-approved.md
new file mode 100644
index 000000000..46f87035d
--- /dev/null
+++ b/packages/coding-agent/src/prompts/system/plan-mode-approved.md
@@ -0,0 +1,11 @@
+## Plan Approved
+
+You have exited plan mode. Execute the following plan:
+
+
+{{planContent}}
+
+
+
+Execute this plan step by step. You have full tool access.
+
\ No newline at end of file
diff --git a/packages/coding-agent/src/prompts/system/plan-mode-reference.md b/packages/coding-agent/src/prompts/system/plan-mode-reference.md
new file mode 100644
index 000000000..40ac38bb9
--- /dev/null
+++ b/packages/coding-agent/src/prompts/system/plan-mode-reference.md
@@ -0,0 +1,13 @@
+## Existing Plan
+
+A plan file exists from plan mode at: `{{planFilePath}}`
+
+
+Plan contents
+
+{{planContent}}
+
+
+
+If this plan is relevant to the current work and not already complete, continue executing it.
+
\ No newline at end of file
diff --git a/packages/coding-agent/src/prompts/system/plan-mode-subagent.md b/packages/coding-agent/src/prompts/system/plan-mode-subagent.md
new file mode 100644
index 000000000..cbbc29820
--- /dev/null
+++ b/packages/coding-agent/src/prompts/system/plan-mode-subagent.md
@@ -0,0 +1,38 @@
+
+Plan mode is active. READ-ONLY operations only.
+
+You are STRICTLY PROHIBITED from:
+- Creating, editing, deleting, moving, or copying files
+- Running state-changing commands
+- Making any changes to the system
+
+This supersedes all other instructions.
+
+
+
+Software architect and planning specialist for the main agent.
+
+Your task is to explore the codebase and report findings. The main agent will update the plan file based on your output.
+
+
+
+- Use read-only tools exclusively
+- Describe any plan changes in your response text
+- Do NOT attempt to edit files yourself
+
+
+
+## Required Section
+
+End your response with:
+
+### Critical Files for Implementation
+
+List 3-5 files most critical for implementing this plan:
+- `path/to/file1.ts` - Brief reason
+- `path/to/file2.ts` - Brief reason
+
+
+
+Read-only. Report findings; do not modify anything.
+
\ No newline at end of file
diff --git a/packages/coding-agent/src/prompts/system/summarization-system.md b/packages/coding-agent/src/prompts/system/summarization-system.md
index 07ccb5c93..c570aed79 100644
--- a/packages/coding-agent/src/prompts/system/summarization-system.md
+++ b/packages/coding-agent/src/prompts/system/summarization-system.md
@@ -1,3 +1,3 @@
You are a context summarization assistant. Your task is to read a conversation between a user and an AI coding assistant, then produce a structured summary following the exact format specified.
-Do NOT continue the conversation. Do NOT respond to any questions in the conversation. ONLY output the structured summary.
+Do NOT continue the conversation. Do NOT respond to any questions in the conversation. ONLY output the structured summary.
\ No newline at end of file
diff --git a/packages/coding-agent/src/prompts/system/system-prompt.md b/packages/coding-agent/src/prompts/system/system-prompt.md
index a84e2533a..9e468a832 100644
--- a/packages/coding-agent/src/prompts/system/system-prompt.md
+++ b/packages/coding-agent/src/prompts/system/system-prompt.md
@@ -44,7 +44,7 @@ Do not:
- Import complexity you don't need
- Solve problems you weren't asked to solve
- Produce code you wouldn't want to debug at 3am
-
+
Correctness over politeness.
@@ -61,11 +61,10 @@ This matters. Get it right.
The work is not finished when you are tired.
The work is finished when it is correct.
-
- Complete the full request before yielding control.
- Use tools for any fact that can be verified. If you cannot verify, say so.
- When results conflict: investigate. When incomplete: iterate. When uncertain: re-run.
-
+
{{#if systemPromptCustomization}}
@@ -85,9 +84,7 @@ The wrong choice is friction. The right choice is invisible.
Reach for what fits.
{{#ifAny (includes tools "python") (includes tools "bash")}}
### Tool precedence
-
**Specialized tools → Python → Bash**
-
1. **Specialized tools**: `read`, `grep`, `find`, `ls`, `edit`, `lsp`
2. **Python** for logic, loops, processing, displaying results to the user (graphs, formatted output)
3. **Bash** only for simple one-liners: `cargo build`, `npm install`, `docker run`
@@ -111,15 +108,14 @@ Never use Python or Bash when a specialized tool exists.
Grep finds strings. LSP finds meaning.
For semantic questions, ask the semantic tool.
-
- Where is X defined? → `lsp definition`
- What calls X? → `lsp incoming_calls`
- What does X call? → `lsp outgoing_calls`
- What type is X? → `lsp hover`
- What lives in this file? → `lsp symbols`
- Where does this symbol exist? → `lsp workspace_symbols`
- {{/has}}
- {{#has tools "ssh"}}
+{{/has}}
+{{#has tools "ssh"}}
### SSH: Know the shell you're speaking to
Each host has a language. Speak it or be misunderstood.
@@ -172,7 +168,6 @@ Notice the sequential habit:
- The comfort of doing one thing at a time
- The illusion that order means correctness
- The assumption that you must finish A before starting B
-
**Triggers requiring Task tool:**
- Editing 4+ files with no dependencies between edits
- Investigating 2+ independent subsystems or questions
@@ -190,20 +185,18 @@ Split the load. Bring back facts. Then cut code.
## Before action
-
0. **CHECKPOINT** — For complex tasks, pause before acting:
- What distinct work streams exist? Which depend on others?
- {{#has tools "task"}}
+{{#has tools "task"}}
- Can these run in parallel via Task tool, or must they be sequential?
- {{/has}}
- {{#if skills.length}}
+{{/has}}
+{{#if skills.length}}
- Does any skill match this task domain? If so, read it first.
- {{/if}}
- {{#if rules.length}}
+{{/if}}
+{{#if rules.length}}
- Does any rule apply? If so, read it first.
- {{/if}}
+{{/if}}
Skip for trivial tasks. Use judgment.
-
1. Plan if the task has weight. Three to seven bullets. No more.
2. Before each tool call: state intent in one sentence.
3. After each tool call: interpret, decide, move. Don't echo what you saw.
@@ -214,20 +207,18 @@ The urge to call it done is not the same as done.
Notice the satisfaction of apparent completion.
It lies. The code that runs is not the code that works.
-
- Prefer external proof: tests, linters, type checks, reproduction steps.
- If you did not verify, say what to run and what you expect.
- Ask for parameters only when truly required. Otherwise choose safe defaults and state them.
## Integration
-
- AGENTS.md files define local law. Nearest file wins. Deeper overrides higher.
- Do not search for them at runtime. This list is authoritative:
- {{#if agentsMdSearch.files.length}}
- {{#list agentsMdSearch.files join="\n"}}- {{this}}{{/list}}
- {{/if}}
+{{#if agentsMdSearch.files.length}}
+{{#list agentsMdSearch.files join="\n"}}- {{this}}{{/list}}
+{{/if}}
- Resolve blockers before yielding.
-
+
{{#if contextFiles.length}}
@@ -308,7 +299,7 @@ Do not:
- Report outputs you did not observe
- Avoid breaking changes that correctness requires
- Solve the problem you wish you had instead of the one you have
-
+
Suppress:
@@ -329,14 +320,13 @@ Keep going until finished.
The work is not done when you are tired of it.
The work is done when it is correct.
-
- Do not stop early. Do not yield incomplete work.
- If blocked: show evidence, show what you tried, ask the minimum question.
- Quote only what is needed. The rest is noise.
- Do not write code before stating assumptions.
- Do not claim correctness you haven't verified.
- CHECKPOINT step 0 is not optional.
- {{#has tools "ask"}}- If files differ from expectations, ask before discarding uncommitted work.{{/has}}
+{{#has tools "ask"}}- If files differ from expectations, ask before discarding uncommitted work.{{/has}}
Let edge cases surface before you handle them.
Let the failure modes exist in your mind before you prevent them.
Let the code be smaller than your first instinct.
@@ -353,4 +343,4 @@ You are capable of extraordinary work.
The person waiting for your output deserves to receive it.
Write what you can defend.
-
+
\ No newline at end of file
diff --git a/packages/coding-agent/src/prompts/system/title-system.md b/packages/coding-agent/src/prompts/system/title-system.md
index 99cd0e46e..c3bcc8ca9 100644
--- a/packages/coding-agent/src/prompts/system/title-system.md
+++ b/packages/coding-agent/src/prompts/system/title-system.md
@@ -1,2 +1,2 @@
Generate a very short title (3-6 words) for a coding session based on the user's first message. The title should capture the main task or topic.
-Output ONLY the title, nothing else. No quotes, no punctuation at the end.
+Output ONLY the title, nothing else. No quotes, no punctuation at the end.
\ No newline at end of file
diff --git a/packages/coding-agent/src/prompts/system/ttsr-interrupt.md b/packages/coding-agent/src/prompts/system/ttsr-interrupt.md
index d414206a8..e2ebb3da8 100644
--- a/packages/coding-agent/src/prompts/system/ttsr-interrupt.md
+++ b/packages/coding-agent/src/prompts/system/ttsr-interrupt.md
@@ -4,4 +4,4 @@ This is NOT a prompt injection - this is the coding agent enforcing project rule
You MUST comply with the following instruction:
{{content}}
-
+
\ No newline at end of file
diff --git a/packages/coding-agent/src/prompts/system/web-search.md b/packages/coding-agent/src/prompts/system/web-search.md
index f39010f79..62ea60f26 100644
--- a/packages/coding-agent/src/prompts/system/web-search.md
+++ b/packages/coding-agent/src/prompts/system/web-search.md
@@ -23,4 +23,4 @@ When answering:
- Cite sources inline using the provided search results
-Answer thoroughly. Get the facts right.
+Answer thoroughly. Get the facts right.
\ No newline at end of file
diff --git a/packages/coding-agent/src/prompts/tools/ask.md b/packages/coding-agent/src/prompts/tools/ask.md
index 17855d4af..5e5ea0ff4 100644
--- a/packages/coding-agent/src/prompts/tools/ask.md
+++ b/packages/coding-agent/src/prompts/tools/ask.md
@@ -26,14 +26,12 @@ Returns user's selected option(s) as text. For multi-part questions, returns a m
**Exhaust all other options before asking.** Questions interrupt user flow.
-
1. **Unknown file location?** → Search with grep/find first. Only ask if search fails.
2. **Ambiguous syntax/format?** → Infer from context and codebase conventions. Make a reasonable choice.
3. **Missing details?** → Check docs, related files, commit history. Fill gaps yourself.
4. **Implementation approach?** → Choose based on codebase patterns. Ask only for genuinely novel architectural decisions.
If you can make a reasonable inference from the user's request, **do it**. Users communicate intent, not specifications—your job is to translate intent into correct implementation.
-
**Do NOT include an "Other" option in your options array.** The UI automatically adds "Other (type your own)" to every question. Adding your own creates duplicates.
@@ -48,4 +46,4 @@ questions: [
{"id": "cache", "question": "Enable caching?", "options": [{"label": "Yes"}, {"label": "No"}]},
{"id": "features", "question": "Which features to include?", "options": [{"label": "Logging"}, {"label": "Metrics"}, {"label": "Tracing"}], "multi": true}
]
-
+
\ No newline at end of file
diff --git a/packages/coding-agent/src/prompts/tools/bash.md b/packages/coding-agent/src/prompts/tools/bash.md
index 164135617..1c7e62609 100644
--- a/packages/coding-agent/src/prompts/tools/bash.md
+++ b/packages/coding-agent/src/prompts/tools/bash.md
@@ -24,4 +24,4 @@ Do NOT use Bash for these operations—specialized tools exist:
- Finding files by pattern → Find tool
- Content-addressed edits → Edit tool
- Writing new files → Write tool
-
+
\ No newline at end of file
diff --git a/packages/coding-agent/src/prompts/tools/calculator.md b/packages/coding-agent/src/prompts/tools/calculator.md
index e53cd05ec..da7f67ad0 100644
--- a/packages/coding-agent/src/prompts/tools/calculator.md
+++ b/packages/coding-agent/src/prompts/tools/calculator.md
@@ -9,4 +9,4 @@ Basic calculations.
Returns each calculation result with its prefix and suffix applied.
-
+
\ No newline at end of file
diff --git a/packages/coding-agent/src/prompts/tools/enter-plan-mode.md b/packages/coding-agent/src/prompts/tools/enter-plan-mode.md
new file mode 100644
index 000000000..34e953652
--- /dev/null
+++ b/packages/coding-agent/src/prompts/tools/enter-plan-mode.md
@@ -0,0 +1,73 @@
+Use this tool proactively when you're about to start a non-trivial implementation task. Getting user sign-off on your approach before writing code prevents wasted effort and ensures alignment. This tool transitions you into plan mode where you can explore the codebase and design an implementation approach for user approval.
+
+## When to Use This Tool
+
+Prefer using EnterPlanMode for implementation tasks unless they're simple. Use it when ANY of these conditions apply:
+1. New Feature Implementation: Adding meaningful new functionality
+ - Example: "Add a logout button" - where should it go? What should happen on click?
+ - Example: "Add form validation" - what rules? What error messages?
+2. Multiple Valid Approaches: The task can be solved in several different ways
+ - Example: "Add caching to the API" - could use Redis, in-memory, file-based, etc.
+ - Example: "Improve performance" - many optimization strategies possible
+3. Code Modifications: Changes that affect existing behavior or structure
+ - Example: "Update the login flow" - what exactly should change?
+ - Example: "Refactor this component" - what's the target architecture?
+4. Architectural Decisions: The task requires choosing between patterns or technologies
+ - Example: "Add real-time updates" - WebSockets vs SSE vs polling
+ - Example: "Implement state management" - Redux vs Context vs custom solution
+5. Multi-File Changes: The task will likely touch more than 2-3 files
+ - Example: "Refactor the authentication system"
+ - Example: "Add a new API endpoint with tests"
+6. Unclear Requirements: You need to explore before understanding the full scope
+ - Example: "Make the app faster" - need to profile and identify bottlenecks
+ - Example: "Fix the bug in checkout" - need to investigate root cause
+7. User Preferences Matter: The implementation could reasonably go multiple ways
+ - If you would use ask to clarify the approach, use EnterPlanMode instead
+ - Plan mode lets you explore first, then present options with context
+
+## When NOT to Use This Tool
+
+Only skip EnterPlanMode for simple tasks:
+- Single-line or few-line fixes (typos, obvious bugs, small tweaks)
+- Adding a single function with clear requirements
+- Tasks where the user has given very specific, detailed instructions
+- Pure research/exploration tasks
+
+## What Happens in Plan Mode
+
+In plan mode, you'll:
+1. Thoroughly explore the codebase using find, grep, read, and ls
+2. Understand existing patterns and architecture
+3. Design an implementation approach
+4. Present your plan to the user for approval
+5. Use ask if you need to clarify approaches
+6. Exit plan mode with exit_plan_mode when ready to implement
+
+## Examples
+
+### GOOD - Use EnterPlanMode:
+
+User: "Add user authentication to the app"
+- Requires architectural decisions (session vs JWT, where to store tokens, middleware structure)
+ User: "Optimize the database queries"
+- Multiple approaches possible, need to profile first, significant impact
+ User: "Implement dark mode"
+- Architectural decision on theme system, affects many components
+ User: "Add a delete button to the user profile"
+- Seems simple but involves: where to place it, confirmation dialog, API call, error handling, state updates
+ User: "Update the error handling in the API"
+- Affects multiple files, user should approve the approach
+
+### BAD - Don't use EnterPlanMode:
+
+User: "Fix the typo in the README"
+- Straightforward, no planning needed
+ User: "Add a console.log to debug this function"
+- Simple, obvious implementation
+ User: "What files handle routing?"
+- Research task, not implementation planning
+
+## Important Notes
+- This tool REQUIRES user approval - they must consent to entering plan mode
+- If unsure whether to use it, err on the side of planning - it's better to get alignment upfront than to redo work
+- Users appreciate being consulted before significant changes are made to their codebase
\ No newline at end of file
diff --git a/packages/coding-agent/src/prompts/tools/exit-plan-mode.md b/packages/coding-agent/src/prompts/tools/exit-plan-mode.md
new file mode 100644
index 000000000..63af53d82
--- /dev/null
+++ b/packages/coding-agent/src/prompts/tools/exit-plan-mode.md
@@ -0,0 +1,23 @@
+Use this tool when you are in plan mode and have finished writing your plan to the plan file and are ready for user approval.
+
+## How This Tool Works
+- You should have already written your plan to the plan file specified in the plan mode system message
+- This tool does NOT take the plan content as a parameter - it will read the plan from the file you wrote
+- This tool simply signals that you're done planning and ready for the user to review and approve
+- The user will see the contents of your plan file when they review it
+
+## When to Use This Tool
+
+IMPORTANT: Only use this tool when the task requires planning the implementation steps of a task that requires writing code. For research tasks where you're gathering information, searching files, reading files or in general trying to understand the codebase - do NOT use this tool.
+
+## Before Using This Tool
+
+Ensure your plan is complete and unambiguous:
+- If you have unresolved questions about requirements or approach, use ask first
+- Once your plan is finalized, use this tool to request approval
+ Important: Do NOT use ask to ask "Is this plan okay?" or "Should I proceed?" - that's exactly what this tool does.
+
+## Examples
+1. Initial task: "Search for and understand the implementation of vim mode in the codebase" - Do not use exit_plan_mode because you are not planning implementation steps.
+2. Initial task: "Help me implement yank mode for vim" - Use exit_plan_mode after you have finished planning the implementation steps of the task.
+3. Initial task: "Add a new feature to handle user authentication" - If unsure about auth method (OAuth, JWT, etc.), use ask first, then use exit_plan_mode after clarifying the approach.
\ No newline at end of file
diff --git a/packages/coding-agent/src/prompts/tools/fetch.md b/packages/coding-agent/src/prompts/tools/fetch.md
index 837aceaad..8415bbec1 100644
--- a/packages/coding-agent/src/prompts/tools/fetch.md
+++ b/packages/coding-agent/src/prompts/tools/fetch.md
@@ -13,4 +13,4 @@ Retrieves content from a URL and returns it in a clean, readable format.
Returns processed, readable content extracted from the URL. HTML is transformed to remove navigation, ads, and boilerplate. PDF and DOCX files are converted to text. JSON endpoints return formatted JSON. With `raw: true`, returns untransformed HTML.
-
+
\ No newline at end of file
diff --git a/packages/coding-agent/src/prompts/tools/find.md b/packages/coding-agent/src/prompts/tools/find.md
index 63471394d..e82ed58b5 100644
--- a/packages/coding-agent/src/prompts/tools/find.md
+++ b/packages/coding-agent/src/prompts/tools/find.md
@@ -13,4 +13,4 @@ Matching file paths sorted by modification time (most recent first). Results tru
Open-ended searches requiring multiple rounds of globbing and grepping — use Task tool instead.
-
+
\ No newline at end of file
diff --git a/packages/coding-agent/src/prompts/tools/gemini-image.md b/packages/coding-agent/src/prompts/tools/gemini-image.md
index fbb93e403..105860463 100644
--- a/packages/coding-agent/src/prompts/tools/gemini-image.md
+++ b/packages/coding-agent/src/prompts/tools/gemini-image.md
@@ -20,4 +20,4 @@ Returns the generated image saved to disk. The response includes the file path w
- For iteration: use `changes` to make targeted adjustments rather than regenerating from scratch
- For text: add "sharp, legible, correctly spelled" for important text; keep text short
- For diagrams: include "scientifically accurate" in style and provide facts explicitly
-
+
\ No newline at end of file
diff --git a/packages/coding-agent/src/prompts/tools/grep.md b/packages/coding-agent/src/prompts/tools/grep.md
index 97ea617b8..17b8efd5e 100644
--- a/packages/coding-agent/src/prompts/tools/grep.md
+++ b/packages/coding-agent/src/prompts/tools/grep.md
@@ -25,4 +25,4 @@ For `files_with_matches` and `count` modes, use `head_limit` to truncate results
- Open-ended searches requiring multiple rounds—use Task tool with explore subagent instead
-
+
\ No newline at end of file
diff --git a/packages/coding-agent/src/prompts/tools/lsp.md b/packages/coding-agent/src/prompts/tools/lsp.md
index 343be8303..a21170d77 100644
--- a/packages/coding-agent/src/prompts/tools/lsp.md
+++ b/packages/coding-agent/src/prompts/tools/lsp.md
@@ -32,4 +32,4 @@ Returns vary by operation:
- Requires a running LSP server for the target language
- Some operations require the file to be saved to disk
- `workspace_diagnostics` may be slow on large projects
-
+
\ No newline at end of file
diff --git a/packages/coding-agent/src/prompts/tools/patch.md b/packages/coding-agent/src/prompts/tools/patch.md
index 93e453d97..dc1b6568f 100644
--- a/packages/coding-agent/src/prompts/tools/patch.md
+++ b/packages/coding-agent/src/prompts/tools/patch.md
@@ -6,7 +6,6 @@ Performs patch operations on a file given a diff. Primary tool for modifying exi
**Hunk Headers:**
- `@@` — bare header when context lines are already unique
- `@@ $ANCHOR` — anchor must be copied verbatim from the file (full line or unique substring)
-
**Anchor Selection Algorithm:**
1. If surrounding context lines are already unique, use bare `@@`
2. Otherwise choose a highly specific anchor copied from the file:
@@ -15,7 +14,6 @@ Performs patch operations on a file given a diff. Primary tool for modifying exi
- unique string literal / error message
- config key with uncommon name
3. If "Found multiple matches" error: add more context lines, use multiple hunks with separate anchors, or use a longer anchor substring
-
**Context Lines:**
- Include enough ` `-prefixed lines to make match unique (usually 2–8 total)
- Must exist in the file exactly as written (preserve indentation/trailing spaces)
@@ -71,4 +69,4 @@ edit {"path":"obsolete.txt","op":"delete"}
- Generic anchors: `import`, `export`, `describe`, `function`, `const`
- Anchor comments: `line 207`, `top of file`, `near imports`, `...`
- Editing without reading the file first (causes stale context errors)
-
+
\ No newline at end of file
diff --git a/packages/coding-agent/src/prompts/tools/python.md b/packages/coding-agent/src/prompts/tools/python.md
index 742659a7b..a99885243 100644
--- a/packages/coding-agent/src/prompts/tools/python.md
+++ b/packages/coding-agent/src/prompts/tools/python.md
@@ -4,13 +4,11 @@ Executes Python cells sequentially in a persistent IPython kernel.
The kernel persists between calls and between cells. **Imports, variables, and functions survive.** Use this.
-
**Work incrementally:**
- One logical step per cell (imports, define a function, test it, use it)
- Pass multiple small cells in one call—they execute sequentially
- Define small functions you can reuse and debug individually
- Put explanations in the assistant message or cell title, **not** inside code
-
**When something fails:**
- The error tells you which cell failed (e.g., "Cell 3 failed")
- Earlier cells already ran—their state persists in the kernel
@@ -24,6 +22,7 @@ All helpers auto-print results and return values for chaining.
{{#if categories.length}}
{{#each categories}}
### {{name}}
+
```
{{#each functions}}
{{name}}{{signature}}
@@ -45,7 +44,6 @@ The user sees output like a Jupyter notebook—rich displays are fully rendered:
- `display(HTML(...))` → rendered HTML
- `display(Markdown(...))` → formatted markdown
- `plt.show()` → inline figures
-
**You will see object repr** (e.g., ``) **but the user sees the rendered output.** Trust that `display()` calls work correctly—do not assume the user sees only the repr.
@@ -108,4 +106,4 @@ subprocess.run(["bun", "run", "check"], ...)
- Re-importing modules you already imported
- Rewriting working cells when only one part failed
- Large functions that are hard to debug piece by piece
-
+
\ No newline at end of file
diff --git a/packages/coding-agent/src/prompts/tools/read.md b/packages/coding-agent/src/prompts/tools/read.md
index 8935cc125..562cb7557 100644
--- a/packages/coding-agent/src/prompts/tools/read.md
+++ b/packages/coding-agent/src/prompts/tools/read.md
@@ -23,4 +23,4 @@ Reads files from the local filesystem or internal URLs.
- PDFs: returns extracted text
- Missing files: returns closest filename matches for correction
- Internal URLs: returns resolved content with pagination support
-
+
\ No newline at end of file
diff --git a/packages/coding-agent/src/prompts/tools/replace.md b/packages/coding-agent/src/prompts/tools/replace.md
index b6d7c7514..dab5a3f11 100644
--- a/packages/coding-agent/src/prompts/tools/replace.md
+++ b/packages/coding-agent/src/prompts/tools/replace.md
@@ -17,22 +17,22 @@ Returns success/failure status. On success, the file is modified in place with t
- You must read the file at least once in the conversation before editing. The tool will error if you attempt an edit without reading the file first.
-
-Replace is for content-addressed changes—you identify *what* to change by its text.
+
+Replace is for content-addressed changes—you identify \_what* to change by its text.
For position-addressed or pattern-addressed changes, bash is more efficient:
-| Operation | Command |
-|-----------|---------|
-| Append to file | `cat >> file <<'EOF'`...`EOF` |
-| Prepend to file | `{ cat - file; } <<'EOF' > tmp && mv tmp file` |
-| Delete lines N-M | `sed -i 'N,Md' file` |
-| Insert after line N | `sed -i 'Na\text' file` |
-| Regex replace | `sd 'pattern' 'replacement' file` |
-| Bulk replace across files | `sd 'pattern' 'replacement' **/*.ts` |
-| Copy lines N-M to another file | `sed -n 'N,Mp' src >> dest` |
-| Move lines N-M to another file | `sed -n 'N,Mp' src >> dest && sed -i 'N,Md' src` |
+|Operation|Command|
+|---|---|
+|Append to file|`cat >> file <<'EOF'`...`EOF`|
+|Prepend to file|`{ cat - file; } <<'EOF' > tmp && mv tmp file`|
+|Delete lines N-M|`sed -i 'N,Md' file`|
+|Insert after line N|`sed -i 'Na\text' file`|
+|Regex replace|`sd 'pattern' 'replacement' file`|
+|Bulk replace across files|`sd 'pattern' 'replacement' **/*.ts`|
+|Copy lines N-M to another file|`sed -n 'N,Mp' src >> dest`|
+|Move lines N-M to another file|`sed -n 'N,Mp' src >> dest && sed -i 'N,Md' src`|
-Use Replace when the *content itself* identifies the location.
-Use bash when *position* or *pattern* identifies what to change.
-
+Use Replace when the _content itself_ identifies the location.
+Use bash when _position_ or _pattern_ identifies what to change.
+
\ No newline at end of file
diff --git a/packages/coding-agent/src/prompts/tools/ssh.md b/packages/coding-agent/src/prompts/tools/ssh.md
index 83be6e85e..7deeb4865 100644
--- a/packages/coding-agent/src/prompts/tools/ssh.md
+++ b/packages/coding-agent/src/prompts/tools/ssh.md
@@ -13,18 +13,15 @@ Execute commands on remote SSH hosts.
- Files: `ls`, `cat`, `head`, `tail`, `grep`, `find`
- System: `ps`, `top`, `df`, `uname`, `free` (Linux), `df`, `uname`, `top` (macOS)
- Navigation: `cd`, `pwd`
-
**windows/bash, windows/sh** — Windows with Unix compatibility layer (WSL, Cygwin, Git Bash):
- Files: `ls`, `cat`, `head`, `tail`, `grep`, `find`
- System: `ps`, `top`, `df`, `uname`
- Navigation: `cd`, `pwd`
- Note: These are Windows hosts but use Unix commands
-
**windows/powershell** — Native Windows PowerShell:
- Files: `Get-ChildItem`, `Get-Content`, `Select-String`
- System: `Get-Process`, `Get-ComputerInfo`
- Navigation: `Set-Location`, `Get-Location`
-
**windows/cmd** — Native Windows Command Prompt:
- Files: `dir`, `type`, `findstr`, `where`
- System: `tasklist`, `systeminfo`
@@ -64,4 +61,4 @@ Note: Windows host with WSL — use Unix commands
Task: Get system info on host "macbook"
Host: macbook (10.0.0.20) | macos/zsh
Command: `uname -a && sw_vers`
-
+
\ No newline at end of file
diff --git a/packages/coding-agent/src/prompts/tools/task.md b/packages/coding-agent/src/prompts/tools/task.md
index b7f102f5e..3036b92b0 100644
--- a/packages/coding-agent/src/prompts/tools/task.md
+++ b/packages/coding-agent/src/prompts/tools/task.md
@@ -57,9 +57,7 @@ Results are keyed by task `id` (e.g., "AuthProvider", "AuthApi").
3. The `task` string you provide
If you discussed requirements, plans, schemas, or decisions with the user, you MUST include that information in `context`. Subagents cannot see prior messages—they start fresh with only what you explicitly pass them.
-
**Never call Task multiple times in parallel.** Use a single Task call with multiple entries in the `tasks` array. Parallel Task calls waste resources and bypass coordination.
-
**For code changes, subagents write files directly.** Never ask an agent to "return the changes" for you to apply—they have Edit and Write tools. Their context window holds the work; asking them to report back wastes it.
@@ -91,4 +89,4 @@ assistant: Uses the Task tool:
- Searching for a specific class/function definition → Use Grep tool instead
- Searching code within 2-3 specific files → Use Read tool instead
- Tasks unrelated to the agent descriptions above
-
+
\ No newline at end of file
diff --git a/packages/coding-agent/src/prompts/tools/todo-write.md b/packages/coding-agent/src/prompts/tools/todo-write.md
index 0e1ebebc4..dc3ff0b1b 100644
--- a/packages/coding-agent/src/prompts/tools/todo-write.md
+++ b/packages/coding-agent/src/prompts/tools/todo-write.md
@@ -18,24 +18,21 @@ Use this tool proactively in these scenarios:
- pending: Task not yet started
- in_progress: Currently working on (limit to ONE task at a time)
- completed: Task finished successfully
-
2. **Task Management**:
- - Update task status in real-time as you work
- - Mark tasks complete IMMEDIATELY after finishing (don't batch completions)
- - Exactly ONE task must be in_progress at any time (not less, not more)
- - Complete current tasks before starting new ones
- - Remove tasks that are no longer relevant from the list entirely
-
+ - Update task status in real-time as you work
+ - Mark tasks complete IMMEDIATELY after finishing (don't batch completions)
+ - Exactly ONE task must be in_progress at any time (not less, not more)
+ - Complete current tasks before starting new ones
+ - Remove tasks that are no longer relevant from the list entirely
3. **Task Completion Requirements**:
- - ONLY mark a task as completed when you have FULLY accomplished it
- - If you encounter errors, blockers, or cannot finish, keep the task as in_progress
- - When blocked, create a new task describing what needs to be resolved
- - Never mark a task as completed if:
- - Tests are failing
- - Implementation is partial
- - You encountered unresolved errors
+ - ONLY mark a task as completed when you have FULLY accomplished it
+ - If you encounter errors, blockers, or cannot finish, keep the task as in_progress
+ - When blocked, create a new task describing what needs to be resolved
+ - Never mark a task as completed if:
+ - Tests are failing
+ - Implementation is partial
+ - You encountered unresolved errors
- You couldn't find necessary files or dependencies
-
4. **Task Breakdown**:
- Create specific, actionable items
- Break complex tasks into smaller, manageable steps
@@ -73,4 +70,4 @@ Skip using this tool when:
4. The task is purely conversational or informational
If there is only one trivial task to do, just do it directly.
-
+
\ No newline at end of file
diff --git a/packages/coding-agent/src/prompts/tools/web-search.md b/packages/coding-agent/src/prompts/tools/web-search.md
index a99e41193..1b68cd722 100644
--- a/packages/coding-agent/src/prompts/tools/web-search.md
+++ b/packages/coding-agent/src/prompts/tools/web-search.md
@@ -16,4 +16,4 @@ Returns search results formatted as blocks with:
Searches are performed automatically within a single API call—no pagination or follow-up requests needed.
-
+
\ No newline at end of file
diff --git a/packages/coding-agent/src/prompts/tools/write.md b/packages/coding-agent/src/prompts/tools/write.md
index 8cf4738f7..583959d87 100644
--- a/packages/coding-agent/src/prompts/tools/write.md
+++ b/packages/coding-agent/src/prompts/tools/write.md
@@ -18,4 +18,4 @@ Confirmation of file creation/write with path. When LSP is available, content ma
- Include emojis only when explicitly requested
-
+
\ No newline at end of file
diff --git a/packages/coding-agent/src/sdk.ts b/packages/coding-agent/src/sdk.ts
index be25735cf..728eb0e6f 100644
--- a/packages/coding-agent/src/sdk.ts
+++ b/packages/coding-agent/src/sdk.ts
@@ -50,6 +50,7 @@ import {
loadCustomCommands as loadCustomCommandsInternal,
} from "./extensibility/custom-commands";
import type { CustomTool, CustomToolContext, CustomToolSessionEvent } from "./extensibility/custom-tools/types";
+import { CustomToolAdapter } from "./extensibility/custom-tools/wrapper";
import {
discoverAndLoadExtensions,
type ExtensionContext,
@@ -69,6 +70,7 @@ import {
AgentProtocolHandler,
ArtifactProtocolHandler,
InternalUrlRouter,
+ PlanProtocolHandler,
RuleProtocolHandler,
SkillProtocolHandler,
} from "./internal-urls";
@@ -743,12 +745,14 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
outputSchema: options.outputSchema,
requireCompleteTool: options.requireCompleteTool,
getSessionFile: () => sessionManager.getSessionFile() ?? null,
+ getSessionId: () => sessionManager.getSessionId?.() ?? null,
getSessionSpawns: () => options.spawns ?? "*",
getModelString: () => (hasExplicitModel && model ? formatModelString(model) : undefined),
getActiveModelString: () => {
const activeModel = agent?.state.model;
return activeModel ? formatModelString(activeModel) : undefined;
},
+ getPlanModeState: () => session.getPlanModeState(),
settings: settingsManager,
settingsManager,
authStorage,
@@ -763,6 +767,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
};
internalRouter.register(new AgentProtocolHandler({ getArtifactsDir }));
internalRouter.register(new ArtifactProtocolHandler({ getArtifactsDir }));
+ internalRouter.register(
+ new PlanProtocolHandler({
+ getPlansDirectory: settingsManager.getPlansDirectory.bind(settingsManager),
+ cwd,
+ }),
+ );
internalRouter.register(
new SkillProtocolHandler({
getSkills: () => skills,
@@ -924,14 +934,33 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
const toolContextStore = new ToolContextStore(getSessionContext);
const registeredTools = extensionRunner?.getAllRegisteredTools() ?? [];
- const allCustomTools = [
- ...registeredTools,
- ...(options.customTools?.map(tool => {
- const definition = isCustomTool(tool) ? customToolToDefinition(tool) : tool;
- return { definition, extensionPath: "" };
- }) ?? []),
- ];
- const wrappedExtensionTools = extensionRunner ? wrapRegisteredTools(allCustomTools, extensionRunner) : [];
+ let wrappedExtensionTools: AgentTool[];
+
+ if (extensionRunner) {
+ // With extension runner: convert CustomTools to ToolDefinitions and wrap all together
+ const allCustomTools = [
+ ...registeredTools,
+ ...(options.customTools?.map(tool => {
+ const definition = isCustomTool(tool) ? customToolToDefinition(tool) : tool;
+ return { definition, extensionPath: "" };
+ }) ?? []),
+ ];
+ wrappedExtensionTools = wrapRegisteredTools(allCustomTools, extensionRunner);
+ } else {
+ // Without extension runner: wrap CustomTools directly with CustomToolAdapter
+ // ToolDefinition items require ExtensionContext and cannot be used without a runner
+ const customToolContext = (): CustomToolContext => ({
+ sessionManager,
+ modelRegistry,
+ model: agent?.state.model,
+ isIdle: () => !session?.isStreaming,
+ hasQueuedMessages: () => (session?.queuedMessageCount ?? 0) > 0,
+ abort: () => session?.abort(),
+ });
+ wrappedExtensionTools = (options.customTools ?? [])
+ .filter(isCustomTool)
+ .map(tool => CustomToolAdapter.wrap(tool, customToolContext) as AgentTool);
+ }
// All built-in tools are active (conditional tools like git/ask return null from factory if disabled)
const toolRegistry = new Map();
@@ -989,7 +1018,25 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
return options.systemPrompt(defaultPrompt);
};
- const systemPrompt = await rebuildSystemPrompt(Array.from(toolRegistry.keys()), toolRegistry);
+ const toolNamesFromRegistry = Array.from(toolRegistry.keys());
+ const requestedToolNames = options.toolNames ?? toolNamesFromRegistry;
+ const normalizedRequested = requestedToolNames.filter(name => toolRegistry.has(name));
+ const includeExitPlanMode = options.toolNames?.includes("exit_plan_mode") ?? false;
+ const initialToolNames = includeExitPlanMode
+ ? normalizedRequested
+ : normalizedRequested.filter(name => name !== "exit_plan_mode");
+
+ // Custom tools are always included regardless of toolNames filter
+ if (options.customTools) {
+ const customToolNames = options.customTools.map(t => (isCustomTool(t) ? t.name : t.name));
+ for (const name of customToolNames) {
+ if (toolRegistry.has(name) && !initialToolNames.includes(name)) {
+ initialToolNames.push(name);
+ }
+ }
+ }
+
+ const systemPrompt = await rebuildSystemPrompt(initialToolNames, toolRegistry);
time("buildSystemPrompt");
const promptTemplates = options.promptTemplates ?? (await discoverPromptTemplates(cwd, agentDir));
@@ -1038,12 +1085,16 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
toolContextStore.setUIContext(uiContext, hasUI);
};
+ const initialTools = initialToolNames
+ .map(name => toolRegistry.get(name))
+ .filter((tool): tool is AgentTool => tool !== undefined);
+
agent = new Agent({
initialState: {
systemPrompt,
model,
thinkingLevel,
- tools: Array.from(toolRegistry.values()),
+ tools: initialTools,
},
convertToLlm: convertToLlmWithBlockImages,
sessionId: sessionManager.getSessionId(),
diff --git a/packages/coding-agent/src/session/agent-session.ts b/packages/coding-agent/src/session/agent-session.ts
index 91ea557e1..589c71e12 100644
--- a/packages/coding-agent/src/session/agent-session.ts
+++ b/packages/coding-agent/src/session/agent-session.ts
@@ -14,6 +14,7 @@
*/
import * as fs from "node:fs";
+
import type { Agent, AgentEvent, AgentMessage, AgentState, AgentTool, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type {
AssistantMessage,
@@ -26,6 +27,7 @@ import type {
UsageReport,
} from "@oh-my-pi/pi-ai";
import { isContextOverflow, modelsAreEqual, supportsXhigh } from "@oh-my-pi/pi-ai";
+import { resolvePlanUrlToPath } from "@oh-my-pi/pi-coding-agent/internal-urls";
import { abortableSleep, isEnoent, logger } from "@oh-my-pi/pi-utils";
import { YAML } from "bun";
import type { Rule } from "../capability/rule";
@@ -62,6 +64,9 @@ import { expandSlashCommand, type FileSlashCommand } from "../extensibility/slas
import { executePython as executePythonCommand, type PythonResult } from "../ipy/executor";
import { theme } from "../modes/theme/theme";
import { normalizeDiff, normalizeToLF, ParseError, previewPatch, stripBom } from "../patch";
+import type { PlanModeState } from "../plan-mode/state";
+import planModeActivePrompt from "../prompts/system/plan-mode-active.md" with { type: "text" };
+import planModeReferencePrompt from "../prompts/system/plan-mode-reference.md" with { type: "text" };
import ttsrInterruptTemplate from "../prompts/system/ttsr-interrupt.md" with { type: "text" };
import { closeAllConnections } from "../ssh/connection-manager";
import { unmountAll } from "../ssh/sshfs-mount";
@@ -188,6 +193,11 @@ export interface SessionStats {
cost: number;
}
+/** Result from handoff() */
+export interface HandoffResult {
+ document: string;
+}
+
/** Internal marker for hook messages queued through the agent loop */
// ============================================================================
// Constants
@@ -255,6 +265,8 @@ export class AgentSession {
private _followUpMessages: string[] = [];
/** Messages queued to be included with the next user prompt as context ("asides"). */
private _pendingNextTurnMessages: CustomMessage[] = [];
+ private _planModeState: PlanModeState | undefined;
+ private _planReferenceSent = false;
// Compaction state
private _compactionAbortController: AbortController | undefined = undefined;
@@ -263,6 +275,9 @@ export class AgentSession {
// Branch summarization state
private _branchSummaryAbortController: AbortController | undefined = undefined;
+ // Handoff state
+ private _handoffAbortController: AbortController | undefined = undefined;
+
// Retry state
private _retryAbortController: AbortController | undefined = undefined;
private _retryAttempt = 0;
@@ -970,6 +985,25 @@ export class AgentSession {
}
/** Prompt templates */
+ getPlanModeState(): PlanModeState | undefined {
+ return this._planModeState;
+ }
+
+ setPlanModeState(state: PlanModeState | undefined): void {
+ this._planModeState = state;
+ if (state?.enabled) {
+ this._planReferenceSent = false;
+ }
+ }
+
+ markPlanReferenceSent(): void {
+ this._planReferenceSent = true;
+ }
+
+ resolveRoleModel(role: string): Model | undefined {
+ return this._resolveRoleModel(role, this._modelRegistry.getAvailable(), this.model);
+ }
+
get promptTemplates(): ReadonlyArray {
return this._promptTemplates;
}
@@ -983,6 +1017,95 @@ export class AgentSession {
// Prompting
// =========================================================================
+ /**
+ * Build a plan mode message.
+ * Returns null if plan mode is not enabled.
+ * @returns The plan mode message, or null if plan mode is not enabled.
+ */
+ private async _buildPlanReferenceMessage(): Promise {
+ if (this._planModeState?.enabled) return null;
+ if (this._planReferenceSent) return null;
+
+ const planFilePath = `plan://${this.sessionManager.getSessionId()}/plan.md`;
+ const resolvedPlanPath = resolvePlanUrlToPath(planFilePath, {
+ getPlansDirectory: this.settingsManager.getPlansDirectory.bind(this.settingsManager),
+ cwd: this.sessionManager.getCwd(),
+ });
+ let planContent: string;
+ try {
+ planContent = await fs.promises.readFile(resolvedPlanPath, "utf-8");
+ } catch (error) {
+ if (isEnoent(error)) {
+ return null;
+ }
+ throw error;
+ }
+
+ const content = renderPromptTemplate(planModeReferencePrompt, {
+ planFilePath,
+ planContent,
+ });
+
+ this._planReferenceSent = true;
+
+ return {
+ role: "custom",
+ customType: "plan-mode-reference",
+ content,
+ display: false,
+ timestamp: Date.now(),
+ };
+ }
+
+ private async _buildPlanModeMessage(): Promise {
+ const state = this._planModeState;
+ if (!state?.enabled) return null;
+ const sessionPlanUrl = `plan://${this.sessionManager.getSessionId()}/plan.md`;
+ const resolvedPlanPath = state.planFilePath.startsWith("plan://")
+ ? resolvePlanUrlToPath(state.planFilePath, {
+ getPlansDirectory: this.settingsManager.getPlansDirectory.bind(this.settingsManager),
+ cwd: this.sessionManager.getCwd(),
+ })
+ : resolveToCwd(state.planFilePath, this.sessionManager.getCwd());
+ const resolvedSessionPlan = resolvePlanUrlToPath(sessionPlanUrl, {
+ getPlansDirectory: this.settingsManager.getPlansDirectory.bind(this.settingsManager),
+ cwd: this.sessionManager.getCwd(),
+ });
+ const displayPlanPath =
+ state.planFilePath.startsWith("plan://") || resolvedPlanPath !== resolvedSessionPlan
+ ? state.planFilePath
+ : sessionPlanUrl;
+
+ let planExists = false;
+ try {
+ const stat = await fs.promises.stat(resolvedPlanPath);
+ planExists = stat.isFile();
+ } catch (error) {
+ if (!isEnoent(error)) {
+ throw error;
+ }
+ }
+
+ const content = renderPromptTemplate(planModeActivePrompt, {
+ planFilePath: displayPlanPath,
+ planExists,
+ askToolName: "ask",
+ writeToolName: "write",
+ editToolName: "edit",
+ exitToolName: "exit_plan_mode",
+ reentry: state.reentry ?? false,
+ iterative: state.workflow === "iterative",
+ });
+
+ return {
+ role: "custom",
+ customType: "plan-mode-context",
+ content,
+ display: false,
+ timestamp: Date.now(),
+ };
+ }
+
/**
* Send a prompt to the agent.
* - Handles extension commands (registered via pi.registerCommand) immediately, even during streaming
@@ -1069,6 +1192,14 @@ export class AgentSession {
// Build messages array (custom messages if any, then user message)
const messages: AgentMessage[] = [];
+ const planReferenceMessage = await this._buildPlanReferenceMessage?.();
+ if (planReferenceMessage) {
+ messages.push(planReferenceMessage);
+ }
+ const planModeMessage = await this._buildPlanModeMessage();
+ if (planModeMessage) {
+ messages.push(planModeMessage);
+ }
// Add user message
const userContent: (TextContent | ImageContent)[] = [{ type: "text", text: expandedText }];
@@ -1512,6 +1643,7 @@ export class AgentSession {
this._followUpMessages = [];
this._pendingNextTurnMessages = [];
this._todoReminderCount = 0;
+ this._planReferenceSent = false;
this._reconnectToAgent();
// Emit session_switch event with reason "new" to hooks
@@ -1945,6 +2077,141 @@ export class AgentSession {
this._branchSummaryAbortController?.abort();
}
+ /**
+ * Cancel in-progress handoff generation.
+ */
+ abortHandoff(): void {
+ this._handoffAbortController?.abort();
+ }
+
+ /**
+ * Check if handoff generation is in progress.
+ */
+ get isGeneratingHandoff(): boolean {
+ return this._handoffAbortController !== undefined;
+ }
+
+ /**
+ * Generate a handoff document by asking the agent, then start a new session with it.
+ *
+ * This prompts the current agent to write a comprehensive handoff document,
+ * waits for completion, then starts a fresh session with the handoff as context.
+ *
+ * @param customInstructions Optional focus for the handoff document
+ * @returns The handoff document text, or undefined if cancelled/failed
+ */
+ async handoff(customInstructions?: string): Promise {
+ const entries = this.sessionManager.getBranch();
+ const messageCount = entries.filter(e => e.type === "message").length;
+
+ if (messageCount < 2) {
+ throw new Error("Nothing to hand off (no messages yet)");
+ }
+
+ this._handoffAbortController = new AbortController();
+
+ // Build the handoff prompt
+ let handoffPrompt = `Write a comprehensive handoff document that will allow another instance of yourself to seamlessly continue this work. The document should capture everything needed to resume without access to this conversation.
+
+Use this format:
+
+## Goal
+[What the user is trying to accomplish]
+
+## Constraints & Preferences
+- [Any constraints, preferences, or requirements mentioned]
+
+## Progress
+### Done
+- [x] [Completed tasks with specifics]
+
+### In Progress
+- [ ] [Current work if any]
+
+### Pending
+- [ ] [Tasks mentioned but not started]
+
+## Key Decisions
+- **[Decision]**: [Rationale]
+
+## Critical Context
+- [Code snippets, file paths, error messages, or data essential to continue]
+- [Repository state if relevant]
+
+## Next Steps
+1. [What should happen next]
+
+Be thorough - include exact file paths, function names, error messages, and technical details. Output ONLY the handoff document, no other text.`;
+
+ if (customInstructions) {
+ handoffPrompt += `\n\nAdditional focus: ${customInstructions}`;
+ }
+
+ // Create a promise that resolves when the agent completes
+ let handoffText: string | undefined;
+ const completionPromise = new Promise((resolve, reject) => {
+ const unsubscribe = this.subscribe(event => {
+ if (this._handoffAbortController?.signal.aborted) {
+ unsubscribe();
+ reject(new Error("Handoff cancelled"));
+ return;
+ }
+
+ if (event.type === "agent_end") {
+ unsubscribe();
+ // Extract text from the last assistant message
+ const messages = this.agent.state.messages;
+ for (let i = messages.length - 1; i >= 0; i--) {
+ const msg = messages[i];
+ if (msg.role === "assistant") {
+ const content = (msg as AssistantMessage).content;
+ const textParts = content
+ .filter((c): c is { type: "text"; text: string } => c.type === "text")
+ .map(c => c.text);
+ if (textParts.length > 0) {
+ handoffText = textParts.join("\n");
+ break;
+ }
+ }
+ }
+ resolve();
+ }
+ });
+ });
+
+ try {
+ // Send the prompt and wait for completion
+ await this.prompt(handoffPrompt, { expandPromptTemplates: false });
+ await completionPromise;
+
+ if (!handoffText || this._handoffAbortController.signal.aborted) {
+ return undefined;
+ }
+
+ // Start a new session
+ await this.sessionManager.flush();
+ this.sessionManager.newSession();
+ this.agent.reset();
+ this.agent.sessionId = this.sessionManager.getSessionId();
+ this._steeringMessages = [];
+ this._followUpMessages = [];
+ this._pendingNextTurnMessages = [];
+ this._todoReminderCount = 0;
+
+ // Inject the handoff document as a custom message
+ const handoffContent = `\n${handoffText}\n\n\nThe above is a handoff document from a previous session. Use this context to continue the work seamlessly.`;
+ this.sessionManager.appendCustomMessageEntry("handoff", handoffContent, true);
+
+ // Rebuild agent messages from session
+ const sessionContext = this.sessionManager.buildSessionContext();
+ this.agent.replaceMessages(sessionContext.messages);
+
+ return { document: handoffText };
+ } finally {
+ this._handoffAbortController = undefined;
+ }
+ }
+
/**
* Check if compaction is needed and run it.
* Called after agent_end and before prompt submission.
diff --git a/packages/coding-agent/src/task/executor.ts b/packages/coding-agent/src/task/executor.ts
index ad10af781..370170620 100644
--- a/packages/coding-agent/src/task/executor.ts
+++ b/packages/coding-agent/src/task/executor.ts
@@ -81,6 +81,7 @@ export interface ExecutorOptions {
modelRegistry?: ModelRegistry;
settingsManager?: {
serialize: () => import("@oh-my-pi/pi-coding-agent/config/settings-manager").Settings;
+ getPlansDirectory: (cwd?: string) => string;
getPythonToolMode?: () => "ipy-only" | "bash-only" | "both";
getPythonKernelMode?: () => "session" | "per-call";
getPythonSharedGateway?: () => boolean;
diff --git a/packages/coding-agent/src/task/index.ts b/packages/coding-agent/src/task/index.ts
index 47e33a6d8..b62cc7b0c 100644
--- a/packages/coding-agent/src/task/index.ts
+++ b/packages/coding-agent/src/task/index.ts
@@ -17,6 +17,9 @@ import * as os from "node:os";
import path from "node:path";
import type { AgentTool, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
import type { Usage } from "@oh-my-pi/pi-ai";
+import planModeSubagentPrompt from "@oh-my-pi/pi-coding-agent/prompts/system/plan-mode-subagent.md" with {
+ type: "text",
+};
import { $ } from "bun";
import { nanoid } from "nanoid";
import type { ToolSession } from "..";
@@ -192,14 +195,25 @@ export class TaskTool implements AgentTool params > inherited from parent session
- const schemaOverridden = outputSchema !== undefined && agent.output !== undefined;
- const effectiveOutputSchema = agent.output ?? outputSchema ?? this.session.outputSchema;
+ const schemaOverridden = outputSchema !== undefined && effectiveAgent.output !== undefined;
+ const effectiveOutputSchema = effectiveAgent.output ?? outputSchema ?? this.session.outputSchema;
// Handle empty or missing tasks
if (!params.tasks || params.tasks.length === 0) {
diff --git a/packages/coding-agent/src/tools/enter-plan-mode.ts b/packages/coding-agent/src/tools/enter-plan-mode.ts
new file mode 100644
index 000000000..80d41a53d
--- /dev/null
+++ b/packages/coding-agent/src/tools/enter-plan-mode.ts
@@ -0,0 +1,76 @@
+import * as fs from "node:fs/promises";
+import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
+import { renderPromptTemplate } from "@oh-my-pi/pi-coding-agent/config/prompt-templates";
+import { resolvePlanUrlToPath } from "@oh-my-pi/pi-coding-agent/internal-urls";
+import enterPlanModeDescription from "@oh-my-pi/pi-coding-agent/prompts/tools/enter-plan-mode.md" with { type: "text" };
+import type { ToolSession } from "@oh-my-pi/pi-coding-agent/tools";
+import { ToolError } from "@oh-my-pi/pi-coding-agent/tools/tool-errors";
+import { isEnoent } from "@oh-my-pi/pi-utils";
+import { Type } from "@sinclair/typebox";
+
+const enterPlanModeSchema = Type.Object({
+ workflow: Type.Optional(Type.Union([Type.Literal("parallel"), Type.Literal("iterative")])),
+});
+
+export interface EnterPlanModeDetails {
+ planFilePath: string;
+ planExists: boolean;
+ workflow?: "parallel" | "iterative";
+}
+
+export class EnterPlanModeTool implements AgentTool {
+ public readonly name = "enter_plan_mode";
+ public readonly label = "EnterPlanMode";
+ public readonly description: string;
+ public readonly parameters = enterPlanModeSchema;
+
+ private readonly session: ToolSession;
+
+ constructor(session: ToolSession) {
+ this.session = session;
+ this.description = renderPromptTemplate(enterPlanModeDescription);
+ }
+
+ public async execute(
+ _toolCallId: string,
+ params: { workflow?: "parallel" | "iterative" },
+ _signal?: AbortSignal,
+ _onUpdate?: AgentToolUpdateCallback,
+ _context?: AgentToolContext,
+ ): Promise> {
+ const state = this.session.getPlanModeState?.();
+ if (state?.enabled) {
+ throw new ToolError("Plan mode is already active.");
+ }
+
+ const sessionId = this.session.getSessionId?.();
+ if (!sessionId) {
+ throw new ToolError("Plan mode requires an active session.");
+ }
+
+ const settingsManager = this.session.settingsManager;
+ if (!settingsManager) {
+ throw new ToolError("Settings manager unavailable for plan mode.");
+ }
+
+ const planFilePath = `plan://${sessionId}/plan.md`;
+ const resolvedPlanPath = resolvePlanUrlToPath(planFilePath, {
+ getPlansDirectory: settingsManager.getPlansDirectory.bind(settingsManager),
+ cwd: this.session.cwd,
+ });
+ let planExists = false;
+ try {
+ const stat = await fs.stat(resolvedPlanPath);
+ planExists = stat.isFile();
+ } catch (error) {
+ if (!isEnoent(error)) {
+ throw error;
+ }
+ }
+
+ return {
+ content: [{ type: "text", text: "Plan mode requested." }],
+ details: { planFilePath, planExists, workflow: params.workflow },
+ };
+ }
+}
diff --git a/packages/coding-agent/src/tools/exit-plan-mode.ts b/packages/coding-agent/src/tools/exit-plan-mode.ts
new file mode 100644
index 000000000..be570fa4e
--- /dev/null
+++ b/packages/coding-agent/src/tools/exit-plan-mode.ts
@@ -0,0 +1,62 @@
+import * as fs from "node:fs/promises";
+import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
+import { isEnoent } from "@oh-my-pi/pi-utils";
+import { Type } from "@sinclair/typebox";
+import { renderPromptTemplate } from "../config/prompt-templates";
+import exitPlanModeDescription from "../prompts/tools/exit-plan-mode.md" with { type: "text" };
+import type { ToolSession } from ".";
+import { resolvePlanPath } from "./plan-mode-guard";
+import { ToolError } from "./tool-errors";
+
+const exitPlanModeSchema = Type.Object({});
+
+export interface ExitPlanModeDetails {
+ planFilePath: string;
+ planExists: boolean;
+}
+
+export class ExitPlanModeTool implements AgentTool {
+ public readonly name = "exit_plan_mode";
+ public readonly label = "ExitPlanMode";
+ public readonly description: string;
+ public readonly parameters = exitPlanModeSchema;
+
+ private readonly session: ToolSession;
+
+ constructor(session: ToolSession) {
+ this.session = session;
+ this.description = renderPromptTemplate(exitPlanModeDescription);
+ }
+
+ public async execute(
+ _toolCallId: string,
+ _params: Record,
+ _signal?: AbortSignal,
+ _onUpdate?: AgentToolUpdateCallback,
+ _context?: AgentToolContext,
+ ): Promise> {
+ const state = this.session.getPlanModeState?.();
+ if (!state?.enabled) {
+ throw new ToolError("Plan mode is not active.");
+ }
+
+ const resolvedPlanPath = resolvePlanPath(this.session, state.planFilePath);
+ let planExists = false;
+ try {
+ const stat = await fs.stat(resolvedPlanPath);
+ planExists = stat.isFile();
+ } catch (error) {
+ if (!isEnoent(error)) {
+ throw error;
+ }
+ }
+
+ return {
+ content: [{ type: "text", text: "Plan ready for approval." }],
+ details: {
+ planFilePath: state.planFilePath,
+ planExists,
+ },
+ };
+ }
+}
diff --git a/packages/coding-agent/src/tools/find.ts b/packages/coding-agent/src/tools/find.ts
index ebdefe3df..164058340 100644
--- a/packages/coding-agent/src/tools/find.ts
+++ b/packages/coding-agent/src/tools/find.ts
@@ -127,7 +127,7 @@ export class FindTool implements AgentTool {
params: Static,
signal?: AbortSignal,
_onUpdate?: AgentToolUpdateCallback,
- _context?: AgentToolContext,
+ context?: AgentToolContext,
): Promise> {
const { pattern, path: searchDir, limit, hidden, type } = params;
@@ -196,7 +196,10 @@ export class FindTool implements AgentTool {
}
// Default: use fd
- const fdPath = await ensureTool("fd", true);
+ const fdPath = await ensureTool("fd", {
+ silent: true,
+ notify: message => context?.ui?.notify(message, "info"),
+ });
if (!fdPath) {
throw new ToolError("fd is not available and could not be downloaded");
}
diff --git a/packages/coding-agent/src/tools/grep.ts b/packages/coding-agent/src/tools/grep.ts
index da1302a7a..74a544f66 100644
--- a/packages/coding-agent/src/tools/grep.ts
+++ b/packages/coding-agent/src/tools/grep.ts
@@ -107,21 +107,17 @@ export class GrepTool implements AgentTool {
private readonly session: ToolSession;
private readonly ops: GrepOperations;
- private readonly rgPath: Promise;
-
constructor(session: ToolSession, options?: GrepToolOptions) {
this.session = session;
this.ops = options?.operations ?? defaultGrepOperations;
this.description = renderPromptTemplate(grepDescription);
- this.rgPath = ensureTool("rg", true);
}
/**
* Validates a pattern against ripgrep's regex engine.
* Uses a quick dry-run against /dev/null to check for parse errors.
*/
- private async validateRegexPattern(pattern: string): Promise<{ valid: boolean; error?: string }> {
- const rgPath = await this.rgPath;
+ private async validateRegexPattern(pattern: string, rgPath?: string): Promise<{ valid: boolean; error?: string }> {
if (!rgPath) {
return { valid: true }; // Can't validate, assume valid
}
@@ -146,7 +142,7 @@ export class GrepTool implements AgentTool {
params: GrepParams,
signal?: AbortSignal,
_onUpdate?: AgentToolUpdateCallback,
- _context?: AgentToolContext,
+ toolContext?: AgentToolContext,
): Promise> {
const {
pattern,
@@ -167,19 +163,24 @@ export class GrepTool implements AgentTool {
return untilAborted(signal, async () => {
// Auto-detect invalid regex patterns and switch to literal mode
// This handles cases like "abort(" which would cause ripgrep regex parse errors
+ const rgPath = await ensureTool("rg", {
+ silent: true,
+ notify: message => toolContext?.ui?.notify(message, "info"),
+ });
+
+ if (!rgPath) {
+ throw new ToolError("rg is not available and could not be downloaded");
+ }
+
let useLiteral = literal ?? false;
if (!useLiteral) {
- const validation = await this.validateRegexPattern(pattern);
+ const validation = await this.validateRegexPattern(pattern, rgPath);
if (!validation.valid) {
useLiteral = true;
}
}
- const rgPath = await this.rgPath;
- if (!rgPath) {
- throw new ToolError("ripgrep (rg) is not available and could not be downloaded");
- }
-
+ // rgPath resolved earlier
const searchPath = resolveToCwd(searchDir || ".", this.session.cwd);
const scopePath = (() => {
const relative = nodePath.relative(this.session.cwd, searchPath).replace(/\\/g, "/");
diff --git a/packages/coding-agent/src/tools/index.ts b/packages/coding-agent/src/tools/index.ts
index 2d46d0065..41bfcc5d9 100644
--- a/packages/coding-agent/src/tools/index.ts
+++ b/packages/coding-agent/src/tools/index.ts
@@ -8,6 +8,7 @@ import { getPreludeDocs, warmPythonEnvironment } from "../ipy/executor";
import { checkPythonKernelAvailability } from "../ipy/kernel";
import { LspTool } from "../lsp";
import { EditTool } from "../patch";
+import type { PlanModeState } from "../plan-mode/state";
import type { ArtifactManager } from "../session/artifacts";
import { TaskTool } from "../task";
import type { AgentOutputManager } from "../task/output-manager";
@@ -18,6 +19,8 @@ import { AskTool } from "./ask";
import { BashTool } from "./bash";
import { CalculatorTool } from "./calculator";
import { CompleteTool } from "./complete";
+import { EnterPlanModeTool } from "./enter-plan-mode";
+import { ExitPlanModeTool } from "./exit-plan-mode";
import { FetchTool } from "./fetch";
import { FindTool } from "./find";
import { GrepTool } from "./grep";
@@ -70,6 +73,8 @@ export { AskTool, type AskToolDetails } from "./ask";
export { BashTool, type BashToolDetails, type BashToolOptions } from "./bash";
export { CalculatorTool, type CalculatorToolDetails } from "./calculator";
export { CompleteTool } from "./complete";
+export { type EnterPlanModeDetails, EnterPlanModeTool } from "./enter-plan-mode";
+export { type ExitPlanModeDetails, ExitPlanModeTool } from "./exit-plan-mode";
export { FetchTool, type FetchToolDetails } from "./fetch";
export { type FindOperations, FindTool, type FindToolDetails, type FindToolOptions } from "./find";
export { setPreferredImageProvider } from "./gemini-image";
@@ -126,6 +131,8 @@ export interface ToolSession {
requireCompleteTool?: boolean;
/** Get session file */
getSessionFile: () => string | null;
+ /** Get session ID */
+ getSessionId?: () => string | null;
/** Cached artifact manager (allocated per ToolSession) */
artifactManager?: ArtifactManager;
/** Get artifacts directory for artifact:// URLs and $ARTIFACTS env var */
@@ -147,7 +154,10 @@ export interface ToolSession {
/** Agent output manager for unique agent:// IDs across task invocations */
agentOutputManager?: AgentOutputManager;
/** Settings manager for passing to subagents (avoids SQLite access in workers) */
- settingsManager?: { serialize: () => import("@oh-my-pi/pi-coding-agent/config/settings-manager").Settings };
+ settingsManager?: {
+ serialize: () => import("@oh-my-pi/pi-coding-agent/config/settings-manager").Settings;
+ getPlansDirectory: (cwd?: string) => string;
+ };
/** Settings manager (optional) */
settings?: {
getImageAutoResize(): boolean;
@@ -165,6 +175,8 @@ export interface ToolSession {
getPythonKernelMode?(): "session" | "per-call";
getPythonSharedGateway?(): boolean;
};
+ /** Plan mode state (if active) */
+ getPlanModeState?: () => PlanModeState | undefined;
}
type ToolFactory = (session: ToolSession) => Tool | null | Promise;
@@ -187,11 +199,13 @@ export const BUILTIN_TOOLS: Record = {
fetch: s => new FetchTool(s),
web_search: s => new WebSearchTool(s),
write: s => new WriteTool(s),
+ enter_plan_mode: s => new EnterPlanModeTool(s),
};
export const HIDDEN_TOOLS: Record = {
complete: s => new CompleteTool(s),
report_finding: () => reportFindingTool,
+ exit_plan_mode: s => new ExitPlanModeTool(s),
};
export type ToolName = keyof typeof BUILTIN_TOOLS;
@@ -234,6 +248,9 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
const includeComplete = session.requireCompleteTool === true;
const enableLsp = session.enableLsp ?? true;
const requestedTools = toolNames && toolNames.length > 0 ? [...new Set(toolNames)] : undefined;
+ if (requestedTools && !requestedTools.includes("exit_plan_mode")) {
+ requestedTools.push("exit_plan_mode");
+ }
const pythonMode = getPythonModeFromEnv() ?? session.settings?.getPythonToolMode?.() ?? "ipy-only";
const skipPythonPreflight = session.skipPythonPreflight === true;
let pythonAvailable = true;
@@ -296,6 +313,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
: [
...Object.entries(BUILTIN_TOOLS).filter(([name]) => isToolAllowed(name)),
...(includeComplete ? ([["complete", HIDDEN_TOOLS.complete]] as const) : []),
+ ...([["exit_plan_mode", HIDDEN_TOOLS.exit_plan_mode]] as const),
];
time("createTools:beforeFactories");
const slowTools: Array<{ name: string; ms: number }> = [];
diff --git a/packages/coding-agent/src/tools/plan-mode-guard.ts b/packages/coding-agent/src/tools/plan-mode-guard.ts
new file mode 100644
index 000000000..cec84972e
--- /dev/null
+++ b/packages/coding-agent/src/tools/plan-mode-guard.ts
@@ -0,0 +1,46 @@
+import { resolvePlanUrlToPath } from "@oh-my-pi/pi-coding-agent/internal-urls";
+import type { ToolSession } from ".";
+import { resolveToCwd } from "./path-utils";
+import { ToolError } from "./tool-errors";
+
+const PLAN_URL_PREFIX = "plan://";
+
+export function resolvePlanPath(session: ToolSession, targetPath: string): string {
+ if (!targetPath.startsWith(PLAN_URL_PREFIX)) {
+ return resolveToCwd(targetPath, session.cwd);
+ }
+
+ const settingsManager = session.settingsManager;
+ if (!settingsManager) {
+ throw new ToolError("Plan mode: settings manager unavailable for plan path resolution.");
+ }
+
+ return resolvePlanUrlToPath(targetPath, {
+ getPlansDirectory: settingsManager.getPlansDirectory.bind(settingsManager),
+ cwd: session.cwd,
+ });
+}
+
+export function enforcePlanModeWrite(
+ session: ToolSession,
+ targetPath: string,
+ options?: { rename?: string; op?: "create" | "update" | "delete" },
+): void {
+ const state = session.getPlanModeState?.();
+ if (!state?.enabled) return;
+
+ const resolvedTarget = resolvePlanPath(session, targetPath);
+ const resolvedPlan = resolvePlanPath(session, state.planFilePath);
+
+ if (options?.rename) {
+ throw new ToolError("Plan mode: renaming files is not allowed.");
+ }
+
+ if (options?.op === "delete") {
+ throw new ToolError("Plan mode: deleting files is not allowed.");
+ }
+
+ if (resolvedTarget !== resolvedPlan) {
+ throw new ToolError(`Plan mode: only the plan file may be modified (${state.planFilePath}).`);
+ }
+}
diff --git a/packages/coding-agent/src/tools/read.ts b/packages/coding-agent/src/tools/read.ts
index c8907a1fc..c1c71a9e1 100644
--- a/packages/coding-agent/src/tools/read.ts
+++ b/packages/coding-agent/src/tools/read.ts
@@ -162,10 +162,11 @@ function similarityScore(a: string, b: string): number {
async function listCandidateFiles(
searchRoot: string,
signal?: AbortSignal,
+ notify?: (message: string) => void,
): Promise<{ files: string[]; truncated: boolean; error?: string }> {
let fdPath: string | undefined;
try {
- fdPath = await ensureTool("fd", true);
+ fdPath = await ensureTool("fd", { silent: true, notify });
} catch {
return { files: [], truncated: false, error: "fd not available" };
}
@@ -248,6 +249,7 @@ async function findReadPathSuggestions(
rawPath: string,
cwd: string,
signal?: AbortSignal,
+ notify?: (message: string) => void,
): Promise<{ suggestions: string[]; scopeLabel?: string; truncated?: boolean; error?: string } | null> {
const resolvedPath = resolveToCwd(rawPath, cwd);
const searchRoot = await findExistingDirectory(path.dirname(resolvedPath), signal);
@@ -262,7 +264,7 @@ async function findReadPathSuggestions(
}
}
- const { files, truncated, error } = await listCandidateFiles(searchRoot, signal);
+ const { files, truncated, error } = await listCandidateFiles(searchRoot, signal, notify);
const scopeLabel = formatScopeLabel(searchRoot, cwd);
if (error && files.length === 0) {
@@ -418,7 +420,7 @@ export class ReadTool implements AgentTool {
params: ReadParams,
signal?: AbortSignal,
_onUpdate?: AgentToolUpdateCallback,
- _context?: AgentToolContext,
+ toolContext?: AgentToolContext,
): Promise> {
const { path: readPath, offset, limit, lines } = params;
@@ -442,7 +444,9 @@ export class ReadTool implements AgentTool {
// Skip fuzzy matching for remote mounts (sshfs) to avoid hangs
if (!isRemoteMountPath(absolutePath)) {
- const suggestions = await findReadPathSuggestions(readPath, this.session.cwd, signal);
+ const suggestions = await findReadPathSuggestions(readPath, this.session.cwd, signal, message =>
+ toolContext?.ui?.notify(message, "info"),
+ );
if (suggestions?.suggestions.length) {
const scopeLabel = suggestions.scopeLabel ? ` in ${suggestions.scopeLabel}` : "";
diff --git a/packages/coding-agent/src/tools/write.ts b/packages/coding-agent/src/tools/write.ts
index 91e8ffb00..b6febf13b 100644
--- a/packages/coding-agent/src/tools/write.ts
+++ b/packages/coding-agent/src/tools/write.ts
@@ -17,7 +17,7 @@ import writeDescription from "../prompts/tools/write.md" with { type: "text" };
import type { ToolSession } from "../sdk";
import { renderStatusLine } from "../tui";
import { type OutputMeta, outputMeta } from "./output-meta";
-import { resolveToCwd } from "./path-utils";
+import { enforcePlanModeWrite, resolvePlanPath } from "./plan-mode-guard";
import {
formatDiagnostics,
formatExpandHint,
@@ -94,7 +94,8 @@ export class WriteTool implements AgentTool> {
return untilAborted(signal, async () => {
- const absolutePath = resolveToCwd(path, this.session.cwd);
+ enforcePlanModeWrite(this.session, path, { op: "create" });
+ const absolutePath = resolvePlanPath(this.session, path);
const batchRequest = getLspBatchRequest(context?.toolCall);
const diagnostics = await this.writethrough(absolutePath, content, signal, undefined, batchRequest);
diff --git a/packages/coding-agent/src/utils/tools-manager.ts b/packages/coding-agent/src/utils/tools-manager.ts
index b0e8023bb..c91964ece 100644
--- a/packages/coding-agent/src/utils/tools-manager.ts
+++ b/packages/coding-agent/src/utils/tools-manager.ts
@@ -6,6 +6,7 @@ import { $ } from "bun";
import { APP_NAME, getBinDir } from "../config";
const TOOLS_DIR = getBinDir();
+const TOOL_DOWNLOAD_TIMEOUT_MS = 15000;
interface ToolConfig {
name: string;
@@ -159,9 +160,18 @@ export async function getToolPath(tool: ToolName): Promise {
// Fetch latest release version from GitHub
async function getLatestVersion(repo: string): Promise {
- const response = await fetch(`https://api.github.com/repos/${repo}/releases/latest`, {
- headers: { "User-Agent": `${APP_NAME}-coding-agent` },
- });
+ let response: Response;
+ try {
+ response = await fetch(`https://api.github.com/repos/${repo}/releases/latest`, {
+ headers: { "User-Agent": `${APP_NAME}-coding-agent` },
+ signal: AbortSignal.timeout(TOOL_DOWNLOAD_TIMEOUT_MS),
+ });
+ } catch (err) {
+ if (err instanceof Error && err.name === "AbortError") {
+ throw new Error("GitHub API request timed out");
+ }
+ throw err;
+ }
if (!response.ok) {
throw new Error(`GitHub API error: ${response.status}`);
@@ -173,7 +183,17 @@ async function getLatestVersion(repo: string): Promise {
// Download a file from URL
async function downloadFile(url: string, dest: string): Promise {
- const response = await fetch(url);
+ let response: Response;
+ try {
+ response = await fetch(url, {
+ signal: AbortSignal.timeout(TOOL_DOWNLOAD_TIMEOUT_MS),
+ });
+ } catch (err) {
+ if (err instanceof Error && err.name === "AbortError") {
+ throw new Error(`Download timed out: ${url}`);
+ }
+ throw err;
+ }
if (!response.ok) {
throw new Error(`Failed to download: ${response.status}`);
} else if (!response.body) {
@@ -223,15 +243,12 @@ async function downloadTool(tool: ToolName): Promise {
const tmp = await TempDir.create("@omp-tools-extract-");
try {
- if (assetName.endsWith(".tar.gz")) {
+ if (assetName.endsWith(".tar.gz") || assetName.endsWith(".zip")) {
const archive = new Bun.Archive(await Bun.file(archivePath).arrayBuffer());
const files = await archive.files();
for (const [filePath, file] of files) {
await Bun.write(path.join(tmp.path(), filePath), file);
}
- } else if (assetName.endsWith(".zip")) {
- await fs.mkdir(tmp.path(), { recursive: true });
- await $`unzip -o ${archivePath} -d ${tmp.path()}`.quiet().nothrow();
}
// Find the binary in extracted files
@@ -284,7 +301,17 @@ async function installPythonPackage(pkg: string): Promise {
// Ensure a tool is available, downloading if necessary
// Returns the path to the tool, or null if unavailable
-export async function ensureTool(tool: ToolName, silent: boolean = false): Promise {
+type EnsureToolOptions = {
+ silent?: boolean;
+ notify?: (message: string) => void;
+};
+
+export async function ensureTool(
+ tool: ToolName,
+ silentOrOptions: boolean | EnsureToolOptions = false,
+): Promise {
+ const options = typeof silentOrOptions === "object" ? silentOrOptions : { silent: silentOrOptions };
+ const silent = options.silent ?? false;
const existingPath = await getToolPath(tool);
if (existingPath) {
return existingPath;
@@ -296,6 +323,7 @@ export async function ensureTool(tool: ToolName, silent: boolean = false): Promi
if (!silent) {
logger.debug(`${pythonConfig.name} not found. Installing via uv/pip...`);
}
+ options.notify?.(`Installing ${pythonConfig.name}...`);
const success = await installPythonPackage(pythonConfig.package);
if (success) {
// Re-check for the command after installation
@@ -320,6 +348,7 @@ export async function ensureTool(tool: ToolName, silent: boolean = false): Promi
if (!silent) {
logger.debug(`${config.name} not found. Downloading...`);
}
+ options.notify?.(`Downloading ${config.name}...`);
try {
const path = await downloadTool(tool);
diff --git a/packages/coding-agent/src/web/search/providers/perplexity.ts b/packages/coding-agent/src/web/search/providers/perplexity.ts
index f49842c8d..9528c24dd 100644
--- a/packages/coding-agent/src/web/search/providers/perplexity.ts
+++ b/packages/coding-agent/src/web/search/providers/perplexity.ts
@@ -169,7 +169,9 @@ export async function searchPerplexity(params: PerplexitySearchParams): Promise<
model: "sonar-pro",
messages,
return_related_questions: true,
- search_context_size: "high",
+ web_search_options: {
+ search_context_size: "high",
+ },
};
if (params.search_recency_filter) {
diff --git a/packages/coding-agent/src/web/search/types.ts b/packages/coding-agent/src/web/search/types.ts
index 398714695..7a52b0b73 100644
--- a/packages/coding-agent/src/web/search/types.ts
+++ b/packages/coding-agent/src/web/search/types.ts
@@ -163,7 +163,9 @@ export interface PerplexityRequest {
search_recency_filter?: "day" | "week" | "month" | "year";
return_images?: boolean;
return_related_questions?: boolean;
- search_context_size?: "low" | "medium" | "high";
+ web_search_options?: {
+ search_context_size?: "low" | "medium" | "high";
+ };
}
export interface PerplexitySearchResult {
diff --git a/packages/coding-agent/test/python-tool-settings.test.ts b/packages/coding-agent/test/python-tool-settings.test.ts
index 21aa57f4e..6852333cd 100644
--- a/packages/coding-agent/test/python-tool-settings.test.ts
+++ b/packages/coding-agent/test/python-tool-settings.test.ts
@@ -51,7 +51,7 @@ describe("python tool settings", () => {
vi.spyOn(pythonKernel, "checkPythonKernelAvailability").mockResolvedValue({ ok: true });
const tools = await createTools(createSession(testDir), ["python"]);
- expect(tools.map(tool => tool.name)).toEqual(["python"]);
+ expect(tools.map(tool => tool.name).sort()).toEqual(["exit_plan_mode", "python"]);
});
it("falls back to bash when python is unavailable", async () => {
@@ -61,7 +61,7 @@ describe("python tool settings", () => {
});
const tools = await createTools(createSession(testDir), ["python"]);
- expect(tools.map(tool => tool.name)).toEqual(["bash"]);
+ expect(tools.map(tool => tool.name).sort()).toEqual(["bash", "exit_plan_mode"]);
});
it("passes kernel mode from settings to executor", async () => {
diff --git a/packages/coding-agent/test/tools/index.test.ts b/packages/coding-agent/test/tools/index.test.ts
index ce80cb655..ae1a14c4a 100644
--- a/packages/coding-agent/test/tools/index.test.ts
+++ b/packages/coding-agent/test/tools/index.test.ts
@@ -49,6 +49,7 @@ describe("createTools", () => {
expect(names).toContain("todo_write");
expect(names).toContain("fetch");
expect(names).toContain("web_search");
+ expect(names).toContain("exit_plan_mode");
});
it("includes bash and python when python mode is both", async () => {
@@ -84,7 +85,7 @@ describe("createTools", () => {
const tools = await createTools(session, ["read", "lsp", "write"]);
const names = tools.map(t => t.name);
- expect(names).toEqual(["read", "write"]);
+ expect(names).toEqual(["read", "write", "exit_plan_mode"]);
});
it("excludes lsp tool when disabled", async () => {
@@ -100,7 +101,7 @@ describe("createTools", () => {
const tools = await createTools(session, ["read", "write"]);
const names = tools.map(t => t.name);
- expect(names).toEqual(["read", "write"]);
+ expect(names).toEqual(["read", "write", "exit_plan_mode"]);
});
it("includes hidden tools when explicitly requested", async () => {
@@ -108,7 +109,7 @@ describe("createTools", () => {
const tools = await createTools(session, ["report_finding"]);
const names = tools.map(t => t.name);
- expect(names).toEqual(["report_finding"]);
+ expect(names).toEqual(["report_finding", "exit_plan_mode"]);
});
it("includes complete tool when required", async () => {
@@ -154,6 +155,7 @@ describe("createTools", () => {
"fetch",
"web_search",
"write",
+ "enter_plan_mode",
];
for (const tool of expectedTools) {
@@ -165,6 +167,6 @@ describe("createTools", () => {
});
it("HIDDEN_TOOLS contains review tools", () => {
- expect(Object.keys(HIDDEN_TOOLS).sort()).toEqual(["complete", "report_finding"]);
+ expect(Object.keys(HIDDEN_TOOLS).sort()).toEqual(["complete", "exit_plan_mode", "report_finding"]);
});
});
diff --git a/packages/coding-agent/test/tools/python-fallback.test.ts b/packages/coding-agent/test/tools/python-fallback.test.ts
index 4d7002f4b..2ce624419 100644
--- a/packages/coding-agent/test/tools/python-fallback.test.ts
+++ b/packages/coding-agent/test/tools/python-fallback.test.ts
@@ -43,7 +43,7 @@ describe("createTools python fallback", () => {
const tools = await createTools(session, ["python"]);
const names = tools.map(tool => tool.name).sort();
- expect(names).toEqual(["bash"]);
+ expect(names).toEqual(["bash", "exit_plan_mode"]);
availabilitySpy.mockRestore();
});
diff --git a/packages/coding-agent/test/tools/python-tool-mode.test.ts b/packages/coding-agent/test/tools/python-tool-mode.test.ts
index a8135288a..5ca97092a 100644
--- a/packages/coding-agent/test/tools/python-tool-mode.test.ts
+++ b/packages/coding-agent/test/tools/python-tool-mode.test.ts
@@ -29,9 +29,9 @@ describe("createTools python fallback", () => {
process.env.OMP_PYTHON_SKIP_CHECK = "1";
const session = createSession();
const tools = await createTools(session, ["python"]);
- const names = tools.map(tool => tool.name);
+ const names = tools.map(tool => tool.name).sort();
- expect(names).toEqual(["bash"]);
+ expect(names).toEqual(["bash", "exit_plan_mode"]);
if (previous === undefined) {
delete process.env.OMP_PYTHON_SKIP_CHECK;
diff --git a/packages/coding-agent/test/tools/schema-validation.test.ts b/packages/coding-agent/test/tools/schema-validation.test.ts
index 317899642..935b37a65 100644
--- a/packages/coding-agent/test/tools/schema-validation.test.ts
+++ b/packages/coding-agent/test/tools/schema-validation.test.ts
@@ -383,6 +383,7 @@ describe("tool schema validation (post-sanitization)", () => {
"fetch",
"web_search",
"write",
+ "enter_plan_mode",
];
expect(Object.keys(BUILTIN_TOOLS).sort()).toEqual(expectedTools.sort());
diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md
index d405e8e39..582105ba2 100644
--- a/packages/tui/CHANGELOG.md
+++ b/packages/tui/CHANGELOG.md
@@ -2,6 +2,8 @@
## [Unreleased]
+### Added
+- Added fuzzy match function for autocomplete suggestions
## [8.4.0] - 2026-01-25
### Changed
diff --git a/packages/tui/src/autocomplete.ts b/packages/tui/src/autocomplete.ts
index 939a70b1d..2b82f40f8 100644
--- a/packages/tui/src/autocomplete.ts
+++ b/packages/tui/src/autocomplete.ts
@@ -2,6 +2,49 @@ import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
+/**
+ * Check if query is a subsequence of target (fuzzy match).
+ * "wig" matches "skill:wig" because w-i-g appear in order.
+ */
+function fuzzyMatch(query: string, target: string): boolean {
+ if (query.length === 0) return true;
+ if (query.length > target.length) return false;
+
+ let qi = 0;
+ for (let ti = 0; ti < target.length && qi < query.length; ti++) {
+ if (query[qi] === target[ti]) qi++;
+ }
+ return qi === query.length;
+}
+
+/**
+ * Score a fuzzy match. Higher = better match.
+ * Prioritizes: exact match > starts-with > contains > subsequence
+ */
+function fuzzyScore(query: string, target: string): number {
+ if (query.length === 0) return 1;
+ if (target === query) return 100;
+ if (target.startsWith(query)) return 80;
+ if (target.includes(query)) return 60;
+
+ // Subsequence match - score by how "tight" the match is
+ // (fewer gaps between matched characters = higher score)
+ let qi = 0;
+ let gaps = 0;
+ let lastMatchIdx = -1;
+ for (let ti = 0; ti < target.length && qi < query.length; ti++) {
+ if (query[qi] === target[ti]) {
+ if (lastMatchIdx >= 0 && ti - lastMatchIdx > 1) gaps++;
+ lastMatchIdx = ti;
+ qi++;
+ }
+ }
+ if (qi !== query.length) return 0;
+
+ // Base score 40 for subsequence, minus penalty for gaps
+ return Math.max(1, 40 - gaps * 5);
+}
+
async function walkDirectoryWithFd(
baseDir: string,
fdPath: string,
@@ -133,21 +176,39 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
if (spaceIndex === -1) {
// No space yet - complete command names
const prefix = textBeforeCursor.slice(1); // Remove the "/"
- const filtered = this.commands
- .filter(cmd => {
- const name = "name" in cmd ? cmd.name : cmd.value; // Check if SlashCommand or AutocompleteItem
- return name?.toLowerCase().startsWith(prefix.toLowerCase());
- })
- .map(cmd => ({
- value: "name" in cmd ? cmd.name : cmd.value,
- label: "name" in cmd ? cmd.name : cmd.label,
- ...(cmd.description && { description: cmd.description }),
- }));
+ const lowerPrefix = prefix.toLowerCase();
- if (filtered.length === 0) return null;
+ // Filter commands using fuzzy matching (subsequence match)
+ const matches = this.commands
+ .filter(cmd => {
+ const name = "name" in cmd ? cmd.name : cmd.value;
+ if (!name) return false;
+ // Match name or description
+ if (fuzzyMatch(lowerPrefix, name.toLowerCase())) return true;
+ const desc = cmd.description?.toLowerCase();
+ return desc ? fuzzyMatch(lowerPrefix, desc) : false;
+ })
+ .map(cmd => {
+ const name = "name" in cmd ? cmd.name : cmd.value;
+ const lowerName = name?.toLowerCase() ?? "";
+ const lowerDesc = cmd.description?.toLowerCase() ?? "";
+ // Score name matches higher than description matches
+ const nameScore = fuzzyMatch(lowerPrefix, lowerName) ? fuzzyScore(lowerPrefix, lowerName) : 0;
+ const descScore = fuzzyMatch(lowerPrefix, lowerDesc) ? fuzzyScore(lowerPrefix, lowerDesc) * 0.5 : 0;
+ return {
+ value: name,
+ label: "name" in cmd ? cmd.name : cmd.label,
+ score: Math.max(nameScore, descScore),
+ ...(cmd.description && { description: cmd.description }),
+ };
+ })
+ .sort((a, b) => b.score - a.score)
+ .map(({ score: _, ...rest }) => rest);
+
+ if (matches.length === 0) return null;
return {
- items: filtered,
+ items: matches,
prefix: textBeforeCursor,
};
} else {