@herbertgao/pi-subagents 0.15.1 → 0.15.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 +14 -0
- package/README.md +6 -2
- package/package.json +3 -3
- package/src/custom-agents.ts +85 -11
- package/src/index.ts +28 -4
- package/src/settings.ts +12 -0
- package/src/types.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.15.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#15](https://github.com/HerbertGao/pi-extensions/pull/15) [`bc083d2`](https://github.com/HerbertGao/pi-extensions/commit/bc083d27c974f6b5239561dbd2295b6dd53526c0) Thanks [@HerbertGao](https://github.com/HerbertGao)! - Skip unreadable or malformed custom agent files by default, warn when an earlier same-named definition remains active, and add opt-in strict startup validation.
|
|
8
|
+
|
|
3
9
|
All notable changes to this project will be documented in this file.
|
|
4
10
|
|
|
5
11
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
@@ -7,6 +13,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
13
|
|
|
8
14
|
## [Unreleased]
|
|
9
15
|
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **`strictAgentFiles` — fail startup on a broken agent file instead of skipping it.** Off by default and applied only during initial extension activation; later per-call reloads remain tolerant.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **One malformed agent file no longer aborts extension activation** ([#212](https://github.com/tintinweb/pi-subagents/issues/212) — thanks [@daromaj](https://github.com/daromaj)). Unreadable and unparseable files are skipped with a path-specific warning, including the earlier source that remains active when a broken file was an override.
|
|
23
|
+
|
|
10
24
|
## [0.15.1] - 2026-08-10
|
|
11
25
|
|
|
12
26
|
### Fixed
|
package/README.md
CHANGED
|
@@ -180,6 +180,8 @@ Agents are discovered from three locations (higher priority wins):
|
|
|
180
180
|
|
|
181
181
|
Project-level agents override global ones with the same name, so you can customize a global agent for a specific project. If both project locations define the same name, **`.pi/agents/` wins** — `.pi` stays the project authority; `.agents/agents/` is an additional read location for projects that keep their agent assets in the `.agents` workspace. 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.
|
|
182
182
|
|
|
183
|
+
An unreadable or unparseable agent file is skipped by default, with a warning that names the file and error. If the skipped file was overriding a same-named agent, another warning names the earlier file that remains active. Set `strictAgentFiles: true` in `subagents.json` (or `/agents → Settings → Strict agent files`) to fail startup on a broken file instead; mid-session reloads remain tolerant.
|
|
184
|
+
|
|
183
185
|
### Example: `.pi/agents/auditor.md`
|
|
184
186
|
|
|
185
187
|
```markdown
|
|
@@ -430,12 +432,14 @@ When on, each subagent spawn's effective model is validated against pi's own `en
|
|
|
430
432
|
|
|
431
433
|
## Persistent Settings
|
|
432
434
|
|
|
433
|
-
Runtime tuning values set via `/agents` → Settings (max concurrency, default max turns, grace turns, nested depth, fallback agent, default join mode, scheduling on/off, scope models on/off, disable defaults on/off, output transcript on/off, tool description full/compact/custom, widget all/background/off) persist across pi restarts. Two files, merged on load:
|
|
435
|
+
Runtime tuning values set via `/agents` → Settings (max concurrency, default max turns, grace turns, nested depth, fallback agent, default join mode, scheduling on/off, scope models on/off, strict agent files on/off, disable defaults on/off, output transcript on/off, tool description full/compact/custom, widget all/background/off) persist across pi restarts. Two files, merged on load:
|
|
434
436
|
|
|
435
437
|
- **Global:** `~/.pi/agent/subagents.json` — your machine-wide defaults. Edit by hand; the `/agents` menu never writes here.
|
|
436
438
|
- **Project:** `<cwd>/.pi/subagents.json` — per-project overrides. Written by `/agents` → Settings.
|
|
437
439
|
|
|
438
|
-
**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`, nested depth `2`, join mode `smart`, defaults enabled).
|
|
440
|
+
**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`, nested depth `2`, join mode `smart`, strict agent files disabled, defaults enabled).
|
|
441
|
+
|
|
442
|
+
**Strict agent files** (`strictAgentFiles`, default `false`): fail extension startup when any discovered agent file is unreadable or malformed. Enable via `/agents → Settings → Strict agent files` or set `true` in `subagents.json`. Strictness applies only to startup; reloads before later Agent calls remain tolerant so a file edited incorrectly mid-session is skipped with a warning instead of aborting the call.
|
|
439
443
|
|
|
440
444
|
**Nested depth** (`maxSubagentDepth`, default `2`): the hard ceiling on [nested delegation](#nested-subagents), counted from the main session (main = 0, its subagents = 1). `0` or `1` disables nesting project-wide regardless of any agent's `allowed_subagents`. Read when a subagent session is built, so a change applies to agents started after it.
|
|
441
445
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@herbertgao/pi-subagents",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.2",
|
|
4
4
|
"description": "Claude Code-style autonomous subagents for Pi, with HerbertGao-maintained UI extensions.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
@@ -64,8 +64,8 @@
|
|
|
64
64
|
},
|
|
65
65
|
"x-upstream": {
|
|
66
66
|
"package": "@tintinweb/pi-subagents",
|
|
67
|
-
"version": "0.
|
|
67
|
+
"version": "0.15.0",
|
|
68
68
|
"repository": "https://github.com/tintinweb/pi-subagents",
|
|
69
|
-
"commit": "
|
|
69
|
+
"commit": "140324c"
|
|
70
70
|
}
|
|
71
71
|
}
|
package/src/custom-agents.ts
CHANGED
|
@@ -8,6 +8,13 @@ import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent"
|
|
|
8
8
|
import { BUILTIN_TOOL_NAMES } from "./agent-types.js"
|
|
9
9
|
import type { AgentConfig, MemoryScope, ThinkingLevel } from "./types.js"
|
|
10
10
|
|
|
11
|
+
interface WarningState {
|
|
12
|
+
previous: Set<string>
|
|
13
|
+
current: Set<string>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const warningHistoryByCwd = new Map<string, Set<string>>()
|
|
17
|
+
|
|
11
18
|
/**
|
|
12
19
|
* Scan for custom agent .md files from multiple locations.
|
|
13
20
|
* Discovery hierarchy (higher priority wins):
|
|
@@ -20,15 +27,36 @@ import type { AgentConfig, MemoryScope, ThinkingLevel } from "./types.js"
|
|
|
20
27
|
* authority; .agents/agents is an additional read location.
|
|
21
28
|
* Any name is allowed — names matching defaults (e.g. "Explore") override them.
|
|
22
29
|
*/
|
|
23
|
-
export function loadCustomAgents(
|
|
30
|
+
export function loadCustomAgents(
|
|
31
|
+
cwd: string,
|
|
32
|
+
strict = false,
|
|
33
|
+
): Map<string, AgentConfig> {
|
|
24
34
|
const globalDir = join(getAgentDir(), "agents")
|
|
25
35
|
const workspaceProjectDir = join(cwd, ".agents", "agents")
|
|
26
36
|
const projectDir = join(cwd, ".pi", "agents")
|
|
27
37
|
|
|
28
38
|
const agents = new Map<string, AgentConfig>()
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
39
|
+
const skippedOverrides = new Set<string>()
|
|
40
|
+
const warnings: WarningState = {
|
|
41
|
+
previous: warningHistoryByCwd.get(cwd) ?? new Set(),
|
|
42
|
+
current: new Set(),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
loadFromDir(globalDir, agents, "global", strict, warnings, skippedOverrides) // lowest priority
|
|
46
|
+
loadFromDir(
|
|
47
|
+
workspaceProjectDir,
|
|
48
|
+
agents,
|
|
49
|
+
"project",
|
|
50
|
+
strict,
|
|
51
|
+
warnings,
|
|
52
|
+
skippedOverrides,
|
|
53
|
+
) // shared workspace
|
|
54
|
+
loadFromDir(projectDir, agents, "project", strict, warnings, skippedOverrides) // highest priority (overwrites)
|
|
55
|
+
|
|
56
|
+
for (const name of skippedOverrides) {
|
|
57
|
+
warnSkippedOverride(name, agents, warnings)
|
|
58
|
+
}
|
|
59
|
+
warningHistoryByCwd.set(cwd, warnings.current)
|
|
32
60
|
return agents
|
|
33
61
|
}
|
|
34
62
|
|
|
@@ -37,6 +65,9 @@ function loadFromDir(
|
|
|
37
65
|
dir: string,
|
|
38
66
|
agents: Map<string, AgentConfig>,
|
|
39
67
|
source: "project" | "global",
|
|
68
|
+
strict: boolean,
|
|
69
|
+
warnings: WarningState,
|
|
70
|
+
skippedOverrides: Set<string>,
|
|
40
71
|
): void {
|
|
41
72
|
if (!existsSync(dir)) return
|
|
42
73
|
|
|
@@ -49,16 +80,15 @@ function loadFromDir(
|
|
|
49
80
|
|
|
50
81
|
for (const file of files) {
|
|
51
82
|
const name = basename(file, ".md")
|
|
83
|
+
const path = join(dir, file)
|
|
52
84
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
} catch {
|
|
85
|
+
const parsed = readAgentFile(path, strict, warnings)
|
|
86
|
+
if (!parsed) {
|
|
87
|
+
skippedOverrides.add(name)
|
|
57
88
|
continue
|
|
58
89
|
}
|
|
59
|
-
|
|
60
|
-
const { frontmatter: fm, body } =
|
|
61
|
-
parseFrontmatter<Record<string, unknown>>(content)
|
|
90
|
+
skippedOverrides.delete(name)
|
|
91
|
+
const { frontmatter: fm, body } = parsed
|
|
62
92
|
|
|
63
93
|
const { builtinToolNames, extSelectors } = parseToolsField(fm.tools)
|
|
64
94
|
|
|
@@ -97,10 +127,54 @@ function loadFromDir(
|
|
|
97
127
|
isolation: fm.isolation === "worktree" ? "worktree" : undefined,
|
|
98
128
|
enabled: fm.enabled !== false, // default true; explicitly false disables
|
|
99
129
|
source,
|
|
130
|
+
sourcePath: path,
|
|
100
131
|
})
|
|
101
132
|
}
|
|
102
133
|
}
|
|
103
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Read and parse one agent file, or warn and return undefined for the caller to
|
|
137
|
+
* skip. Under strict mode the same failure aborts startup while naming the file.
|
|
138
|
+
*/
|
|
139
|
+
function readAgentFile(
|
|
140
|
+
path: string,
|
|
141
|
+
strict: boolean,
|
|
142
|
+
warnings: WarningState,
|
|
143
|
+
): { frontmatter: Record<string, unknown>; body: string } | undefined {
|
|
144
|
+
try {
|
|
145
|
+
return parseFrontmatter<Record<string, unknown>>(
|
|
146
|
+
readFileSync(path, "utf-8"),
|
|
147
|
+
)
|
|
148
|
+
} catch (err) {
|
|
149
|
+
const reason = err instanceof Error ? err.message : String(err)
|
|
150
|
+
if (strict) throw new Error(`${path}: ${reason}`)
|
|
151
|
+
warnIfNew(`Skipping agent file ${path}: ${reason}`, warnings)
|
|
152
|
+
return undefined
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Warn when a broken higher-priority file exposes an earlier definition. */
|
|
157
|
+
function warnSkippedOverride(
|
|
158
|
+
name: string,
|
|
159
|
+
agents: Map<string, AgentConfig>,
|
|
160
|
+
warnings: WarningState,
|
|
161
|
+
): void {
|
|
162
|
+
const surviving = agents.get(name)
|
|
163
|
+
if (!surviving?.sourcePath || surviving.enabled === false) return
|
|
164
|
+
warnIfNew(
|
|
165
|
+
`Agent "${name}" now loads from ${surviving.sourcePath} instead`,
|
|
166
|
+
warnings,
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Warn once while an error is unchanged, but report it again after recovery. */
|
|
171
|
+
function warnIfNew(message: string, warnings: WarningState): void {
|
|
172
|
+
if (warnings.current.has(message)) return
|
|
173
|
+
warnings.current.add(message)
|
|
174
|
+
if (warnings.previous.has(message)) return
|
|
175
|
+
console.warn(`[pi-subagents] ${message}`)
|
|
176
|
+
}
|
|
177
|
+
|
|
104
178
|
// ---- Field parsers ----
|
|
105
179
|
// All follow the same convention: omitted → default, "none"/empty → nothing, value → exact.
|
|
106
180
|
|
package/src/index.ts
CHANGED
|
@@ -83,6 +83,7 @@ import { SubagentScheduler } from "./schedule.js"
|
|
|
83
83
|
import { resolveStorePath, ScheduleStore } from "./schedule-store.js"
|
|
84
84
|
import {
|
|
85
85
|
applyAndEmitLoaded,
|
|
86
|
+
loadSettings,
|
|
86
87
|
type SubagentsSettings,
|
|
87
88
|
saveAndEmitChanged,
|
|
88
89
|
type ToolDescriptionMode,
|
|
@@ -426,14 +427,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
426
427
|
},
|
|
427
428
|
)
|
|
428
429
|
|
|
430
|
+
// This setting controls the initial load, which runs before the normal settings
|
|
431
|
+
// application below. Later per-call reloads deliberately remain tolerant.
|
|
432
|
+
let strictAgentFiles = loadSettings(process.cwd()).strictAgentFiles === true
|
|
433
|
+
|
|
429
434
|
/** Reload agents from project/global custom agent dirs and merge with defaults (called on init and each Agent invocation). */
|
|
430
|
-
const reloadCustomAgents = () => {
|
|
431
|
-
const userAgents = loadCustomAgents(process.cwd())
|
|
435
|
+
const reloadCustomAgents = (strict = false) => {
|
|
436
|
+
const userAgents = loadCustomAgents(process.cwd(), strict)
|
|
432
437
|
registerAgents(userAgents)
|
|
433
438
|
}
|
|
434
439
|
|
|
435
|
-
// Initial load
|
|
436
|
-
reloadCustomAgents()
|
|
440
|
+
// Initial load — the only strict one.
|
|
441
|
+
reloadCustomAgents(strictAgentFiles)
|
|
437
442
|
|
|
438
443
|
// ---- Agent activity tracking + widget ----
|
|
439
444
|
const agentActivity = new Map<string, AgentActivity>()
|
|
@@ -990,6 +995,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
990
995
|
setDefaultJoinMode,
|
|
991
996
|
setSchedulingEnabled,
|
|
992
997
|
setScopeModels: setScopeModelsEnabled,
|
|
998
|
+
setStrictAgentFiles: (enabled) => {
|
|
999
|
+
strictAgentFiles = enabled
|
|
1000
|
+
},
|
|
993
1001
|
setDisableDefaultAgents: setDisableDefaultAgents,
|
|
994
1002
|
setToolDescriptionMode: setToolDescriptionMode,
|
|
995
1003
|
setFleetView: setFleetViewEnabled,
|
|
@@ -2673,6 +2681,7 @@ ${systemPrompt}
|
|
|
2673
2681
|
defaultJoinMode: getDefaultJoinMode(),
|
|
2674
2682
|
schedulingEnabled: isSchedulingEnabled(),
|
|
2675
2683
|
scopeModels: isScopeModelsEnabled(),
|
|
2684
|
+
strictAgentFiles,
|
|
2676
2685
|
disableDefaultAgents: isDefaultsDisabled(),
|
|
2677
2686
|
toolDescriptionMode: getToolDescriptionMode(),
|
|
2678
2687
|
fleetView: isFleetViewEnabled(),
|
|
@@ -2762,6 +2771,14 @@ ${systemPrompt}
|
|
|
2762
2771
|
currentValue: isScopeModelsEnabled() ? "on" : "off",
|
|
2763
2772
|
values: ["on", "off"],
|
|
2764
2773
|
},
|
|
2774
|
+
{
|
|
2775
|
+
id: "strictAgentFiles",
|
|
2776
|
+
label: "Strict agent files",
|
|
2777
|
+
description:
|
|
2778
|
+
"Fail startup on an unreadable or unparseable agent .md instead of skipping it with a warning",
|
|
2779
|
+
currentValue: strictAgentFiles ? "on" : "off",
|
|
2780
|
+
values: ["on", "off"],
|
|
2781
|
+
},
|
|
2765
2782
|
{
|
|
2766
2783
|
id: "disableDefaultAgents",
|
|
2767
2784
|
label: "Disable defaults",
|
|
@@ -2867,6 +2884,13 @@ ${systemPrompt}
|
|
|
2867
2884
|
const enabled = value === "on"
|
|
2868
2885
|
setScopeModelsEnabled(enabled)
|
|
2869
2886
|
notifyApplied(ctx, `Scope models ${enabled ? "enabled" : "disabled"}`)
|
|
2887
|
+
} else if (id === "strictAgentFiles") {
|
|
2888
|
+
const enabled = value === "on"
|
|
2889
|
+
strictAgentFiles = enabled
|
|
2890
|
+
notifyApplied(
|
|
2891
|
+
ctx,
|
|
2892
|
+
`Strict agent files ${enabled ? "enabled" : "disabled"}. Takes effect on next pi session.`,
|
|
2893
|
+
)
|
|
2870
2894
|
} else if (id === "disableDefaultAgents") {
|
|
2871
2895
|
const enabled = value === "on"
|
|
2872
2896
|
setDisableDefaultAgents(enabled)
|
package/src/settings.ts
CHANGED
|
@@ -49,6 +49,12 @@ export interface SubagentsSettings {
|
|
|
49
49
|
* against. Defaults to false: subagents may use any model.
|
|
50
50
|
*/
|
|
51
51
|
scopeModels?: boolean
|
|
52
|
+
/**
|
|
53
|
+
* When true, an unreadable or unparseable agent `.md` aborts extension load
|
|
54
|
+
* instead of being skipped with a warning. This applies only during startup;
|
|
55
|
+
* later per-call reloads remain tolerant. Defaults to false.
|
|
56
|
+
*/
|
|
57
|
+
strictAgentFiles?: boolean
|
|
52
58
|
/**
|
|
53
59
|
* When true, the three built-in default agents (general-purpose, Explore, Plan)
|
|
54
60
|
* are not registered at startup. User-defined agents from project/global custom
|
|
@@ -129,6 +135,7 @@ export interface SettingsAppliers {
|
|
|
129
135
|
setDefaultJoinMode: (mode: JoinMode) => void
|
|
130
136
|
setSchedulingEnabled: (b: boolean) => void
|
|
131
137
|
setScopeModels: (enabled: boolean) => void
|
|
138
|
+
setStrictAgentFiles: (b: boolean) => void
|
|
132
139
|
setDisableDefaultAgents: (b: boolean) => void
|
|
133
140
|
setToolDescriptionMode: (mode: ToolDescriptionMode) => void
|
|
134
141
|
setFleetView: (b: boolean) => void
|
|
@@ -207,6 +214,9 @@ function sanitize(raw: unknown): SubagentsSettings {
|
|
|
207
214
|
if (typeof r.scopeModels === "boolean") {
|
|
208
215
|
out.scopeModels = r.scopeModels
|
|
209
216
|
}
|
|
217
|
+
if (typeof r.strictAgentFiles === "boolean") {
|
|
218
|
+
out.strictAgentFiles = r.strictAgentFiles
|
|
219
|
+
}
|
|
210
220
|
if (typeof r.disableDefaultAgents === "boolean") {
|
|
211
221
|
out.disableDefaultAgents = r.disableDefaultAgents
|
|
212
222
|
}
|
|
@@ -315,6 +325,8 @@ export function applySettings(
|
|
|
315
325
|
if (typeof s.schedulingEnabled === "boolean")
|
|
316
326
|
appliers.setSchedulingEnabled(s.schedulingEnabled)
|
|
317
327
|
if (typeof s.scopeModels === "boolean") appliers.setScopeModels(s.scopeModels)
|
|
328
|
+
if (typeof s.strictAgentFiles === "boolean")
|
|
329
|
+
appliers.setStrictAgentFiles(s.strictAgentFiles)
|
|
318
330
|
if (typeof s.disableDefaultAgents === "boolean")
|
|
319
331
|
appliers.setDisableDefaultAgents(s.disableDefaultAgents)
|
|
320
332
|
if (s.toolDescriptionMode)
|
package/src/types.ts
CHANGED
|
@@ -77,6 +77,8 @@ export interface AgentConfig {
|
|
|
77
77
|
enabled?: boolean
|
|
78
78
|
/** Where this agent was loaded from */
|
|
79
79
|
source?: "default" | "project" | "global"
|
|
80
|
+
/** Path of the .md it was loaded from. Unset for embedded defaults. */
|
|
81
|
+
sourcePath?: string
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
export type JoinMode = "async" | "group" | "smart"
|