@tintinweb/pi-subagents 0.10.0 → 0.10.2
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 +20 -0
- package/README.md +27 -2
- package/dist/agent-runner.js +54 -10
- package/dist/agent-types.d.ts +5 -0
- package/dist/agent-types.js +13 -3
- package/dist/custom-agents.js +1 -0
- package/dist/index.js +137 -14
- package/dist/settings.d.ts +21 -0
- package/dist/settings.js +11 -0
- package/dist/types.d.ts +4 -0
- package/dist/ui/conversation-viewer.d.ts +5 -1
- package/dist/ui/conversation-viewer.js +10 -5
- package/dist/ui/viewer-keys.d.ts +20 -0
- package/dist/ui/viewer-keys.js +17 -0
- package/dist/worktree.d.ts +2 -0
- package/dist/worktree.js +28 -16
- package/examples/agent-tool-description.md +42 -0
- package/package.json +1 -1
- package/src/agent-runner.ts +53 -10
- package/src/agent-types.ts +17 -3
- package/src/custom-agents.ts +1 -0
- package/src/index.ts +139 -16
- package/src/settings.ts +31 -0
- package/src/types.ts +4 -1
- package/src/ui/conversation-viewer.ts +9 -4
- package/src/ui/viewer-keys.ts +39 -0
- package/src/worktree.ts +30 -17
- package/vitest.config.ts +18 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.10.2] - 2026-06-10
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **`exclude_extensions:` agent frontmatter — extension denylist for subagents** ([#94](https://github.com/tintinweb/pi-subagents/issues/94) — thanks [@ramhaidar](https://github.com/ramhaidar)). Applied after the `extensions:` include set; exclude wins, including over `tools: ext:` selectors (an excluded extension never loads, so its `ext:` reference becomes the usual orphan warning). The key use case: `extensions: true` + `exclude_extensions: pi-notify` — all extensions except a noisy one, without hand-maintaining an allowlist. Plain canonical names only (case-insensitive); paths, `*`, and unmatched names fire `extension-error:…` warnings (warn-not-abort, as with `extensions:` mismatches); `extensions: false` + an exclude warns that the exclude has no effect. **Not a sandbox:** excluded extensions' factory code still executes once during loading — exclusion suppresses handler binding and tool registration, not load-time side effects. The negation syntax `extensions: ["*", "!name"]` was deliberately rejected: an unquoted `!name` is a YAML tag and silently mis-parses.
|
|
14
|
+
- **`toolDescriptionMode` setting — opt-in compact Agent tool description** ([#91](https://github.com/tintinweb/pi-subagents/issues/91) — thanks [@tiberiuichim](https://github.com/tiberiuichim)). The full Claude Code-style description costs ~1,400 tokens with the default agents and grows with each custom agent (the type list embeds full agent descriptions) — significant for small/local models. `toolDescriptionMode: "compact"` (via `/agents → Settings → Tool description` or `subagents.json`) swaps in a ~75% smaller description: one-line type list (first sentence of each agent description), terse usage notes, per-option details left to the parameter descriptions. Default `"full"` is byte-identical to before — the rich description's guardrails are deliberately load-bearing and stay the default. A third mode, `"custom"`, registers a user-authored description from `<cwd>/.pi/agent-tool-description.md` (project) or `<agentDir>/agent-tool-description.md` (global; project wins), with `{{placeholder}}` substitution keeping the dynamic parts live — `{{typeList}}`, `{{compactTypeList}}`, `{{agentDir}}`, `{{scheduleGuideline}}` — so a hand-written description can't drift out of sync with the registered agents (the advertised-vs-spawnable staleness [#92](https://github.com/tintinweb/pi-subagents/issues/92) just fixed). Unknown placeholders are left verbatim with a stderr warning; a missing/empty file falls back to `"full"`. Only the prose is customizable — the parameter schema stays code-owned. A ready-made starting point ships at `examples/agent-tool-description.md`, reproducing the full description exactly (CI-enforced byte-identical, so the example can't go stale). Like `schedulingEnabled`, the mode is read at tool registration — changing it applies on the next pi session. The issue's original ask (move the description to a skill) isn't possible in pi: tools must register their description in the tool schema for the model to call them; skills are lazily-loaded instructions, not tool registrations.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- **Conversation viewer honors custom `tui.select.*` keybindings** ([#99](https://github.com/tintinweb/pi-subagents/issues/99) — thanks [@owenniles](https://github.com/owenniles)). The viewer hardcoded its scroll keys and discarded the `KeybindingsManager` pi injects into `ctx.ui.custom()`, so user bindings (e.g. emacs-style `ctrl+p`/`ctrl+n` on `tui.select.up`/`down`) worked in pi core selectors but not here. Scrolling now resolves through `tui.select.up`/`down`/`pageUp`/`pageDown`; the viewer-specific `k`/`j` and `shift+arrow` aliases still work alongside, and behavior without custom bindings is unchanged (the `tui.select.*` defaults are the previously hardcoded keys).
|
|
18
|
+
|
|
19
|
+
## [0.10.1] - 2026-06-10
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- **`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).
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- **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.
|
|
26
|
+
- **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`.
|
|
27
|
+
- **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.
|
|
28
|
+
- **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).
|
|
29
|
+
|
|
10
30
|
## [0.10.0] - 2026-06-01
|
|
11
31
|
|
|
12
32
|
> **⚠️ 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
|
@@ -196,6 +196,7 @@ All fields are optional — sensible defaults for everything.
|
|
|
196
196
|
| `display_name` | — | Display name for UI (e.g. widget, agent list) |
|
|
197
197
|
| `tools` | all 7 | Which tools the agent can call. Built-in names (`read, grep, …`), `*` / `all` (all built-ins), `none`, and `ext:<extension>` / `ext:<extension>/<tool>` selectors for extension tools. See [Tool & extension scoping](#tool--extension-scoping) below |
|
|
198
198
|
| `extensions` | `true` | Which extensions to load for the agent. `true` (all defaults), `false` (none), or an explicit list: `[mcp, "/abs/path.ts", "*"]`. See [Tool & extension scoping](#tool--extension-scoping) below |
|
|
199
|
+
| `exclude_extensions` | — | Extension denylist applied after `extensions:` — exclude wins. Plain names only (case-insensitive), no paths or `*`. Useful with `extensions: true` to drop one extension (e.g. `pi-notify`) |
|
|
199
200
|
| `skills` | `true` | Inherit skills from parent. Can be a comma-separated list of skill names to preload (see [Skill Preloading](#skill-preloading) for discovery locations) |
|
|
200
201
|
| `memory` | — | Persistent agent memory scope: `project`, `local`, or `user`. Auto-detects read-only agents |
|
|
201
202
|
| `disallowed_tools` | — | Comma-separated tools to deny even if extensions provide them |
|
|
@@ -227,6 +228,8 @@ extensions: false # no extensions load
|
|
|
227
228
|
extensions: [mcp] # only mcp loads
|
|
228
229
|
extensions: ["*", "/abs/foo.ts"] # all defaults plus one path-loaded extension
|
|
229
230
|
|
|
231
|
+
exclude_extensions: pi-notify # everything except pi-notify (with extensions: true)
|
|
232
|
+
|
|
230
233
|
# Specialist: load one extension, expose only one of its tools, keep built-ins
|
|
231
234
|
extensions: [mcp]
|
|
232
235
|
tools: "*, ext:mcp/search"
|
|
@@ -240,6 +243,8 @@ A few rules the examples don't make obvious:
|
|
|
240
243
|
- Any `ext:` entry flips extension tools to an explicit allowlist — unnamed extensions still load (handlers fire) but expose no tools. So `tools: "*, ext:mcp/search"` exposes only `search` from `mcp`, nothing from any other extension.
|
|
241
244
|
- Extension names match case-insensitively (`[Mcp]` = `[mcp]`); tool names in `ext:foo/bar` stay case-sensitive.
|
|
242
245
|
- Plain `tools:` typos fail loudly: `tools: reed, grep` fires `tools-error:…` instead of silently producing an under-tooled agent.
|
|
246
|
+
- `exclude_extensions:` wins over `extensions:` and over `ext:` selectors — an excluded extension never loads and a `tools: ext:` entry can't pull it back. Plain names only (no paths, no `*`); a name matching nothing fires an `extension-error:…` warning.
|
|
247
|
+
- `exclude_extensions:` is **not a sandbox**: excluded extensions' factory code still executes once during loading — exclusion suppresses their handlers and tools, not their load-time side effects. Don't rely on it to contain an untrusted extension.
|
|
243
248
|
- Array and string forms are equivalent: `[a, b]` == `"a, b"`.
|
|
244
249
|
|
|
245
250
|
## Tools
|
|
@@ -365,12 +370,29 @@ When on, each subagent spawn's effective model is validated against pi's own `en
|
|
|
365
370
|
|
|
366
371
|
## Persistent Settings
|
|
367
372
|
|
|
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:
|
|
373
|
+
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, tool description full/compact/custom) persist across pi restarts. Two files, merged on load:
|
|
369
374
|
|
|
370
375
|
- **Global:** `~/.pi/agent/subagents.json` — your machine-wide defaults. Edit by hand; the `/agents` menu never writes here.
|
|
371
376
|
- **Project:** `<cwd>/.pi/subagents.json` — per-project overrides. Written by `/agents` → Settings.
|
|
372
377
|
|
|
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
|
|
378
|
+
**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).
|
|
379
|
+
|
|
380
|
+
**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).
|
|
381
|
+
|
|
382
|
+
**Tool description** (`toolDescriptionMode`, default `"full"`): which Agent tool description the LLM sees. `"full"` is the rich Claude Code-style prompt (~1,400 tokens with the default agents); `"compact"` is ~75% smaller — one-line agent type list, terse usage notes — for small/local models where tool-spec tokens are expensive. Per-option details stay in the parameter descriptions in every mode (the parameter schema is never customizable). Applies on the next pi session.
|
|
383
|
+
|
|
384
|
+
`"custom"` registers your own description from `<cwd>/.pi/agent-tool-description.md` (project) or `<agentDir>/agent-tool-description.md` (global; project wins). The file is read once at tool registration, so edits also apply on the next pi session. Dynamic parts stay live via placeholders — a static agent list would go stale the moment you add a custom agent:
|
|
385
|
+
|
|
386
|
+
```markdown
|
|
387
|
+
Launch an autonomous agent. Available types:
|
|
388
|
+
{{typeList}}
|
|
389
|
+
|
|
390
|
+
Custom agents live in .pi/agents/ or {{agentDir}}/agents/.
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
Placeholders: `{{typeList}}` (full per-agent descriptions), `{{compactTypeList}}` (first sentence each), `{{agentDir}}`, `{{scheduleGuideline}}` (expands with its own leading newline + `- ` bullet when scheduling is on — place it directly after your last rule line; empty when scheduling is off). Unknown placeholders are left verbatim with a stderr warning; a missing or empty file falls back to `"full"` with a warning. Note the usual trust umbrella: a project-level file shapes the orchestrator's prompt, same as project agents and extensions do.
|
|
394
|
+
|
|
395
|
+
**Starting point:** copy [`examples/agent-tool-description.md`](examples/agent-tool-description.md) — it reproduces the default full description exactly (a CI test keeps it in sync), so you can trim from a known-good baseline instead of writing from scratch.
|
|
374
396
|
|
|
375
397
|
**Example — global defaults for a beefy machine:**
|
|
376
398
|
|
|
@@ -507,6 +529,9 @@ Agent({ subagent_type: "refactor", prompt: "...", isolation: "worktree" })
|
|
|
507
529
|
The agent gets a full, isolated copy of the repository. On completion:
|
|
508
530
|
- **No changes:** worktree is cleaned up automatically
|
|
509
531
|
- **Changes made:** changes are committed to a new branch (`pi-agent-<id>`) and returned in the result
|
|
532
|
+
- **Agent committed its own work:** the branch is created at the agent's HEAD, preserving its commits (uncommitted leftovers are committed on top first)
|
|
533
|
+
|
|
534
|
+
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
535
|
|
|
511
536
|
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
537
|
|
package/dist/agent-runner.js
CHANGED
|
@@ -206,6 +206,9 @@ export async function runAgent(ctx, type, prompt, options) {
|
|
|
206
206
|
const extras = {};
|
|
207
207
|
// Resolve extensions/skills: isolated overrides to false
|
|
208
208
|
const extensions = options.isolated ? false : config.extensions;
|
|
209
|
+
// Nulling excludes under isolated also suppresses the orphaned-exclude warning —
|
|
210
|
+
// isolation is an intentional override, not a misconfiguration.
|
|
211
|
+
const excludeExtensions = options.isolated ? undefined : config.excludeExtensions;
|
|
209
212
|
const skills = options.isolated ? false : config.skills;
|
|
210
213
|
// Skill preloading: when skills is string[], preload their content into prompt
|
|
211
214
|
if (Array.isArray(skills)) {
|
|
@@ -277,17 +280,35 @@ export async function runAgent(ctx, type, prompt, options) {
|
|
|
277
280
|
? parseExtensionsSpec(extensions, effectiveCwd)
|
|
278
281
|
: undefined;
|
|
279
282
|
const keepNames = extensionsSpec?.names ?? new Set();
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
+
// `exclude_extensions:` is a denylist applied AFTER the include set — exclude wins.
|
|
284
|
+
// Plain canonical names only (case-insensitive). Note: excluded extensions'
|
|
285
|
+
// factories still run once during reload() (see comment above) — exclusion
|
|
286
|
+
// suppresses handler binding and tool registration; it is not a sandbox.
|
|
287
|
+
const excludeNames = new Set((excludeExtensions ?? []).map((n) => n.toLowerCase()));
|
|
288
|
+
const hasExcludes = excludeNames.size > 0;
|
|
289
|
+
// The override filters loaded extensions down to `keepNames` minus `excludeNames`.
|
|
290
|
+
// It's only needed when we're neither loading everything without excludes
|
|
291
|
+
// (`extensions: true` or a `"*"` wildcard) nor nothing (`noExtensions`).
|
|
283
292
|
const loadAll = extensions === true || extensionsSpec?.wildcard === true;
|
|
284
293
|
const additionalExtensionPaths = extensionsSpec?.paths.length ? extensionsSpec.paths : undefined;
|
|
285
|
-
|
|
294
|
+
// Pre-filter discovered set, captured by the override — the exclude-typo warning
|
|
295
|
+
// must compare against this, not the surviving set (absence from survivors is
|
|
296
|
+
// an exclude *succeeding*).
|
|
297
|
+
let discoveredNames;
|
|
298
|
+
const extensionsOverride = noExtensions || (loadAll && !hasExcludes)
|
|
286
299
|
? undefined
|
|
287
|
-
: (base) =>
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
300
|
+
: (base) => {
|
|
301
|
+
discoveredNames = new Set(base.extensions.map((e) => extensionCanonicalName(e.path)));
|
|
302
|
+
return {
|
|
303
|
+
...base,
|
|
304
|
+
extensions: base.extensions.filter((e) => {
|
|
305
|
+
const name = extensionCanonicalName(e.path);
|
|
306
|
+
if (excludeNames.has(name))
|
|
307
|
+
return false; // exclude wins
|
|
308
|
+
return loadAll || keepNames.has(name);
|
|
309
|
+
}),
|
|
310
|
+
};
|
|
311
|
+
};
|
|
291
312
|
const loader = new DefaultResourceLoader({
|
|
292
313
|
cwd: effectiveCwd,
|
|
293
314
|
agentDir,
|
|
@@ -325,13 +346,36 @@ export async function runAgent(ctx, type, prompt, options) {
|
|
|
325
346
|
// - `tools: ext:foo` but foo isn't in the loaded set (because `extensions:`
|
|
326
347
|
// didn't include it). Since v0.9, `ext:` no longer pulls extensions in;
|
|
327
348
|
// loading is `extensions:`-authoritative.
|
|
349
|
+
// An exclude_extensions: alongside extensions: false is contradictory — nothing
|
|
350
|
+
// loads, so there is nothing to exclude.
|
|
351
|
+
if (hasExcludes && noExtensions) {
|
|
352
|
+
options.onToolActivity?.({
|
|
353
|
+
type: "end",
|
|
354
|
+
toolName: `extension-error:exclude_extensions has no effect for agent "${type}" — extensions: false loads nothing`,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
// Exclude typo check: compares against the PRE-filter discovered set (an excluded
|
|
358
|
+
// name absent from the surviving set is the exclude working as intended). Also
|
|
359
|
+
// flags path-like and "*" entries — excludes are plain names only.
|
|
360
|
+
if (hasExcludes && discoveredNames) {
|
|
361
|
+
for (const name of excludeNames) {
|
|
362
|
+
if (!discoveredNames.has(name)) {
|
|
363
|
+
options.onToolActivity?.({
|
|
364
|
+
type: "end",
|
|
365
|
+
toolName: `extension-error:exclude_extensions: "${name}" for agent "${type}" did not match any discovered extension`,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
328
370
|
if (keepNames.size > 0 || extNames.size > 0) {
|
|
329
371
|
const survivingNames = new Set(loader.getExtensions().extensions.map((e) => extensionCanonicalName(e.path)));
|
|
330
372
|
for (const name of keepNames) {
|
|
331
373
|
if (!survivingNames.has(name)) {
|
|
332
374
|
options.onToolActivity?.({
|
|
333
375
|
type: "end",
|
|
334
|
-
toolName:
|
|
376
|
+
toolName: excludeNames.has(name)
|
|
377
|
+
? `extension-error:extension "${name}" is in both extensions: and exclude_extensions: for agent "${type}" — exclude wins`
|
|
378
|
+
: `extension-error:extension "${name}" requested by agent "${type}" was not loaded`,
|
|
335
379
|
});
|
|
336
380
|
}
|
|
337
381
|
}
|
|
@@ -339,7 +383,7 @@ export async function runAgent(ctx, type, prompt, options) {
|
|
|
339
383
|
if (!survivingNames.has(name)) {
|
|
340
384
|
options.onToolActivity?.({
|
|
341
385
|
type: "end",
|
|
342
|
-
toolName: `extension-error:ext:${name} referenced by agent "${type}" but extension "${name}" is not loaded (
|
|
386
|
+
toolName: `extension-error:ext:${name} referenced by agent "${type}" but extension "${name}" is not loaded (check extensions:/exclude_extensions:)`,
|
|
343
387
|
});
|
|
344
388
|
}
|
|
345
389
|
}
|
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).
|
|
@@ -50,6 +54,7 @@ export declare function getConfig(type: string): {
|
|
|
50
54
|
description: string;
|
|
51
55
|
builtinToolNames: string[];
|
|
52
56
|
extensions: true | string[] | false;
|
|
57
|
+
excludeExtensions?: string[];
|
|
53
58
|
skills: true | string[] | false;
|
|
54
59
|
promptMode: "replace" | "append";
|
|
55
60
|
};
|
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) {
|
|
@@ -119,6 +127,7 @@ export function getConfig(type) {
|
|
|
119
127
|
description: config.description,
|
|
120
128
|
builtinToolNames: config.builtinToolNames ?? BUILTIN_TOOL_NAMES,
|
|
121
129
|
extensions: config.extensions,
|
|
130
|
+
excludeExtensions: config.excludeExtensions,
|
|
122
131
|
skills: config.skills,
|
|
123
132
|
promptMode: config.promptMode,
|
|
124
133
|
};
|
|
@@ -131,6 +140,7 @@ export function getConfig(type) {
|
|
|
131
140
|
description: gp.description,
|
|
132
141
|
builtinToolNames: gp.builtinToolNames ?? BUILTIN_TOOL_NAMES,
|
|
133
142
|
extensions: gp.extensions,
|
|
143
|
+
excludeExtensions: gp.excludeExtensions,
|
|
134
144
|
skills: gp.skills,
|
|
135
145
|
promptMode: gp.promptMode,
|
|
136
146
|
};
|
package/dist/custom-agents.js
CHANGED
|
@@ -52,6 +52,7 @@ function loadFromDir(dir, agents, source) {
|
|
|
52
52
|
extSelectors,
|
|
53
53
|
disallowedTools: csvListOptional(fm.disallowed_tools),
|
|
54
54
|
extensions: inheritField(fm.extensions ?? fm.inherit_extensions),
|
|
55
|
+
excludeExtensions: csvListOptional(fm.exclude_extensions),
|
|
55
56
|
skills: inheritField(fm.skills ?? fm.inherit_skills),
|
|
56
57
|
model: str(fm.model),
|
|
57
58
|
thinking: str(fm.thinking),
|
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,24 @@ 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
|
+
}
|
|
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; }
|
|
465
483
|
// ---- Batch tracking for smart join mode ----
|
|
466
484
|
// Collects background agent IDs spawned in the current turn for smart grouping.
|
|
467
485
|
// Uses a debounced timer: each new agent resets the 100ms window so that all
|
|
@@ -518,16 +536,26 @@ export default function (pi) {
|
|
|
518
536
|
&& BUILTIN_TOOL_NAMES.every((t) => tools.includes(t));
|
|
519
537
|
return isFullSet ? "*" : tools.join(", ");
|
|
520
538
|
};
|
|
521
|
-
/** Build the full type list text dynamically from
|
|
539
|
+
/** Build the full type list text dynamically from available agents only. */
|
|
522
540
|
const buildTypeListText = () => {
|
|
523
|
-
const
|
|
524
|
-
return
|
|
541
|
+
const available = getAvailableTypes();
|
|
542
|
+
return available.map((name) => {
|
|
525
543
|
const cfg = getAgentConfig(name);
|
|
526
544
|
const modelSuffix = cfg?.model ? ` (${getModelLabelFromConfig(cfg.model)})` : "";
|
|
527
545
|
const toolsSuffix = ` (Tools: ${formatToolsSuffix(cfg)})`;
|
|
528
546
|
return `- ${name}: ${cfg?.description ?? name}${modelSuffix}${toolsSuffix}`;
|
|
529
547
|
}).join("\n");
|
|
530
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");
|
|
531
559
|
/** Derive a short model label from a model string. */
|
|
532
560
|
function getModelLabelFromConfig(model) {
|
|
533
561
|
// Strip provider prefix (e.g. "anthropic/claude-sonnet-4-6" → "claude-sonnet-4-6")
|
|
@@ -535,7 +563,6 @@ export default function (pi) {
|
|
|
535
563
|
// Strip trailing date suffix (e.g. "claude-haiku-4-5-20251001" → "claude-haiku-4-5")
|
|
536
564
|
return name.replace(/-\d{8}$/, "");
|
|
537
565
|
}
|
|
538
|
-
const typeListText = buildTypeListText();
|
|
539
566
|
// Apply persisted settings on startup and emit `subagents:settings_loaded`.
|
|
540
567
|
// Global + project merged; missing → defaults; corrupt file emits a warning
|
|
541
568
|
// to stderr and falls back to defaults.
|
|
@@ -546,6 +573,8 @@ export default function (pi) {
|
|
|
546
573
|
setDefaultJoinMode,
|
|
547
574
|
setSchedulingEnabled,
|
|
548
575
|
setScopeModels: setScopeModelsEnabled,
|
|
576
|
+
setDisableDefaultAgents: setDisableDefaultAgents,
|
|
577
|
+
setToolDescriptionMode: setToolDescriptionMode,
|
|
549
578
|
}, (event, payload) => pi.events.emit(event, payload));
|
|
550
579
|
// ---- Agent tool ----
|
|
551
580
|
// Schedule param + its guideline are gated on `schedulingEnabled` (read once
|
|
@@ -564,13 +593,24 @@ export default function (pi) {
|
|
|
564
593
|
const scheduleGuideline = isSchedulingEnabled()
|
|
565
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.`
|
|
566
595
|
: "";
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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.
|
|
571
611
|
|
|
572
612
|
Available agent types and the tools they have access to:
|
|
573
|
-
${
|
|
613
|
+
${buildTypeListText()}
|
|
574
614
|
|
|
575
615
|
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
616
|
|
|
@@ -608,7 +648,61 @@ Provide clear, detailed prompts so the agent can work autonomously. Brief it lik
|
|
|
608
648
|
|
|
609
649
|
Terse command-style prompts produce shallow, generic work.
|
|
610
650
|
|
|
611
|
-
**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,
|
|
612
706
|
promptSnippet: "Launch autonomous sub-agents for complex multi-step tasks",
|
|
613
707
|
promptGuidelines: [
|
|
614
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.",
|
|
@@ -1018,8 +1112,10 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1018
1112
|
// Get final token count
|
|
1019
1113
|
const tokenText = formatLifetimeTokens(fgState);
|
|
1020
1114
|
const details = buildDetails(detailBase, record, fgState, { tokens: tokenText });
|
|
1115
|
+
// "general-purpose" may itself be unregistered (defaults disabled, no
|
|
1116
|
+
// user override) — getConfig then uses the hardcoded fallback config.
|
|
1021
1117
|
const fallbackNote = fellBack
|
|
1022
|
-
? `Note: Unknown agent type "${rawType}" — using general-purpose.\n\n`
|
|
1118
|
+
? `Note: Unknown agent type "${rawType}" — using ${resolveType("general-purpose") ? "general-purpose" : "the fallback agent config"}.\n\n`
|
|
1023
1119
|
: "";
|
|
1024
1120
|
if (record.status === "error") {
|
|
1025
1121
|
return textResult(`${fallbackNote}Agent failed: ${record.error}`, details);
|
|
@@ -1313,12 +1409,12 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1313
1409
|
const { ConversationViewer, VIEWPORT_HEIGHT_PCT } = await import("./ui/conversation-viewer.js");
|
|
1314
1410
|
const session = record.session;
|
|
1315
1411
|
const activity = agentActivity.get(record.id);
|
|
1316
|
-
await ctx.ui.custom((tui, theme,
|
|
1412
|
+
await ctx.ui.custom((tui, theme, keybindings, done) => {
|
|
1317
1413
|
return new ConversationViewer(tui, session, record, activity, theme, done, () => {
|
|
1318
1414
|
if (manager.abort(record.id)) {
|
|
1319
1415
|
ctx.ui.notify(`Stopped "${record.description}".`, "info");
|
|
1320
1416
|
}
|
|
1321
|
-
});
|
|
1417
|
+
}, keybindings);
|
|
1322
1418
|
}, {
|
|
1323
1419
|
overlay: true,
|
|
1324
1420
|
overlayOptions: { anchor: "center", width: "90%", maxHeight: `${VIEWPORT_HEIGHT_PCT}%` },
|
|
@@ -1426,6 +1522,8 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1426
1522
|
fmFields.push("extensions: false");
|
|
1427
1523
|
else if (Array.isArray(cfg.extensions))
|
|
1428
1524
|
fmFields.push(`extensions: ${cfg.extensions.join(", ")}`);
|
|
1525
|
+
if (cfg.excludeExtensions?.length)
|
|
1526
|
+
fmFields.push(`exclude_extensions: ${cfg.excludeExtensions.join(", ")}`);
|
|
1429
1527
|
if (cfg.skills === false)
|
|
1430
1528
|
fmFields.push("skills: false");
|
|
1431
1529
|
else if (Array.isArray(cfg.skills))
|
|
@@ -1690,6 +1788,8 @@ ${systemPrompt}
|
|
|
1690
1788
|
defaultJoinMode: getDefaultJoinMode(),
|
|
1691
1789
|
schedulingEnabled: isSchedulingEnabled(),
|
|
1692
1790
|
scopeModels: isScopeModelsEnabled(),
|
|
1791
|
+
disableDefaultAgents: isDefaultsDisabled(),
|
|
1792
|
+
toolDescriptionMode: getToolDescriptionMode(),
|
|
1693
1793
|
};
|
|
1694
1794
|
}
|
|
1695
1795
|
const NUMERIC_IDS = new Set(["maxConcurrent", "defaultMaxTurns", "graceTurns"]);
|
|
@@ -1741,6 +1841,20 @@ ${systemPrompt}
|
|
|
1741
1841
|
currentValue: isScopeModelsEnabled() ? "on" : "off",
|
|
1742
1842
|
values: ["on", "off"],
|
|
1743
1843
|
},
|
|
1844
|
+
{
|
|
1845
|
+
id: "disableDefaultAgents",
|
|
1846
|
+
label: "Disable defaults",
|
|
1847
|
+
description: "Hide built-in agents (general-purpose, Explore, Plan) — custom agents are unaffected",
|
|
1848
|
+
currentValue: isDefaultsDisabled() ? "on" : "off",
|
|
1849
|
+
values: ["on", "off"],
|
|
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
|
+
},
|
|
1744
1858
|
];
|
|
1745
1859
|
}
|
|
1746
1860
|
function applyValue(id, value) {
|
|
@@ -1790,6 +1904,15 @@ ${systemPrompt}
|
|
|
1790
1904
|
setScopeModelsEnabled(enabled);
|
|
1791
1905
|
notifyApplied(ctx, `Scope models ${enabled ? "enabled" : "disabled"}`);
|
|
1792
1906
|
}
|
|
1907
|
+
else if (id === "disableDefaultAgents") {
|
|
1908
|
+
const enabled = value === "on";
|
|
1909
|
+
setDisableDefaultAgents(enabled);
|
|
1910
|
+
notifyApplied(ctx, `Default agents ${enabled ? "disabled" : "enabled"}. Tool spec change takes effect on next pi session.`);
|
|
1911
|
+
}
|
|
1912
|
+
else if (id === "toolDescriptionMode") {
|
|
1913
|
+
setToolDescriptionMode(value);
|
|
1914
|
+
notifyApplied(ctx, `Tool description set to ${value}. Takes effect on next pi session.`);
|
|
1915
|
+
}
|
|
1793
1916
|
}
|
|
1794
1917
|
let list;
|
|
1795
1918
|
// Track current selection index directly (SettingsList doesn't expose it).
|
package/dist/settings.d.ts
CHANGED
|
@@ -40,7 +40,26 @@ 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;
|
|
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;
|
|
43
61
|
}
|
|
62
|
+
export type ToolDescriptionMode = "full" | "compact" | "custom";
|
|
44
63
|
/** Setter hooks used by applySettings to wire persisted values into in-memory state. */
|
|
45
64
|
export interface SettingsAppliers {
|
|
46
65
|
setMaxConcurrent: (n: number) => void;
|
|
@@ -49,6 +68,8 @@ export interface SettingsAppliers {
|
|
|
49
68
|
setDefaultJoinMode: (mode: JoinMode) => void;
|
|
50
69
|
setSchedulingEnabled: (b: boolean) => void;
|
|
51
70
|
setScopeModels: (enabled: boolean) => void;
|
|
71
|
+
setDisableDefaultAgents: (b: boolean) => void;
|
|
72
|
+
setToolDescriptionMode: (mode: ToolDescriptionMode) => void;
|
|
52
73
|
}
|
|
53
74
|
/** Emit callback — a subset of `pi.events.emit` to keep helpers testable. */
|
|
54
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.
|
|
@@ -41,6 +42,12 @@ function sanitize(raw) {
|
|
|
41
42
|
if (typeof r.scopeModels === "boolean") {
|
|
42
43
|
out.scopeModels = r.scopeModels;
|
|
43
44
|
}
|
|
45
|
+
if (typeof r.disableDefaultAgents === "boolean") {
|
|
46
|
+
out.disableDefaultAgents = r.disableDefaultAgents;
|
|
47
|
+
}
|
|
48
|
+
if (typeof r.toolDescriptionMode === "string" && VALID_TOOL_DESCRIPTION_MODES.has(r.toolDescriptionMode)) {
|
|
49
|
+
out.toolDescriptionMode = r.toolDescriptionMode;
|
|
50
|
+
}
|
|
44
51
|
return out;
|
|
45
52
|
}
|
|
46
53
|
function globalPath() {
|
|
@@ -100,6 +107,10 @@ export function applySettings(s, appliers) {
|
|
|
100
107
|
appliers.setSchedulingEnabled(s.schedulingEnabled);
|
|
101
108
|
if (typeof s.scopeModels === "boolean")
|
|
102
109
|
appliers.setScopeModels(s.scopeModels);
|
|
110
|
+
if (typeof s.disableDefaultAgents === "boolean")
|
|
111
|
+
appliers.setDisableDefaultAgents(s.disableDefaultAgents);
|
|
112
|
+
if (s.toolDescriptionMode)
|
|
113
|
+
appliers.setToolDescriptionMode(s.toolDescriptionMode);
|
|
103
114
|
}
|
|
104
115
|
/**
|
|
105
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;
|
|
@@ -74,6 +77,7 @@ export interface AgentRecord {
|
|
|
74
77
|
worktree?: {
|
|
75
78
|
path: string;
|
|
76
79
|
branch: string;
|
|
80
|
+
baseSha: string;
|
|
77
81
|
};
|
|
78
82
|
/** Worktree cleanup result after agent completion. */
|
|
79
83
|
worktreeResult?: {
|