@oh-my-pi/pi-coding-agent 9.4.0 → 9.6.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 (70) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/package.json +9 -8
  3. package/src/capability/index.ts +7 -9
  4. package/src/cli/config-cli.ts +86 -73
  5. package/src/cli/update-cli.ts +45 -3
  6. package/src/commit/agentic/agent.ts +4 -4
  7. package/src/commit/agentic/index.ts +6 -5
  8. package/src/commit/agentic/tools/analyze-file.ts +5 -7
  9. package/src/commit/agentic/tools/index.ts +3 -3
  10. package/src/commit/model-selection.ts +13 -17
  11. package/src/commit/pipeline.ts +5 -5
  12. package/src/config/model-registry.ts +7 -0
  13. package/src/config/settings-schema.ts +836 -0
  14. package/src/config/settings.ts +702 -0
  15. package/src/discovery/helpers.ts +55 -11
  16. package/src/exa/index.ts +1 -1
  17. package/src/exec/bash-executor.ts +13 -13
  18. package/src/exec/shell-session.ts +15 -3
  19. package/src/export/ttsr.ts +1 -1
  20. package/src/extensibility/skills.ts +40 -9
  21. package/src/index.ts +2 -10
  22. package/src/ipy/gateway-coordinator.ts +5 -143
  23. package/src/ipy/kernel.ts +11 -171
  24. package/src/ipy/runtime.ts +198 -0
  25. package/src/lsp/client.ts +14 -1
  26. package/src/lsp/defaults.json +0 -6
  27. package/src/lsp/index.ts +1 -1
  28. package/src/lsp/types.ts +2 -0
  29. package/src/main.ts +26 -48
  30. package/src/modes/components/extensions/extension-dashboard.ts +22 -11
  31. package/src/modes/components/index.ts +1 -1
  32. package/src/modes/components/model-selector.ts +7 -7
  33. package/src/modes/components/settings-defs.ts +210 -915
  34. package/src/modes/components/settings-selector.ts +80 -106
  35. package/src/modes/components/status-line/types.ts +2 -8
  36. package/src/modes/components/status-line-segment-editor.ts +1 -1
  37. package/src/modes/components/status-line.ts +26 -3
  38. package/src/modes/controllers/event-controller.ts +9 -8
  39. package/src/modes/controllers/input-controller.ts +19 -15
  40. package/src/modes/controllers/selector-controller.ts +30 -14
  41. package/src/modes/interactive-mode.ts +10 -10
  42. package/src/modes/rpc/rpc-mode.ts +10 -0
  43. package/src/modes/rpc/rpc-types.ts +3 -0
  44. package/src/modes/types.ts +2 -2
  45. package/src/modes/utils/ui-helpers.ts +4 -3
  46. package/src/patch/index.ts +7 -7
  47. package/src/prompts/system/system-prompt.md +0 -1
  48. package/src/prompts/tools/bash.md +12 -2
  49. package/src/prompts/tools/task.md +180 -73
  50. package/src/sdk.ts +38 -61
  51. package/src/session/agent-session.ts +66 -55
  52. package/src/session/agent-storage.ts +1 -1
  53. package/src/session/session-manager.ts +10 -10
  54. package/src/system-prompt.ts +2 -2
  55. package/src/task/executor.ts +9 -9
  56. package/src/task/index.ts +2 -2
  57. package/src/tools/ask.ts +5 -6
  58. package/src/tools/bash-interceptor.ts +39 -1
  59. package/src/tools/bash-normalize.ts +126 -0
  60. package/src/tools/bash.ts +31 -5
  61. package/src/tools/find.ts +51 -33
  62. package/src/tools/index.ts +5 -23
  63. package/src/tools/plan-mode-guard.ts +1 -6
  64. package/src/tools/python.ts +2 -2
  65. package/src/tools/read.ts +2 -2
  66. package/src/tools/write.ts +2 -2
  67. package/src/utils/ignore-files.ts +119 -0
  68. package/src/web/search/providers/perplexity.ts +1 -1
  69. package/examples/sdk/10-settings.ts +0 -37
  70. package/src/config/settings-manager.ts +0 -2015
@@ -3,6 +3,7 @@ import type { OAuthProvider } from "@oh-my-pi/pi-ai";
3
3
  import type { Component } from "@oh-my-pi/pi-tui";
4
4
  import { Input, Loader, Spacer, Text } from "@oh-my-pi/pi-tui";
5
5
  import { getAgentDbPath } from "../../config";
6
+ import { settings } from "../../config/settings";
6
7
  import { DebugSelectorComponent } from "../../debug";
7
8
  import { disableProvider, enableProvider } from "../../discovery";
8
9
  import { AssistantMessageComponent } from "../../modes/components/assistant-message";
