@tintinweb/pi-subagents 0.10.1 → 0.10.3

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.
package/dist/index.js CHANGED
@@ -473,6 +473,13 @@ export default function (pi) {
473
473
  setDefaultsDisabled(b);
474
474
  reloadCustomAgents(); // re-register with new setting
475
475
  }
476
+ // ---- Agent tool description mode ----
477
+ // "full" (default) keeps the rich Claude Code-style description; "compact"
478
+ // swaps in a ~75% smaller one for small/local models (#91). Read once at
479
+ // tool registration — flipping it applies on the next pi session.
480
+ let toolDescriptionMode = "full";
481
+ function getToolDescriptionMode() { return toolDescriptionMode; }
482
+ function setToolDescriptionMode(mode) { toolDescriptionMode = mode; }
476
483
  // ---- Batch tracking for smart join mode ----
477
484
  // Collects background agent IDs spawned in the current turn for smart grouping.
478
485
  // Uses a debounced timer: each new agent resets the 100ms window so that all
@@ -539,6 +546,16 @@ export default function (pi) {
539
546
  return `- ${name}: ${cfg?.description ?? name}${modelSuffix}${toolsSuffix}`;
540
547
  }).join("\n");
541
548
  };
549
+ /** First sentence of an agent description — for the compact type list. */
550
+ const firstSentence = (text) => {
551
+ const match = text.match(/^.*?[.!?](?=\s|$)/s);
552
+ return (match ? match[0] : text).replace(/\s+/g, " ").trim();
553
+ };
554
+ /** Compact type list: one line per agent, first sentence only. */
555
+ const buildCompactTypeListText = () => getAvailableTypes().map((name) => {
556
+ const cfg = getAgentConfig(name);
557
+ return `- ${name}: ${firstSentence(cfg?.description ?? name)} (Tools: ${formatToolsSuffix(cfg)})`;
558
+ }).join("\n");
542
559
  /** Derive a short model label from a model string. */
543
560
  function getModelLabelFromConfig(model) {
544
561
  // Strip provider prefix (e.g. "anthropic/claude-sonnet-4-6" → "claude-sonnet-4-6")
@@ -557,6 +574,7 @@ export default function (pi) {
557
574
  setSchedulingEnabled,
558
575
  setScopeModels: setScopeModelsEnabled,
559
576
  setDisableDefaultAgents: setDisableDefaultAgents,
577
+ setToolDescriptionMode: setToolDescriptionMode,
560
578
  }, (event, payload) => pi.events.emit(event, payload));
561
579
  // ---- Agent tool ----
562
580
  // Schedule param + its guideline are gated on `schedulingEnabled` (read once
