@oh-my-pi/pi-coding-agent 4.1.0 → 4.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +2 -1
  3. package/docs/sdk.md +0 -3
  4. package/package.json +6 -5
  5. package/src/config.ts +9 -0
  6. package/src/core/agent-session.ts +3 -3
  7. package/src/core/agent-storage.ts +450 -0
  8. package/src/core/auth-storage.ts +102 -183
  9. package/src/core/compaction/branch-summarization.ts +5 -4
  10. package/src/core/compaction/compaction.ts +7 -6
  11. package/src/core/compaction/utils.ts +6 -11
  12. package/src/core/custom-commands/bundled/review/index.ts +22 -94
  13. package/src/core/custom-share.ts +66 -0
  14. package/src/core/export-html/index.ts +1 -33
  15. package/src/core/history-storage.ts +15 -7
  16. package/src/core/prompt-templates.ts +271 -1
  17. package/src/core/sdk.ts +14 -3
  18. package/src/core/settings-manager.ts +100 -34
  19. package/src/core/slash-commands.ts +4 -1
  20. package/src/core/storage-migration.ts +215 -0
  21. package/src/core/system-prompt.ts +130 -290
  22. package/src/core/title-generator.ts +3 -2
  23. package/src/core/tools/ask.ts +2 -2
  24. package/src/core/tools/bash.ts +2 -1
  25. package/src/core/tools/calculator.ts +2 -1
  26. package/src/core/tools/complete.ts +5 -2
  27. package/src/core/tools/edit.ts +2 -1
  28. package/src/core/tools/find.ts +2 -1
  29. package/src/core/tools/gemini-image.ts +2 -1
  30. package/src/core/tools/git.ts +2 -2
  31. package/src/core/tools/grep.ts +2 -1
  32. package/src/core/tools/index.test.ts +0 -28
  33. package/src/core/tools/index.ts +0 -6
  34. package/src/core/tools/lsp/index.ts +2 -1
  35. package/src/core/tools/output.ts +2 -1
  36. package/src/core/tools/read.ts +4 -1
  37. package/src/core/tools/ssh.ts +4 -2
  38. package/src/core/tools/task/agents.ts +56 -30
  39. package/src/core/tools/task/commands.ts +5 -8
  40. package/src/core/tools/task/index.ts +7 -15
  41. package/src/core/tools/web-fetch.ts +2 -1
  42. package/src/core/tools/web-search/auth.ts +106 -16
  43. package/src/core/tools/web-search/index.ts +3 -2
  44. package/src/core/tools/web-search/providers/anthropic.ts +44 -6
  45. package/src/core/tools/write.ts +2 -1
  46. package/src/core/voice.ts +3 -1
  47. package/src/discovery/builtin.ts +9 -54
  48. package/src/discovery/claude.ts +16 -69
  49. package/src/discovery/codex.ts +11 -36
  50. package/src/discovery/helpers.ts +52 -1
  51. package/src/main.ts +1 -1
  52. package/src/migrations.ts +20 -20
  53. package/src/modes/interactive/controllers/command-controller.ts +527 -0
  54. package/src/modes/interactive/controllers/event-controller.ts +340 -0
  55. package/src/modes/interactive/controllers/extension-ui-controller.ts +600 -0
  56. package/src/modes/interactive/controllers/input-controller.ts +585 -0
  57. package/src/modes/interactive/controllers/selector-controller.ts +585 -0
  58. package/src/modes/interactive/interactive-mode.ts +363 -3139
  59. package/src/modes/interactive/theme/theme.ts +5 -5
  60. package/src/modes/interactive/types.ts +189 -0
  61. package/src/modes/interactive/utils/ui-helpers.ts +449 -0
  62. package/src/modes/interactive/utils/voice-manager.ts +96 -0
  63. package/src/prompts/{explore.md → agents/explore.md} +7 -5
  64. package/src/prompts/agents/frontmatter.md +7 -0
  65. package/src/prompts/{plan.md → agents/plan.md} +3 -3
  66. package/src/prompts/agents/planner.md +112 -0
  67. package/src/prompts/agents/task.md +15 -0
  68. package/src/prompts/review-request.md +44 -8
  69. package/src/prompts/system/custom-system-prompt.md +80 -0
  70. package/src/prompts/system/file-operations.md +12 -0
  71. package/src/prompts/system/system-prompt.md +237 -0
  72. package/src/prompts/system/title-system.md +2 -0
  73. package/src/prompts/tools/bash.md +1 -1
  74. package/src/prompts/tools/read.md +1 -1
  75. package/src/prompts/tools/task.md +34 -22
  76. package/src/core/tools/rulebook.ts +0 -132
  77. package/src/prompts/architect-plan.md +0 -10
  78. package/src/prompts/implement-with-critic.md +0 -11
  79. package/src/prompts/implement.md +0 -11
  80. package/src/prompts/system-prompt.md +0 -43
  81. package/src/prompts/task.md +0 -14
  82. package/src/prompts/title-system.md +0 -8
  83. /package/src/prompts/{init.md → agents/init.md} +0 -0
  84. /package/src/prompts/{reviewer.md → agents/reviewer.md} +0 -0
  85. /package/src/prompts/{branch-summary-preamble.md → compaction/branch-summary-preamble.md} +0 -0
  86. /package/src/prompts/{branch-summary.md → compaction/branch-summary.md} +0 -0
  87. /package/src/prompts/{compaction-summary.md → compaction/compaction-summary.md} +0 -0
  88. /package/src/prompts/{compaction-turn-prefix.md → compaction/compaction-turn-prefix.md} +0 -0
  89. /package/src/prompts/{compaction-update-summary.md → compaction/compaction-update-summary.md} +0 -0
  90. /package/src/prompts/{summarization-system.md → system/summarization-system.md} +0 -0
