@tintinweb/pi-subagents 0.6.0 → 0.6.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 +18 -0
- package/README.md +30 -4
- package/dist/agent-runner.js +8 -1
- package/dist/custom-agents.d.ts +2 -2
- package/dist/custom-agents.js +4 -5
- package/dist/index.js +40 -13
- package/dist/output-file.d.ts +7 -0
- package/dist/output-file.js +22 -2
- package/dist/settings.d.ts +56 -0
- package/dist/settings.js +125 -0
- package/package.json +5 -5
- package/src/agent-runner.ts +8 -1
- package/src/custom-agents.ts +4 -5
- package/src/index.ts +50 -13
- package/src/output-file.ts +21 -2
- package/src/settings.ts +172 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.6.2] - 2026-04-28
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **`Agent` tool fails on Windows with `ENOENT` creating output directory** ([#27](https://github.com/tintinweb/pi-subagents/issues/27) — thanks [@sixnathan](https://github.com/sixnathan) for the diagnosis). The cwd-encoding regex in `output-file.ts` only handled POSIX `/` separators, so on Windows `cwd = "C:\\Users\\foo\\project"` survived unchanged and `path.join(tmpRoot, encoded, …)` produced an invalid nested-absolute path. Now extracts a small `encodeCwd()` helper that handles both `/` and `\\` separators, strips the Windows drive-letter prefix, and preserves UNC server/share segments. The `chmodSync(root, 0o700)` call is also wrapped in a try/catch that swallows errors only on Windows (where chmod is a no-op and can throw on some filesystems); on Unix the error still propagates so umask-defeating `0o700` enforcement is preserved.
|
|
14
|
+
|
|
15
|
+
## [0.6.1] - 2026-04-25
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **Persistent `/agents` → Settings** ([#24](https://github.com/tintinweb/pi-subagents/issues/24)) — the four runtime tuning values (`maxConcurrent`, `defaultMaxTurns`, `graceTurns`, `defaultJoinMode`) now survive pi restarts via a two-file dual-scope model mirroring pi's own `SettingsManager`. Global `~/.pi/agent/subagents.json` provides machine-wide defaults (edit by hand; the menu never writes here); project `<cwd>/.pi/subagents.json` holds per-project overrides (written by `/agents` → Settings). Load merges both with project winning on conflicts. Invalid fields are silently dropped per field; malformed JSON emits a warning to stderr and falls back to defaults so startup always proceeds; write failures downgrade the settings toast to a warning with `(session only; failed to persist)` so changes aren't silently reverted on next restart.
|
|
19
|
+
- **New lifecycle events** — `subagents:settings_loaded` (emitted once at extension init with the merged settings) and `subagents:settings_changed` (emitted on each `/agents` → Settings mutation with the new snapshot and a `persisted: boolean` flag so listeners can react to write failures).
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- **`AGENTS.md` / `CLAUDE.md` / `APPEND_SYSTEM.md` no longer leak into sub-agent prompts** ([#26](https://github.com/tintinweb/pi-subagents/pull/26) — thanks [@mikeyobrien](https://github.com/mikeyobrien) for the diagnosis). Upstream `buildSystemPrompt()` re-appends `contextFiles` and `appendSystemPrompt` *after* our `systemPromptOverride` runs, which silently defeated `prompt_mode: replace` and `isolated: true` — parent project context (e.g. autoresearch-mode blocks) was bleeding into fresh `Explore` / custom sub-agents regardless of frontmatter. Fix uses upstream's `noContextFiles: true` flag (skips the load entirely, introduced in pi 0.68) plus `appendSystemPromptOverride: () => []` (no flag equivalent for append sources). **Behavior change:** subagents no longer implicitly inherit parent `AGENTS.md`/`CLAUDE.md`/`APPEND_SYSTEM.md`. To get parent project context into a subagent, use `prompt_mode: append` (parent's already-built system prompt flows in via `systemPromptOverride`), or `inherit_context: true` (parent conversation), or inline the content into the agent's own frontmatter.
|
|
23
|
+
- **Custom agent discovery respects `PI_CODING_AGENT_DIR`** ([#35](https://github.com/tintinweb/pi-subagents/pull/35), closes [#23](https://github.com/tintinweb/pi-subagents/issues/23) — thanks [@Amolith](https://github.com/Amolith) for the diagnosis). Two remaining hardcoded `~/.pi/agent/agents/` paths in `custom-agents.ts` and `index.ts` bypassed the env var, so users who relocated their agent directory (e.g. via `PI_CODING_AGENT_DIR`) still had global agents loaded from the default location and help text referencing the wrong path. Both now use upstream `getAgentDir()`, consistent with `agent-runner.ts` and `settings.ts`; tilde expansion is handled by upstream.
|
|
24
|
+
|
|
10
25
|
## [0.6.0] - 2026-04-24
|
|
11
26
|
|
|
12
27
|
> **⚠️ Breaking: drops support for `pi` < 0.68.** The upstream `pi-coding-agent` package shipped breaking API changes in v0.68 (and further ones in v0.70). This release migrates to `^0.70.2` and is **not** backward-compatible with hosts on `pi` 0.62–0.67. Users on those versions must upgrade their `pi` installation (`npm install -g @mariozechner/pi-coding-agent@latest`) before updating this extension.
|
|
@@ -359,6 +374,9 @@ Initial release.
|
|
|
359
374
|
- **Thinking level** — per-agent extended thinking control
|
|
360
375
|
- **`/agent` and `/agents` commands**
|
|
361
376
|
|
|
377
|
+
[0.6.2]: https://github.com/tintinweb/pi-subagents/compare/v0.6.1...v0.6.2
|
|
378
|
+
[0.6.1]: https://github.com/tintinweb/pi-subagents/compare/v0.6.0...v0.6.1
|
|
379
|
+
[0.6.0]: https://github.com/tintinweb/pi-subagents/compare/v0.5.2...v0.6.0
|
|
362
380
|
[0.5.2]: https://github.com/tintinweb/pi-subagents/compare/v0.5.1...v0.5.2
|
|
363
381
|
[0.5.1]: https://github.com/tintinweb/pi-subagents/compare/v0.5.0...v0.5.1
|
|
364
382
|
[0.5.0]: https://github.com/tintinweb/pi-subagents/compare/v0.4.9...v0.5.0
|
package/README.md
CHANGED
|
@@ -116,9 +116,9 @@ Agents are discovered from two locations (higher priority wins):
|
|
|
116
116
|
| Priority | Location | Scope |
|
|
117
117
|
|----------|----------|-------|
|
|
118
118
|
| 1 (highest) | `.pi/agents/<name>.md` | Project — per-repo agents |
|
|
119
|
-
| 2 | `~/.pi/agent/agents/<name>.md` | Global — available everywhere |
|
|
119
|
+
| 2 | `$PI_CODING_AGENT_DIR/agents/<name>.md` (default `~/.pi/agent/agents/<name>.md`) | Global — available everywhere |
|
|
120
120
|
|
|
121
|
-
Project-level agents override global ones with the same name, so you can customize a global agent for a specific project.
|
|
121
|
+
Project-level agents override global ones with the same name, so you can customize a global agent for a specific project. The global location follows the upstream `PI_CODING_AGENT_DIR` env var — set it to relocate all pi-coding-agent state (agents, skills, settings) to a custom directory.
|
|
122
122
|
|
|
123
123
|
### Example: `.pi/agents/auditor.md`
|
|
124
124
|
|
|
@@ -163,10 +163,9 @@ All fields are optional — sensible defaults for everything.
|
|
|
163
163
|
| `model` | inherit parent | Model — `provider/modelId` or fuzzy name (`"haiku"`, `"sonnet"`) |
|
|
164
164
|
| `thinking` | inherit | off, minimal, low, medium, high, xhigh |
|
|
165
165
|
| `max_turns` | unlimited | Max agentic turns before graceful shutdown. `0` or omit for unlimited |
|
|
166
|
-
| `prompt_mode` | `replace` | `replace`: body is the full system prompt. `append`: body appended to parent's prompt (agent acts as a "parent twin"
|
|
166
|
+
| `prompt_mode` | `replace` | `replace`: body is the full system prompt (no AGENTS.md / CLAUDE.md inheritance). `append`: body appended to parent's prompt (agent acts as a "parent twin" — inherits parent's AGENTS.md / CLAUDE.md) |
|
|
167
167
|
| `inherit_context` | `false` | Fork parent conversation into agent |
|
|
168
168
|
| `run_in_background` | `false` | Run in background by default |
|
|
169
|
-
| `isolation` | — | `worktree`: run in a temporary git worktree for full repo isolation |
|
|
170
169
|
| `isolated` | `false` | No extension/MCP tools, only built-in |
|
|
171
170
|
| `enabled` | `true` | Set to `false` to disable an agent (useful for hiding a default agent per-project) |
|
|
172
171
|
|
|
@@ -272,6 +271,31 @@ When background agents complete, they notify the main agent. The **join mode** c
|
|
|
272
271
|
**Configuration:**
|
|
273
272
|
- Configure join mode in `/agents` → Settings → Join mode
|
|
274
273
|
|
|
274
|
+
## Persistent Settings
|
|
275
|
+
|
|
276
|
+
Runtime tuning values set via `/agents` → Settings (max concurrency, default max turns, grace turns, default join mode) persist across pi restarts. Two files, merged on load:
|
|
277
|
+
|
|
278
|
+
- **Global:** `~/.pi/agent/subagents.json` — your machine-wide defaults. Edit by hand; the `/agents` menu never writes here.
|
|
279
|
+
- **Project:** `<cwd>/.pi/subagents.json` — per-project overrides. Written by `/agents` → Settings.
|
|
280
|
+
|
|
281
|
+
**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`).
|
|
282
|
+
|
|
283
|
+
**Example — global defaults for a beefy machine:**
|
|
284
|
+
|
|
285
|
+
```bash
|
|
286
|
+
mkdir -p ~/.pi/agent
|
|
287
|
+
cat > ~/.pi/agent/subagents.json <<'EOF'
|
|
288
|
+
{
|
|
289
|
+
"maxConcurrent": 16,
|
|
290
|
+
"graceTurns": 10
|
|
291
|
+
}
|
|
292
|
+
EOF
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Every project now starts with concurrency 16 and grace 10, without ever touching the menu. Individual projects can still override via `/agents` → Settings.
|
|
296
|
+
|
|
297
|
+
**Failure behavior:** missing file is silent; malformed JSON logs a `[pi-subagents] Ignoring malformed settings at …` warning to stderr; invalid/out-of-range field values are dropped per-field; write failures downgrade the `/agents` toast to a warning with `(session only; failed to persist)`.
|
|
298
|
+
|
|
275
299
|
## Events
|
|
276
300
|
|
|
277
301
|
Agent lifecycle events are emitted via `pi.events.emit()` so other extensions can react:
|
|
@@ -284,6 +308,8 @@ Agent lifecycle events are emitted via `pi.events.emit()` so other extensions ca
|
|
|
284
308
|
| `subagents:failed` | Agent errored, stopped, or aborted | same as completed + `error`, `status` |
|
|
285
309
|
| `subagents:steered` | Steering message sent | `id`, `message` |
|
|
286
310
|
| `subagents:ready` | Extension loaded and RPC handlers registered | — |
|
|
311
|
+
| `subagents:settings_loaded` | Persisted settings applied at extension init | `settings` (merged global + project) |
|
|
312
|
+
| `subagents:settings_changed` | `/agents` → Settings mutation was applied | `settings`, `persisted` (`boolean` — `false` on write failure) |
|
|
287
313
|
|
|
288
314
|
## Cross-Extension RPC
|
|
289
315
|
|
package/dist/agent-runner.js
CHANGED
|
@@ -157,7 +157,12 @@ export async function runAgent(ctx, type, prompt, options) {
|
|
|
157
157
|
// Still pass noSkills: true since we don't need the skill loader to load them again.
|
|
158
158
|
const noSkills = skills === false || Array.isArray(skills);
|
|
159
159
|
const agentDir = getAgentDir();
|
|
160
|
-
// Load extensions/skills: true or string[] → load; false → don't
|
|
160
|
+
// Load extensions/skills: true or string[] → load; false → don't.
|
|
161
|
+
// Suppress AGENTS.md/CLAUDE.md and APPEND_SYSTEM.md — upstream's
|
|
162
|
+
// buildSystemPrompt() re-appends both AFTER systemPromptOverride, which
|
|
163
|
+
// would defeat prompt_mode: replace and isolated: true. Parent context, if
|
|
164
|
+
// wanted, reaches the subagent via prompt_mode: append (parentSystemPrompt
|
|
165
|
+
// is embedded in systemPromptOverride) or inherit_context (conversation).
|
|
161
166
|
const loader = new DefaultResourceLoader({
|
|
162
167
|
cwd: effectiveCwd,
|
|
163
168
|
agentDir,
|
|
@@ -165,7 +170,9 @@ export async function runAgent(ctx, type, prompt, options) {
|
|
|
165
170
|
noSkills,
|
|
166
171
|
noPromptTemplates: true,
|
|
167
172
|
noThemes: true,
|
|
173
|
+
noContextFiles: true,
|
|
168
174
|
systemPromptOverride: () => systemPrompt,
|
|
175
|
+
appendSystemPromptOverride: () => [],
|
|
169
176
|
});
|
|
170
177
|
await loader.reload();
|
|
171
178
|
// Resolve model: explicit option > config.model > parent model
|
package/dist/custom-agents.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* custom-agents.ts — Load user-defined agents from project (.pi/agents/) and global (~/.pi/agent/agents/) locations.
|
|
2
|
+
* custom-agents.ts — Load user-defined agents from project (.pi/agents/) and global ($PI_CODING_AGENT_DIR/agents/, default ~/.pi/agent/agents/) locations.
|
|
3
3
|
*/
|
|
4
4
|
import type { AgentConfig } from "./types.js";
|
|
5
5
|
/**
|
|
6
6
|
* Scan for custom agent .md files from multiple locations.
|
|
7
7
|
* Discovery hierarchy (higher priority wins):
|
|
8
8
|
* 1. Project: <cwd>/.pi/agents/*.md
|
|
9
|
-
* 2. Global: ~/.pi/agent/agents/*.md
|
|
9
|
+
* 2. Global: $PI_CODING_AGENT_DIR/agents/*.md (default: ~/.pi/agent/agents/*.md)
|
|
10
10
|
*
|
|
11
11
|
* Project-level agents override global ones with the same name.
|
|
12
12
|
* Any name is allowed — names matching defaults (e.g. "Explore") override them.
|
package/dist/custom-agents.js
CHANGED
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* custom-agents.ts — Load user-defined agents from project (.pi/agents/) and global (~/.pi/agent/agents/) locations.
|
|
2
|
+
* custom-agents.ts — Load user-defined agents from project (.pi/agents/) and global ($PI_CODING_AGENT_DIR/agents/, default ~/.pi/agent/agents/) locations.
|
|
3
3
|
*/
|
|
4
4
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
5
|
-
import { homedir } from "node:os";
|
|
6
5
|
import { basename, join } from "node:path";
|
|
7
|
-
import { parseFrontmatter } from "@mariozechner/pi-coding-agent";
|
|
6
|
+
import { getAgentDir, parseFrontmatter } from "@mariozechner/pi-coding-agent";
|
|
8
7
|
import { BUILTIN_TOOL_NAMES } from "./agent-types.js";
|
|
9
8
|
/**
|
|
10
9
|
* Scan for custom agent .md files from multiple locations.
|
|
11
10
|
* Discovery hierarchy (higher priority wins):
|
|
12
11
|
* 1. Project: <cwd>/.pi/agents/*.md
|
|
13
|
-
* 2. Global: ~/.pi/agent/agents/*.md
|
|
12
|
+
* 2. Global: $PI_CODING_AGENT_DIR/agents/*.md (default: ~/.pi/agent/agents/*.md)
|
|
14
13
|
*
|
|
15
14
|
* Project-level agents override global ones with the same name.
|
|
16
15
|
* Any name is allowed — names matching defaults (e.g. "Explore") override them.
|
|
17
16
|
*/
|
|
18
17
|
export function loadCustomAgents(cwd) {
|
|
19
|
-
const globalDir = join(
|
|
18
|
+
const globalDir = join(getAgentDir(), "agents");
|
|
20
19
|
const projectDir = join(cwd, ".pi", "agents");
|
|
21
20
|
const agents = new Map();
|
|
22
21
|
loadFromDir(globalDir, agents, "global"); // lower priority
|
package/dist/index.js
CHANGED
|
@@ -10,9 +10,8 @@
|
|
|
10
10
|
* /agents — Interactive agent management menu
|
|
11
11
|
*/
|
|
12
12
|
import { existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
13
|
-
import { homedir } from "node:os";
|
|
14
13
|
import { join } from "node:path";
|
|
15
|
-
import { defineTool } from "@mariozechner/pi-coding-agent";
|
|
14
|
+
import { defineTool, getAgentDir } from "@mariozechner/pi-coding-agent";
|
|
16
15
|
import { Text } from "@mariozechner/pi-tui";
|
|
17
16
|
import { Type } from "@sinclair/typebox";
|
|
18
17
|
import { AgentManager } from "./agent-manager.js";
|
|
@@ -24,6 +23,7 @@ import { GroupJoinManager } from "./group-join.js";
|
|
|
24
23
|
import { resolveAgentInvocationConfig, resolveJoinMode } from "./invocation-config.js";
|
|
25
24
|
import { resolveModel } from "./model-resolver.js";
|
|
26
25
|
import { createOutputFilePath, streamToOutputFile, writeInitialEntry } from "./output-file.js";
|
|
26
|
+
import { applyAndEmitLoaded, saveAndEmitChanged } from "./settings.js";
|
|
27
27
|
import { AgentWidget, describeActivity, formatDuration, formatMs, formatTokens, formatTurns, getDisplayName, getPromptModeLabel, SPINNER, } from "./ui/agent-widget.js";
|
|
28
28
|
// ---- Shared helpers ----
|
|
29
29
|
/** Tool execute return value for a text response. */
|
|
@@ -478,7 +478,7 @@ export default function (pi) {
|
|
|
478
478
|
...defaultDescs,
|
|
479
479
|
...(customDescs.length > 0 ? ["", "Custom agents:", ...customDescs] : []),
|
|
480
480
|
"",
|
|
481
|
-
|
|
481
|
+
`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.`,
|
|
482
482
|
].join("\n");
|
|
483
483
|
};
|
|
484
484
|
/** Derive a short model label from a model string. */
|
|
@@ -489,6 +489,15 @@ export default function (pi) {
|
|
|
489
489
|
return name.replace(/-\d{8}$/, "");
|
|
490
490
|
}
|
|
491
491
|
const typeListText = buildTypeListText();
|
|
492
|
+
// Apply persisted settings on startup and emit `subagents:settings_loaded`.
|
|
493
|
+
// Global + project merged; missing → defaults; corrupt file emits a warning
|
|
494
|
+
// to stderr and falls back to defaults.
|
|
495
|
+
applyAndEmitLoaded({
|
|
496
|
+
setMaxConcurrent: (n) => manager.setMaxConcurrent(n),
|
|
497
|
+
setDefaultMaxTurns,
|
|
498
|
+
setGraceTurns,
|
|
499
|
+
setDefaultJoinMode,
|
|
500
|
+
}, (event, payload) => pi.events.emit(event, payload));
|
|
492
501
|
// ---- Agent tool ----
|
|
493
502
|
pi.registerTool(defineTool({
|
|
494
503
|
name: "Agent",
|
|
@@ -522,7 +531,7 @@ Guidelines:
|
|
|
522
531
|
description: "A short (3-5 word) description of the task (shown in UI).",
|
|
523
532
|
}),
|
|
524
533
|
subagent_type: Type.String({
|
|
525
|
-
description: `The type of specialized agent to use. Available types: ${getAvailableTypes().join(", ")}. Custom agents from .pi/agents/*.md (project) or
|
|
534
|
+
description: `The type of specialized agent to use. Available types: ${getAvailableTypes().join(", ")}. Custom agents from .pi/agents/*.md (project) or ${getAgentDir()}/agents/*.md (global) are also available.`,
|
|
526
535
|
}),
|
|
527
536
|
model: Type.Optional(Type.String({
|
|
528
537
|
description: 'Optional model override. Accepts "provider/modelId" or fuzzy name (e.g. "haiku", "sonnet"). Omit to use the agent type\'s default.',
|
|
@@ -952,7 +961,7 @@ Guidelines:
|
|
|
952
961
|
}));
|
|
953
962
|
// ---- /agents interactive menu ----
|
|
954
963
|
const projectAgentsDir = () => join(process.cwd(), ".pi", "agents");
|
|
955
|
-
const personalAgentsDir = () => join(
|
|
964
|
+
const personalAgentsDir = () => join(getAgentDir(), "agents");
|
|
956
965
|
/** Find the file path of a custom agent by name (project first, then global). */
|
|
957
966
|
function findAgentFile(name) {
|
|
958
967
|
const projectPath = join(projectAgentsDir(), `${name}.md`);
|
|
@@ -1180,7 +1189,7 @@ Guidelines:
|
|
|
1180
1189
|
async function ejectAgent(ctx, name, cfg) {
|
|
1181
1190
|
const location = await ctx.ui.select("Choose location", [
|
|
1182
1191
|
"Project (.pi/agents/)",
|
|
1183
|
-
|
|
1192
|
+
`Personal (${personalAgentsDir()})`,
|
|
1184
1193
|
]);
|
|
1185
1194
|
if (!location)
|
|
1186
1195
|
return;
|
|
@@ -1251,7 +1260,7 @@ Guidelines:
|
|
|
1251
1260
|
// No file (built-in default) — create a stub
|
|
1252
1261
|
const location = await ctx.ui.select("Choose location", [
|
|
1253
1262
|
"Project (.pi/agents/)",
|
|
1254
|
-
|
|
1263
|
+
`Personal (${personalAgentsDir()})`,
|
|
1255
1264
|
]);
|
|
1256
1265
|
if (!location)
|
|
1257
1266
|
return;
|
|
@@ -1286,7 +1295,7 @@ Guidelines:
|
|
|
1286
1295
|
async function showCreateWizard(ctx) {
|
|
1287
1296
|
const location = await ctx.ui.select("Choose location", [
|
|
1288
1297
|
"Project (.pi/agents/)",
|
|
1289
|
-
|
|
1298
|
+
`Personal (${personalAgentsDir()})`,
|
|
1290
1299
|
]);
|
|
1291
1300
|
if (!location)
|
|
1292
1301
|
return;
|
|
@@ -1463,6 +1472,16 @@ ${systemPrompt}
|
|
|
1463
1472
|
reloadCustomAgents();
|
|
1464
1473
|
ctx.ui.notify(`Created ${targetPath}`, "info");
|
|
1465
1474
|
}
|
|
1475
|
+
function snapshotSettings() {
|
|
1476
|
+
return {
|
|
1477
|
+
maxConcurrent: manager.getMaxConcurrent(),
|
|
1478
|
+
// 0 = unlimited — per SubagentsSettings.defaultMaxTurns docstring and
|
|
1479
|
+
// normalizeMaxTurns() in agent-runner.ts (which maps 0 → undefined).
|
|
1480
|
+
defaultMaxTurns: getDefaultMaxTurns() ?? 0,
|
|
1481
|
+
graceTurns: getGraceTurns(),
|
|
1482
|
+
defaultJoinMode: getDefaultJoinMode(),
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1466
1485
|
async function showSettings(ctx) {
|
|
1467
1486
|
const choice = await ctx.ui.select("Settings", [
|
|
1468
1487
|
`Max concurrency (current: ${manager.getMaxConcurrent()})`,
|
|
@@ -1478,7 +1497,7 @@ ${systemPrompt}
|
|
|
1478
1497
|
const n = parseInt(val, 10);
|
|
1479
1498
|
if (n >= 1) {
|
|
1480
1499
|
manager.setMaxConcurrent(n);
|
|
1481
|
-
ctx
|
|
1500
|
+
notifyApplied(ctx, `Max concurrency set to ${n}`);
|
|
1482
1501
|
}
|
|
1483
1502
|
else {
|
|
1484
1503
|
ctx.ui.notify("Must be a positive integer.", "warning");
|
|
@@ -1491,11 +1510,11 @@ ${systemPrompt}
|
|
|
1491
1510
|
const n = parseInt(val, 10);
|
|
1492
1511
|
if (n === 0) {
|
|
1493
1512
|
setDefaultMaxTurns(undefined);
|
|
1494
|
-
ctx
|
|
1513
|
+
notifyApplied(ctx, "Default max turns set to unlimited");
|
|
1495
1514
|
}
|
|
1496
1515
|
else if (n >= 1) {
|
|
1497
1516
|
setDefaultMaxTurns(n);
|
|
1498
|
-
ctx
|
|
1517
|
+
notifyApplied(ctx, `Default max turns set to ${n}`);
|
|
1499
1518
|
}
|
|
1500
1519
|
else {
|
|
1501
1520
|
ctx.ui.notify("Must be 0 (unlimited) or a positive integer.", "warning");
|
|
@@ -1508,7 +1527,7 @@ ${systemPrompt}
|
|
|
1508
1527
|
const n = parseInt(val, 10);
|
|
1509
1528
|
if (n >= 1) {
|
|
1510
1529
|
setGraceTurns(n);
|
|
1511
|
-
ctx
|
|
1530
|
+
notifyApplied(ctx, `Grace turns set to ${n}`);
|
|
1512
1531
|
}
|
|
1513
1532
|
else {
|
|
1514
1533
|
ctx.ui.notify("Must be a positive integer.", "warning");
|
|
@@ -1524,10 +1543,18 @@ ${systemPrompt}
|
|
|
1524
1543
|
if (val) {
|
|
1525
1544
|
const mode = val.split(" ")[0];
|
|
1526
1545
|
setDefaultJoinMode(mode);
|
|
1527
|
-
ctx
|
|
1546
|
+
notifyApplied(ctx, `Default join mode set to ${mode}`);
|
|
1528
1547
|
}
|
|
1529
1548
|
}
|
|
1530
1549
|
}
|
|
1550
|
+
// Persist the current snapshot, emit `subagents:settings_changed`, and surface
|
|
1551
|
+
// the right toast. Successful saves show info; persistence failures downgrade
|
|
1552
|
+
// to warning so users aren't silently reverted on restart. Event fires regardless
|
|
1553
|
+
// of outcome so listeners see the in-memory change.
|
|
1554
|
+
function notifyApplied(ctx, successMsg) {
|
|
1555
|
+
const { message, level } = saveAndEmitChanged(snapshotSettings(), successMsg, (event, payload) => pi.events.emit(event, payload));
|
|
1556
|
+
ctx.ui.notify(message, level);
|
|
1557
|
+
}
|
|
1531
1558
|
pi.registerCommand("agents", {
|
|
1532
1559
|
description: "Manage agents",
|
|
1533
1560
|
handler: async (_args, ctx) => { await showAgentsMenu(ctx); },
|
package/dist/output-file.d.ts
CHANGED
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
* matching Claude Code's task output file format.
|
|
6
6
|
*/
|
|
7
7
|
import type { AgentSession } from "@mariozechner/pi-coding-agent";
|
|
8
|
+
/**
|
|
9
|
+
* Encode a cwd path as a filesystem-safe directory name. Handles:
|
|
10
|
+
* - POSIX: "/home/user/project" → "home-user-project"
|
|
11
|
+
* - Windows: "C:\Users\foo\project" → "Users-foo-project"
|
|
12
|
+
* - UNC: "\\\\server\\share\\project" → "server-share-project"
|
|
13
|
+
*/
|
|
14
|
+
export declare function encodeCwd(cwd: string): string;
|
|
8
15
|
/** Create the output file path, ensuring the directory exists.
|
|
9
16
|
* Mirrors Claude Code's layout: /tmp/{prefix}-{uid}/{encoded-cwd}/{sessionId}/tasks/{agentId}.output */
|
|
10
17
|
export declare function createOutputFilePath(cwd: string, agentId: string, sessionId: string): string;
|
package/dist/output-file.js
CHANGED
|
@@ -7,13 +7,33 @@
|
|
|
7
7
|
import { appendFileSync, chmodSync, mkdirSync, writeFileSync } from "node:fs";
|
|
8
8
|
import { tmpdir } from "node:os";
|
|
9
9
|
import { join } from "node:path";
|
|
10
|
+
/**
|
|
11
|
+
* Encode a cwd path as a filesystem-safe directory name. Handles:
|
|
12
|
+
* - POSIX: "/home/user/project" → "home-user-project"
|
|
13
|
+
* - Windows: "C:\Users\foo\project" → "Users-foo-project"
|
|
14
|
+
* - UNC: "\\\\server\\share\\project" → "server-share-project"
|
|
15
|
+
*/
|
|
16
|
+
export function encodeCwd(cwd) {
|
|
17
|
+
return cwd
|
|
18
|
+
.replace(/[/\\]/g, "-") // both separators → dash
|
|
19
|
+
.replace(/^[A-Za-z]:-/, "") // strip Windows drive prefix ("C:-")
|
|
20
|
+
.replace(/^-+/, ""); // strip leading dashes (POSIX root, UNC)
|
|
21
|
+
}
|
|
10
22
|
/** Create the output file path, ensuring the directory exists.
|
|
11
23
|
* Mirrors Claude Code's layout: /tmp/{prefix}-{uid}/{encoded-cwd}/{sessionId}/tasks/{agentId}.output */
|
|
12
24
|
export function createOutputFilePath(cwd, agentId, sessionId) {
|
|
13
|
-
const encoded = cwd
|
|
25
|
+
const encoded = encodeCwd(cwd);
|
|
14
26
|
const root = join(tmpdir(), `pi-subagents-${process.getuid?.() ?? 0}`);
|
|
15
27
|
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
16
|
-
|
|
28
|
+
// chmod is a no-op on Windows and throws on some Windows filesystems.
|
|
29
|
+
// On Unix we still want to enforce 0o700 past umask, so only swallow on Windows.
|
|
30
|
+
try {
|
|
31
|
+
chmodSync(root, 0o700);
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
if (process.platform !== "win32")
|
|
35
|
+
throw err;
|
|
36
|
+
}
|
|
17
37
|
const dir = join(root, encoded, sessionId, "tasks");
|
|
18
38
|
mkdirSync(dir, { recursive: true });
|
|
19
39
|
return join(dir, `${agentId}.output`);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { JoinMode } from "./types.js";
|
|
2
|
+
export interface SubagentsSettings {
|
|
3
|
+
maxConcurrent?: number;
|
|
4
|
+
/**
|
|
5
|
+
* 0 = unlimited — the extension's single source of truth for that convention:
|
|
6
|
+
* `normalizeMaxTurns()` in agent-runner.ts treats 0 → `undefined`, and the
|
|
7
|
+
* `/agents` → Settings input prompt explicitly says "0 = unlimited".
|
|
8
|
+
*/
|
|
9
|
+
defaultMaxTurns?: number;
|
|
10
|
+
graceTurns?: number;
|
|
11
|
+
defaultJoinMode?: JoinMode;
|
|
12
|
+
}
|
|
13
|
+
/** Setter hooks used by applySettings to wire persisted values into in-memory state. */
|
|
14
|
+
export interface SettingsAppliers {
|
|
15
|
+
setMaxConcurrent: (n: number) => void;
|
|
16
|
+
setDefaultMaxTurns: (n: number) => void;
|
|
17
|
+
setGraceTurns: (n: number) => void;
|
|
18
|
+
setDefaultJoinMode: (mode: JoinMode) => void;
|
|
19
|
+
}
|
|
20
|
+
/** Emit callback — a subset of `pi.events.emit` to keep helpers testable. */
|
|
21
|
+
export type SettingsEmit = (event: string, payload: unknown) => void;
|
|
22
|
+
/** Load merged settings: global provides defaults, project overrides. */
|
|
23
|
+
export declare function loadSettings(cwd?: string): SubagentsSettings;
|
|
24
|
+
/**
|
|
25
|
+
* Write project-local settings. Global is never touched from code.
|
|
26
|
+
* Returns `true` on success, `false` if the write (or mkdir) failed so the
|
|
27
|
+
* caller can surface a warning — persistence isn't fatal but isn't silent.
|
|
28
|
+
*/
|
|
29
|
+
export declare function saveSettings(s: SubagentsSettings, cwd?: string): boolean;
|
|
30
|
+
/** Apply persisted settings to the in-memory state via caller-supplied setters. */
|
|
31
|
+
export declare function applySettings(s: SubagentsSettings, appliers: SettingsAppliers): void;
|
|
32
|
+
/**
|
|
33
|
+
* Format the user-facing toast for a settings mutation. Pure function —
|
|
34
|
+
* routes the success/failure of `saveSettings` into the right message + level
|
|
35
|
+
* so the UI layer (index.ts) stays a thin wire between input and notification.
|
|
36
|
+
*/
|
|
37
|
+
export declare function persistToastFor(successMsg: string, persisted: boolean): {
|
|
38
|
+
message: string;
|
|
39
|
+
level: "info" | "warning";
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Load merged settings, apply them to in-memory state, and emit the
|
|
43
|
+
* `subagents:settings_loaded` lifecycle event. Returns the loaded settings so
|
|
44
|
+
* callers can log/inspect. Extension init wires this once.
|
|
45
|
+
*/
|
|
46
|
+
export declare function applyAndEmitLoaded(appliers: SettingsAppliers, emit: SettingsEmit, cwd?: string): SubagentsSettings;
|
|
47
|
+
/**
|
|
48
|
+
* Persist a settings snapshot, emit the `subagents:settings_changed` event
|
|
49
|
+
* (regardless of persist outcome so listeners see the in-memory change), and
|
|
50
|
+
* return the toast the UI should display. Event payload carries the `persisted`
|
|
51
|
+
* flag so listeners can react to write failures.
|
|
52
|
+
*/
|
|
53
|
+
export declare function saveAndEmitChanged(snapshot: SubagentsSettings, successMsg: string, emit: SettingsEmit, cwd?: string): {
|
|
54
|
+
message: string;
|
|
55
|
+
level: "info" | "warning";
|
|
56
|
+
};
|
package/dist/settings.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Persistence for pi-subagents operational settings.
|
|
2
|
+
// - Global: ~/.pi/agent/subagents.json (via getAgentDir()) — manual defaults, never written here
|
|
3
|
+
// - Project: <cwd>/.pi/subagents.json — written by /agents → Settings; overrides global on load
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { getAgentDir } from "@mariozechner/pi-coding-agent";
|
|
7
|
+
const VALID_JOIN_MODES = new Set(["async", "group", "smart"]);
|
|
8
|
+
// Sanity ceilings — prevent hand-edited configs from asking for values that
|
|
9
|
+
// make no operational sense (e.g. 1e6 concurrent subagents). Permissive enough
|
|
10
|
+
// that any realistic power-user setting passes through.
|
|
11
|
+
const MAX_CONCURRENT_CEILING = 1024;
|
|
12
|
+
const MAX_TURNS_CEILING = 10_000;
|
|
13
|
+
const GRACE_TURNS_CEILING = 1_000;
|
|
14
|
+
/** Drop fields that don't match the expected shape. Silent — garbage becomes absent. */
|
|
15
|
+
function sanitize(raw) {
|
|
16
|
+
if (!raw || typeof raw !== "object")
|
|
17
|
+
return {};
|
|
18
|
+
const r = raw;
|
|
19
|
+
const out = {};
|
|
20
|
+
if (Number.isInteger(r.maxConcurrent) &&
|
|
21
|
+
r.maxConcurrent >= 1 &&
|
|
22
|
+
r.maxConcurrent <= MAX_CONCURRENT_CEILING) {
|
|
23
|
+
out.maxConcurrent = r.maxConcurrent;
|
|
24
|
+
}
|
|
25
|
+
if (Number.isInteger(r.defaultMaxTurns) &&
|
|
26
|
+
r.defaultMaxTurns >= 0 &&
|
|
27
|
+
r.defaultMaxTurns <= MAX_TURNS_CEILING) {
|
|
28
|
+
out.defaultMaxTurns = r.defaultMaxTurns;
|
|
29
|
+
}
|
|
30
|
+
if (Number.isInteger(r.graceTurns) &&
|
|
31
|
+
r.graceTurns >= 1 &&
|
|
32
|
+
r.graceTurns <= GRACE_TURNS_CEILING) {
|
|
33
|
+
out.graceTurns = r.graceTurns;
|
|
34
|
+
}
|
|
35
|
+
if (typeof r.defaultJoinMode === "string" && VALID_JOIN_MODES.has(r.defaultJoinMode)) {
|
|
36
|
+
out.defaultJoinMode = r.defaultJoinMode;
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
function globalPath() {
|
|
41
|
+
return join(getAgentDir(), "subagents.json");
|
|
42
|
+
}
|
|
43
|
+
function projectPath(cwd) {
|
|
44
|
+
return join(cwd, ".pi", "subagents.json");
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Read a settings file. Missing file is silent (returns `{}`). A file that
|
|
48
|
+
* exists but can't be parsed emits a warning to stderr so users aren't
|
|
49
|
+
* silently reverted to defaults — and still returns `{}` so startup proceeds.
|
|
50
|
+
*/
|
|
51
|
+
function readSettingsFile(path) {
|
|
52
|
+
if (!existsSync(path))
|
|
53
|
+
return {};
|
|
54
|
+
try {
|
|
55
|
+
return sanitize(JSON.parse(readFileSync(path, "utf-8")));
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
59
|
+
console.warn(`[pi-subagents] Ignoring malformed settings at ${path}: ${reason}`);
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** Load merged settings: global provides defaults, project overrides. */
|
|
64
|
+
export function loadSettings(cwd = process.cwd()) {
|
|
65
|
+
return { ...readSettingsFile(globalPath()), ...readSettingsFile(projectPath(cwd)) };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Write project-local settings. Global is never touched from code.
|
|
69
|
+
* Returns `true` on success, `false` if the write (or mkdir) failed so the
|
|
70
|
+
* caller can surface a warning — persistence isn't fatal but isn't silent.
|
|
71
|
+
*/
|
|
72
|
+
export function saveSettings(s, cwd = process.cwd()) {
|
|
73
|
+
const path = projectPath(cwd);
|
|
74
|
+
try {
|
|
75
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
76
|
+
writeFileSync(path, JSON.stringify(s, null, 2), "utf-8");
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** Apply persisted settings to the in-memory state via caller-supplied setters. */
|
|
84
|
+
export function applySettings(s, appliers) {
|
|
85
|
+
if (typeof s.maxConcurrent === "number")
|
|
86
|
+
appliers.setMaxConcurrent(s.maxConcurrent);
|
|
87
|
+
if (typeof s.defaultMaxTurns === "number")
|
|
88
|
+
appliers.setDefaultMaxTurns(s.defaultMaxTurns);
|
|
89
|
+
if (typeof s.graceTurns === "number")
|
|
90
|
+
appliers.setGraceTurns(s.graceTurns);
|
|
91
|
+
if (s.defaultJoinMode)
|
|
92
|
+
appliers.setDefaultJoinMode(s.defaultJoinMode);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Format the user-facing toast for a settings mutation. Pure function —
|
|
96
|
+
* routes the success/failure of `saveSettings` into the right message + level
|
|
97
|
+
* so the UI layer (index.ts) stays a thin wire between input and notification.
|
|
98
|
+
*/
|
|
99
|
+
export function persistToastFor(successMsg, persisted) {
|
|
100
|
+
return persisted
|
|
101
|
+
? { message: successMsg, level: "info" }
|
|
102
|
+
: { message: `${successMsg} (session only; failed to persist)`, level: "warning" };
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Load merged settings, apply them to in-memory state, and emit the
|
|
106
|
+
* `subagents:settings_loaded` lifecycle event. Returns the loaded settings so
|
|
107
|
+
* callers can log/inspect. Extension init wires this once.
|
|
108
|
+
*/
|
|
109
|
+
export function applyAndEmitLoaded(appliers, emit, cwd = process.cwd()) {
|
|
110
|
+
const settings = loadSettings(cwd);
|
|
111
|
+
applySettings(settings, appliers);
|
|
112
|
+
emit("subagents:settings_loaded", { settings });
|
|
113
|
+
return settings;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Persist a settings snapshot, emit the `subagents:settings_changed` event
|
|
117
|
+
* (regardless of persist outcome so listeners see the in-memory change), and
|
|
118
|
+
* return the toast the UI should display. Event payload carries the `persisted`
|
|
119
|
+
* flag so listeners can react to write failures.
|
|
120
|
+
*/
|
|
121
|
+
export function saveAndEmitChanged(snapshot, successMsg, emit, cwd = process.cwd()) {
|
|
122
|
+
const persisted = saveSettings(snapshot, cwd);
|
|
123
|
+
emit("subagents:settings_changed", { settings: snapshot, persisted });
|
|
124
|
+
return persistToastFor(successMsg, persisted);
|
|
125
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tintinweb/pi-subagents",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
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",
|
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
"autonomous"
|
|
22
22
|
],
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@mariozechner/pi-ai": "^0.70.
|
|
25
|
-
"@mariozechner/pi-coding-agent": "^0.70.
|
|
26
|
-
"@mariozechner/pi-tui": "^0.70.
|
|
24
|
+
"@mariozechner/pi-ai": "^0.70.5",
|
|
25
|
+
"@mariozechner/pi-coding-agent": "^0.70.5",
|
|
26
|
+
"@mariozechner/pi-tui": "^0.70.5",
|
|
27
27
|
"@sinclair/typebox": "latest"
|
|
28
28
|
},
|
|
29
29
|
"scripts": {
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@biomejs/biome": "^2.3.5",
|
|
40
40
|
"@types/node": "^25.5.0",
|
|
41
|
-
"typescript": "^
|
|
41
|
+
"typescript": "^6.0.0",
|
|
42
42
|
"vitest": "^4.0.18"
|
|
43
43
|
},
|
|
44
44
|
"pi": {
|
package/src/agent-runner.ts
CHANGED
|
@@ -233,7 +233,12 @@ export async function runAgent(
|
|
|
233
233
|
|
|
234
234
|
const agentDir = getAgentDir();
|
|
235
235
|
|
|
236
|
-
// Load extensions/skills: true or string[] → load; false → don't
|
|
236
|
+
// Load extensions/skills: true or string[] → load; false → don't.
|
|
237
|
+
// Suppress AGENTS.md/CLAUDE.md and APPEND_SYSTEM.md — upstream's
|
|
238
|
+
// buildSystemPrompt() re-appends both AFTER systemPromptOverride, which
|
|
239
|
+
// would defeat prompt_mode: replace and isolated: true. Parent context, if
|
|
240
|
+
// wanted, reaches the subagent via prompt_mode: append (parentSystemPrompt
|
|
241
|
+
// is embedded in systemPromptOverride) or inherit_context (conversation).
|
|
237
242
|
const loader = new DefaultResourceLoader({
|
|
238
243
|
cwd: effectiveCwd,
|
|
239
244
|
agentDir,
|
|
@@ -241,7 +246,9 @@ export async function runAgent(
|
|
|
241
246
|
noSkills,
|
|
242
247
|
noPromptTemplates: true,
|
|
243
248
|
noThemes: true,
|
|
249
|
+
noContextFiles: true,
|
|
244
250
|
systemPromptOverride: () => systemPrompt,
|
|
251
|
+
appendSystemPromptOverride: () => [],
|
|
245
252
|
});
|
|
246
253
|
await loader.reload();
|
|
247
254
|
|
package/src/custom-agents.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* custom-agents.ts — Load user-defined agents from project (.pi/agents/) and global (~/.pi/agent/agents/) locations.
|
|
2
|
+
* custom-agents.ts — Load user-defined agents from project (.pi/agents/) and global ($PI_CODING_AGENT_DIR/agents/, default ~/.pi/agent/agents/) locations.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
6
|
-
import { homedir } from "node:os";
|
|
7
6
|
import { basename, join } from "node:path";
|
|
8
|
-
import { parseFrontmatter } from "@mariozechner/pi-coding-agent";
|
|
7
|
+
import { getAgentDir, parseFrontmatter } from "@mariozechner/pi-coding-agent";
|
|
9
8
|
import { BUILTIN_TOOL_NAMES } from "./agent-types.js";
|
|
10
9
|
import type { AgentConfig, MemoryScope, ThinkingLevel } from "./types.js";
|
|
11
10
|
|
|
@@ -13,13 +12,13 @@ import type { AgentConfig, MemoryScope, ThinkingLevel } from "./types.js";
|
|
|
13
12
|
* Scan for custom agent .md files from multiple locations.
|
|
14
13
|
* Discovery hierarchy (higher priority wins):
|
|
15
14
|
* 1. Project: <cwd>/.pi/agents/*.md
|
|
16
|
-
* 2. Global: ~/.pi/agent/agents/*.md
|
|
15
|
+
* 2. Global: $PI_CODING_AGENT_DIR/agents/*.md (default: ~/.pi/agent/agents/*.md)
|
|
17
16
|
*
|
|
18
17
|
* Project-level agents override global ones with the same name.
|
|
19
18
|
* Any name is allowed — names matching defaults (e.g. "Explore") override them.
|
|
20
19
|
*/
|
|
21
20
|
export function loadCustomAgents(cwd: string): Map<string, AgentConfig> {
|
|
22
|
-
const globalDir = join(
|
|
21
|
+
const globalDir = join(getAgentDir(), "agents");
|
|
23
22
|
const projectDir = join(cwd, ".pi", "agents");
|
|
24
23
|
|
|
25
24
|
const agents = new Map<string, AgentConfig>();
|
package/src/index.ts
CHANGED
|
@@ -11,9 +11,8 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
14
|
-
import { homedir } from "node:os";
|
|
15
14
|
import { join } from "node:path";
|
|
16
|
-
import { defineTool, type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
15
|
+
import { defineTool, type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext, getAgentDir } from "@mariozechner/pi-coding-agent";
|
|
17
16
|
import { Text } from "@mariozechner/pi-tui";
|
|
18
17
|
import { Type } from "@sinclair/typebox";
|
|
19
18
|
import { AgentManager } from "./agent-manager.js";
|
|
@@ -25,6 +24,7 @@ import { GroupJoinManager } from "./group-join.js";
|
|
|
25
24
|
import { resolveAgentInvocationConfig, resolveJoinMode } from "./invocation-config.js";
|
|
26
25
|
import { type ModelRegistry, resolveModel } from "./model-resolver.js";
|
|
27
26
|
import { createOutputFilePath, streamToOutputFile, writeInitialEntry } from "./output-file.js";
|
|
27
|
+
import { applyAndEmitLoaded, type SubagentsSettings, saveAndEmitChanged } from "./settings.js";
|
|
28
28
|
import { type AgentConfig, type AgentRecord, type JoinMode, type NotificationDetails, type SubagentType } from "./types.js";
|
|
29
29
|
import {
|
|
30
30
|
type AgentActivity,
|
|
@@ -534,7 +534,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
534
534
|
...defaultDescs,
|
|
535
535
|
...(customDescs.length > 0 ? ["", "Custom agents:", ...customDescs] : []),
|
|
536
536
|
"",
|
|
537
|
-
|
|
537
|
+
`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.`,
|
|
538
538
|
].join("\n");
|
|
539
539
|
};
|
|
540
540
|
|
|
@@ -548,6 +548,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
548
548
|
|
|
549
549
|
const typeListText = buildTypeListText();
|
|
550
550
|
|
|
551
|
+
// Apply persisted settings on startup and emit `subagents:settings_loaded`.
|
|
552
|
+
// Global + project merged; missing → defaults; corrupt file emits a warning
|
|
553
|
+
// to stderr and falls back to defaults.
|
|
554
|
+
applyAndEmitLoaded(
|
|
555
|
+
{
|
|
556
|
+
setMaxConcurrent: (n) => manager.setMaxConcurrent(n),
|
|
557
|
+
setDefaultMaxTurns,
|
|
558
|
+
setGraceTurns,
|
|
559
|
+
setDefaultJoinMode,
|
|
560
|
+
},
|
|
561
|
+
(event, payload) => pi.events.emit(event, payload),
|
|
562
|
+
);
|
|
563
|
+
|
|
551
564
|
// ---- Agent tool ----
|
|
552
565
|
|
|
553
566
|
pi.registerTool(defineTool({
|
|
@@ -582,7 +595,7 @@ Guidelines:
|
|
|
582
595
|
description: "A short (3-5 word) description of the task (shown in UI).",
|
|
583
596
|
}),
|
|
584
597
|
subagent_type: Type.String({
|
|
585
|
-
description: `The type of specialized agent to use. Available types: ${getAvailableTypes().join(", ")}. Custom agents from .pi/agents/*.md (project) or
|
|
598
|
+
description: `The type of specialized agent to use. Available types: ${getAvailableTypes().join(", ")}. Custom agents from .pi/agents/*.md (project) or ${getAgentDir()}/agents/*.md (global) are also available.`,
|
|
586
599
|
}),
|
|
587
600
|
model: Type.Optional(
|
|
588
601
|
Type.String({
|
|
@@ -1085,7 +1098,7 @@ Guidelines:
|
|
|
1085
1098
|
// ---- /agents interactive menu ----
|
|
1086
1099
|
|
|
1087
1100
|
const projectAgentsDir = () => join(process.cwd(), ".pi", "agents");
|
|
1088
|
-
const personalAgentsDir = () => join(
|
|
1101
|
+
const personalAgentsDir = () => join(getAgentDir(), "agents");
|
|
1089
1102
|
|
|
1090
1103
|
/** Find the file path of a custom agent by name (project first, then global). */
|
|
1091
1104
|
function findAgentFile(name: string): { path: string; location: "project" | "personal" } | undefined {
|
|
@@ -1324,7 +1337,7 @@ Guidelines:
|
|
|
1324
1337
|
async function ejectAgent(ctx: ExtensionCommandContext, name: string, cfg: AgentConfig) {
|
|
1325
1338
|
const location = await ctx.ui.select("Choose location", [
|
|
1326
1339
|
"Project (.pi/agents/)",
|
|
1327
|
-
|
|
1340
|
+
`Personal (${personalAgentsDir()})`,
|
|
1328
1341
|
]);
|
|
1329
1342
|
if (!location) return;
|
|
1330
1343
|
|
|
@@ -1386,7 +1399,7 @@ Guidelines:
|
|
|
1386
1399
|
// No file (built-in default) — create a stub
|
|
1387
1400
|
const location = await ctx.ui.select("Choose location", [
|
|
1388
1401
|
"Project (.pi/agents/)",
|
|
1389
|
-
|
|
1402
|
+
`Personal (${personalAgentsDir()})`,
|
|
1390
1403
|
]);
|
|
1391
1404
|
if (!location) return;
|
|
1392
1405
|
|
|
@@ -1424,7 +1437,7 @@ Guidelines:
|
|
|
1424
1437
|
async function showCreateWizard(ctx: ExtensionCommandContext) {
|
|
1425
1438
|
const location = await ctx.ui.select("Choose location", [
|
|
1426
1439
|
"Project (.pi/agents/)",
|
|
1427
|
-
|
|
1440
|
+
`Personal (${personalAgentsDir()})`,
|
|
1428
1441
|
]);
|
|
1429
1442
|
if (!location) return;
|
|
1430
1443
|
|
|
@@ -1605,6 +1618,17 @@ ${systemPrompt}
|
|
|
1605
1618
|
ctx.ui.notify(`Created ${targetPath}`, "info");
|
|
1606
1619
|
}
|
|
1607
1620
|
|
|
1621
|
+
function snapshotSettings(): SubagentsSettings {
|
|
1622
|
+
return {
|
|
1623
|
+
maxConcurrent: manager.getMaxConcurrent(),
|
|
1624
|
+
// 0 = unlimited — per SubagentsSettings.defaultMaxTurns docstring and
|
|
1625
|
+
// normalizeMaxTurns() in agent-runner.ts (which maps 0 → undefined).
|
|
1626
|
+
defaultMaxTurns: getDefaultMaxTurns() ?? 0,
|
|
1627
|
+
graceTurns: getGraceTurns(),
|
|
1628
|
+
defaultJoinMode: getDefaultJoinMode(),
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1608
1632
|
async function showSettings(ctx: ExtensionCommandContext) {
|
|
1609
1633
|
const choice = await ctx.ui.select("Settings", [
|
|
1610
1634
|
`Max concurrency (current: ${manager.getMaxConcurrent()})`,
|
|
@@ -1620,7 +1644,7 @@ ${systemPrompt}
|
|
|
1620
1644
|
const n = parseInt(val, 10);
|
|
1621
1645
|
if (n >= 1) {
|
|
1622
1646
|
manager.setMaxConcurrent(n);
|
|
1623
|
-
ctx
|
|
1647
|
+
notifyApplied(ctx, `Max concurrency set to ${n}`);
|
|
1624
1648
|
} else {
|
|
1625
1649
|
ctx.ui.notify("Must be a positive integer.", "warning");
|
|
1626
1650
|
}
|
|
@@ -1631,10 +1655,10 @@ ${systemPrompt}
|
|
|
1631
1655
|
const n = parseInt(val, 10);
|
|
1632
1656
|
if (n === 0) {
|
|
1633
1657
|
setDefaultMaxTurns(undefined);
|
|
1634
|
-
ctx
|
|
1658
|
+
notifyApplied(ctx, "Default max turns set to unlimited");
|
|
1635
1659
|
} else if (n >= 1) {
|
|
1636
1660
|
setDefaultMaxTurns(n);
|
|
1637
|
-
ctx
|
|
1661
|
+
notifyApplied(ctx, `Default max turns set to ${n}`);
|
|
1638
1662
|
} else {
|
|
1639
1663
|
ctx.ui.notify("Must be 0 (unlimited) or a positive integer.", "warning");
|
|
1640
1664
|
}
|
|
@@ -1645,7 +1669,7 @@ ${systemPrompt}
|
|
|
1645
1669
|
const n = parseInt(val, 10);
|
|
1646
1670
|
if (n >= 1) {
|
|
1647
1671
|
setGraceTurns(n);
|
|
1648
|
-
ctx
|
|
1672
|
+
notifyApplied(ctx, `Grace turns set to ${n}`);
|
|
1649
1673
|
} else {
|
|
1650
1674
|
ctx.ui.notify("Must be a positive integer.", "warning");
|
|
1651
1675
|
}
|
|
@@ -1659,11 +1683,24 @@ ${systemPrompt}
|
|
|
1659
1683
|
if (val) {
|
|
1660
1684
|
const mode = val.split(" ")[0] as JoinMode;
|
|
1661
1685
|
setDefaultJoinMode(mode);
|
|
1662
|
-
ctx
|
|
1686
|
+
notifyApplied(ctx, `Default join mode set to ${mode}`);
|
|
1663
1687
|
}
|
|
1664
1688
|
}
|
|
1665
1689
|
}
|
|
1666
1690
|
|
|
1691
|
+
// Persist the current snapshot, emit `subagents:settings_changed`, and surface
|
|
1692
|
+
// the right toast. Successful saves show info; persistence failures downgrade
|
|
1693
|
+
// to warning so users aren't silently reverted on restart. Event fires regardless
|
|
1694
|
+
// of outcome so listeners see the in-memory change.
|
|
1695
|
+
function notifyApplied(ctx: ExtensionCommandContext, successMsg: string) {
|
|
1696
|
+
const { message, level } = saveAndEmitChanged(
|
|
1697
|
+
snapshotSettings(),
|
|
1698
|
+
successMsg,
|
|
1699
|
+
(event, payload) => pi.events.emit(event, payload),
|
|
1700
|
+
);
|
|
1701
|
+
ctx.ui.notify(message, level);
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1667
1704
|
pi.registerCommand("agents", {
|
|
1668
1705
|
description: "Manage agents",
|
|
1669
1706
|
handler: async (_args, ctx) => { await showAgentsMenu(ctx); },
|
package/src/output-file.ts
CHANGED
|
@@ -10,13 +10,32 @@ import { tmpdir } from "node:os";
|
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import type { AgentSession, AgentSessionEvent } from "@mariozechner/pi-coding-agent";
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Encode a cwd path as a filesystem-safe directory name. Handles:
|
|
15
|
+
* - POSIX: "/home/user/project" → "home-user-project"
|
|
16
|
+
* - Windows: "C:\Users\foo\project" → "Users-foo-project"
|
|
17
|
+
* - UNC: "\\\\server\\share\\project" → "server-share-project"
|
|
18
|
+
*/
|
|
19
|
+
export function encodeCwd(cwd: string): string {
|
|
20
|
+
return cwd
|
|
21
|
+
.replace(/[/\\]/g, "-") // both separators → dash
|
|
22
|
+
.replace(/^[A-Za-z]:-/, "") // strip Windows drive prefix ("C:-")
|
|
23
|
+
.replace(/^-+/, ""); // strip leading dashes (POSIX root, UNC)
|
|
24
|
+
}
|
|
25
|
+
|
|
13
26
|
/** Create the output file path, ensuring the directory exists.
|
|
14
27
|
* Mirrors Claude Code's layout: /tmp/{prefix}-{uid}/{encoded-cwd}/{sessionId}/tasks/{agentId}.output */
|
|
15
28
|
export function createOutputFilePath(cwd: string, agentId: string, sessionId: string): string {
|
|
16
|
-
const encoded = cwd
|
|
29
|
+
const encoded = encodeCwd(cwd);
|
|
17
30
|
const root = join(tmpdir(), `pi-subagents-${process.getuid?.() ?? 0}`);
|
|
18
31
|
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
19
|
-
|
|
32
|
+
// chmod is a no-op on Windows and throws on some Windows filesystems.
|
|
33
|
+
// On Unix we still want to enforce 0o700 past umask, so only swallow on Windows.
|
|
34
|
+
try {
|
|
35
|
+
chmodSync(root, 0o700);
|
|
36
|
+
} catch (err) {
|
|
37
|
+
if (process.platform !== "win32") throw err;
|
|
38
|
+
}
|
|
20
39
|
const dir = join(root, encoded, sessionId, "tasks");
|
|
21
40
|
mkdirSync(dir, { recursive: true });
|
|
22
41
|
return join(dir, `${agentId}.output`);
|
package/src/settings.ts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// Persistence for pi-subagents operational settings.
|
|
2
|
+
// - Global: ~/.pi/agent/subagents.json (via getAgentDir()) — manual defaults, never written here
|
|
3
|
+
// - Project: <cwd>/.pi/subagents.json — written by /agents → Settings; overrides global on load
|
|
4
|
+
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
import { getAgentDir } from "@mariozechner/pi-coding-agent";
|
|
8
|
+
import type { JoinMode } from "./types.js";
|
|
9
|
+
|
|
10
|
+
export interface SubagentsSettings {
|
|
11
|
+
maxConcurrent?: number;
|
|
12
|
+
/**
|
|
13
|
+
* 0 = unlimited — the extension's single source of truth for that convention:
|
|
14
|
+
* `normalizeMaxTurns()` in agent-runner.ts treats 0 → `undefined`, and the
|
|
15
|
+
* `/agents` → Settings input prompt explicitly says "0 = unlimited".
|
|
16
|
+
*/
|
|
17
|
+
defaultMaxTurns?: number;
|
|
18
|
+
graceTurns?: number;
|
|
19
|
+
defaultJoinMode?: JoinMode;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Setter hooks used by applySettings to wire persisted values into in-memory state. */
|
|
23
|
+
export interface SettingsAppliers {
|
|
24
|
+
setMaxConcurrent: (n: number) => void;
|
|
25
|
+
setDefaultMaxTurns: (n: number) => void;
|
|
26
|
+
setGraceTurns: (n: number) => void;
|
|
27
|
+
setDefaultJoinMode: (mode: JoinMode) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Emit callback — a subset of `pi.events.emit` to keep helpers testable. */
|
|
31
|
+
export type SettingsEmit = (event: string, payload: unknown) => void;
|
|
32
|
+
|
|
33
|
+
const VALID_JOIN_MODES: ReadonlySet<string> = new Set<JoinMode>(["async", "group", "smart"]);
|
|
34
|
+
|
|
35
|
+
// Sanity ceilings — prevent hand-edited configs from asking for values that
|
|
36
|
+
// make no operational sense (e.g. 1e6 concurrent subagents). Permissive enough
|
|
37
|
+
// that any realistic power-user setting passes through.
|
|
38
|
+
const MAX_CONCURRENT_CEILING = 1024;
|
|
39
|
+
const MAX_TURNS_CEILING = 10_000;
|
|
40
|
+
const GRACE_TURNS_CEILING = 1_000;
|
|
41
|
+
|
|
42
|
+
/** Drop fields that don't match the expected shape. Silent — garbage becomes absent. */
|
|
43
|
+
function sanitize(raw: unknown): SubagentsSettings {
|
|
44
|
+
if (!raw || typeof raw !== "object") return {};
|
|
45
|
+
const r = raw as Record<string, unknown>;
|
|
46
|
+
const out: SubagentsSettings = {};
|
|
47
|
+
if (
|
|
48
|
+
Number.isInteger(r.maxConcurrent) &&
|
|
49
|
+
(r.maxConcurrent as number) >= 1 &&
|
|
50
|
+
(r.maxConcurrent as number) <= MAX_CONCURRENT_CEILING
|
|
51
|
+
) {
|
|
52
|
+
out.maxConcurrent = r.maxConcurrent as number;
|
|
53
|
+
}
|
|
54
|
+
if (
|
|
55
|
+
Number.isInteger(r.defaultMaxTurns) &&
|
|
56
|
+
(r.defaultMaxTurns as number) >= 0 &&
|
|
57
|
+
(r.defaultMaxTurns as number) <= MAX_TURNS_CEILING
|
|
58
|
+
) {
|
|
59
|
+
out.defaultMaxTurns = r.defaultMaxTurns as number;
|
|
60
|
+
}
|
|
61
|
+
if (
|
|
62
|
+
Number.isInteger(r.graceTurns) &&
|
|
63
|
+
(r.graceTurns as number) >= 1 &&
|
|
64
|
+
(r.graceTurns as number) <= GRACE_TURNS_CEILING
|
|
65
|
+
) {
|
|
66
|
+
out.graceTurns = r.graceTurns as number;
|
|
67
|
+
}
|
|
68
|
+
if (typeof r.defaultJoinMode === "string" && VALID_JOIN_MODES.has(r.defaultJoinMode)) {
|
|
69
|
+
out.defaultJoinMode = r.defaultJoinMode as JoinMode;
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function globalPath(): string {
|
|
75
|
+
return join(getAgentDir(), "subagents.json");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function projectPath(cwd: string): string {
|
|
79
|
+
return join(cwd, ".pi", "subagents.json");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Read a settings file. Missing file is silent (returns `{}`). A file that
|
|
84
|
+
* exists but can't be parsed emits a warning to stderr so users aren't
|
|
85
|
+
* silently reverted to defaults — and still returns `{}` so startup proceeds.
|
|
86
|
+
*/
|
|
87
|
+
function readSettingsFile(path: string): SubagentsSettings {
|
|
88
|
+
if (!existsSync(path)) return {};
|
|
89
|
+
try {
|
|
90
|
+
return sanitize(JSON.parse(readFileSync(path, "utf-8")));
|
|
91
|
+
} catch (err) {
|
|
92
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
93
|
+
console.warn(`[pi-subagents] Ignoring malformed settings at ${path}: ${reason}`);
|
|
94
|
+
return {};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Load merged settings: global provides defaults, project overrides. */
|
|
99
|
+
export function loadSettings(cwd: string = process.cwd()): SubagentsSettings {
|
|
100
|
+
return { ...readSettingsFile(globalPath()), ...readSettingsFile(projectPath(cwd)) };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Write project-local settings. Global is never touched from code.
|
|
105
|
+
* Returns `true` on success, `false` if the write (or mkdir) failed so the
|
|
106
|
+
* caller can surface a warning — persistence isn't fatal but isn't silent.
|
|
107
|
+
*/
|
|
108
|
+
export function saveSettings(s: SubagentsSettings, cwd: string = process.cwd()): boolean {
|
|
109
|
+
const path = projectPath(cwd);
|
|
110
|
+
try {
|
|
111
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
112
|
+
writeFileSync(path, JSON.stringify(s, null, 2), "utf-8");
|
|
113
|
+
return true;
|
|
114
|
+
} catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Apply persisted settings to the in-memory state via caller-supplied setters. */
|
|
120
|
+
export function applySettings(s: SubagentsSettings, appliers: SettingsAppliers): void {
|
|
121
|
+
if (typeof s.maxConcurrent === "number") appliers.setMaxConcurrent(s.maxConcurrent);
|
|
122
|
+
if (typeof s.defaultMaxTurns === "number") appliers.setDefaultMaxTurns(s.defaultMaxTurns);
|
|
123
|
+
if (typeof s.graceTurns === "number") appliers.setGraceTurns(s.graceTurns);
|
|
124
|
+
if (s.defaultJoinMode) appliers.setDefaultJoinMode(s.defaultJoinMode);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Format the user-facing toast for a settings mutation. Pure function —
|
|
129
|
+
* routes the success/failure of `saveSettings` into the right message + level
|
|
130
|
+
* so the UI layer (index.ts) stays a thin wire between input and notification.
|
|
131
|
+
*/
|
|
132
|
+
export function persistToastFor(
|
|
133
|
+
successMsg: string,
|
|
134
|
+
persisted: boolean,
|
|
135
|
+
): { message: string; level: "info" | "warning" } {
|
|
136
|
+
return persisted
|
|
137
|
+
? { message: successMsg, level: "info" }
|
|
138
|
+
: { message: `${successMsg} (session only; failed to persist)`, level: "warning" };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Load merged settings, apply them to in-memory state, and emit the
|
|
143
|
+
* `subagents:settings_loaded` lifecycle event. Returns the loaded settings so
|
|
144
|
+
* callers can log/inspect. Extension init wires this once.
|
|
145
|
+
*/
|
|
146
|
+
export function applyAndEmitLoaded(
|
|
147
|
+
appliers: SettingsAppliers,
|
|
148
|
+
emit: SettingsEmit,
|
|
149
|
+
cwd: string = process.cwd(),
|
|
150
|
+
): SubagentsSettings {
|
|
151
|
+
const settings = loadSettings(cwd);
|
|
152
|
+
applySettings(settings, appliers);
|
|
153
|
+
emit("subagents:settings_loaded", { settings });
|
|
154
|
+
return settings;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Persist a settings snapshot, emit the `subagents:settings_changed` event
|
|
159
|
+
* (regardless of persist outcome so listeners see the in-memory change), and
|
|
160
|
+
* return the toast the UI should display. Event payload carries the `persisted`
|
|
161
|
+
* flag so listeners can react to write failures.
|
|
162
|
+
*/
|
|
163
|
+
export function saveAndEmitChanged(
|
|
164
|
+
snapshot: SubagentsSettings,
|
|
165
|
+
successMsg: string,
|
|
166
|
+
emit: SettingsEmit,
|
|
167
|
+
cwd: string = process.cwd(),
|
|
168
|
+
): { message: string; level: "info" | "warning" } {
|
|
169
|
+
const persisted = saveSettings(snapshot, cwd);
|
|
170
|
+
emit("subagents:settings_changed", { settings: snapshot, persisted });
|
|
171
|
+
return persistToastFor(successMsg, persisted);
|
|
172
|
+
}
|