-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathagentModeResolver.js
More file actions
602 lines (548 loc) · 30 KB
/
Copy pathagentModeResolver.js
File metadata and controls
602 lines (548 loc) · 30 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
'use strict';
const path = require('path');
const PLAN_MODE_ALLOWED_TOOLS = new Set([
'read_file', 'list_directory', 'grep_search', 'find_files', 'get_file_info',
'search_in_file', 'search_codebase', 'git_status', 'git_diff', 'git_log',
'write_file', 'edit_file', 'create_directory', 'ask_question', 'write_todos', 'update_todo',
]);
const PLAN_FILE_PATH_RE = /\.guide[/\\]plans[/\\].+\.plan\.md$/i;
const PLAN_PLANS_DIR_RE = /\.guide[/\\]plans\/?$/i;
const GUIDE_RULES_FILE_RE = /\.guide[/\\]rules[/\\].+\.md$/i;
const GUIDE_RULES_DIR_RE = /\.guide[/\\]rules\/?$/i;
const GUIDE_SCRATCH_RE = /^\.guide-scratch(\/|$)/i;
const GUIDE_PATH_MUTATING_TOOLS = new Set([
'write_file', 'edit_file', 'append_to_file', 'replace_in_file', 'delete_file',
]);
function normalizePathSlashes(filePath) {
return String(filePath || '').replace(/\\/g, '/');
}
function isPlanFilePath(filePath) {
return PLAN_FILE_PATH_RE.test(normalizePathSlashes(filePath));
}
function isPlansDirectoryPath(dirPath) {
return PLAN_PLANS_DIR_RE.test(normalizePathSlashes(dirPath).replace(/\/+$/, ''));
}
function isGuideRulesDirectoryPath(dirPath) {
return GUIDE_RULES_DIR_RE.test(normalizePathSlashes(dirPath).replace(/\/+$/, ''));
}
function isGuideRulesFilePath(filePath) {
return GUIDE_RULES_FILE_RE.test(normalizePathSlashes(filePath));
}
function isGuideScratchPath(filePath) {
const norm = normalizePathSlashes(filePath);
return GUIDE_SCRATCH_RE.test(norm) || /\.guide-scratch(\/|$)/i.test(norm);
}
function pathContainsGuideMetadata(filePath) {
return /\.guide[/\\]/i.test(normalizePathSlashes(filePath));
}
function isAllowedGuideMetadataWritePath(filePath) {
if (isGuideScratchPath(filePath)) return true;
if (isPlanFilePath(filePath)) return true;
if (isGuideRulesFilePath(filePath)) return true;
return false;
}
function extractGuideGatePath(toolName, params) {
if (toolName === 'create_directory') {
return String(params?.path || params?.directory || params?.dir || '');
}
return String(
params?.filePath || params?.path || params?.oldPath || params?.newPath
|| params?.source || params?.destination || '',
);
}
/**
* Block writes/edits under .guide/ except plan docs and rules (all agent modes).
*/
function checkGuideMetadataPathGate(toolName, params) {
if (toolName === 'create_directory') {
const dp = extractGuideGatePath(toolName, params);
const norm = normalizePathSlashes(dp).replace(/\/+$/, '');
if (!norm.startsWith('.guide') && !/\.guide[/\\]/i.test(norm)) return { allowed: true };
if (isPlansDirectoryPath(dp) || isGuideRulesDirectoryPath(dp)) return { allowed: true };
return {
allowed: false,
error: `create_directory blocked for "${dp}". Only .guide/plans and .guide/rules are allowed under .guide/.`,
};
}
if (!GUIDE_PATH_MUTATING_TOOLS.has(toolName)) return { allowed: true };
const fp = extractGuideGatePath(toolName, params);
if (!fp) return { allowed: true };
if (!pathContainsGuideMetadata(fp) && !isGuideScratchPath(fp)) return { allowed: true };
if (isAllowedGuideMetadataWritePath(fp)) return { allowed: true };
const basename = path.basename(normalizePathSlashes(fp));
const display = normalizePathSlashes(fp).replace(/^.*\.guide[/\\]/, '.guide/');
return {
allowed: false,
error: `${toolName} blocked: "${display}" is under .guide/ (hidden from the file explorer). `
+ `Write application source to the project root instead, e.g. "${basename}". `
+ 'Only .guide/plans/*.plan.md and .guide/rules/*.md are for guIDE metadata.',
guidePathBlocked: true,
};
}
/** Plan workflow phase — artifact/state based, not user-message heuristics. */
function resolvePlanPhase(options = {}) {
const {
planMode = false,
agentPhase = 'planning',
planReady = false,
planFileExists = false,
} = options;
if (!planMode) return null;
if (agentPhase === 'building') return 'building';
if (planReady || planFileExists) return 'plan_ready';
return 'awaiting_plan';
}
function filterPlanModeToolCalls(calls) {
if (!calls?.length) return { calls: [], blocked: [] };
const kept = [];
const blocked = [];
for (const c of calls) {
if (!PLAN_MODE_ALLOWED_TOOLS.has(c.tool)) {
blocked.push(c);
continue;
}
if (c.tool === 'write_file') {
const fp = String(c.params?.filePath || c.params?.path || '');
if (!isPlanFilePath(fp)) {
blocked.push(c);
continue;
}
}
if (c.tool === 'edit_file') {
const fp = String(c.params?.filePath || c.params?.path || '');
if (!isPlanFilePath(fp)) {
blocked.push(c);
continue;
}
}
if (c.tool === 'create_directory') {
const dp = String(c.params?.path || c.params?.directory || c.params?.dir || '');
if (!isPlansDirectoryPath(dp)) {
blocked.push(c);
continue;
}
}
if (c.tool === 'update_todo') {
const status = String(c.params?.status || '').toLowerCase().replace(/_/g, '-');
if (status === 'done' || status === 'in-progress' || status === 'completed') {
blocked.push(c);
continue;
}
}
kept.push(c);
}
return { calls: kept, blocked };
}
function filterToolDefinitions(allDefs, allowedTools) {
if (!allowedTools) return allDefs;
if (allowedTools.size === 0) return [];
return allDefs.filter((d) => allowedTools.has(d.name));
}
function getAskSystemPrompt() {
return 'You are guIDE, an AI assistant embedded in a general-purpose IDE.\n\n'
+ '## Ask mode\n'
+ 'You are in **Ask mode** (Q&A only). Answer the user\'s question directly in prose.\n'
+ 'Do NOT call tools. Do NOT create, edit, or delete files. Do NOT run commands.\n\n'
+ '## How to respond\n'
+ '- Reply in clear, helpful prose.\n'
+ '- Base answers on user-provided context — not assumptions.\n'
+ '- If you need project file contents or live data to answer accurately, say what you would need and suggest the user switch to Agent or Plan mode.\n\n'
+ '## Images\n'
+ 'When you receive an image description from the vision system, treat it as what you observed.';
}
function getAskModePromptAddition() {
return '';
}
/** One-line tool listing for compact catalog (small models). */
function formatCompactToolLine(tool, options = {}) {
const params = tool.parameters ? Object.entries(tool.parameters)
.map(([n, info]) => `${n}${info.required ? '*' : ''}`)
.join(', ') : '';
if (options.compactDescriptions) {
const desc = String(tool.description || '').split(/[.!?\n]/)[0].trim();
const short = desc.length > 72 ? `${desc.slice(0, 69)}…` : desc;
return short ? `- ${tool.name}(${params}) — ${short}\n` : `- ${tool.name}(${params})\n`;
}
return `- **${tool.name}**(${params}) — ${tool.description}\n`;
}
/** Shared tool-call format header — used by mcpToolServer full and compact catalogs. */
function getAgentToolPromptHeader(options = {}) {
const planning = !!options.planning;
const compact = !!options.compact;
let header = '## Tools\n';
if (compact) {
header += 'To call a tool, output a ```json block:\n```json\n{"tool":"<name>","params":{...}}\n```\n';
} else {
header += 'Call tools with one ```json fenced block per action:\n```json\n{"tool":"tool_name","params":{"param":"value"}}\n```\n';
header += 'Examples (use exact parameter names shown in each tool listing below):\n';
}
if (planning) {
header += '```json\n{"tool":"create_directory","params":{"path":".guide/plans"}}\n```\n';
header += '```json\n{"tool":"write_file","params":{"filePath":".guide/plans/my-feature.plan.md","content":"---\\ntitle: My Feature\\noverview: ...\\n---\\n\\n## Summary\\n..."}}\n```\n';
header += '```json\n{"tool":"write_todos","params":{"items":[{"text":"First step","status":"pending"}]}}\n```\n';
header += '```json\n{"tool":"read_file","params":{"filePath":"src/index.js"}}\n```\n';
if (!compact) {
header += 'Plan mode: write_file and edit_file ONLY for `.guide/plans/*.plan.md`. Do not create application source files until Build.\n';
} else {
header += 'Plan mode examples (build requests):\n\n';
}
} else {
header += '```json\n{"tool":"read_file","params":{"filePath":"src/index.js"}}\n```\n';
if (compact) {
header += '```json\n{"tool":"edit_file","params":{"filePath":"src/app.js","oldText":"const x = 1","newText":"const x = 2"}}\n```\n';
header += '```json\n{"tool":"list_directory","params":{"dirPath":"."}}\n```\n\n';
} else {
header += '```json\n{"tool":"write_file","params":{"filePath":"index.html","content":"<html><body>Hello</body></html>"}}\n```\n';
header += '```json\n{"tool":"edit_file","params":{"filePath":"src/app.js","oldText":"const x = 1","newText":"const x = 2"}}\n```\n';
header += '```json\n{"tool":"list_directory","params":{"dirPath":"."}}\n```\n';
header += 'You HAVE the tools listed below — use them. Do not say you cannot access files, the terminal, or the network when a tool can perform the task.\n';
header += 'Do not output full file content as chat prose — use write_file, edit_file, or append_to_file.\n';
header += 'Do not give manual step-by-step instructions when a tool can perform the action.\n';
}
}
if (!compact) {
header += '\n### Tool-call formatting\n';
header += 'Each tool call is ONE ```json block with ONE JSON object. All arguments go under `params`. Use plain double quotes. To perform N actions, emit N separate blocks.\n';
header += '```json\n{"tool":"create_directory","params":{"path":"src"}}\n```\n';
header += '```json\n{"tool":"write_file","params":{"filePath":".gitignore","content":"node_modules\\n.env\\n"}}\n```\n\n';
}
return header;
}
/** Shared rules footer for tool catalogs — single source for agent/plan tool prompts. */
function getAgentToolCatalogRules(options = {}) {
const planning = !!options.planning;
const compact = !!options.compact;
if (planning) {
if (compact) {
return '### Rules\n'
+ '- Build/plan requests: create_directory(.guide/plans) → write_file(.guide/plans/{slug}.plan.md) → write_todos — then STOP\n'
+ '- write_file and edit_file ONLY for `.guide/plans/*.plan.md` during planning\n'
+ '- Do not paste implementation source in chat — put the plan in the plan file\n'
+ '- update_todo: text changes only until the user clicks Build\n';
}
return `### Plan workflow
- **New build request**: create_directory(.guide/plans) → write_file(.guide/plans/{slug}.plan.md) → write_todos → short prose summary only
- **Explore first** (optional): read_file, list_directory, grep_search, search_codebase, git_status
- **Revise plan**: edit_file or write_file on existing .guide/plans/*.plan.md only
### Rules
- Use tool results as ground truth; do not fabricate tool output
- If a tool fails, read the error and retry once with corrected parameters
- Do not output implementation source in chat — write the plan to the plan file
- Do not run terminal commands, browser, or web tools in Plan mode
`;
}
if (compact) {
return '### Rules\n'
+ '- Use write_file to create new files, append_to_file to add to existing files\n'
+ '- For edits: read_file first, then edit_file with exact oldText\n'
+ '- Web: after web_search, fetch_webpage on top result URLs before answering\n'
+ '- Browser: browser_navigate → interact using refs from snapshot; retry or use fetch_webpage on failure\n'
+ '- After write_todos: update_todo(in-progress/done) as you work each step\n';
}
return `### Common patterns
- **Web lookup**: web_search → fetch_webpage for top result URL(s) → answer from fetched text in the same continuation
- **Project files**: read_file (known path), grep_search or search_codebase (unknown location), list_directory (folder listing)
- **Edit existing file**: read_file → edit_file with exact oldText/newText from the file
- **Browser**: browser_navigate → interact using refs from the snapshot (browser_click, browser_type, etc.). If browser_navigate fails, retry it or use fetch_webpage — do NOT shell-debug Chrome/Playwright via run_command.
- **New file**: write_file with full content in params; append_to_file for additional sections
- **Large rules**: write_file to \`.guide/rules/<name>.md\` instead of embedding multi-KB content in save_rule JSON
### Rules
- Use tool results as ground truth; do not fabricate tool output
- After web_search, fetch page content before answering from snippets alone
- If a tool fails, read the error and retry once with corrected parameters
- Do not output substantive file content as chat prose — use file tools
`;
}
function getAgentSystemPrompt() {
return 'You are guIDE, an AI assistant embedded in a general-purpose IDE. You help users with software projects: reading and writing code, running commands, searching the web, using the browser, and answering questions.\n\n'
+ '## How to respond\n'
+ '- If the user\'s message is conversational (greetings, thanks, clarifying questions, opinions) and needs no action, reply in plain prose only. Do not call tools.\n'
+ '- If the user asks you to do something you cannot do with text alone — create or change files, run commands, search the project or web, use the browser, inspect git state, etc. — use the appropriate tool from the ## Tools section below.\n'
+ '- You have real tools. When action is required, use them. Do not say you cannot access files, the terminal, or the network when a tool can perform the task.\n'
+ '- Put explanations and reasoning in **prose**. Put actions in **tool calls** (the system runs tools and shows results in tool cards). Do not paste raw tool-call JSON in your visible reply.\n\n'
+ '## Clarification (before you guess)\n'
+ '- When requirements are ambiguous, multiple approaches are valid, or you need facts only the user has (credentials, which account, API keys, destructive confirmation, missing env values): call **ask_question** with an **options** array of {label, description} objects before proceeding.\n'
+ '- Do not invent usernames, passwords, tokens, or one-time codes. Do not assume values from unrelated documents or prior chats.\n'
+ '- For simple chat, prose is fine. When the user\'s answer determines the next tool call, prefer **ask_question** over guessing.\n\n'
+ '## Tools (required reading)\n'
+ 'Tool definitions, call format, parameter schemas, and examples are in the ## Tools section appended below this message. Follow that section exactly for tool names, parameter names, and JSON format. Do not invent tool names or parameter names.\n\n'
+ 'After calling a tool, wait for the result before continuing. Never output fabricated tool results or blocks labeled [Tool Results] or [System: Tool Results] — the system injects real results.\n\n'
+ '## Grounding\n'
+ '- Base answers on tool results, file contents, and user-provided context — not assumptions.\n'
+ '- If a tool fails, read the error, adjust, and retry once with corrected parameters; then explain or use ask_question if blocked.\n\n'
+ '## Images\n'
+ 'When you receive an image description from the vision system, treat it as what you observed. Do not use read_file on image files to "see" them.\n\n'
+ '## File locations\n'
+ '- Application source (HTML, CSS, JS, etc.) belongs in the **project root** (visible in the file explorer), never under `.guide/`.\n'
+ '- `.guide/` is guIDE metadata (hidden from the explorer); users cannot see files written there except rules you save under `.guide/rules/`.\n\n'
+ '## Todo List Discipline\n'
+ 'Use write_todos only for multi-step builds — skip it for simple one-shot tasks. If you called write_todos, call update_todo when you start each todo item (status: \'in-progress\') and when you finish it (status: \'done\'). The user sees the todo list in real time.\n\n'
+ '## Browser and authentication flows\n'
+ '- Call browser_navigate first (returns a snapshot). After browser_type, browser_click, or any action that changes the page, call browser_snapshot before the next browser_click so [ref=N] numbers match the current DOM. Do not reuse stale refs. Prefer elements marked [SUBMIT] for login/forms.\n'
+ '- Read the snapshot: if the page reports an incorrect username, a password field is missing, or 2FA/phone verification appears, stop cycling clicks. Use **ask_question** or prose to get what you need from the user.\n'
+ '- Never type passwords from memory. The user provides secrets when needed.\n\n'
+ '## Session memory\n'
+ 'When older turns were condensed, a brief progress summary may appear in context. Never mention context limits, rotation, or compression to the user. Continue the current task immediately using that summary and the next required tool call.\n\n'
+ '## Cloud response style (cloud models only)\n'
+ 'When this block is present you are guIDE Cloud AI: keep answers concise — short paragraphs, minimal preamble, no filler. Still use tools whenever the task requires real actions in the project.\n\n'
+ '## Only call a tool when required. Never call a tool when plain prose is sufficient.\n\n'
+ 'Examples of when to call tools:\n\n'
+ 'Pattern — user wants to create or write a file:\n'
+ 'Call write_file with the target path and the full file content. Do not output the content as a markdown code block.\n\n'
+ 'Pattern — user asks to edit or modify an existing file:\n'
+ 'Use edit_file or replace_in_file on that file path. Call read_file first if you need the current content. Do NOT use write_file to create a new file or a renamed copy (e.g. file-v2.html) unless the user explicitly asked for a new file or a full rewrite from scratch.\n\n'
+ 'Pattern — user asks to run a command, script, or terminal operation:\n'
+ 'Call run_terminal_command with the command string. Do not describe what the command would do — run it.\n\n'
+ 'Pattern — user asks to search the web, find current information, or look up something online:\n'
+ 'Call web_search with a rephrased query. Do not generate an answer from memory if the information may be outdated.\n\n'
+ 'Pattern — user asks to open, navigate, or interact with a website or browser:\n'
+ 'Follow the Browser and authentication flows section above.\n\n'
+ 'Pattern — user wants to find files in the project, list a directory, or search codebase:\n'
+ 'Call list_directory, search_codebase, or find_files as appropriate. Do not guess at file contents.\n\n'
+ 'Pattern — user asks a conversational question or makes a greeting:\n'
+ 'Reply in prose only. Do not call any tool.';
}
function getPlanSystemPrompt() {
return 'You are guIDE, an AI assistant embedded in a general-purpose IDE.\n\n'
+ '## Plan mode — READ FIRST\n'
+ 'You are in **Plan mode**: **planning only**, not implementing. The user clicks **Build** (or Ctrl+Enter) to implement later.\n'
+ '**Do NOT deliver the finished product in chat.** When the user asks you to build or design something, write an implementation plan to disk with tools — do not paste source code or long implementation in your reply.\n'
+ '**Terminal, browser, web, and other implementation tools are NOT available** in Plan mode. For live external data, answer from knowledge or suggest Ask mode.\n\n'
+ '## How to respond\n'
+ '- Put brief explanations in **prose**. Put plan artifacts in **tool calls** (write_file to `.guide/plans/*.plan.md`, write_todos).\n'
+ '- Do not paste raw tool-call JSON in your visible reply — the system runs tools and shows tool cards.\n'
+ '- If you must show code in chat (rare), wrap it in markdown fences: ` ```lang ` … ` ``` ` so it renders as a code block.\n\n'
+ '## Clarification\n'
+ '- When requirements are ambiguous, use **ask_question** or ask 1–2 clarifying questions in prose before planning.\n\n'
+ '## Tools (required reading)\n'
+ 'Tool definitions, call format, and examples are in the ## Tools section below. Follow exact tool names and parameter names.\n\n'
+ '## Allowed tools in Plan mode\n'
+ 'read/search/git tools; **create_directory** (`.guide/plans` only); **write_file** and **edit_file** (`.guide/plans/*.plan.md` only); **write_todos**; **update_todo**; **ask_question**.\n\n'
+ '## Worked example — build request (use tools in this order)\n'
+ 'When the user wants something built (e.g. "make me a website"):\n'
+ '```json\n{"tool":"create_directory","params":{"path":".guide/plans"}}\n```\n'
+ '```json\n{"tool":"write_file","params":{"filePath":".guide/plans/community-website.plan.md","content":"---\\ntitle: Community Website\\noverview: Professional site for the community\\n---\\n\\n## Summary\\n...\\n\\n## Approach\\n...\\n\\n## Key files\\n- index.html in project root\\n"}}\n```\n'
+ '```json\n{"tool":"write_todos","params":{"items":[{"text":"Scaffold HTML structure","status":"pending"},{"text":"Add styles and layout","status":"pending"}]}}\n```\n'
+ 'Then STOP. Do not create app source files, run commands, or install packages.\n\n'
+ '## Plan file format\n'
+ '- Path: `.guide/plans/{descriptive-slug}.plan.md`\n'
+ '- YAML frontmatter: `title`, optional `overview` only — no todos in the file body\n'
+ '- Plan body: summary, approach, key files, phases — not full source code\n\n'
+ '## Grounding\n'
+ '- Base the plan on user requirements and optional read/list/search/git exploration.\n'
+ '- If a tool fails, read the error and retry once with corrected parameters.\n\n'
+ '## Images\n'
+ 'When you receive an image description from the vision system, treat it as what you observed.';
}
/** Phase-specific deltas only — core plan identity lives in getPlanSystemPrompt(). */
function getPlanModePromptAddition(planPhase = 'awaiting_plan') {
if (planPhase === 'awaiting_plan') {
return '\n\n## Current phase: awaiting plan\n'
+ '### Tier A — Conversational (no plan artifact)\n'
+ 'For greetings, small talk, or general questions unrelated to building something: respond in **prose only**. Do NOT call tools. Do NOT write a plan file.\n\n'
+ '### Tier B — Build / plan requests\n'
+ 'When the user wants something built or designed: create the plan file and write_todos as shown in the worked example above.\n'
+ '- Optionally explore first with read/list/search/git tools.\n'
+ '- If the request is vague, clarify before or while planning.\n';
}
return '\n\n## Current phase: plan ready\n'
+ 'A plan already exists on disk. Answer questions in prose.\n'
+ 'To revise the plan: edit_file or write_file on `.guide/plans/*.plan.md` only.\n'
+ 'To change todo text: update_todo with todo `id` and new `text`. To replace the full checklist: write_todos.\n'
+ 'Do NOT modify source files, run commands, or install packages until the user clicks **Build**.\n';
}
function getBuildingPhasePromptAddition() {
return '\n\n## BUILD PHASE\n'
+ 'Implement the approved plan in the PROJECT ROOT (the opened workspace folder).\n'
+ 'NEVER create application source under `.guide/` — that directory is guIDE metadata only.\n'
+ '`.guide/plans/` holds `*.plan.md` plan documents only — not HTML, CSS, JS, or other implementation files.\n\n'
+ '### Todo list discipline (multi-step builds only)\n'
+ 'For multi-step builds, call **write_todos** first with the full todo list.\n'
+ 'If you called **write_todos**, call **update_todo** as you work:\n'
+ '- When you **start** a todo item: `update_todo` with that item\'s `id` and `status: "in-progress"`.\n'
+ '- When you **finish** a todo item: `update_todo` with `status: "done"` before moving on.\n'
+ 'Skip write_todos for simple one-shot tasks.';
}
/**
* Shared mode resolver — identical rules for local and cloud backends.
*/
function resolveAgentMode(options = {}) {
const {
askOnly = false,
planMode = false,
chatMode = 'agent',
agentPhase = 'planning',
toolsEnabled = true,
} = options;
const effectiveAskOnly = askOnly || chatMode === 'ask';
const effectivePlanMode = planMode || chatMode === 'plan';
const planning = effectivePlanMode && agentPhase !== 'building';
const building = agentPhase === 'building';
let allowedTools = null;
if (!toolsEnabled || effectiveAskOnly) {
allowedTools = new Set();
} else if (planning) {
allowedTools = new Set(PLAN_MODE_ALLOWED_TOOLS);
}
let baseSystemPrompt = getAgentSystemPrompt();
let systemPromptAdditions = '';
if (effectiveAskOnly) {
baseSystemPrompt = getAskSystemPrompt();
systemPromptAdditions += getAskModePromptAddition();
} else if (building) {
systemPromptAdditions += getBuildingPhasePromptAddition();
} else if (planning) {
baseSystemPrompt = getPlanSystemPrompt();
const planPhase = resolvePlanPhase({
planMode: effectivePlanMode,
agentPhase,
planReady: !!options.planReady,
planFileExists: !!options.planFileExists,
});
systemPromptAdditions += getPlanModePromptAddition(planPhase || 'awaiting_plan');
}
const toolsActive = toolsEnabled && !effectiveAskOnly && (allowedTools === null || allowedTools.size > 0);
return {
askOnly: effectiveAskOnly,
planMode: effectivePlanMode,
planning,
building,
allowedTools,
toolsActive,
baseSystemPrompt,
systemPromptAdditions,
agentPhase,
planPhase: effectivePlanMode && !building
? resolvePlanPhase({
planMode: effectivePlanMode,
agentPhase,
planReady: !!options.planReady,
planFileExists: !!options.planFileExists,
})
: (building ? 'building' : null),
};
}
function checkPlanModeToolGate(toolName, params, agentContext = {}) {
const { planMode, agentPhase = 'planning' } = agentContext;
if (!planMode || agentPhase === 'building') return { allowed: true };
if (!PLAN_MODE_ALLOWED_TOOLS.has(toolName)) {
return {
allowed: false,
error: `Plan mode: tool "${toolName}" is blocked. Use read/search/git tools and write_file for .guide/plans/*.plan.md only. Click Build to implement.`,
};
}
if (toolName === 'write_file') {
const fp = String(params?.filePath || params?.path || '');
if (!isPlanFilePath(fp)) {
return {
allowed: false,
error: `Plan mode: write_file blocked for "${fp}". Only .guide/plans/*.plan.md is allowed during planning. Click Build to implement.`,
};
}
}
if (toolName === 'edit_file') {
const fp = String(params?.filePath || params?.path || '');
if (!isPlanFilePath(fp)) {
return {
allowed: false,
error: `Plan mode: edit_file blocked for "${fp}". Only .guide/plans/*.plan.md is allowed during planning.`,
};
}
}
if (toolName === 'create_directory') {
const dp = String(params?.path || params?.directory || params?.dir || '');
if (!isPlansDirectoryPath(dp)) {
return {
allowed: false,
error: `Plan mode: create_directory blocked for "${dp}". Only .guide/plans is allowed during planning.`,
};
}
}
if (toolName === 'update_todo') {
const status = String(params?.status || '').toLowerCase().replace(/_/g, '-');
if (status === 'done' || status === 'in-progress' || status === 'completed') {
return {
allowed: false,
error: 'Plan mode: update_todo status changes (done/in-progress) are blocked until the user clicks Build. Use update_todo with `text` only to revise checklist wording.',
};
}
}
return { allowed: true };
}
function parsePlanFileContent(content) {
const text = String(content || '');
const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
if (!match) {
return { title: 'Implementation Plan', overview: text.split('\n').find((l) => l.trim()) || '', todos: [], body: text };
}
const frontmatter = match[1];
const body = match[2];
let title = 'Implementation Plan';
let overview = '';
const todos = [];
const titleMatch = frontmatter.match(/^title:\s*(.+)$/m);
if (titleMatch) title = titleMatch[1].replace(/^["']|["']$/g, '').trim();
const overviewMatch = frontmatter.match(/^overview:\s*(.+)$/m);
if (overviewMatch) overview = overviewMatch[1].replace(/^["']|["']$/g, '').trim();
const todosBlock = frontmatter.match(/todos:\s*\n([\s\S]*?)(?:\n[a-zA-Z_]+:|$)/);
if (todosBlock) {
const lines = todosBlock[1].split('\n');
let current = null;
for (const line of lines) {
const idMatch = line.match(/^\s*-\s*id:\s*(.+)$/);
const contentMatch = line.match(/^\s*content:\s*(.+)$/);
const statusMatch = line.match(/^\s*status:\s*(.+)$/);
if (idMatch) {
if (current) todos.push(current);
current = { id: idMatch[1].trim(), content: '', status: 'pending' };
} else if (contentMatch && current) {
current.content = contentMatch[1].replace(/^["']|["']$/g, '').trim();
} else if (statusMatch && current) {
current.status = statusMatch[1].trim();
}
}
if (current) todos.push(current);
}
if (!overview) {
const overviewSection = body.match(/##\s*Overview\s*\n([\s\S]*?)(?:\n##|$)/i);
overview = overviewSection ? overviewSection[1].trim().split('\n')[0] : body.split('\n').find((l) => l.trim() && !l.startsWith('#')) || '';
}
return { title, overview, todos, body };
}
/** Whether file-content UI may stream during plan mode (plan files only until Build). */
function shouldStreamFileContentForAgent(settings, filePath) {
if (!settings?.planMode || settings.agentPhase === 'building') return true;
return isPlanFilePath(filePath || '');
}
function relativePlanPath(fullPath, projectPath) {
if (!fullPath) return fullPath;
if (projectPath) {
try {
return path.relative(projectPath, fullPath).replace(/\\/g, '/');
} catch (_) {}
}
return normalizePathSlashes(fullPath);
}
module.exports = {
PLAN_MODE_ALLOWED_TOOLS,
isPlanFilePath,
isPlansDirectoryPath,
resolvePlanPhase,
filterPlanModeToolCalls,
filterToolDefinitions,
resolveAgentMode,
checkPlanModeToolGate,
checkGuideMetadataPathGate,
getAskSystemPrompt,
getAgentSystemPrompt,
getPlanSystemPrompt,
getAskModePromptAddition,
getPlanModePromptAddition,
getBuildingPhasePromptAddition,
formatCompactToolLine,
getAgentToolPromptHeader,
getAgentToolCatalogRules,
parsePlanFileContent,
relativePlanPath,
shouldStreamFileContentForAgent,
};