@@ -575,10 +593,21 @@ export default function (pi) {
575
593
  const scheduleGuideline = isSchedulingEnabled()
576
594
  ? `\n- Use \`schedule\` only when the user explicitly asked for scheduled / recurring / delayed execution (e.g. "every Monday", "in an hour"). Don't auto-schedule from vague intent like "monitor X" — run once now or ask.`
577
595
  : "";
578
- pi.registerTool(defineTool({
579
- name: SUBAGENT_TOOL_NAMES.AGENT,
580
- label: "Agent",
581
- description: `Launch a new agent to handle complex, multi-step tasks autonomously. Each agent type has specific capabilities and tools available to it.
596
+ // Compact Agent tool description (#91, `toolDescriptionMode: "compact"`) —
597
+ // the same load-bearing facts as the full version at ~75% fewer tokens, for
598
+ // small/local models. Per-option details live in the param descriptions.
599
+ const compactAgentToolDescription = `Launch an autonomous agent for complex, multi-step tasks. Agent types:
600
+ ${buildCompactTypeListText()}
601
+
602
+ Custom agents: .pi/agents/<name>.md (project) or ${getAgentDir()}/agents/<name>.md (global).
603
+
604
+ Notes:
605
+ - description: 3-5 words (shown in UI). Prompts must be self-contained — the agent has not seen this conversation.
606
+ - Parallel work: one message, multiple Agent calls, run_in_background: true on each. You are notified when background agents finish — never poll or sleep.
607
+ - The result is not shown to the user — summarize it for them. Verify an agent's claimed code changes before reporting work done.
608
+ - resume continues a previous agent by ID; steer_subagent messages a running one.
609
+ - isolation: "worktree" runs the agent in an isolated git worktree; changes land on a branch.`;
610
+ const fullAgentToolDescription = `Launch a new agent to handle complex, multi-step tasks autonomously. Each agent type has specific capabilities and tools available to it.
582
611
 
583
612
  Available agent types and the tools they have access to:
584
613
  ${buildTypeListText()}
@@ -619,7 +648,61 @@ Provide clear, detailed prompts so the agent can work autonomously. Brief it lik
619
648
 
620
649
  Terse command-style prompts produce shallow, generic work.
621
650
 
622
- **Never delegate understanding.** Don't write "based on your findings, fix the bug" or "based on the research, implement it." Those phrases push synthesis onto the agent instead of doing it yourself. Write prompts that prove you understood: include file paths, line numbers, what specifically to change.`,
651
+ **Never delegate understanding.** Don't write "based on your findings, fix the bug" or "based on the research, implement it." Those phrases push synthesis onto the agent instead of doing it yourself. Write prompts that prove you understood: include file paths, line numbers, what specifically to change.`;
652
+ // `toolDescriptionMode: "custom"` — user-authored description with live
653
+ // dynamic parts. Project file wins over global; missing/empty falls back to
654
+ // "full" (a stale fallback beats a blank tool description). Only the prose
655
+ // is customizable — the parameter schema stays code-owned.
656
+ const renderToolDescriptionTemplate = (template) => {
657
+ const vars = {
658
+ typeList: buildTypeListText,
659
+ compactTypeList: buildCompactTypeListText,
660
+ agentDir: getAgentDir,
661
+ scheduleGuideline: () => scheduleGuideline,
662
+ };
663
+ // Replacement callback (not a string) — agent descriptions may contain `$&` etc.
664
+ return template.replace(/\{\{(\w+)\}\}/g, (raw, name) => {
665
+ if (vars[name])
666
+ return vars[name]();
667
+ console.warn(`[pi-subagents] agent-tool-description.md: unknown placeholder ${raw} left as-is`);
668
+ return raw;
669
+ });
670
+ };
671
+ const loadCustomToolDescription = () => {
672
+ for (const path of [
673
+ join(process.cwd(), ".pi", "agent-tool-description.md"),
674
+ join(getAgentDir(), "agent-tool-description.md"),
675
+ ]) {
676
+ try {
677
+ if (!existsSync(path))
678
+ continue;
679
+ const text = readFileSync(path, "utf-8").trim();
680
+ if (text)
681
+ return renderToolDescriptionTemplate(text);
682
+ console.warn(`[pi-subagents] ${path} is empty — ignoring`);
683
+ }
684
+ catch (err) {
685
+ console.warn(`[pi-subagents] failed to read ${path}: ${err instanceof Error ? err.message : String(err)}`);
686
+ }
687
+ }
688
+ return undefined;
689
+ };
690
+ const agentToolDescription = (() => {
691
+ const mode = getToolDescriptionMode();
692
+ if (mode === "compact")
693
+ return compactAgentToolDescription;
694
+ if (mode === "custom") {
695
+ const custom = loadCustomToolDescription();
696
+ if (custom)
697
+ return custom;
698
+ console.warn('[pi-subagents] toolDescriptionMode is "custom" but no agent-tool-description.md found — using "full"');
699
+ }
700
+ return fullAgentToolDescription;
701
+ })();
702
+ pi.registerTool(defineTool({
703
+ name: SUBAGENT_TOOL_NAMES.AGENT,
704
+ label: "Agent",
705
+ description: agentToolDescription,
623
706
  promptSnippet: "Launch autonomous sub-agents for complex multi-step tasks",
624
707
  promptGuidelines: [
625
708
  "Use Agent with specialized agents when the task matches an agent type's description. Subagents are valuable for parallelizing independent queries or for protecting the main context window from excessive results, but should not be used excessively when not needed. Importantly, avoid duplicating work that subagents are already doing — if you delegate research to a subagent, do not also perform the same searches yourself.",
@@ -1326,12 +1409,12 @@ Terse command-style prompts produce shallow, generic work.
1326
1409
  const { ConversationViewer, VIEWPORT_HEIGHT_PCT } = await import("./ui/conversation-viewer.js");
1327
1410
  const session = record.session;
1328
1411
  const activity = agentActivity.get(record.id);
1329
- await ctx.ui.custom((tui, theme, _keybindings, done) => {
1412
+ await ctx.ui.custom((tui, theme, keybindings, done) => {
1330
1413
  return new ConversationViewer(tui, session, record, activity, theme, done, () => {
1331
1414
  if (manager.abort(record.id)) {
1332
1415
  ctx.ui.notify(`Stopped "${record.description}".`, "info");
1333
1416
  }
1334
- });
1417
+ }, keybindings);
1335
1418
  }, {
1336
1419
  overlay: true,
1337
1420
  overlayOptions: { anchor: "center", width: "90%", maxHeight: `${VIEWPORT_HEIGHT_PCT}%` },
@@ -1439,6 +1522,8 @@ Terse command-style prompts produce shallow, generic work.
1439
1522
  fmFields.push("extensions: false");
1440
1523
  else if (Array.isArray(cfg.extensions))
1441
1524
  fmFields.push(`extensions: ${cfg.extensions.join(", ")}`);
1525
+ if (cfg.excludeExtensions?.length)
1526
+ fmFields.push(`exclude_extensions: ${cfg.excludeExtensions.join(", ")}`);
1442
1527
  if (cfg.skills === false)
1443
1528
  fmFields.push("skills: false");
1444
1529
  else if (Array.isArray(cfg.skills))
@@ -1704,6 +1789,7 @@ ${systemPrompt}
1704
1789
  schedulingEnabled: isSchedulingEnabled(),
1705
1790
  scopeModels: isScopeModelsEnabled(),
1706
1791
  disableDefaultAgents: isDefaultsDisabled(),
1792
+ toolDescriptionMode: getToolDescriptionMode(),
1707
1793
  };
1708
1794
  }
1709
1795
  const NUMERIC_IDS = new Set(["maxConcurrent", "defaultMaxTurns", "graceTurns"]);
@@ -1762,6 +1848,13 @@ ${systemPrompt}
1762
1848
  currentValue: isDefaultsDisabled() ? "on" : "off",
1763
1849
  values: ["on", "off"],
1764
1850
  },
1851
+ {
1852
+ id: "toolDescriptionMode",
1853
+ label: "Tool description",
1854
+ description: "Agent tool description sent to the LLM: full (rich, default), compact (~75% fewer tokens, for small/local models), or custom (.pi/agent-tool-description.md with {{placeholders}})",
1855
+ currentValue: getToolDescriptionMode(),
1856
+ values: ["full", "compact", "custom"],
1857
+ },
1765
1858
  ];
1766
1859
  }
1767
1860
  function applyValue(id, value) {
@@ -1816,6 +1909,10 @@ ${systemPrompt}
1816
1909
  setDisableDefaultAgents(enabled);
1817
1910
  notifyApplied(ctx, `Default agents ${enabled ? "disabled" : "enabled"}. Tool spec change takes effect on next pi session.`);
1818
1911
  }
1912
+ else if (id === "toolDescriptionMode") {
1913
+ setToolDescriptionMode(value);
1914
+ notifyApplied(ctx, `Tool description set to ${value}. Takes effect on next pi session.`);
1915
+ }
1819
1916
  }
1820
1917
  let list;
1821
1918
  // Track current selection index directly (SettingsList doesn't expose it).
@@ -47,7 +47,19 @@ export interface SubagentsSettings {
47
47
  * Defaults to false.
48
48
  */
49
49
  disableDefaultAgents?: boolean;
50
+ /**
51
+ * Which Agent tool description the LLM sees. "full" (default) is the rich
52
+ * Claude Code-style prompt; "compact" is a ~75% smaller version (one-line
53
+ * agent type list, terse usage notes) for small/local models where tool-spec
54
+ * tokens are expensive; "custom" reads `.pi/agent-tool-description.md`
55
+ * (project, falling back to `<agentDir>/agent-tool-description.md`) with
56
+ * `{{placeholder}}` substitution — a missing/empty file falls back to "full".
57
+ * The mode is read once at tool registration — changing it applies on the
58
+ * next pi session.
59
+ */
60
+ toolDescriptionMode?: ToolDescriptionMode;
50
61
  }
62
+ export type ToolDescriptionMode = "full" | "compact" | "custom";
51
63
  /** Setter hooks used by applySettings to wire persisted values into in-memory state. */
52
64
  export interface SettingsAppliers {
53
65
  setMaxConcurrent: (n: number) => void;
@@ -57,6 +69,7 @@ export interface SettingsAppliers {
57
69
  setSchedulingEnabled: (b: boolean) => void;
58
70
  setScopeModels: (enabled: boolean) => void;
59
71
  setDisableDefaultAgents: (b: boolean) => void;
72
+ setToolDescriptionMode: (mode: ToolDescriptionMode) => void;
60
73
  }
61
74
  /** Emit callback — a subset of `pi.events.emit` to keep helpers testable. */
62
75
  export type SettingsEmit = (event: string, payload: unknown) => void;
package/dist/settings.js CHANGED
@@ -5,6 +5,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
5
  import { dirname, join } from "node:path";
6
6
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
7
7
  const VALID_JOIN_MODES = new Set(["async", "group", "smart"]);
8
+ const VALID_TOOL_DESCRIPTION_MODES = new Set(["full", "compact", "custom"]);
8
9
  // Sanity ceilings — prevent hand-edited configs from asking for values that
9
10
  // make no operational sense (e.g. 1e6 concurrent subagents). Permissive enough
10
11
  // that any realistic power-user setting passes through.
@@ -44,6 +45,9 @@ function sanitize(raw) {
44
45
  if (typeof r.disableDefaultAgents === "boolean") {
45
46
  out.disableDefaultAgents = r.disableDefaultAgents;
46
47
  }
48
+ if (typeof r.toolDescriptionMode === "string" && VALID_TOOL_DESCRIPTION_MODES.has(r.toolDescriptionMode)) {
49
+ out.toolDescriptionMode = r.toolDescriptionMode;
50
+ }
47
51
  return out;
48
52
  }
49
53
  function globalPath() {
@@ -105,6 +109,8 @@ export function applySettings(s, appliers) {
105
109
  appliers.setScopeModels(s.scopeModels);
106
110
  if (typeof s.disableDefaultAgents === "boolean")
107
111
  appliers.setDisableDefaultAgents(s.disableDefaultAgents);
112
+ if (s.toolDescriptionMode)
113
+ appliers.setToolDescriptionMode(s.toolDescriptionMode);
108
114
  }
109
115
  /**
110
116
  * Format the user-facing toast for a settings mutation. Pure function —
package/dist/types.d.ts CHANGED
@@ -26,6 +26,9 @@ export interface AgentConfig {
26
26
  disallowedTools?: string[];
27
27
  /** true = inherit all, string[] = only listed, false = none */
28
28
  extensions: true | string[] | false;
29
+ /** Extension-name denylist applied after the `extensions:` include set. Exclude wins.
30
+ * Plain canonical names only (case-insensitive); no paths, no wildcard. */
31
+ excludeExtensions?: string[];
29
32
  /** true = inherit all, string[] = only listed, false = none */
30
33
  skills: true | string[] | false;
31
34
  model?: string;
@@ -75,6 +78,7 @@ export interface AgentRecord {
75
78
  path: string;
76
79
  branch: string;
77
80
  baseSha: string;
81
+ workPath: string;
78
82
  };
79
83
  /** Worktree cleanup result after agent completion. */
80
84
  worktreeResult?: {
@@ -9,6 +9,7 @@ import { type Component, type TUI } from "@earendil-works/pi-tui";
9
9
  import type { AgentRecord } from "../types.js";
10
10
  import type { Theme } from "./agent-widget.js";
11
11
  import { type AgentActivity } from "./agent-widget.js";
12
+ import { type ViewerKeybindings } from "./viewer-keys.js";
12
13
  /** Height ceiling shared by the overlay's `maxHeight` and the viewer's internal viewport cap. */
13
14
  export declare const VIEWPORT_HEIGHT_PCT = 70;
14
15
  export declare class ConversationViewer implements Component {
@@ -27,9 +28,12 @@ export declare class ConversationViewer implements Component {
27
28
  private closed;
28
29
  /** Two-press confirm guard for the stop key, so a stray key can't kill the agent. */
29
30
  private stopArmed;
31
+ private keys;
30
32
  constructor(tui: TUI, session: AgentSession, record: AgentRecord, activity: AgentActivity | undefined, theme: Theme, done: (result: undefined) => void,
31
33
  /** Abort the agent shown here. Omitted → no stop affordance (e.g. read-only history). */
32
- onStop?: (() => void) | undefined);
34
+ onStop?: (() => void) | undefined,
35
+ /** User keybindings from `ctx.ui.custom()`. Omitted → hardcoded defaults. */
36
+ keybindings?: ViewerKeybindings);
33
37
  handleInput(data: string): void;
34
38
  render(width: number): string[];
35
39
  /** Stoppable only when a stop handler exists and the agent is still active. */
@@ -8,6 +8,7 @@ import { matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@ea
8
8
  import { extractText } from "../context.js";
9
9
  import { getLifetimeTotal, getSessionContextPercent } from "../usage.js";
10
10
  import { buildInvocationTags, describeActivity, formatDuration, formatSessionTokens, getDisplayName, getPromptModeLabel } from "./agent-widget.js";
11
+ import { createViewerKeys } from "./viewer-keys.js";
11
12
  /** Base lines consumed by chrome: top border + header + header sep + footer sep + footer + bottom border. */
12
13
  const CHROME_LINES_BASE = 6;
13
14
  const MIN_VIEWPORT = 3;
@@ -28,9 +29,12 @@ export class ConversationViewer {
28
29
  closed = false;
29
30
  /** Two-press confirm guard for the stop key, so a stray key can't kill the agent. */
30
31
  stopArmed = false;
32
+ keys;
31
33
  constructor(tui, session, record, activity, theme, done,
32
34
  /** Abort the agent shown here. Omitted → no stop affordance (e.g. read-only history). */
33
- onStop) {
35
+ onStop,
36
+ /** User keybindings from `ctx.ui.custom()`. Omitted → hardcoded defaults. */
37
+ keybindings) {
34
38
  this.tui = tui;
35
39
  this.session = session;
36
40
  this.record = record;
@@ -38,6 +42,7 @@ export class ConversationViewer {
38
42
  this.theme = theme;
39
43
  this.done = done;
40
44
  this.onStop = onStop;
45
+ this.keys = createViewerKeys(keybindings);
41
46
  this.unsubscribe = session.subscribe(() => {
42
47
  if (this.closed)
43
48
  return;
@@ -70,19 +75,19 @@ export class ConversationViewer {
70
75
  const totalLines = this.buildContentLines(this.lastInnerW).length;
71
76
  const viewportHeight = this.viewportHeight();
72
77
  const maxScroll = Math.max(0, totalLines - viewportHeight);
73
- if (matchesKey(data, "up") || matchesKey(data, "k")) {
78
+ if (this.keys.scrollUp(data)) {
74
79
  this.scrollOffset = Math.max(0, this.scrollOffset - 1);
75
80
  this.autoScroll = this.scrollOffset >= maxScroll;
76
81
  }
77
- else if (matchesKey(data, "down") || matchesKey(data, "j")) {
82
+ else if (this.keys.scrollDown(data)) {
78
83
  this.scrollOffset = Math.min(maxScroll, this.scrollOffset + 1);
79
84
  this.autoScroll = this.scrollOffset >= maxScroll;
80
85
  }
81
- else if (matchesKey(data, "pageUp") || matchesKey(data, "shift+up")) {
86
+ else if (this.keys.pageUp(data)) {
82
87
  this.scrollOffset = Math.max(0, this.scrollOffset - viewportHeight);
83
88
  this.autoScroll = false;
84
89
  }
85
- else if (matchesKey(data, "pageDown") || matchesKey(data, "shift+down")) {
90
+ else if (this.keys.pageDown(data)) {
86
91
  this.scrollOffset = Math.min(maxScroll, this.scrollOffset + viewportHeight);
87
92
  this.autoScroll = this.scrollOffset >= maxScroll;
88
93
  }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * viewer-keys.ts — Scroll key matchers for the conversation viewer.
3
+ *
4
+ * Resolves `tui.select.*` through the user's keybindings when pi provides a
5
+ * manager, falling back to the previous hardcoded keys otherwise. The viewer's
6
+ * k/j and shift+arrow aliases always work alongside whatever is bound.
7
+ */
8
+ /** The `tui.select.*` keybinding ids the viewer resolves. */
9
+ export type ViewerScrollKeybinding = "tui.select.up" | "tui.select.down" | "tui.select.pageUp" | "tui.select.pageDown";
10
+ /** Structural subset of pi-tui's `KeybindingsManager` (which satisfies it). */
11
+ export interface ViewerKeybindings {
12
+ matches(data: string, keybinding: ViewerScrollKeybinding): boolean;
13
+ }
14
+ export interface ViewerKeys {
15
+ scrollUp(data: string): boolean;
16
+ scrollDown(data: string): boolean;
17
+ pageUp(data: string): boolean;
18
+ pageDown(data: string): boolean;
19
+ }
20
+ export declare function createViewerKeys(keybindings?: ViewerKeybindings): ViewerKeys;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * viewer-keys.ts — Scroll key matchers for the conversation viewer.
3
+ *
4
+ * Resolves `tui.select.*` through the user's keybindings when pi provides a
5
+ * manager, falling back to the previous hardcoded keys otherwise. The viewer's
6
+ * k/j and shift+arrow aliases always work alongside whatever is bound.
7
+ */
8
+ import { matchesKey } from "@earendil-works/pi-tui";
9
+ export function createViewerKeys(keybindings) {
10
+ const matches = (data, id, fallback) => keybindings ? keybindings.matches(data, id) : matchesKey(data, fallback);
11
+ return {
12
+ scrollUp: (data) => matches(data, "tui.select.up", "up") || matchesKey(data, "k"),
13
+ scrollDown: (data) => matches(data, "tui.select.down", "down") || matchesKey(data, "j"),
14
+ pageUp: (data) => matches(data, "tui.select.pageUp", "pageUp") || matchesKey(data, "shift+up"),
15
+ pageDown: (data) => matches(data, "tui.select.pageDown", "pageDown") || matchesKey(data, "shift+down"),
16
+ };
17
+ }
@@ -6,12 +6,19 @@
6
6
  * If changes exist, a branch is created and returned in the result.
7
7
  */
8
8
  export interface WorktreeInfo {
9
- /** Absolute path to the worktree directory. */
9
+ /** Absolute path to the worktree directory (the copied repo's root). */
10
10
  path: string;
11
11
  /** Branch name created for this worktree (if changes exist). */
12
12
  branch: string;
13
13
  /** Commit SHA that the worktree was created from. */
14
14
  baseSha: string;
15
+ /**
16
+ * Where the agent should work inside the worktree: the equivalent of the
17
+ * cwd the worktree was created from. Equals `path` when that cwd was the
18
+ * repo root; points at the copied subdirectory when it was deeper (e.g. a
19
+ * monorepo package), so the requested scoping survives isolation.
20
+ */
21
+ workPath: string;
15
22
  }
16
23
  export interface WorktreeCleanupResult {
17
24
  /** Whether changes were found in the worktree. */
package/dist/worktree.js CHANGED
@@ -7,9 +7,9 @@
7
7
  */
8
8
  import { execFileSync } from "node:child_process";
9
9
  import { randomUUID } from "node:crypto";
10
- import { existsSync } from "node:fs";
10
+ import { existsSync, realpathSync } from "node:fs";
11
11
  import { tmpdir } from "node:os";
12
- import { join } from "node:path";
12
+ import { join, relative } from "node:path";
13
13
  /**
14
14
  * Create a temporary git worktree for an agent.
15
15
  * Returns the worktree path, or undefined if not in a git repo.
@@ -17,11 +17,20 @@ import { join } from "node:path";
17
17
  export function createWorktree(cwd, agentId) {
18
18
  // Verify we're in a git repo with at least one commit (HEAD must exist)
19
19
  let baseSha;
20
+ let subdir;
20
21
  try {
21
22
  execFileSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd, stdio: "pipe", timeout: 5000 });
22
23
  baseSha = execFileSync("git", ["rev-parse", "HEAD"], { cwd, stdio: "pipe", timeout: 5000 })
23
24
  .toString()
24
25
  .trim();
26
+ // Where cwd sits inside the repo ("" at the root): the agent must work at
27
+ // the same subdirectory inside the copy, or a monorepo-package cwd would
28
+ // silently widen to the whole repo. realpath both sides — git emits
29
+ // resolved paths while cwd may arrive through a symlink (macOS /tmp).
30
+ const topLevel = execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd, stdio: "pipe", timeout: 5000 })
31
+ .toString()
32
+ .trim();
33
+ subdir = relative(realpathSync(topLevel), realpathSync(cwd));
25
34
  }
26
35
  catch {
27
36
  return undefined;
@@ -36,7 +45,7 @@ export function createWorktree(cwd, agentId) {
36
45
  stdio: "pipe",
37
46
  timeout: 30000,
38
47
  });
39
- return { path: worktreePath, branch, baseSha };
48
+ return { path: worktreePath, branch, baseSha, workPath: subdir ? join(worktreePath, subdir) : worktreePath };
40
49
  }
41
50
  catch {
42
51
  // If worktree creation fails, return undefined (agent runs in normal cwd)
@@ -0,0 +1,42 @@
1
+ Launch a new agent to handle complex, multi-step tasks autonomously. Each agent type has specific capabilities and tools available to it.
2
+
3
+ Available agent types and the tools they have access to:
4
+ {{typeList}}
5
+
6
+ Custom agents can be defined in .pi/agents/<name>.md (project) or {{agentDir}}/agents/<name>.md (global) — they are picked up automatically. Project-level agents override global ones. Creating a .md file with the same name as a default agent overrides it.
7
+
8
+ When using the Agent tool, specify a subagent_type parameter to select which agent type to use.
9
+
10
+ ## When not to use
11
+
12
+ If the target is already known, use a direct tool — `read` for a known path, `grep`/`find` for a specific symbol or string. Reserve this tool for open-ended questions that span the codebase, or tasks that match an available agent type.
13
+
14
+ ## Usage notes
15
+
16
+ - Always include a short (3-5 word) description summarizing what the agent will do (shown in UI).
17
+ - When you launch multiple agents for independent work, send them in a single message with multiple tool uses, with run_in_background: true on each, so they run concurrently. If the user specifies that they want agents run "in parallel", you MUST send a single message with multiple tool calls. Foreground calls run sequentially — only one executes at a time.
18
+ - When the agent is done, it returns a single message back to you. The result is not visible to the user — to show the user, send a text message with a concise summary.
19
+ - Trust but verify: an agent's summary describes what it intended to do, not necessarily what it did. When an agent writes or edits code, check the actual changes before reporting work as done.
20
+ - Use run_in_background for work you don't need immediately. You will be notified when it completes — do NOT poll or sleep waiting for it. Continue with other work or respond to the user instead.
21
+ - Foreground vs background: use foreground (default) when you need the agent's results before you can proceed. Use background when you have genuinely independent work to do in parallel.
22
+ - Use resume with an agent ID to continue a previous agent's work. A new (non-resume) Agent call starts a fresh agent with no memory of prior runs, so the prompt must be self-contained.
23
+ - Use steer_subagent to send mid-run messages to a running background agent.
24
+ - Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, etc.), since it is not aware of the user's intent.
25
+ - If an agent's description says it should be used proactively, try to use it without the user having to ask for it first.
26
+ - Use model to specify a different model (as "provider/modelId", or fuzzy e.g. "haiku", "sonnet").
27
+ - Use thinking to control extended thinking level.
28
+ - Use inherit_context if the agent needs the parent conversation history.
29
+ - Use isolation: "worktree" to run the agent in an isolated git worktree (safe parallel file modifications). The worktree is automatically cleaned up if the agent makes no changes; otherwise the path and branch are returned in the result.{{scheduleGuideline}}
30
+
31
+ ## Writing the prompt
32
+
33
+ Provide clear, detailed prompts so the agent can work autonomously. Brief it like a smart colleague who just walked into the room — it hasn't seen this conversation, doesn't know what you've tried, doesn't understand why this task matters.
34
+ - Explain what you're trying to accomplish and why.
35
+ - Describe what you've already learned or ruled out.
36
+ - Give enough context about the surrounding problem that the agent can make judgment calls rather than just following a narrow instruction.
37
+ - If you need a short response, say so ("report in under 200 words").
38
+ - Lookups: hand over the exact command. Investigations: hand over the question — prescribed steps become dead weight when the premise is wrong.
39
+
40
+ Terse command-style prompts produce shallow, generic work.
41
+
42
+ **Never delegate understanding.** Don't write "based on your findings, fix the bug" or "based on the research, implement it." Those phrases push synthesis onto the agent instead of doing it yourself. Write prompts that prove you understood: include file paths, line numbers, what specifically to change.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tintinweb/pi-subagents",
3
- "version": "0.10.1",
3
+ "version": "0.10.3",
4
4
  "description": "A pi extension extension that brings smart Claude Code-style autonomous sub-agents to pi.",
5
5
  "author": "tintinweb",
6
6
  "license": "MIT",