@@ -7,14 +7,14 @@ import { homedir } from "node:os";
7
7
  import { join } from "node:path";
8
8
  import chalk from "chalk";
9
9
  import { contextFileCapability } from "../capability/context-file";
10
- import type { Rule } from "../capability/rule";
11
10
  import { systemPromptCapability } from "../capability/system-prompt";
12
11
  import { type ContextFile, loadSync, type SystemPrompt as SystemPromptFile } from "../discovery/index";
13
- import systemPromptTemplate from "../prompts/system-prompt.md" with { type: "text" };
12
+ import customSystemPromptTemplate from "../prompts/system/custom-system-prompt.md" with { type: "text" };
13
+ import systemPromptTemplate from "../prompts/system/system-prompt.md" with { type: "text" };
14
+ import { renderPromptTemplate } from "./prompt-templates";
14
15
  import type { SkillsSettings } from "./settings-manager";
15
- import { formatSkillsForPrompt, loadSkills, type Skill } from "./skills";
16
+ import { loadSkills, type Skill } from "./skills";
16
17
  import type { ToolName } from "./tools/index";
17
- import { formatRulesForPrompt } from "./tools/rulebook";
18
18
 
19
19
  /**
20
20
  * Execute a git command synchronously and return stdout or null on failure.
@@ -25,11 +25,19 @@ function execGit(args: string[], cwd: string): string | null {
25
25
  return result.stdout.toString().trim() || null;
26
26
  }
27
27
 
28
+ interface GitContext {
29
+ isRepo: boolean;
30
+ currentBranch: string;
31
+ mainBranch: string;
32
+ status: string;
33
+ commits: string;
34
+ }
35
+
28
36
  /**
29
37
  * Load git context for the system prompt.
30
- * Returns formatted git status or null if not in a git repo.
38
+ * Returns structured git data or null if not in a git repo.
31
39
  */
