@tintinweb/pi-subagents 0.10.0 → 0.10.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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.10.1] - 2026-06-10
11
+
12
+ ### Added
13
+ - **`disableDefaultAgents` setting** ([#92](https://github.com/tintinweb/pi-subagents/issues/92) — thanks [@TommyC81](https://github.com/TommyC81)). When on, the three built-in default agents (general-purpose, Explore, Plan) are skipped at registration — only user-defined `.pi/agents/*.md` agents are advertised and spawnable. User agents are unaffected, including ones overriding a default by name; with no user agents defined, spawning falls back to the hardcoded generic config. Off by default; toggle via `/agents → Settings → Disable defaults` or `disableDefaultAgents` in `subagents.json`. Like `schedulingEnabled`, the Agent tool's type list reflects the change on the next pi session (tool schema is registered at startup).
14
+
15
+ ### Fixed
16
+ - **Agents with `enabled: false` are no longer advertised in the Agent tool description** ([#92](https://github.com/tintinweb/pi-subagents/issues/92)). `buildTypeListText` listed every registered agent, including disabled ones that `isValidType` then refused to spawn — the LLM was offered types it could never use. The type list now filters through `getAvailableTypes()`, matching the `subagent_type` parameter description.
17
+ - **Agent tool type list no longer built from pre-settings state.** The description text was captured into a variable before persisted settings were applied; it's now built at tool-registration time, after `subagents:settings_loaded`.
18
+ - **Committed work from `isolation: "worktree"` subagents is now preserved** ([#68](https://github.com/tintinweb/pi-subagents/pull/68) — thanks [@rylwin](https://github.com/rylwin)). If an isolated subagent creates its own commit, cleanup previously saw a clean `git status`, treated it as "no changes", and removed the detached worktree — silently discarding the commits. The worktree now records its base SHA at creation, and cleanup creates the expected `pi-agent-*` branch whenever HEAD moved past it, even with a clean tree.
19
+ - **Automatic commits in isolated worktrees skip local Git hooks** ([#68](https://github.com/tintinweb/pi-subagents/pull/68)). The preservation commit at worktree cleanup now uses `--no-verify`, so a failing local pre-commit hook can't abort it (which previously surfaced as `hasChanges: false` — the agent's work lost).
20
+
10
21
  ## [0.10.0] - 2026-06-01
11
22
 
12
23
  > **⚠️ Breaking: `extensions:` and `tools:` in agent frontmatter semantics changed.** The `extensions: [...]` array now selects which extensions *load*, not which tool names surface. Agents that previously used the array form will behave differently — see migration below. The `tools:` field also grew new `ext:` and `*` selector forms; existing `tools:` values without these selectors are unchanged.
package/README.md CHANGED
@@ -365,12 +365,14 @@ When on, each subagent spawn's effective model is validated against pi's own `en
365
365
 
366
366
  ## Persistent Settings
367
367
 
368
- Runtime tuning values set via `/agents` → Settings (max concurrency, default max turns, grace turns, default join mode, scheduling on/off, scope models on/off) persist across pi restarts. Two files, merged on load:
368
+ Runtime tuning values set via `/agents` → Settings (max concurrency, default max turns, grace turns, default join mode, scheduling on/off, scope models on/off, disable defaults on/off) persist across pi restarts. Two files, merged on load:
369
369
 
370
370
  - **Global:** `~/.pi/agent/subagents.json` — your machine-wide defaults. Edit by hand; the `/agents` menu never writes here.
371
371
  - **Project:** `<cwd>/.pi/subagents.json` — per-project overrides. Written by `/agents` → Settings.
372
372
 
373
- **Precedence:** project overrides global on any field present in both. Missing fields fall back to the hardcoded defaults (max concurrency `4`, default max turns unlimited, grace turns `5`, join mode `smart`).
373
+ **Precedence:** project overrides global on any field present in both. Missing fields fall back to the hardcoded defaults (max concurrency `4`, default max turns unlimited, grace turns `5`, join mode `smart`, defaults enabled).
374
+
375
+ **Disable defaults** (`disableDefaultAgents`, default `false`): when on, the three built-in agents (general-purpose, Explore, Plan) are not registered — only your `.pi/agents/*.md` agents are advertised and spawnable. User-defined agents are unaffected, including ones that override a default by name. The Agent tool's type list updates on the next pi session (the tool schema is registered at startup).
374
376
 
375
377
  **Example — global defaults for a beefy machine:**
376
378
 
@@ -507,6 +509,9 @@ Agent({ subagent_type: "refactor", prompt: "...", isolation: "worktree" })
507
509
  The agent gets a full, isolated copy of the repository. On completion:
508
510
  - **No changes:** worktree is cleaned up automatically
509
511
  - **Changes made:** changes are committed to a new branch (`pi-agent-<id>`) and returned in the result
512
+ - **Agent committed its own work:** the branch is created at the agent's HEAD, preserving its commits (uncommitted leftovers are committed on top first)
513
+
514
+ The automatic preservation commit uses `--no-verify`, so local pre-commit hooks can't block it — the commit is local-only and never pushed, and pre-push/server-side hooks still apply.
510
515
 
511
516
  If the worktree cannot be created (not a git repo, no commits, or `git worktree add` fails), the `Agent` tool returns a clear error instead of running unisolated — `isolation: "worktree"` is a strict guarantee, not a hint. Initialize git and commit at least once, or omit `isolation`.
512
517
 
@@ -14,6 +14,10 @@ import type { AgentConfig } from "./types.js";
14
14
  * operations we never invoke here — we read each tool's `.name` and discard it.
15
15
  */
16
16
  export declare const BUILTIN_TOOL_NAMES: string[];
17
+ /** Check whether default agents are disabled. */
18
+ export declare function isDefaultsDisabled(): boolean;
19
+ /** Set whether default agents are disabled. */
20
+ export declare function setDefaultsDisabled(b: boolean): void;
17
21
  /**
18
22
  * Register agents into the unified registry.
19
23
  * Starts with DEFAULT_AGENTS, then overlays user agents (overrides defaults with same name).
@@ -19,6 +19,12 @@ export const BUILTIN_TOOL_NAMES = [
19
19
  ];
20
20
  /** Unified runtime registry of all agents (defaults + user-defined). */
21
21
  const agents = new Map();
22
+ /** When true, DEFAULT_AGENTS are skipped during registration. */
23
+ let disableDefaults = false;
24
+ /** Check whether default agents are disabled. */
25
+ export function isDefaultsDisabled() { return disableDefaults; }
26
+ /** Set whether default agents are disabled. */
27
+ export function setDefaultsDisabled(b) { disableDefaults = b; }
22
28
  /**
23
29
  * Register agents into the unified registry.
24
30
  * Starts with DEFAULT_AGENTS, then overlays user agents (overrides defaults with same name).
@@ -26,9 +32,11 @@ const agents = new Map();
26
32
  */
27
33
  export function registerAgents(userAgents) {
28
34
  agents.clear();
29
- // Start with defaults
30
- for (const [name, config] of DEFAULT_AGENTS) {
31
- agents.set(name, config);
35
+ // Start with defaults (unless disabled via settings)
36
+ if (!disableDefaults) {
37
+ for (const [name, config] of DEFAULT_AGENTS) {
38
+ agents.set(name, config);
39
+ }
32
40
  }
33
41
  // Overlay user agents (overrides defaults with same name)
34
42
  for (const [name, config] of userAgents) {
package/dist/index.js CHANGED
@@ -16,7 +16,7 @@ import { Container, Key, matchesKey, SettingsList, Spacer, Text } from "@earendi
16
16
  import { Type } from "@sinclair/typebox";
17
17
  import { AgentManager } from "./agent-manager.js";
18
18
  import { getAgentConversation, getDefaultMaxTurns, getGraceTurns, normalizeMaxTurns, SUBAGENT_TOOL_NAMES, setDefaultMaxTurns, setGraceTurns, steerAgent } from "./agent-runner.js";
19
- import { BUILTIN_TOOL_NAMES, getAgentConfig, getAllTypes, getAvailableTypes, getDefaultAgentNames, getUserAgentNames, registerAgents, resolveType } from "./agent-types.js";
19
+ import { BUILTIN_TOOL_NAMES, getAgentConfig, getAllTypes, getAvailableTypes, isDefaultsDisabled, registerAgents, resolveType, setDefaultsDisabled } from "./agent-types.js";
20
20
  import { registerRpcHandlers } from "./cross-extension-rpc.js";
21
21
  import { loadCustomAgents } from "./custom-agents.js";
22
22
  import { isModelInScope, readEnabledModels, resolveEnabledModels } from "./enabled-models.js";
@@ -462,6 +462,17 @@ export default function (pi) {
462
462
  let scopeModelsEnabled = false;
463
463
  function isScopeModelsEnabled() { return scopeModelsEnabled; }
464
464
  function setScopeModelsEnabled(enabled) { scopeModelsEnabled = enabled; }
465
+ // ---- Disable default agents configuration ----
466
+ // When enabled, the three hardcoded default agents (general-purpose, Explore,
467
+ // Plan) are not registered. User-defined agents from .pi/agents/*.md are
468
+ // completely unaffected — only DEFAULT_AGENTS are suppressed.
469
+ // Defaults to false; opt-in via `/agents → Settings` or subagents.json.
470
+ // State lives in agent-types.ts (isDefaultsDisabled) because registerAgents
471
+ // needs it; this wrapper just re-registers after flipping it.
472
+ function setDisableDefaultAgents(b) {
473
+ setDefaultsDisabled(b);
474
+ reloadCustomAgents(); // re-register with new setting
475
+ }
465
476
  // ---- Batch tracking for smart join mode ----
466
477
  // Collects background agent IDs spawned in the current turn for smart grouping.
467
478
  // Uses a debounced timer: each new agent resets the 100ms window so that all
@@ -518,10 +529,10 @@ export default function (pi) {
518
529
  && BUILTIN_TOOL_NAMES.every((t) => tools.includes(t));
519
530
  return isFullSet ? "*" : tools.join(", ");
520
531
  };
521
- /** Build the full type list text dynamically from the unified registry. */
532
+ /** Build the full type list text dynamically from available agents only. */
522
533
  const buildTypeListText = () => {
523
- const allNames = [...getDefaultAgentNames(), ...getUserAgentNames()];
524
- return allNames.map((name) => {
534
+ const available = getAvailableTypes();
535
+ return available.map((name) => {
525
536
  const cfg = getAgentConfig(name);
526
537
  const modelSuffix = cfg?.model ? ` (${getModelLabelFromConfig(cfg.model)})` : "";
527
538
  const toolsSuffix = ` (Tools: ${formatToolsSuffix(cfg)})`;
@@ -535,7 +546,6 @@ export default function (pi) {
535
546
  // Strip trailing date suffix (e.g. "claude-haiku-4-5-20251001" → "claude-haiku-4-5")
536
547
  return name.replace(/-\d{8}$/, "");
537
548
  }
538
- const typeListText = buildTypeListText();
539
549
  // Apply persisted settings on startup and emit `subagents:settings_loaded`.
540
550
  // Global + project merged; missing → defaults; corrupt file emits a warning
541
551
  // to stderr and falls back to defaults.
@@ -546,6 +556,7 @@ export default function (pi) {
546
556
  setDefaultJoinMode,
547
557
  setSchedulingEnabled,
548
558
  setScopeModels: setScopeModelsEnabled,
559
+ setDisableDefaultAgents: setDisableDefaultAgents,
549
560
  }, (event, payload) => pi.events.emit(event, payload));
550
561
  // ---- Agent tool ----
551
562
  // Schedule param + its guideline are gated on `schedulingEnabled` (read once
@@ -570,7 +581,7 @@ export default function (pi) {
570
581
  description: `Launch a new agent to handle complex, multi-step tasks autonomously. Each agent type has specific capabilities and tools available to it.
571
582
 
572
583
  Available agent types and the tools they have access to:
573
- ${typeListText}
584
+ ${buildTypeListText()}
574
585
 
575
586
  Custom agents can be defined in .pi/agents/<name>.md (project) or ${getAgentDir()}/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.
576
587
 
@@ -1018,8 +1029,10 @@ Terse command-style prompts produce shallow, generic work.
1018
1029
  // Get final token count
1019
1030
  const tokenText = formatLifetimeTokens(fgState);
1020
1031
  const details = buildDetails(detailBase, record, fgState, { tokens: tokenText });
1032
+ // "general-purpose" may itself be unregistered (defaults disabled, no
1033
+ // user override) — getConfig then uses the hardcoded fallback config.
1021
1034
  const fallbackNote = fellBack
1022
- ? `Note: Unknown agent type "${rawType}" — using general-purpose.\n\n`
1035
+ ? `Note: Unknown agent type "${rawType}" — using ${resolveType("general-purpose") ? "general-purpose" : "the fallback agent config"}.\n\n`
1023
1036
  : "";
1024
1037
  if (record.status === "error") {
1025
1038
  return textResult(`${fallbackNote}Agent failed: ${record.error}`, details);
@@ -1690,6 +1703,7 @@ ${systemPrompt}
1690
1703
  defaultJoinMode: getDefaultJoinMode(),
1691
1704
  schedulingEnabled: isSchedulingEnabled(),
1692
1705
  scopeModels: isScopeModelsEnabled(),
1706
+ disableDefaultAgents: isDefaultsDisabled(),
1693
1707
  };
1694
1708
  }
1695
1709
  const NUMERIC_IDS = new Set(["maxConcurrent", "defaultMaxTurns", "graceTurns"]);
@@ -1741,6 +1755,13 @@ ${systemPrompt}
1741
1755
  currentValue: isScopeModelsEnabled() ? "on" : "off",
1742
1756
  values: ["on", "off"],
1743
1757
  },
1758
+ {
1759
+ id: "disableDefaultAgents",
1760
+ label: "Disable defaults",
1761
+ description: "Hide built-in agents (general-purpose, Explore, Plan) — custom agents are unaffected",
1762
+ currentValue: isDefaultsDisabled() ? "on" : "off",
1763
+ values: ["on", "off"],
1764
+ },
1744
1765
  ];
1745
1766
  }
1746
1767
  function applyValue(id, value) {
@@ -1790,6 +1811,11 @@ ${systemPrompt}
1790
1811
  setScopeModelsEnabled(enabled);
1791
1812
  notifyApplied(ctx, `Scope models ${enabled ? "enabled" : "disabled"}`);
1792
1813
  }
1814
+ else if (id === "disableDefaultAgents") {
1815
+ const enabled = value === "on";
1816
+ setDisableDefaultAgents(enabled);
1817
+ notifyApplied(ctx, `Default agents ${enabled ? "disabled" : "enabled"}. Tool spec change takes effect on next pi session.`);
1818
+ }
1793
1819
  }
1794
1820
  let list;
1795
1821
  // Track current selection index directly (SettingsList doesn't expose it).
@@ -40,6 +40,13 @@ export interface SubagentsSettings {
40
40
  * against. Defaults to false: subagents may use any model.
41
41
  */
42
42
  scopeModels?: boolean;
43
+ /**
44
+ * When true, the three built-in default agents (general-purpose, Explore, Plan)
45
+ * are not registered at startup. User-defined agents from .pi/agents/*.md are
46
+ * completely unaffected — only the hardcoded DEFAULT_AGENTS are suppressed.
47
+ * Defaults to false.
48
+ */
49
+ disableDefaultAgents?: boolean;
43
50
  }
44
51
  /** Setter hooks used by applySettings to wire persisted values into in-memory state. */
45
52
  export interface SettingsAppliers {
@@ -49,6 +56,7 @@ export interface SettingsAppliers {
49
56
  setDefaultJoinMode: (mode: JoinMode) => void;
50
57
  setSchedulingEnabled: (b: boolean) => void;
51
58
  setScopeModels: (enabled: boolean) => void;
59
+ setDisableDefaultAgents: (b: boolean) => void;
52
60
  }
53
61
  /** Emit callback — a subset of `pi.events.emit` to keep helpers testable. */
54
62
  export type SettingsEmit = (event: string, payload: unknown) => void;
package/dist/settings.js CHANGED
@@ -41,6 +41,9 @@ function sanitize(raw) {
41
41
  if (typeof r.scopeModels === "boolean") {
42
42
  out.scopeModels = r.scopeModels;
43
43
  }
44
+ if (typeof r.disableDefaultAgents === "boolean") {
45
+ out.disableDefaultAgents = r.disableDefaultAgents;
46
+ }
44
47
  return out;
45
48
  }
46
49
  function globalPath() {
@@ -100,6 +103,8 @@ export function applySettings(s, appliers) {
100
103
  appliers.setSchedulingEnabled(s.schedulingEnabled);
101
104
  if (typeof s.scopeModels === "boolean")
102
105
  appliers.setScopeModels(s.scopeModels);
106
+ if (typeof s.disableDefaultAgents === "boolean")
107
+ appliers.setDisableDefaultAgents(s.disableDefaultAgents);
103
108
  }
104
109
  /**
105
110
  * Format the user-facing toast for a settings mutation. Pure function —
package/dist/types.d.ts CHANGED
@@ -74,6 +74,7 @@ export interface AgentRecord {
74
74
  worktree?: {
75
75
  path: string;
76
76
  branch: string;
77
+ baseSha: string;
77
78
  };
78
79
  /** Worktree cleanup result after agent completion. */
79
80
  worktreeResult?: {
@@ -10,6 +10,8 @@ export interface WorktreeInfo {
10
10
  path: string;
11
11
  /** Branch name created for this worktree (if changes exist). */
12
12
  branch: string;
13
+ /** Commit SHA that the worktree was created from. */
14
+ baseSha: string;
13
15
  }
14
16
  export interface WorktreeCleanupResult {
15
17
  /** Whether changes were found in the worktree. */
package/dist/worktree.js CHANGED
@@ -16,9 +16,12 @@ import { join } from "node:path";
16
16
  */
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
+ let baseSha;
19
20
  try {
20
21
  execFileSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd, stdio: "pipe", timeout: 5000 });
21
- execFileSync("git", ["rev-parse", "HEAD"], { cwd, stdio: "pipe", timeout: 5000 });
22
+ baseSha = execFileSync("git", ["rev-parse", "HEAD"], { cwd, stdio: "pipe", timeout: 5000 })
23
+ .toString()
24
+ .trim();
22
25
  }
23
26
  catch {
24
27
  return undefined;
@@ -33,7 +36,7 @@ export function createWorktree(cwd, agentId) {
33
36
  stdio: "pipe",
34
37
  timeout: 30000,
35
38
  });
36
- return { path: worktreePath, branch };
39
+ return { path: worktreePath, branch, baseSha };
37
40
  }
38
41
  catch {
39
42
  // If worktree creation fails, return undefined (agent runs in normal cwd)
@@ -56,21 +59,30 @@ export function cleanupWorktree(cwd, worktree, agentDescription) {
56
59
  stdio: "pipe",
57
60
  timeout: 10000,
58
61
  }).toString().trim();
59
- if (!status) {
60
- // No changesremove worktree
61
- removeWorktree(cwd, worktree.path);
62
- return { hasChanges: false };
62
+ if (status) {
63
+ // Changes existstage, commit, and create a branch
64
+ execFileSync("git", ["add", "-A"], { cwd: worktree.path, stdio: "pipe", timeout: 10000 });
65
+ // Truncate description for commit message (no shell sanitization needed — execFileSync uses argv)
66
+ const safeDesc = agentDescription.slice(0, 200);
67
+ const commitMsg = `pi-agent: ${safeDesc}`;
68
+ execFileSync("git", ["commit", "--no-verify", "-m", commitMsg], {
69
+ cwd: worktree.path,
70
+ stdio: "pipe",
71
+ timeout: 10000,
72
+ });
73
+ }
74
+ else {
75
+ const currentSha = execFileSync("git", ["rev-parse", "HEAD"], {
76
+ cwd: worktree.path,
77
+ stdio: "pipe",
78
+ timeout: 5000,
79
+ }).toString().trim();
80
+ if (currentSha === worktree.baseSha) {
81
+ // No changes — remove worktree
82
+ removeWorktree(cwd, worktree.path);
83
+ return { hasChanges: false };
84
+ }
63
85
  }
64
- // Changes exist — stage, commit, and create a branch
65
- execFileSync("git", ["add", "-A"], { cwd: worktree.path, stdio: "pipe", timeout: 10000 });
66
- // Truncate description for commit message (no shell sanitization needed — execFileSync uses argv)
67
- const safeDesc = agentDescription.slice(0, 200);
68
- const commitMsg = `pi-agent: ${safeDesc}`;
69
- execFileSync("git", ["commit", "-m", commitMsg], {
70
- cwd: worktree.path,
71
- stdio: "pipe",
72
- timeout: 10000,
73
- });
74
86
  // Create a branch pointing to the worktree's HEAD.
75
87
  // If the branch already exists, append a suffix to avoid overwriting previous work.
76
88
  let branchName = worktree.branch;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tintinweb/pi-subagents",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
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",
@@ -24,6 +24,15 @@ export const BUILTIN_TOOL_NAMES: string[] = [
24
24
  /** Unified runtime registry of all agents (defaults + user-defined). */
25
25
  const agents = new Map<string, AgentConfig>();
26
26
 
27
+ /** When true, DEFAULT_AGENTS are skipped during registration. */
28
+ let disableDefaults = false;
29
+
30
+ /** Check whether default agents are disabled. */
31
+ export function isDefaultsDisabled(): boolean { return disableDefaults; }
32
+
33
+ /** Set whether default agents are disabled. */
34
+ export function setDefaultsDisabled(b: boolean): void { disableDefaults = b; }
35
+
27
36
  /**
28
37
  * Register agents into the unified registry.
29
38
  * Starts with DEFAULT_AGENTS, then overlays user agents (overrides defaults with same name).
@@ -32,9 +41,11 @@ const agents = new Map<string, AgentConfig>();
32
41
  export function registerAgents(userAgents: Map<string, AgentConfig>): void {
33
42
  agents.clear();
34
43
 
35
- // Start with defaults
36
- for (const [name, config] of DEFAULT_AGENTS) {
37
- agents.set(name, config);
44
+ // Start with defaults (unless disabled via settings)
45
+ if (!disableDefaults) {
46
+ for (const [name, config] of DEFAULT_AGENTS) {
47
+ agents.set(name, config);
48
+ }
38
49
  }
39
50
 
40
51
  // Overlay user agents (overrides defaults with same name)
package/src/index.ts CHANGED
@@ -17,7 +17,7 @@ import { Container, Key, matchesKey, type SettingItem, SettingsList, Spacer, Tex
17
17
  import { Type } from "@sinclair/typebox";
18
18
  import { AgentManager } from "./agent-manager.js";
19
19
  import { getAgentConversation, getDefaultMaxTurns, getGraceTurns, normalizeMaxTurns, SUBAGENT_TOOL_NAMES, setDefaultMaxTurns, setGraceTurns, steerAgent } from "./agent-runner.js";
20
- import { BUILTIN_TOOL_NAMES, getAgentConfig, getAllTypes, getAvailableTypes, getDefaultAgentNames, getUserAgentNames, registerAgents, resolveType } from "./agent-types.js";
20
+ import { BUILTIN_TOOL_NAMES, getAgentConfig, getAllTypes, getAvailableTypes, isDefaultsDisabled, registerAgents, resolveType, setDefaultsDisabled } from "./agent-types.js";
21
21
  import { registerRpcHandlers } from "./cross-extension-rpc.js";
22
22
  import { loadCustomAgents } from "./custom-agents.js";
23
23
  import { isModelInScope, readEnabledModels, resolveEnabledModels } from "./enabled-models.js";
@@ -521,6 +521,18 @@ export default function (pi: ExtensionAPI) {
521
521
  function isScopeModelsEnabled(): boolean { return scopeModelsEnabled; }
522
522
  function setScopeModelsEnabled(enabled: boolean): void { scopeModelsEnabled = enabled; }
523
523
 
524
+ // ---- Disable default agents configuration ----
525
+ // When enabled, the three hardcoded default agents (general-purpose, Explore,
526
+ // Plan) are not registered. User-defined agents from .pi/agents/*.md are
527
+ // completely unaffected — only DEFAULT_AGENTS are suppressed.
528
+ // Defaults to false; opt-in via `/agents → Settings` or subagents.json.
529
+ // State lives in agent-types.ts (isDefaultsDisabled) because registerAgents
530
+ // needs it; this wrapper just re-registers after flipping it.
531
+ function setDisableDefaultAgents(b: boolean): void {
532
+ setDefaultsDisabled(b);
533
+ reloadCustomAgents(); // re-register with new setting
534
+ }
535
+
524
536
  // ---- Batch tracking for smart join mode ----
525
537
  // Collects background agent IDs spawned in the current turn for smart grouping.
526
538
  // Uses a debounced timer: each new agent resets the 100ms window so that all
@@ -580,11 +592,11 @@ export default function (pi: ExtensionAPI) {
580
592
  return isFullSet ? "*" : tools.join(", ");
581
593
  };
582
594
 
583
- /** Build the full type list text dynamically from the unified registry. */
595
+ /** Build the full type list text dynamically from available agents only. */
584
596
  const buildTypeListText = () => {
585
- const allNames = [...getDefaultAgentNames(), ...getUserAgentNames()];
597
+ const available = getAvailableTypes();
586
598
 
587
- return allNames.map((name) => {
599
+ return available.map((name) => {
588
600
  const cfg = getAgentConfig(name);
589
601
  const modelSuffix = cfg?.model ? ` (${getModelLabelFromConfig(cfg.model)})` : "";
590
602
  const toolsSuffix = ` (Tools: ${formatToolsSuffix(cfg)})`;
@@ -600,8 +612,6 @@ export default function (pi: ExtensionAPI) {
600
612
  return name.replace(/-\d{8}$/, "");
601
613
  }
602
614
 
603
- const typeListText = buildTypeListText();
604
-
605
615
  // Apply persisted settings on startup and emit `subagents:settings_loaded`.
606
616
  // Global + project merged; missing → defaults; corrupt file emits a warning
607
617
  // to stderr and falls back to defaults.
@@ -613,6 +623,7 @@ export default function (pi: ExtensionAPI) {
613
623
  setDefaultJoinMode,
614
624
  setSchedulingEnabled,
615
625
  setScopeModels: setScopeModelsEnabled,
626
+ setDisableDefaultAgents: setDisableDefaultAgents,
616
627
  },
617
628
  (event, payload) => pi.events.emit(event, payload),
618
629
  );
@@ -647,7 +658,7 @@ export default function (pi: ExtensionAPI) {
647
658
  description: `Launch a new agent to handle complex, multi-step tasks autonomously. Each agent type has specific capabilities and tools available to it.
648
659
 
649
660
  Available agent types and the tools they have access to:
650
- ${typeListText}
661
+ ${buildTypeListText()}
651
662
 
652
663
  Custom agents can be defined in .pi/agents/<name>.md (project) or ${getAgentDir()}/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.
653
664
 
@@ -1157,8 +1168,10 @@ Terse command-style prompts produce shallow, generic work.
1157
1168
 
1158
1169
  const details = buildDetails(detailBase, record, fgState, { tokens: tokenText });
1159
1170
 
1171
+ // "general-purpose" may itself be unregistered (defaults disabled, no
1172
+ // user override) — getConfig then uses the hardcoded fallback config.
1160
1173
  const fallbackNote = fellBack
1161
- ? `Note: Unknown agent type "${rawType}" — using general-purpose.\n\n`
1174
+ ? `Note: Unknown agent type "${rawType}" — using ${resolveType("general-purpose") ? "general-purpose" : "the fallback agent config"}.\n\n`
1162
1175
  : "";
1163
1176
 
1164
1177
  if (record.status === "error") {
@@ -1855,6 +1868,7 @@ ${systemPrompt}
1855
1868
  defaultJoinMode: getDefaultJoinMode(),
1856
1869
  schedulingEnabled: isSchedulingEnabled(),
1857
1870
  scopeModels: isScopeModelsEnabled(),
1871
+ disableDefaultAgents: isDefaultsDisabled(),
1858
1872
  };
1859
1873
  }
1860
1874
 
@@ -1909,6 +1923,13 @@ ${systemPrompt}
1909
1923
  currentValue: isScopeModelsEnabled() ? "on" : "off",
1910
1924
  values: ["on", "off"],
1911
1925
  },
1926
+ {
1927
+ id: "disableDefaultAgents",
1928
+ label: "Disable defaults",
1929
+ description: "Hide built-in agents (general-purpose, Explore, Plan) — custom agents are unaffected",
1930
+ currentValue: isDefaultsDisabled() ? "on" : "off",
1931
+ values: ["on", "off"],
1932
+ },
1912
1933
  ];
1913
1934
  }
1914
1935
 
@@ -1953,6 +1974,10 @@ ${systemPrompt}
1953
1974
  const enabled = value === "on";
1954
1975
  setScopeModelsEnabled(enabled);
1955
1976
  notifyApplied(ctx, `Scope models ${enabled ? "enabled" : "disabled"}`);
1977
+ } else if (id === "disableDefaultAgents") {
1978
+ const enabled = value === "on";
1979
+ setDisableDefaultAgents(enabled);
1980
+ notifyApplied(ctx, `Default agents ${enabled ? "disabled" : "enabled"}. Tool spec change takes effect on next pi session.`);
1956
1981
  }
1957
1982
  }
1958
1983
 
package/src/settings.ts CHANGED
@@ -48,6 +48,13 @@ export interface SubagentsSettings {
48
48
  * against. Defaults to false: subagents may use any model.
49
49
  */
50
50
  scopeModels?: boolean;
51
+ /**
52
+ * When true, the three built-in default agents (general-purpose, Explore, Plan)
53
+ * are not registered at startup. User-defined agents from .pi/agents/*.md are
54
+ * completely unaffected — only the hardcoded DEFAULT_AGENTS are suppressed.
55
+ * Defaults to false.
56
+ */
57
+ disableDefaultAgents?: boolean;
51
58
  }
52
59
 
53
60
  /** Setter hooks used by applySettings to wire persisted values into in-memory state. */
@@ -58,6 +65,7 @@ export interface SettingsAppliers {
58
65
  setDefaultJoinMode: (mode: JoinMode) => void;
59
66
  setSchedulingEnabled: (b: boolean) => void;
60
67
  setScopeModels: (enabled: boolean) => void;
68
+ setDisableDefaultAgents: (b: boolean) => void;
61
69
  }
62
70
 
63
71
  /** Emit callback — a subset of `pi.events.emit` to keep helpers testable. */
@@ -107,6 +115,9 @@ function sanitize(raw: unknown): SubagentsSettings {
107
115
  if (typeof r.scopeModels === "boolean") {
108
116
  out.scopeModels = r.scopeModels;
109
117
  }
118
+ if (typeof r.disableDefaultAgents === "boolean") {
119
+ out.disableDefaultAgents = r.disableDefaultAgents;
120
+ }
110
121
  return out;
111
122
  }
112
123
 
@@ -163,6 +174,7 @@ export function applySettings(s: SubagentsSettings, appliers: SettingsAppliers):
163
174
  if (s.defaultJoinMode) appliers.setDefaultJoinMode(s.defaultJoinMode);
164
175
  if (typeof s.schedulingEnabled === "boolean") appliers.setSchedulingEnabled(s.schedulingEnabled);
165
176
  if (typeof s.scopeModels === "boolean") appliers.setScopeModels(s.scopeModels);
177
+ if (typeof s.disableDefaultAgents === "boolean") appliers.setDisableDefaultAgents(s.disableDefaultAgents);
166
178
  }
167
179
 
168
180
  /**
package/src/types.ts CHANGED
@@ -80,7 +80,7 @@ export interface AgentRecord {
80
80
  /** Steering messages queued before the session was ready. */
81
81
  pendingSteers?: string[];
82
82
  /** Worktree info if the agent is running in an isolated worktree. */
83
- worktree?: { path: string; branch: string };
83
+ worktree?: { path: string; branch: string; baseSha: string };
84
84
  /** Worktree cleanup result after agent completion. */
85
85
  worktreeResult?: { hasChanges: boolean; branch?: string };
86
86
  /** The tool_use_id from the original Agent tool call. */
package/src/worktree.ts CHANGED
@@ -17,6 +17,8 @@ export interface WorktreeInfo {
17
17
  path: string;
18
18
  /** Branch name created for this worktree (if changes exist). */
19
19
  branch: string;
20
+ /** Commit SHA that the worktree was created from. */
21
+ baseSha: string;
20
22
  }
21
23
 
22
24
  export interface WorktreeCleanupResult {
@@ -34,9 +36,12 @@ export interface WorktreeCleanupResult {
34
36
  */
35
37
  export function createWorktree(cwd: string, agentId: string): WorktreeInfo | undefined {
36
38
  // Verify we're in a git repo with at least one commit (HEAD must exist)
39
+ let baseSha: string;
37
40
  try {
38
41
  execFileSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd, stdio: "pipe", timeout: 5000 });
39
- execFileSync("git", ["rev-parse", "HEAD"], { cwd, stdio: "pipe", timeout: 5000 });
42
+ baseSha = execFileSync("git", ["rev-parse", "HEAD"], { cwd, stdio: "pipe", timeout: 5000 })
43
+ .toString()
44
+ .trim();
40
45
  } catch {
41
46
  return undefined;
42
47
  }
@@ -52,7 +57,7 @@ export function createWorktree(cwd: string, agentId: string): WorktreeInfo | und
52
57
  stdio: "pipe",
53
58
  timeout: 30000,
54
59
  });
55
- return { path: worktreePath, branch };
60
+ return { path: worktreePath, branch, baseSha };
56
61
  } catch {
57
62
  // If worktree creation fails, return undefined (agent runs in normal cwd)
58
63
  return undefined;
@@ -81,22 +86,30 @@ export function cleanupWorktree(
81
86
  timeout: 10000,
82
87
  }).toString().trim();
83
88
 
84
- if (!status) {
85
- // No changesremove worktree
86
- removeWorktree(cwd, worktree.path);
87
- return { hasChanges: false };
88
- }
89
+ if (status) {
90
+ // Changes existstage, commit, and create a branch
91
+ execFileSync("git", ["add", "-A"], { cwd: worktree.path, stdio: "pipe", timeout: 10000 });
92
+ // Truncate description for commit message (no shell sanitization needed — execFileSync uses argv)
93
+ const safeDesc = agentDescription.slice(0, 200);
94
+ const commitMsg = `pi-agent: ${safeDesc}`;
95
+ execFileSync("git", ["commit", "--no-verify", "-m", commitMsg], {
96
+ cwd: worktree.path,
97
+ stdio: "pipe",
98
+ timeout: 10000,
99
+ });
100
+ } else {
101
+ const currentSha = execFileSync("git", ["rev-parse", "HEAD"], {
102
+ cwd: worktree.path,
103
+ stdio: "pipe",
104
+ timeout: 5000,
105
+ }).toString().trim();
89
106
 
90
- // Changes exist stage, commit, and create a branch
91
- execFileSync("git", ["add", "-A"], { cwd: worktree.path, stdio: "pipe", timeout: 10000 });
92
- // Truncate description for commit message (no shell sanitization needed — execFileSync uses argv)
93
- const safeDesc = agentDescription.slice(0, 200);
94
- const commitMsg = `pi-agent: ${safeDesc}`;
95
- execFileSync("git", ["commit", "-m", commitMsg], {
96
- cwd: worktree.path,
97
- stdio: "pipe",
98
- timeout: 10000,
99
- });
107
+ if (currentSha === worktree.baseSha) {
108
+ // No changes remove worktree
109
+ removeWorktree(cwd, worktree.path);
110
+ return { hasChanges: false };
111
+ }
112
+ }
100
113
 
101
114
  // Create a branch pointing to the worktree's HEAD.
102
115
  // If the branch already exists, append a suffix to avoid overwriting previous work.
@@ -0,0 +1,18 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ // The print-mode e2e suite (test/subagents-print-mode-e2e.test.ts) drives REAL
5
+ // faux-model turns through pi-coding-agent + pi-agent-core. That requires ONE
6
+ // shared @earendil-works/pi-ai instance so the faux provider the test registers
7
+ // lands in the same api-registry the session streams through. npm physically
8
+ // duplicates pi-ai (a top-level copy and one nested under pi-coding-agent), which
9
+ // otherwise yields two registries and "No API provider registered" errors.
10
+ // Inlining the @earendil-works packages routes them through Vite's resolver so
11
+ // dedupe can collapse pi-ai to a single instance — for the parent AND for every
12
+ // subagent session the extension spawns. dedupe alone is insufficient (it only
13
+ // affects modules Vite resolves; without inline the runtime stays externalized).
14
+ test: {
15
+ server: { deps: { inline: [/@earendil-works\/pi-/] } },
16
+ },
17
+ resolve: { dedupe: ["@earendil-works/pi-ai"] },
18
+ });