@@ -51,7 +52,6 @@ export class SelectorController {
51
52
  getAvailableThemes().then(availableThemes => {
52
53
  this.showSelector(done => {
53
54
  const selector = new SettingsSelectorComponent(
54
- this.ctx.settingsManager,
55
55
  {
56
56
  availableThinkingLevels: this.ctx.session.getAvailableThinkingLevels(),
57
57
  thinkingLevel: this.ctx.session.thinkingLevel,
@@ -68,10 +68,16 @@ export class SelectorController {
68
68
  }
69
69
  });
70
70
  },
71
- onStatusLinePreview: settings => {
71
+ onStatusLinePreview: previewSettings => {
72
72
  // Update status line with preview settings
73
- const currentSettings = this.ctx.settingsManager.getStatusLineSettings();
74
- this.ctx.statusLine.updateSettings({ ...currentSettings, ...settings });
73
+ this.ctx.statusLine.updateSettings({
74
+ preset: settings.get("statusLine.preset"),
75
+ leftSegments: settings.get("statusLine.leftSegments"),
76
+ rightSegments: settings.get("statusLine.rightSegments"),
77
+ separator: settings.get("statusLine.separator"),
78
+ showHookStatus: settings.get("statusLine.showHookStatus"),
79
+ ...previewSettings,
80
+ });
75
81
  this.ctx.updateEditorTopBorder();
76
82
  this.ctx.ui.requestRender();
77
83
  },
@@ -86,7 +92,13 @@ export class SelectorController {
86
92
  onCancel: () => {
87
93
  done();
88
94
  // Restore status line to saved settings
89
- this.ctx.statusLine.updateSettings(this.ctx.settingsManager.getStatusLineSettings());
95
+ this.ctx.statusLine.updateSettings({
96
+ preset: settings.get("statusLine.preset"),
97
+ leftSegments: settings.get("statusLine.leftSegments"),
98
+ rightSegments: settings.get("statusLine.rightSegments"),
99
+ separator: settings.get("statusLine.separator"),
100
+ showHookStatus: settings.get("statusLine.showHookStatus"),
101
+ });
90
102
  this.ctx.updateEditorTopBorder();
91
103
  this.ctx.ui.requestRender();
92
104
  },
@@ -123,11 +135,7 @@ export class SelectorController {
123
135
  * Replaces /status with a unified view of all providers and extensions.
124
136
  */
125
137
  async showExtensionsDashboard(): Promise<void> {
126
- const dashboard = await ExtensionDashboard.create(
127
- process.cwd(),
128
- this.ctx.settingsManager,
129
- this.ctx.ui.terminal.rows,
130
- );
138
+ const dashboard = await ExtensionDashboard.create(process.cwd(), this.ctx.settings, this.ctx.ui.terminal.rows);
131
139
  this.showSelector(done => {
132
140
  dashboard.onClose = () => {
133
141
  done();
@@ -142,7 +150,7 @@ export class SelectorController {
142
150
  * Most settings are saved directly via SettingsManager in the definitions.
143
151
  * This handles side effects and session-specific settings.
144
152
  */
145
- handleSettingChange(id: string, value: string | boolean): void {
153
+ handleSettingChange(id: string, value: unknown): void {
146
154
  // Discovery provider toggles
147
155
  if (id.startsWith("discovery.")) {
148
156
  const providerId = id.replace("discovery.", "");
@@ -232,7 +240,15 @@ export class SelectorController {
232
240
  case "statusLineGitShowUntracked":
233
241
  case "statusLineTimeFormat":
234
242
  case "statusLineTimeShowSeconds": {
235
- this.ctx.statusLine.updateSettings(this.ctx.settingsManager.getStatusLineSettings());
243
+ const statusLineSettings = {
244
+ preset: settings.get("statusLine.preset"),
245
+ leftSegments: settings.get("statusLine.leftSegments"),
246
+ rightSegments: settings.get("statusLine.rightSegments"),
247
+ separator: settings.get("statusLine.separator"),
248
+ showHookStatus: settings.get("statusLine.showHookStatus"),
249
+ segmentOptions: settings.get("statusLine.segmentOptions"),
250
+ };
251
+ this.ctx.statusLine.updateSettings(statusLineSettings);
236
252
  this.ctx.updateEditorTopBorder();
237
253
  this.ctx.ui.requestRender();
238
254
  break;
@@ -256,7 +272,7 @@ export class SelectorController {
256
272
  const selector = new ModelSelectorComponent(
257
273
  this.ctx.ui,
258
274
  this.ctx.session.model,
259
- this.ctx.settingsManager,
275
+ this.ctx.settings,
260
276
  this.ctx.session.modelRegistry,
261
277
  this.ctx.session.scopedModels,
262
278
  async (model, role) => {
@@ -369,7 +385,7 @@ export class SelectorController {
369
385
  let wantsSummary = false;
370
386
  let customInstructions: string | undefined;
371
387
 
372
- const branchSummariesEnabled = this.ctx.settingsManager.getBranchSummaryEnabled();
388
+ const branchSummariesEnabled = settings.get("branchSummary.enabled");
373
389
 
374
390
  while (branchSummariesEnabled) {
375
391
  const summaryChoice = await this.ctx.showHookSelector("Summarize branch?", [
@@ -19,7 +19,7 @@ import { isEnoent, logger, postmortem } from "@oh-my-pi/pi-utils";
19
19
  import chalk from "chalk";
20
20
  import { KeybindingsManager } from "../config/keybindings";
21
21
  import { renderPromptTemplate } from "../config/prompt-templates";
22
- import type { SettingsManager } from "../config/settings-manager";
22
+ import { type Settings, settings } from "../config/settings";
23
23
  import type { ExtensionUIContext, ExtensionUIDialogOptions } from "../extensibility/extensions";
24
24
  import type { CompactOptions } from "../extensibility/extensions/types";
25
25
  import { loadSlashCommands } from "../extensibility/slash-commands";
@@ -72,7 +72,7 @@ export interface InteractiveModeOptions {
72
72
  export class InteractiveMode implements InteractiveModeContext {
73
73
  public session: AgentSession;
74
74
  public sessionManager: SessionManager;
75
- public settingsManager: SettingsManager;
75
+ public settings: Settings;
76
76
  public keybindings: KeybindingsManager;
77
77
  public agent: AgentSession["agent"];
78
78
  public historyStorage?: HistoryStorage;
@@ -159,7 +159,7 @@ export class InteractiveMode implements InteractiveModeContext {
159
159
  ) {
160
160
  this.session = session;
161
161
  this.sessionManager = session.sessionManager;
162
- this.settingsManager = session.settingsManager;
162
+ this.settings = session.settings;
163
163
  this.keybindings = KeybindingsManager.inMemory();
164
164
  this.agent = session.agent;
165
165
  this.version = version;
@@ -168,7 +168,7 @@ export class InteractiveMode implements InteractiveModeContext {
168
168
  this.lspServers = lspServers;
169
169
  this.mcpManager = mcpManager;
170
170
 
171
- this.ui = new TUI(new ProcessTerminal(), this.settingsManager.getShowHardwareCursor());
171
+ this.ui = new TUI(new ProcessTerminal(), settings.get("showHardwareCursor"));
172
172
  setMermaidRenderCallback(() => this.ui.requestRender());
173
173
  this.chatContainer = new Container();
174
174
  this.pendingMessagesContainer = new Container();
@@ -193,7 +193,7 @@ export class InteractiveMode implements InteractiveModeContext {
193
193
  this.statusLine = new StatusLineComponent(session);
194
194
  this.statusLine.setAutoCompactEnabled(session.autoCompactionEnabled);
195
195
 
196
- this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
196
+ this.hideThinkingBlock = settings.get("hideThinkingBlock");
197
197
 
198
198
  // Define slash commands for autocomplete
199
199
  const slashCommands: SlashCommand[] = [
@@ -240,7 +240,7 @@ export class InteractiveMode implements InteractiveModeContext {
240
240
 
241
241
  // Build skill commands from session.skills (if enabled)
242
242
  const skillCommandList: SlashCommand[] = [];
243
- if (this.settingsManager.getEnableSkillCommands?.()) {
243
+ if (settings.get("skills.enableSkillCommands")) {
244
244
  for (const skill of this.session.skills) {
245
245
  const commandName = `skill:${skill.name}`;
246
246
  this.skillCommands.set(commandName, skill.filePath);
@@ -300,7 +300,7 @@ export class InteractiveMode implements InteractiveModeContext {
300
300
  fileTypes: s.fileTypes,
301
301
  })) ?? [];
302
302
 
303
- const startupQuiet = this.settingsManager.getStartupQuiet();
303
+ const startupQuiet = settings.get("startup.quiet");
304
304
 
305
305
  if (!startupQuiet) {
306
306
  // Add welcome header
@@ -314,7 +314,7 @@ export class InteractiveMode implements InteractiveModeContext {
314
314
  // Add changelog if provided
315
315
  if (this.changelogMarkdown) {
316
316
  this.ui.addChild(new DynamicBorder());
317
- if (this.settingsManager.getCollapseChangelog()) {
317
+ if (settings.get("collapseChangelog")) {
318
318
  const versionMatch = this.changelogMarkdown.match(/##\s+\[?(\d+\.\d+\.\d+)\]?/);
319
319
  const latestVersion = versionMatch ? versionMatch[1] : this.version;
320
320
  const condensedText = `Updated to v${latestVersion}. Use ${theme.bold("/changelog")} to view full changelog.`;
@@ -330,7 +330,7 @@ export class InteractiveMode implements InteractiveModeContext {
330
330
  }
331
331
 
332
332
  // Set terminal title if session already has one (resumed session)
333
- const existingTitle = this.sessionManager.getSessionTitle();
333
+ const existingTitle = this.sessionManager.getSessionName();
334
334
  if (existingTitle) {
335
335
  setTerminalTitle(`pi: ${existingTitle}`);
336
336
  }
@@ -494,7 +494,7 @@ export class InteractiveMode implements InteractiveModeContext {
494
494
  private resolvePlanFilePath(planFilePath: string): string {
495
495
  if (planFilePath.startsWith("plan://")) {
496
496
  return resolvePlanUrlToPath(planFilePath, {
497
- getPlansDirectory: this.settingsManager.getPlansDirectory.bind(this.settingsManager),
497
+ getPlansDirectory: () => this.settings.getPlansDirectory(),
498
498
  cwd: this.sessionManager.getCwd(),
499
499
  });
500
500
  }
@@ -448,6 +448,7 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
448
448
  interruptMode: session.interruptMode,
449
449
  sessionFile: session.sessionFile,
450
450
  sessionId: session.sessionId,
451
+ sessionName: session.sessionName,
451
452
  autoCompactionEnabled: session.autoCompactionEnabled,
452
453
  messageCount: session.messages.length,
453
454
  queuedMessageCount: session.queuedMessageCount,
@@ -594,6 +595,15 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
594
595
  return success(id, "get_last_assistant_text", { text });
595
596
  }
596
597
 
598
+ case "set_session_name": {
599
+ const name = command.name.trim();
600
+ if (!name) {
601
+ return error(id, "set_session_name", "Session name cannot be empty");
602
+ }
603
+ session.setSessionName(name);
604
+ return success(id, "set_session_name");
605
+ }
606
+
597
607
  // =================================================================
598
608
  // Messages
599
609
  // =================================================================
@@ -58,6 +58,7 @@ export type RpcCommand =
58
58
  | { id?: string; type: "branch"; entryId: string }
59
59
  | { id?: string; type: "get_branch_messages" }
60
60
  | { id?: string; type: "get_last_assistant_text" }
61
+ | { id?: string; type: "set_session_name"; name: string }
61
62
 
62
63
  // Messages
63
64
  | { id?: string; type: "get_messages" };
@@ -76,6 +77,7 @@ export interface RpcSessionState {
76
77
  interruptMode: "immediate" | "wait";
77
78
  sessionFile?: string;
78
79
  sessionId: string;
80
+ sessionName?: string;
79
81
  autoCompactionEnabled: boolean;
80
82
  messageCount: number;
81
83
  queuedMessageCount: number;
@@ -166,6 +168,7 @@ export type RpcResponse =
166
168
  success: true;
167
169
  data: { text: string | null };
168
170
  }
171
+ | { id?: string; type: "response"; command: "set_session_name"; success: true }
169
172
 
170
173
  // Messages
171
174
  | { id?: string; type: "response"; command: "get_messages"; success: true; data: { messages: AgentMessage[] } }
@@ -2,7 +2,7 @@ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
2
2
  import type { AssistantMessage, ImageContent, Message, UsageReport } from "@oh-my-pi/pi-ai";
3
3
  import type { Component, Container, Loader, Spacer, Text, TUI } from "@oh-my-pi/pi-tui";
4
4
  import type { KeybindingsManager } from "../config/keybindings";
5
- import type { SettingsManager } from "../config/settings-manager";
5
+ import type { Settings } from "../config/settings";
6
6
  import type { ExtensionUIContext, ExtensionUIDialogOptions } from "../extensibility/extensions";
7
7
  import type { CompactOptions } from "../extensibility/extensions/types";
8
8
  import type { MCPManager } from "../mcp";
@@ -46,7 +46,7 @@ export interface InteractiveModeContext {
46
46
  // Session access
47
47
  session: AgentSession;
48
48
  sessionManager: SessionManager;
49
- settingsManager: SettingsManager;
49
+ settings: Settings;
50
50
  keybindings: KeybindingsManager;
51
51
  agent: AgentSession["agent"];
52
52
  historyStorage?: HistoryStorage;
@@ -1,6 +1,7 @@
1
1
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
2
2
  import type { AssistantMessage, Message } from "@oh-my-pi/pi-ai";
3
3
  import { Spacer, Text, TruncatedText } from "@oh-my-pi/pi-tui";
4
+ import { settings } from "../../config/settings";
4
5
  import { AssistantMessageComponent } from "../../modes/components/assistant-message";
5
6
  import { BashExecutionComponent } from "../../modes/components/bash-execution";
6
7
  import { BranchSummaryMessageComponent } from "../../modes/components/branch-summary-message";
@@ -224,9 +225,9 @@ export class UiHelpers {
224
225
  content.name,
225
226
  content.arguments,
226
227
  {
227
- showImages: this.ctx.settingsManager.getShowImages(),
228
- editFuzzyThreshold: this.ctx.settingsManager.getEditFuzzyThreshold(),
229
- editAllowFuzzy: this.ctx.settingsManager.getEditFuzzyMatch(),
228
+ showImages: settings.get("terminal.showImages"),
229
+ editFuzzyThreshold: settings.get("edit.fuzzyThreshold"),
230
+ editAllowFuzzy: settings.get("edit.fuzzyMatch"),
230
231
  },
231
232
  tool,
232
233
  this.ctx.ui,
@@ -26,7 +26,7 @@ import { outputMeta } from "../tools/output-meta";
26
26
  import { enforcePlanModeWrite, resolvePlanPath } from "../tools/plan-mode-guard";
27
27
  import { applyPatch } from "./applicator";
28
28
  import { generateDiffString, generateUnifiedDiffString, replaceText } from "./diff";
29
- import { DEFAULT_FUZZY_THRESHOLD, findMatch } from "./fuzzy";
29
+ import { findMatch } from "./fuzzy";
30
30
  import { detectLineEnding, normalizeToLF, restoreLineEndings, stripBom } from "./normalize";
31
31
  import { buildNormativeUpdateInput } from "./normative";
32
32
  import { type EditToolDetails, getLspBatchRequest } from "./shared";
@@ -233,14 +233,14 @@ export class EditTool implements AgentTool<TInput> {
233
233
  this.allowFuzzy = false;
234
234
  break;
235
235
  case "auto":
236
- this.allowFuzzy = session.settings?.getEditFuzzyMatch() ?? true;
236
+ this.allowFuzzy = session.settings.get("edit.fuzzyMatch");
237
237
  break;
238
238
  default:
239
239
  throw new Error(`Invalid OMP_EDIT_FUZZY: ${editFuzzy}`);
240
240
  }
241
241
  switch (editFuzzyThreshold) {
242
242
  case "auto":
243
- this.fuzzyThreshold = session.settings?.getEditFuzzyThreshold?.() ?? DEFAULT_FUZZY_THRESHOLD;
243
+ this.fuzzyThreshold = session.settings.get("edit.fuzzyThreshold");
244
244
  break;
245
245
  default:
246
246
  this.fuzzyThreshold = parseFloat(editFuzzyThreshold);
@@ -251,8 +251,8 @@ export class EditTool implements AgentTool<TInput> {
251
251
  }
252
252
 
253
253
  const enableLsp = session.enableLsp ?? true;
254
- const enableDiagnostics = enableLsp ? (session.settings?.getLspDiagnosticsOnEdit() ?? false) : false;
255
- const enableFormat = enableLsp ? (session.settings?.getLspFormatOnWrite() ?? true) : false;
254
+ const enableDiagnostics = enableLsp && session.settings.get("lsp.diagnosticsOnEdit");
255
+ const enableFormat = enableLsp && session.settings.get("lsp.formatOnWrite");
256
256
  this.writethrough = enableLsp
257
257
  ? createLspWritethrough(session.cwd, { enableFormat, enableDiagnostics })
258
258
  : writethroughNoop;
@@ -268,11 +268,11 @@ export class EditTool implements AgentTool<TInput> {
268
268
 
269
269
  // Auto mode: check model-specific settings
270
270
  const activeModel = this.session.getActiveModelString?.();
271
- const modelVariant = this.session.settings?.getEditVariantForModel?.(activeModel);
271
+ const modelVariant = this.session.settings.getEditVariantForModel(activeModel);
272
272
  if (modelVariant === "replace") return false;
273
273
  if (modelVariant === "patch") return true;
274
274
 
275
- return this.session.settings?.getEditPatchMode?.() ?? true;
275
+ return this.session.settings.get("edit.patchMode");
276
276
  }
277
277
 
278
278
  /**
@@ -330,7 +330,6 @@ Write what you can defend.
330
330
  {{#if isCoordinator}}
331
331
  {{#has tools "task"}}
332
332
  <critical id="coordinator">
333
- As the coordinator, **ALWAYS prefer** delegation via Task tool for all SUBSTANTIAL work.
334
333
  Your context window is limited—especially the output. Work in discrete steps and run each step using Task tool. Avoid putting substantial work in the main context when possible. Run multiple tasks in parallel whenever possible.
335
334
 
336
335
  ## Triggers requiring Task tool
@@ -12,7 +12,7 @@ Executes a bash command in a shell session for terminal operations like git, bun
12
12
 
13
13
  <output>
14
14
  Returns stdout, stderr, and exit code from command execution.
15
- - Output truncated after 50KB or 2000 lines (whichever comes first); use `| head -n 50` for large output
15
+ - Output truncated after 50KB or 2000 lines (whichever comes first); use `head` parameter to limit output
16
16
  - If output is truncated, full output is stored under $ARTIFACTS and referenced as `artifact://<id>` in metadata
17
17
  - Exit codes shown on non-zero exit; stderr captured
18
18
  </output>
@@ -24,4 +24,14 @@ Do NOT use Bash for these operations—specialized tools exist:
24
24
  - Finding files by pattern → Find tool
25
25
  - Content-addressed edits → Edit tool
26
26
  - Writing new files → Write tool
27
- </critical>
27
+ </critical>
28
+
29
+ <avoid>
30
+ Do NOT pipe through `head` or `tail`—use the `head` and `tail` parameters instead:
31
+ - `command | head -n 50` → use `head: 50` parameter
32
+ - `command | tail -n 100` → use `tail: 100` parameter
33
+
34
+ The pipe pattern breaks streaming output and prevents artifact storage.
35
+
36
+ Do NOT use `2>&1`—stdout and stderr are already merged.
37
+ </avoid>
@@ -1,22 +1,183 @@
1
1
  # Task
2
2
 
3
- Launch a new agent to handle complex, multi-step tasks autonomously. Each agent type has specific capabilities and tools available to it.
3
+ Launch agents to handle complex, multi-step tasks autonomously.
4
4
 
5
5
  <critical>
6
- This matters. Get it right.
6
+ ## Context is everything
7
7
 
8
- Subagents can access parent conversation context via a file—they can grep or tail it for details you don't include. Don't repeat information unnecessarily; focus `context` on:
9
- - Task-specific constraints and decisions
10
- - Critical requirements that must not be missed
11
- - Information not easily found in the codebase
8
+ Subagents fail when context is vague. They cannot read your mind or infer project conventions. Every task needs:
9
+ 1. **Goal** - What this accomplishes (one sentence)
10
+ 2. **Constraints** - Hard requirements, banned approaches, naming conventions
11
+ 3. **Existing Code** - File paths and function signatures to use as patterns
12
+ 4. **API Contract** - If the task produces or consumes an interface, spell it out
12
13
 
13
- Use a single Task call with multiple `tasks` entries when parallelizing. Multiple concurrent Task calls bypass coordination.
14
+ Subagents CAN grep the parent conversation file for supplementary details. They CANNOT grep for:
15
+ - Decisions you made but didn't write down
16
+ - Conventions that exist only in your head
17
+ - Which of 50 possible approaches you want
18
+ **Rule of thumb:** If you'd need to answer a clarifying question for a junior dev to do this task, that information belongs in context.
19
+ </critical>
14
20
 
15
- For code changes, have subagents write files directly with Edit/Write. Do not ask them to return patches for you to apply.
21
+ <context-structure>
22
+ ## Required context structure
16
23
 
17
- Agents with `output="structured"` enforce their own schema; the `schema` parameter is ignored for those agents.
18
- **Never describe expected output in `context` or task descriptions.** All response format requirements go in the `schema` parameter. Use structured schemas with typed properties—not `{ "type": "string" }`. Prose like "respond as a bullet list" is prohibited.
19
- </critical>
24
+ Use this template. Sections can be omitted only if truly N/A.
25
+
26
+ ````
27
+ ## Goal
28
+ [One sentence: what this task accomplishes]
29
+
30
+ ## Constraints
31
+ - [Hard requirements - MUST/MUST NOT style]
32
+ - [API conventions, naming patterns, error handling]
33
+ - [What already exists vs what to create]
34
+
35
+ ## Existing Code
36
+ Reference files the agent MUST read or use as patterns:
37
+ - `path/to/file.ts` - [what pattern it demonstrates]
38
+ - `path/to/other.rs` - [what to reuse from it]
39
+
40
+ ## API Contract (if applicable)
41
+ ```language
42
+ // Exact signatures the agent must implement or consume
43
+ fn example(input: Type) -> Result<Output>
44
+ ```
45
+
46
+ ## Task
47
+ {{description}}
48
+
49
+ ## Files
50
+ {{files}}
51
+ ````
52
+
53
+ ### Bad context (agent will fail or guess wrong)
54
+
55
+ ```
56
+ N-API migration. Keep highlight sync. Use JsString. No WASM.
57
+ Task: {{description}} Files: {{files}}
58
+ ```
59
+
60
+ Why it fails:
61
+ - No existing code to reference - agent doesn't know your patterns
62
+ - No API contract - agent will invent signatures that don't match consumers
63
+ - No goal - agent doesn't know what success looks like
64
+ - "Keep highlight sync" is meaningless without knowing what highlight is or where it lives
65
+
66
+ ### Good context (agent can act confidently)
67
+
68
+ ````
69
+ ## Goal
70
+ Port grep module from WASM to N-API, matching existing text module patterns.
71
+
72
+ ## Constraints
73
+ - Use `#[napi]` attribute macro on all exports (not `#[napi(js_name = "...")]`)
74
+ - Return `napi::Result<T>` for fallible ops, never panic
75
+ - Use `spawn_blocking` for any operation that touches filesystem or runs >1ms
76
+ - Accept `JsString` for string params (NOT JsStringUtf8 - it has lifetime issues)
77
+ - Keep all existing function names - TS bindings depend on them
78
+ - No new crate dependencies
79
+
80
+ ## Existing Code
81
+ - `crates/pi-natives/src/text.rs` - reference N-API pattern: see how `visible_width` uses JsString
82
+ - `crates/pi-natives/src/lib.rs` - module registration pattern
83
+ - `crates/pi-natives/Cargo.toml` - available dependencies (ignore, regex already present)
84
+
85
+ ## API Contract
86
+ Current sync API to convert to async:
87
+ ```rust
88
+ // BEFORE (sync, blocks event loop)
89
+ #[napi]
90
+ pub fn search(pattern: String, path: String) -> Vec<Match>
91
+
92
+ // AFTER (async, uses spawn_blocking)
93
+ #[napi]
94
+ pub async fn search(pattern: JsString, path: JsString, env: Env) -> napi::Result<Vec<Match>>
95
+ ```
96
+
97
+ ## Task
98
+ {{description}}
99
+
100
+ ## Files
101
+ {{files}}
102
+ ````
103
+ </context-structure>
104
+
105
+ <parallelization>
106
+ ## When to parallelize vs sequence
107
+ **The test:** Can agent B write correct code without seeing agent A's output?
108
+ - If YES → parallelize
109
+ - If NO → sequence (A completes, then B runs with A's output in context)
110
+
111
+ ### Dependency patterns that MUST be sequential
112
+
113
+ |First|Then|Why|
114
+ |---|---|---|
115
+ |Create Rust API|Update TS bindings|Bindings need to know export names and signatures|
116
+ |Define interface/types|Implement consumers|Consumers need the contract|
117
+ |Scaffold with signatures|Implement bodies|Implementations need the shape|
118
+ |Core module|Dependent modules|Dependents import from core|
119
+
120
+ ### Safe to parallelize
121
+ - Independent modules that don't import each other
122
+ - Tests for already-implemented code
123
+ - Documentation for stable APIs
124
+ - Refactors in isolated file scopes
125
+
126
+ ### Phased execution pattern
127
+
128
+ For migrations/refactors with layers:
129
+ **Phase 1 - Foundation (do yourself or single task):**
130
+ Create the scaffold, define interfaces, establish API shape. Never fan out until the contract is known.
131
+ **Phase 2 - Parallel implementation:**
132
+ Fan out to independent tasks that all consume the same known interface. Include the API contract from Phase 1 in every task's context.
133
+ **Phase 3 - Integration (do yourself):**
134
+ Wire things together, update build/CI, fix any mismatches.
135
+ **Phase 4 - Dependent layer:**
136
+ Fan out again for work that consumes Phase 2 outputs.
137
+
138
+ ### Example: WASM to N-API migration
139
+ **WRONG** (launched together, will fail):
140
+
141
+ ```
142
+ tasks: [
143
+ { id: "RustApi", description: "Implement N-API exports" },
144
+ { id: "TsBindings", description: "Update TS to use N-API" }, // ← needs RustApi output!
145
+ ]
146
+ ```
147
+ **RIGHT** (phased):
148
+
149
+ ```
150
+ // Phase 1: You create scaffold with signatures in lib.rs
151
+
152
+ // Phase 2: Fan out Rust implementation
153
+ tasks: [
154
+ { id: "Grep", description: "Implement grep module", args: { files: "src/grep.rs" } },
155
+ { id: "Text", description: "Implement text module", args: { files: "src/text.rs" } },
156
+ // Each task gets the API contract you defined in Phase 1
157
+ ]
158
+
159
+ // Phase 3: You verify Rust compiles, exports are correct
160
+
161
+ // Phase 4: Fan out TS bindings (now they know what Rust exports)
162
+ tasks: [
163
+ { id: "GrepBindings", description: "Update grep TS", args: { files: "src/grep/index.ts" } },
164
+ // Context includes actual export names from Phase 2
165
+ ]
166
+ ```
167
+ </parallelization>
168
+
169
+ <parameters>
170
+ - `agent`: Agent type for all tasks
171
+ - `context`: Template with `{{placeholders}}`. **Must follow the structure above.** Include Goal, Constraints, Existing Code references. Subagents can search parent context for background, but core requirements must be explicit here.
172
+ - `isolated`: (optional) Run in git worktree, return patches
173
+ - `tasks`: Array of `{id, description, args}`
174
+ - `id`: CamelCase identifier (max 32 chars)
175
+ - `description`: What the task does (for logging)
176
+ - `args`: Object with keys matching `{{placeholders}}` in context
177
+ - `skills`: (optional) Skill names to preload
178
+ - `schema`: JTD schema for response structure. **Required.** Use typed properties, not `{ "type": "string" }`.
179
+ **Schema goes in `schema` parameter. Never describe output format in `context`.**
180
+ </parameters>
20
181
 
21
182
  <agents>
22
183
  {{#list agents join="\n"}}
@@ -27,66 +188,12 @@ Agents with `output="structured"` enforce their own schema; the `schema` paramet
27
188
  {{/list}}
28
189
  </agents>
29
190
 
30
- <instruction>
31
- This matters. Be thorough.
32
- 1. Plan before acting. Define the goal, acceptance criteria, and scope per task.
33
- 2. Put shared constraints and decisions in `context`; keep each task request short and unambiguous. **Do not describe response format here.**
34
- 3. State whether each task is research-only or should modify files.
35
- 4. **Always provide a `schema`** with typed properties. Avoid `{ "type": "string" }`—if data has any structure (list, fields, categories), model it. Plain text is almost never the right choice.
36
- 5. Assign distinct file scopes per task to avoid conflicts.
37
- 6. Trust the returned data, then verify with tools when correctness matters.
38
- 7. For critical constraints, be explicit in `context`. For general background, subagents can search the parent context file themselves.
39
- </instruction>
40
-
41
- <parameters>
42
- - `agent`: Agent type to use for all tasks
43
- - `context`: Template with `\{{placeholders}}` for multi-task. Include critical constraints and task-specific decisions. Subagents have access to parent conversation context via a searchable file, so don't repeat everything—focus on what matters. `\{{id}}` and `\{{description}}` are always available.
44
- - `isolated`: (optional) Run each task in its own git worktree and return patches; patches are applied only if all apply cleanly.
45
- - `tasks`: Array of `{id, description, args}` - tasks to run in parallel
46
- - `id`: Short CamelCase identifier (max 32 chars, e.g., "SessionStore", "LspRefactor")
47
- - `description`: Short human-readable description of what the task does
48
- - `args`: Object with keys matching `\{{placeholders}}` in context (always include this, even if empty)
49
- - `skills`: (optional) Array of skill names to preload into this task's system prompt. When set, the skills index section is omitted and the full SKILL.md contents are embedded.
50
- - `schema`: JTD schema defining expected response structure. **Required.** Use objects with typed properties—e.g., `{ "properties": { "items": { "elements": { "type": "string" } } } }` for lists.
51
- </parameters>
52
-
53
- <output>
54
- Returns task results for each spawned agent:
55
- - Truncated preview of agent output (use `read agent://<id>` for full content if truncated)
56
- - Summary with line/character counts
57
- - For agents with `schema`: structured JSON accessible via `agent://<id>?q=<query>` or `agent://<id>/<path>`
58
-
59
- Results are keyed by task `id` (e.g., "AuthProvider", "AuthApi").
60
- </output>
61
-
62
- <example>
63
- user: "Looks good, execute the plan"
64
- assistant: I'll execute the refactoring plan.
65
- assistant: Uses the Task tool:
66
- {
67
- "agent": "task",
68
- "context": "Refactoring the auth module into separate concerns.\n\nPlan:\n1. AuthProvider - Extract React context and provider from src/auth/index.tsx\n2. AuthApi - Extract API calls to src/auth/api.ts, use existing fetchJson helper\n3. AuthTypes - Move types to types.ts, re-export from index\n\nConstraints:\n- Preserve all existing exports from src/auth/index.tsx\n- Use project's fetchJson (src/utils/http.ts), don't use raw fetch\n- No new dependencies\n\nTask: \{{step}}\n\nFiles: \{{files}}",
69
- "schema": {
70
- "properties": {
71
- "summary": { "type": "string" },
72
- "decisions": { "elements": { "type": "string" } },
73
- "concerns": { "elements": { "type": "string" } }
74
- }
75
- },
76
- "tasks": [
77
- { "id": "AuthProvider", "description": "Extract React context", "args": { "step": "Execute step 1: Extract AuthProvider and AuthContext", "files": "src/auth/index.tsx" } },
78
- { "id": "AuthApi", "description": "Extract API layer", "args": { "step": "Execute step 2: Extract API calls to api.ts", "files": "src/auth/api.ts" } },
79
- { "id": "AuthTypes", "description": "Extract types", "args": { "step": "Execute step 3: Move types to types.ts", "files": "src/auth/types.ts" } }
80
- ]
81
- }
82
- </example>
83
-
84
191
  <avoid>
85
- - Describing response format in `context` (e.g., "respond as JSON", "return a bullet list")—use `schema` parameter instead
86
- - Confirmation bias: ask for factual discovery instead of yes/no exploration prompts
87
- - Reading a specific file path Use Read tool instead
88
- - Finding files by pattern/name Use Find tool instead
89
- - Searching for a specific class/function definition Use Grep tool instead
90
- - Searching code within 2-3 specific files Use Read tool instead
91
- - Tasks unrelated to the agent descriptions above
92
- </avoid>
192
+ - Terse context that requires agents to guess conventions
193
+ - Launching dependent tasks in parallel (bindings + API, consumer + producer)
194
+ - Missing "Existing Code" references - agents need patterns to follow
195
+ - Assuming agents know your codebase - they start fresh each time
196
+ - Describing output format in context instead of schema
197
+ - Single tasks doing too much - prefer focused, file-scoped tasks
198
+ </avoid>
199
+ ````