32
- export function loadGitContext(cwd: string): string | null {
40
+ export function loadGitContext(cwd: string): GitContext | null {
33
41
  // Check if inside a git repo
34
42
  const isGitRepo = execGit(["rev-parse", "--is-inside-work-tree"], cwd);
35
43
  if (isGitRepo !== "true") return null;
@@ -48,22 +56,19 @@ export function loadGitContext(cwd: string): string | null {
48
56
 
49
57
  // Get git status (porcelain format for parsing)
50
58
  const gitStatus = execGit(["status", "--porcelain"], cwd);
51
- const statusText = gitStatus?.trim() || "(clean)";
59
+ const status = gitStatus?.trim() || "(clean)";
52
60
 
53
61
  // Get recent commits
54
62
  const recentCommits = execGit(["log", "--oneline", "-5"], cwd);
55
- const commitsText = recentCommits?.trim() || "(no commits)";
56
-
57
- return `This is the git status at the start of the conversation. Note that this status is a snapshot in time, and will not update during the conversation.
58
- Current branch: ${currentBranch}
63
+ const commits = recentCommits?.trim() || "(no commits)";
59
64
 
60
- Main branch (you will usually use this for PRs): ${mainBranch}
61
-
62
- Status:
63
- ${statusText}
64
-
65
- Recent commits:
66
- ${commitsText}`;
65
+ return {
66
+ isRepo: true,
67
+ currentBranch,
68
+ mainBranch,
69
+ status,
70
+ commits,
71
+ };
67
72
  }
68
73
 
69
74
  /** Tool descriptions for system prompt */
@@ -88,47 +93,6 @@ const toolDescriptions: Record<ToolName, string> = {
88
93
  report_finding: "Report a finding during code review",
89
94
  };
90
95
 
91
- function applyTemplate(template: string, values: Record<string, string>): string {
92
- let output = template;
93
- for (const [key, value] of Object.entries(values)) {
94
- output = output.replaceAll(`{{${key}}}`, value);
95
- }
96
- return output;
97
- }
98
-
99
- function appendBlock(prompt: string, block: string | null | undefined, separator = "\n\n"): string {
100
- if (!block) return prompt;
101
- if (block.startsWith("\n")) {
102
- return `${prompt}${block}`;
103
- }
104
- return `${prompt}${separator}${block}`;
105
- }
106
-
107
- function appendSection(prompt: string, title: string, content: string | null | undefined): string {
108
- if (!content) return prompt;
109
- return `${prompt}\n\n# ${title}\n\n${content}`;
110
- }
111
-
112
- function formatProjectContext(contextFiles: Array<{ path: string; content: string; depth?: number }>): string | null {
113
- if (contextFiles.length === 0) return null;
114
- const parts: string[] = ["The following project context files have been loaded:", ""];
115
- for (const { path: filePath, content } of contextFiles) {
116
- parts.push(`## ${filePath}`, "", content, "");
117
- }
118
- return parts.join("\n").trimEnd();
119
- }
120
-
121
- function formatToolDescriptions(tools: Map<string, { description: string; label: string }> | undefined): string | null {
122
- if (!tools || tools.size === 0) return null;
123
- return Array.from(tools.entries())
124
- .map(([name, { description }]) => `- ${name}: ${description}`)
125
- .join("\n");
126
- }
127
-
128
- function buildPromptFooter(dateTime: string, cwd: string): string {
129
- return `Current date and time: ${dateTime}\nCurrent working directory: ${cwd}`;
130
- }
131
-
132
96
  function execCommand(args: string[]): string | null {
133
97
  const result = Bun.spawnSync(args, { stdin: "ignore", stdout: "pipe", stderr: "pipe" });
134
98
  if (result.exitCode !== 0) return null;
@@ -184,6 +148,45 @@ function stripQuotes(value: string): string {
184
148
  return value.replace(/^"|"$/g, "");
185
149
  }
186
150
 
151
+ const AGENTS_MD_PATTERN = "**/AGENTS.md";
152
+ const AGENTS_MD_LIMIT = 200;
153
+
154
+ interface AgentsMdSearch {
155
+ scopePath: string;
156
+ limit: number;
157
+ pattern: string;
158
+ files: string[];
159
+ }
160
+
161
+ function normalizePath(value: string): string {
162
+ return value.replace(/\\/g, "/");
163
+ }
164
+
165
+ function listAgentsMdFiles(root: string, limit: number): string[] {
166
+ try {
167
+ const entries = Array.from(
168
+ new Bun.Glob(AGENTS_MD_PATTERN).scanSync({ cwd: root, onlyFiles: true, dot: false, absolute: false }),
169
+ );
170
+ const normalized = entries
171
+ .map((entry) => normalizePath(entry))
172
+ .filter((entry) => entry.length > 0 && !entry.includes("node_modules"))
173
+ .sort();
174
+ return normalized.length > limit ? normalized.slice(0, limit) : normalized;
175
+ } catch {
176
+ return [];
177
+ }
178
+ }
179
+
180
+ function buildAgentsMdSearch(cwd: string): AgentsMdSearch {
181
+ const files = listAgentsMdFiles(cwd, AGENTS_MD_LIMIT);
182
+ return {
183
+ scopePath: ".",
184
+ limit: AGENTS_MD_LIMIT,
185
+ pattern: AGENTS_MD_PATTERN,
186
+ files,
187
+ };
188
+ }
189
+
187
190
  function getOsName(): string {
188
191
  switch (process.platform) {
189
192
  case "win32":
@@ -504,7 +507,7 @@ function getDiskInfo(): string | null {
504
507
  }
505
508
  }
506
509
 
507
- function formatEnvironmentInfo(): string {
510
+ function getEnvironmentInfo(): Array<{ label: string; value: string }> {
508
511
  // Load cached system info or collect fresh
509
512
  let sysInfo = loadSystemInfoCache();
510
513
  if (!sysInfo) {
@@ -512,127 +515,19 @@ function formatEnvironmentInfo(): string {
512
515
  saveSystemInfoCache(sysInfo);
513
516
  }
514
517
 
515
- // Session-specific values (not cached)
516
- const items: Array<[string, string]> = [
517
- ["OS", sysInfo.os],
518
- ["Distro", sysInfo.distro],
519
- ["Kernel", sysInfo.kernel],
520
- ["Arch", sysInfo.arch],
521
- ["CPU", sysInfo.cpu],
522
- ["GPU", sysInfo.gpu],
523
- ["Disk", sysInfo.disk],
524
- ["Shell", getShellName()],
525
- ["Terminal", getTerminalName()],
526
- ["DE", getDesktopEnvironment()],
527
- ["WM", getWindowManager()],
518
+ return [
519
+ { label: "OS", value: sysInfo.os },
520
+ { label: "Distro", value: sysInfo.distro },
521
+ { label: "Kernel", value: sysInfo.kernel },
522
+ { label: "Arch", value: sysInfo.arch },
523
+ { label: "CPU", value: sysInfo.cpu },
524
+ { label: "GPU", value: sysInfo.gpu },
525
+ { label: "Disk", value: sysInfo.disk },
526
+ { label: "Shell", value: getShellName() },
527
+ { label: "Terminal", value: getTerminalName() },
528
+ { label: "DE", value: getDesktopEnvironment() },
529
+ { label: "WM", value: getWindowManager() },
528
530
  ];
529
- return items.map(([label, value]) => `- ${label}: ${value}`).join("\n");
530
- }
531
-
532
- /**
533
- * Generate anti-bash rules section if the agent has both bash and specialized tools.
534
- * Only include rules for tools that are actually available.
535
- */
536
- function generateAntiBashRules(tools: ToolName[]): string | null {
537
- const hasBash = tools.includes("bash");
538
- if (!hasBash) return null;
539
-
540
- const hasRead = tools.includes("read");
541
- const hasGrep = tools.includes("grep");
542
- const hasFind = tools.includes("find");
543
- const hasLs = tools.includes("ls");
544
- const hasEdit = tools.includes("edit");
545
- const hasLsp = tools.includes("lsp");
546
- const hasGit = tools.includes("git");
547
-
548
- // Only show rules if we have specialized tools that should be preferred
549
- const hasSpecializedTools = hasRead || hasGrep || hasFind || hasLs || hasEdit || hasGit;
550
- if (!hasSpecializedTools) return null;
551
-
552
- const lines: string[] = [];
553
- lines.push("## Tool Usage Rules — MANDATORY\n");
554
- lines.push("### Forbidden Bash Patterns");
555
- lines.push("NEVER use bash for these operations:\n");
556
-
557
- if (hasRead) lines.push("- **File reading**: Use `read` instead of cat/head/tail/less/more");
558
- if (hasGrep) lines.push("- **Content search**: Use `grep` instead of grep/rg/ag/ack");
559
- if (hasFind) lines.push("- **File finding**: Use `find` instead of find/fd/locate");
560
- if (hasLs) lines.push("- **Directory listing**: Use `ls` instead of bash ls");
561
- if (hasEdit) lines.push("- **File editing**: Use `edit` instead of sed/awk/perl -pi/echo >/cat <<EOF");
562
- if (hasGit) lines.push("- **Git operations**: Use `git` tool instead of bash git commands");
563
-
564
- lines.push("\n### Tool Preference (highest → lowest priority)");
565
- const ladder: string[] = [];
566
- if (hasLsp) ladder.push("lsp (go-to-definition, references, type info) — DETERMINISTIC");
567
- if (hasGrep) ladder.push("grep (text/regex search)");
568
- if (hasFind) ladder.push("find (locate files by pattern)");
569
- if (hasRead) ladder.push("read (view file contents)");
570
- if (hasEdit) ladder.push("edit (precise text replacement)");
571
- if (hasGit) ladder.push("git (structured git operations with safety guards)");
572
- ladder.push(`bash (ONLY for ${hasGit ? "" : "git, "}npm, docker, make, cargo, etc.)`);
573
- lines.push(ladder.map((t, i) => `${i + 1}. ${t}`).join("\n"));
574
-
575
- // Add LSP guidance if available
576
- if (hasLsp) {
577
- lines.push("\n### LSP — Preferred for Semantic Queries");
578
- lines.push("Use `lsp` instead of grep/bash when you need:");
579
- lines.push("- **Where is X defined?** → `lsp definition`");
580
- lines.push("- **What calls X?** → `lsp incoming_calls`");
581
- lines.push("- **What does X call?** → `lsp outgoing_calls`");
582
- lines.push("- **What type is X?** → `lsp hover`");
583
- lines.push("- **What symbols are in this file?** → `lsp symbols`");
584
- lines.push("- **Find symbol across codebase** → `lsp workspace_symbols`\n");
585
- }
586
-
587
- // Add Git guidance if available
588
- if (hasGit) {
589
- lines.push("\n### Git Tool — Preferred for Git Operations");
590
- lines.push("Use `git` instead of bash git when you need:");
591
- lines.push(
592
- "- **Status/diff/log**: `git { operation: 'status' }`, `git { operation: 'diff' }`, `git { operation: 'log' }`",
593
- );
594
- lines.push(
595
- "- **Commit workflow**: `git { operation: 'add', paths: [...] }` then `git { operation: 'commit', message: '...' }`",
596
- );
597
- lines.push("- **Branching**: `git { operation: 'branch', action: 'create', name: '...' }`");
598
- lines.push("- **GitHub PRs**: `git { operation: 'pr', action: 'create', title: '...', body: '...' }`");
599
- lines.push(
600
- "- **GitHub Issues**: `git { operation: 'issue', action: 'list' }` or `{ operation: 'issue', number: 123 }`",
601
- );
602
- lines.push(
603
- "The git tool provides typed output, safety guards, and a clean API for all git and GitHub operations.\n",
604
- );
605
- }
606
-
607
- // Add SSH remote filesystem guidance if available
608
- const hasSSH = tools.includes("ssh");
609
- if (hasSSH) {
610
- lines.push("\n### SSH Command Execution");
611
- lines.push(
612
- "**Critical**: Each SSH host runs a specific shell. **You MUST match commands to the host's shell type**.",
613
- );
614
- lines.push("Check the host list in the ssh tool description. Shell types:");
615
- lines.push("- linux/bash, linux/zsh, macos/bash, macos/zsh: ls, cat, grep, find, ps, df, uname");
616
- lines.push("- windows/bash, windows/sh: ls, cat, grep, find (Windows with WSL/Cygwin — Unix commands)");
617
- lines.push("- windows/cmd: dir, type, findstr, tasklist, systeminfo");
618
- lines.push("- windows/powershell: Get-ChildItem, Get-Content, Select-String, Get-Process");
619
- lines.push("");
620
- lines.push("### SSH Filesystems");
621
- lines.push("Mounted at `~/.omp/remote/<hostname>/` — use read/edit/write tools directly.");
622
- lines.push("Windows paths need colon: `~/.omp/remote/host/C:/Users/...` not `C/Users/...`\n");
623
- }
624
-
625
- // Add search-first protocol
626
- if (hasGrep || hasFind) {
627
- lines.push("\n### Search-First Protocol");
628
- lines.push("Before reading any file:");
629
- if (hasFind) lines.push("1. Unknown structure → `find` to see file layout");
630
- if (hasGrep) lines.push("2. Known location → `grep` for specific symbol/error");
631
- if (hasRead) lines.push("3. Use `read offset/limit` for line ranges, not entire large files");
632
- lines.push("4. Never read a large file hoping to find something — search first");
633
- }
634
-
635
- return lines.join("\n");
636
531
  }
637
532
 
638
533
  /** Resolve input as file path or literal string */
@@ -732,7 +627,7 @@ export interface BuildSystemPromptOptions {
732
627
  /** Pre-loaded skills (skips discovery if provided). */
733
628
  skills?: Skill[];
734
629
  /** Pre-loaded rulebook rules (rules with descriptions, excluding TTSR and always-apply). */
735
- rules?: Rule[];
630
+ rules?: Array<{ name: string; description?: string; path: string; globs?: string[] }>;
736
631
  }
737
632
 
738
633
  /** Build the system prompt with tools, guidelines, and context */
@@ -746,7 +641,7 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
746
641
  cwd,
747
642
  contextFiles: providedContextFiles,
748
643
  skills: providedSkills,
749
- rules: rulebookRules,
644
+ rules,
750
645
  } = options;
751
646
  const resolvedCwd = cwd ?? process.cwd();
752
647
  const resolvedCustomPrompt = resolvePromptInput(customPrompt, "system prompt");
@@ -769,122 +664,67 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
769
664
 
770
665
  // Resolve context files: use provided or discover
771
666
  const contextFiles = providedContextFiles ?? loadProjectContextFiles({ cwd: resolvedCwd });
772
-
773
- // Build tools list based on selected tools
774
- const selectedToolNames = toolNames ?? (["read", "bash", "edit", "write"] as ToolName[]);
775
- const toolsList =
776
- selectedToolNames.length > 0
777
- ? selectedToolNames.map((name) => `- ${name}: ${toolDescriptions[name as ToolName]}`).join("\n")
778
- : "(none)";
667
+ const agentsMdSearch = buildAgentsMdSearch(resolvedCwd);
668
+
669
+ // Build tool descriptions array
670
+ // Priority: toolNames (explicit list) > tools (Map) > defaults
671
+ const defaultToolNames: ToolName[] = ["read", "bash", "edit", "write"];
672
+ let toolNamesArray: string[];
673
+ if (toolNames !== undefined) {
674
+ // Explicit toolNames list provided (could be empty)
675
+ toolNamesArray = toolNames;
676
+ } else if (tools !== undefined) {
677
+ // Tools map provided
678
+ toolNamesArray = Array.from(tools.keys());
679
+ } else {
680
+ // Use defaults
681
+ toolNamesArray = defaultToolNames;
682
+ }
683
+ const toolDescriptionsArray = toolNamesArray.map((name) => ({
684
+ name,
685
+ description: toolDescriptions[name as ToolName] ?? "",
686
+ }));
779
687
 
780
688
  // Resolve skills: use provided or discover
781
689
  const skills =
782
690
  providedSkills ??
783
691
  (skillsSettings?.enabled !== false ? loadSkills({ ...skillsSettings, cwd: resolvedCwd }).skills : []);
784
692
 
785
- if (resolvedCustomPrompt) {
786
- let prompt = systemPromptCustomization
787
- ? `${systemPromptCustomization}\n\n${resolvedCustomPrompt}`
788
- : resolvedCustomPrompt;
789
-
790
- prompt = appendBlock(prompt, resolvedAppendPrompt);
791
- prompt = appendSection(prompt, "Project Context", formatProjectContext(contextFiles));
792
- prompt = appendSection(prompt, "Tools", formatToolDescriptions(tools));
793
-
794
- const gitContext = loadGitContext(resolvedCwd);
795
- prompt = appendSection(prompt, "Git Status", gitContext);
693
+ // Get git context
694
+ const git = loadGitContext(resolvedCwd);
796
695
 
797
- if (tools?.has("read") && skills.length > 0) {
798
- prompt = appendBlock(prompt, formatSkillsForPrompt(skills));
799
- }
800
-
801
- if (rulebookRules && rulebookRules.length > 0) {
802
- prompt = appendBlock(prompt, formatRulesForPrompt(rulebookRules));
803
- }
804
-
805
- prompt = appendBlock(prompt, buildPromptFooter(dateTime, resolvedCwd), "\n");
806
-
807
- return prompt;
808
- }
809
-
810
- // Generate anti-bash rules (returns null if not applicable)
811
- const antiBashSection = generateAntiBashRules(Array.from(tools?.keys() ?? []));
812
- const environmentInfo = formatEnvironmentInfo();
813
-
814
- // Build guidelines based on which tools are actually available
815
- const guidelinesList: string[] = [];
816
-
817
- const hasBash = tools?.has("bash");
818
- const hasEdit = tools?.has("edit");
819
- const hasWrite = tools?.has("write");
696
+ // Filter skills to only include those with read tool
820
697
  const hasRead = tools?.has("read");
698
+ const filteredSkills = hasRead ? skills : [];
821
699
 
822
- // Bash without edit/write = read-only bash mode
823
- if (hasBash && !hasEdit && !hasWrite) {
824
- guidelinesList.push(
825
- "Use bash ONLY for read-only operations (git log, gh issue view, curl, etc.) - do NOT modify any files",
826
- );
827
- }
828
-
829
- // Read before edit guideline
830
- if (hasRead && hasEdit) {
831
- guidelinesList.push("Use read to examine files before editing");
832
- }
833
-
834
- // Edit guideline
835
- if (hasEdit) {
836
- guidelinesList.push(
837
- "Use edit for precise changes (old text must match exactly, fuzzy matching handles whitespace)",
838
- );
839
- }
840
-
841
- // Write guideline
842
- if (hasWrite) {
843
- guidelinesList.push("Use write only for new files or complete rewrites");
844
- }
845
-
846
- // Output guideline (only when actually writing/executing)
847
- if (hasEdit || hasWrite) {
848
- guidelinesList.push(
849
- "When summarizing your actions, output plain text directly - do NOT use cat or bash to display what you did",
850
- );
851
- }
852
-
853
- // Always include these
854
- guidelinesList.push("Be concise in your responses");
855
- guidelinesList.push("Show file paths clearly when working with files");
856
-
857
- const guidelines = guidelinesList.map((g) => `- ${g}`).join("\n");
858
-
859
- // Build the prompt with anti-bash rules prominently placed
860
- const antiBashBlock = antiBashSection ? `\n${antiBashSection}\n` : "";
861
- let prompt = applyTemplate(systemPromptTemplate, {
862
- toolsList,
863
- antiBashSection: antiBashBlock,
864
- guidelines,
865
- environmentInfo,
700
+ if (resolvedCustomPrompt) {
701
+ return renderPromptTemplate(customSystemPromptTemplate, {
702
+ systemPromptCustomization: systemPromptCustomization ?? "",
703
+ customPrompt: resolvedCustomPrompt,
704
+ appendPrompt: resolvedAppendPrompt ?? "",
705
+ contextFiles,
706
+ agentsMdSearch,
707
+ toolDescriptions: toolDescriptionsArray,
708
+ git,
709
+ skills: filteredSkills,
710
+ rules: rules ?? [],
711
+ dateTime,
712
+ cwd: resolvedCwd,
713
+ });
714
+ }
715
+
716
+ return renderPromptTemplate(systemPromptTemplate, {
717
+ tools: toolNamesArray,
718
+ toolDescriptions: toolDescriptionsArray,
719
+ environment: getEnvironmentInfo(),
720
+ systemPromptCustomization: systemPromptCustomization ?? "",
721
+ contextFiles,
722
+ agentsMdSearch,
723
+ git,
724
+ skills: filteredSkills,
725
+ rules: rules ?? [],
726
+ dateTime,
727
+ cwd: resolvedCwd,
728
+ appendSystemPrompt: resolvedAppendPrompt ?? "",
866
729
  });
867
-
868
- prompt = appendBlock(prompt, resolvedAppendPrompt);
869
- prompt = appendSection(prompt, "Project Context", formatProjectContext(contextFiles));
870
-
871
- const gitContext = loadGitContext(resolvedCwd);
872
- prompt = appendSection(prompt, "Git Status", gitContext);
873
-
874
- if (hasRead && skills.length > 0) {
875
- prompt = appendBlock(prompt, formatSkillsForPrompt(skills));
876
- }
877
-
878
- if (rulebookRules && rulebookRules.length > 0) {
879
- prompt = appendBlock(prompt, formatRulesForPrompt(rulebookRules));
880
- }
881
-
882
- prompt = appendBlock(prompt, buildPromptFooter(dateTime, resolvedCwd), "\n");
883
-
884
- // Prepend SYSTEM.md customization if present
885
- if (systemPromptCustomization) {
886
- prompt = `${systemPromptCustomization}\n\n${prompt}`;
887
- }
888
-
889
- return prompt;
890
730
  }
@@ -4,12 +4,13 @@
4
4
 
5
5
  import type { Api, Model } from "@oh-my-pi/pi-ai";
6
6
  import { completeSimple } from "@oh-my-pi/pi-ai";
7
- import titleSystemPrompt from "../prompts/title-system.md" with { type: "text" };
7
+ import titleSystemPrompt from "../prompts/system/title-system.md" with { type: "text" };
8
8
  import { logger } from "./logger";
9
9
  import type { ModelRegistry } from "./model-registry";
10
10
  import { parseModelString, SMOL_MODEL_PRIORITY } from "./model-resolver";
11
+ import { renderPromptTemplate } from "./prompt-templates";
11
12
 
12
- const TITLE_SYSTEM_PROMPT = titleSystemPrompt;
13
+ const TITLE_SYSTEM_PROMPT = renderPromptTemplate(titleSystemPrompt);
13
14
 
14
15
  const MAX_INPUT_CHARS = 2000;
15
16
 
@@ -22,6 +22,7 @@ import { Type } from "@sinclair/typebox";
22
22
  import { type Theme, theme } from "../../modes/interactive/theme/theme";
23
23
  import askDescription from "../../prompts/tools/ask.md" with { type: "text" };
24
24
  import type { RenderResultOptions } from "../custom-tools/types";
25
+ import { renderPromptTemplate } from "../prompt-templates";
25
26
  import type { ToolSession } from "./index";
26
27
  import { createToolUIKit } from "./render-utils";
27
28
 
@@ -75,7 +76,7 @@ export function createAskTool(session: ToolSession): null | AgentTool<typeof ask
75
76
  return {
76
77
  name: "ask",
77
78
  label: "Ask",
78
- description: askDescription,
79
+ description: renderPromptTemplate(askDescription),
79
80
  parameters: askSchema,
80
81
 
81
82
  async execute(
@@ -201,7 +202,6 @@ export function createAskTool(session: ToolSession): null | AgentTool<typeof ask
201
202
  export const askTool = createAskTool({
202
203
  cwd: process.cwd(),
203
204
  hasUI: false,
204
- rulebookRules: [],
205
205
  getSessionFile: () => null,
206
206
  getSessionSpawns: () => "*",
207
207
  });
@@ -8,6 +8,7 @@ import type { Theme } from "../../modes/interactive/theme/theme";
8
8
  import bashDescription from "../../prompts/tools/bash.md" with { type: "text" };
9
9
  import { type BashExecutorOptions, executeBash, executeBashWithOperations } from "../bash-executor";
10
10
  import type { RenderResultOptions } from "../custom-tools/types";
11
+ import { renderPromptTemplate } from "../prompt-templates";
11
12
  import { checkBashInterception, checkSimpleLsInterception } from "./bash-interceptor";
12
13
  import type { ToolSession } from "./index";
13
14
  import { resolveToCwd } from "./path-utils";
@@ -52,7 +53,7 @@ export function createBashTool(session: ToolSession, options?: BashToolOptions):
52
53
  return {
53
54
  name: "bash",
54
55
  label: "Bash",
55
- description: bashDescription,
56
+ description: renderPromptTemplate(bashDescription),
56
57
  parameters: bashSchema,
57
58
  execute: async (
58
59
  _toolCallId: string,
@@ -5,6 +5,7 @@ import { Type } from "@sinclair/typebox";
5
5
  import type { Theme } from "../../modes/interactive/theme/theme";
6
6
  import calculatorDescription from "../../prompts/tools/calculator.md" with { type: "text" };
7
7
  import type { RenderResultOptions } from "../custom-tools/types";
8
+ import { renderPromptTemplate } from "../prompt-templates";
8
9
  import { untilAborted } from "../utils";
9
10
  import type { ToolSession } from "./index";
10
11
  import {
@@ -393,7 +394,7 @@ export function createCalculatorTool(_session: ToolSession): AgentTool<typeof ca
393
394
  return {
394
395
  name: "calc",
395
396
  label: "Calc",
396
- description: calculatorDescription,
397
+ description: renderPromptTemplate(calculatorDescription),
397
398
  parameters: calculatorSchema,
398
399
  execute: async (
399
400
  _toolCallId: string,
@@ -79,7 +79,7 @@ export function createCompleteTool(session: ToolSession) {
79
79
  : Type.Any({ description: "Structured JSON output (no schema specified)" });
80
80
 
81
81
  const completeParams = Type.Object({
82
- data: dataSchema,
82
+ data: Type.Optional(dataSchema),
83
83
  status: Type.Optional(
84
84
  Type.Union([Type.Literal("success"), Type.Literal("aborted")], {
85
85
  default: "success",
@@ -99,8 +99,11 @@ export function createCompleteTool(session: ToolSession) {
99
99
  execute: async (_toolCallId, params) => {
100
100
  const status = params.status ?? "success";
101
101
 
102
- // Skip schema validation when aborting - the agent is giving up
102
+ // Skip validation when aborting - data is optional for aborts
103
103
  if (status === "success") {
104
+ if (params.data === undefined) {
105
+ throw new Error("data is required when status is 'success'");
106
+ }
104
107
  if (schemaError) {
105
108
  throw new Error(`Invalid output schema: ${schemaError}`);
106
109
  }
@@ -5,6 +5,7 @@ import { Type } from "@sinclair/typebox";
5
5
  import { getLanguageFromPath, type Theme } from "../../modes/interactive/theme/theme";
6
6
  import editDescription from "../../prompts/tools/edit.md" with { type: "text" };
7
7
  import type { RenderResultOptions } from "../custom-tools/types";
8
+ import { renderPromptTemplate } from "../prompt-templates";
8
9
  import {
9
10
  DEFAULT_FUZZY_THRESHOLD,
10
11
  detectLineEnding,
@@ -48,7 +49,7 @@ export function createEditTool(session: ToolSession): AgentTool<typeof editSchem
48
49
  return {
49
50
  name: "edit",
50
51
  label: "Edit",
51
- description: editDescription,
52
+ description: renderPromptTemplate(editDescription),
52
53
  parameters: editSchema,
53
54
  execute: async (
54
55
  _toolCallId: string,
@@ -7,6 +7,7 @@ import { getLanguageFromPath, type Theme } from "../../modes/interactive/theme/t
7
7
  import findDescription from "../../prompts/tools/find.md" with { type: "text" };
8
8
  import { ensureTool } from "../../utils/tools-manager";
9
9
  import type { RenderResultOptions } from "../custom-tools/types";
10
+ import { renderPromptTemplate } from "../prompt-templates";
10
11
  import { ScopeSignal, untilAborted } from "../utils";
11
12
  import type { ToolSession } from "./index";
12
13
  import { resolveToCwd } from "./path-utils";
@@ -113,7 +114,7 @@ export function createFindTool(session: ToolSession, options?: FindToolOptions):
113
114
  return {
114
115
  name: "find",
115
116
  label: "Find",
116
- description: findDescription,
117
+ description: renderPromptTemplate(findDescription),
117
118
  parameters: findSchema,
118
119
  execute: async (
119
120
  _toolCallId: string,
@@ -5,6 +5,7 @@ import { nanoid } from "nanoid";
5
5
  import geminiImageDescription from "../../prompts/tools/gemini-image.md" with { type: "text" };
6
6
  import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime";
7
7
  import type { CustomTool } from "../custom-tools/types";
8
+ import { renderPromptTemplate } from "../prompt-templates";
8
9
  import { untilAborted } from "../utils";
9
10
  import { resolveReadPath } from "./path-utils";
10
11
  import { getEnv } from "./web-search/auth";
@@ -367,7 +368,7 @@ function createRequestSignal(signal: AbortSignal | undefined, timeoutSeconds: nu
367
368
  export const geminiImageTool: CustomTool<typeof geminiImageSchema, GeminiImageToolDetails> = {
368
369
  name: "generate_image",
369
370
  label: "GenerateImage",
370
- description: geminiImageDescription,
371
+ description: renderPromptTemplate(geminiImageDescription),
371
372
  parameters: geminiImageSchema,
372
373
  async execute(_toolCallId, params, _onUpdate, ctx, signal) {
373
374
  return untilAborted(signal, async () => {