@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 +11 -0
- package/README.md +7 -2
- package/dist/agent-types.d.ts +4 -0
- package/dist/agent-types.js +11 -3
- package/dist/index.js +33 -7
- package/dist/settings.d.ts +8 -0
- package/dist/settings.js +5 -0
- package/dist/types.d.ts +1 -0
- package/dist/worktree.d.ts +2 -0
- package/dist/worktree.js +28 -16
- package/package.json +1 -1
- package/src/agent-types.ts +14 -3
- package/src/index.ts +33 -8
- package/src/settings.ts +12 -0
- package/src/types.ts +1 -1
- package/src/worktree.ts +30 -17
- package/vitest.config.ts +18 -0
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
|
|
package/dist/agent-types.d.ts
CHANGED
|
@@ -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).
|
package/dist/agent-types.js
CHANGED
|
@@ -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
|
-
|
|
31
|
-
|
|
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,
|
|
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
|
|
532
|
+
/** Build the full type list text dynamically from available agents only. */
|
|
522
533
|
const buildTypeListText = () => {
|
|
523
|
-
const
|
|
524
|
-
return
|
|
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
|
-
${
|
|
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).
|
package/dist/settings.d.ts
CHANGED
|
@@ -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
package/dist/worktree.d.ts
CHANGED
|
@@ -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 (
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
if (status) {
|
|
63
|
+
// Changes exist — stage, 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
package/src/agent-types.ts
CHANGED
|
@@ -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
|
-
|
|
37
|
-
|
|
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,
|
|
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
|
|
595
|
+
/** Build the full type list text dynamically from available agents only. */
|
|
584
596
|
const buildTypeListText = () => {
|
|
585
|
-
const
|
|
597
|
+
const available = getAvailableTypes();
|
|
586
598
|
|
|
587
|
-
return
|
|
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
|
-
${
|
|
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 (
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
+
if (status) {
|
|
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", "--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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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.
|
package/vitest.config.ts
ADDED
|
@@ -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
|
+
});
|