@herbertgao/pi-subagents 0.15.1 → 0.15.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.15.3
4
+
5
+ ### Patch Changes
6
+
7
+ - [#27](https://github.com/HerbertGao/pi-extensions/pull/27) [`32bb76c`](https://github.com/HerbertGao/pi-extensions/commit/32bb76c117971de27db9c2b521d19df3ba9ea322) Thanks [@HerbertGao](https://github.com/HerbertGao)! - Keep worktree-isolated agents inside their copy, preserve real Agent tool startup errors, and render unknown or failed Agent results without misleading completion status.
8
+
9
+ ## 0.15.2
10
+
11
+ ### Patch Changes
12
+
13
+ - [#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.
14
+
3
15
  All notable changes to this project will be documented in this file.
4
16
 
5
17
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
@@ -7,6 +19,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
19
 
8
20
  ## [Unreleased]
9
21
 
22
+ ### Added
23
+
24
+ - **`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.
25
+
26
+ ### Fixed
27
+
28
+ - **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.
29
+
10
30
  ## [0.15.1] - 2026-08-10
11
31
 
12
32
  ### 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.1",
3
+ "version": "0.15.3",
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.14.3",
67
+ "version": "0.15.0",
68
68
  "repository": "https://github.com/tintinweb/pi-subagents",
69
- "commit": "2966cd5"
69
+ "commit": "c83dd82"
70
70
  }
71
71
  }
@@ -321,6 +321,7 @@ export class AgentManager {
321
321
  // stay undefined otherwise so plain worktree runs keep resolving config
322
322
  // (incl. relative extension paths and memory) inside the worktree copy.
323
323
  cwd: worktreeCwd ?? customCwd,
324
+ worktreeBase: worktreeCwd ? baseCwd : undefined,
324
325
  configCwd:
325
326
  options.configCwd ?? (customCwd !== undefined ? ctx.cwd : undefined),
326
327
  signal: record.abortController!.signal,
@@ -414,6 +414,8 @@ export interface RunOptions {
414
414
  thinkingLevel?: ThinkingLevel
415
415
  /** Override working directory (e.g. for worktree isolation). */
416
416
  cwd?: string
417
+ /** Original checkout path when cwd is an isolated worktree copy. */
418
+ worktreeBase?: string
417
419
  /**
418
420
  * Where .pi config is discovered (project extensions, skills, pi settings,
419
421
  * agent memory). Default: same as the working directory. The manager sets
@@ -602,7 +604,7 @@ export async function runAgent(
602
604
  const parentSystemPrompt = ctx.getSystemPrompt()
603
605
 
604
606
  // Build prompt extras (memory, skill preloading)
605
- const extras: PromptExtras = {}
607
+ const extras: PromptExtras = { worktreeBase: options.worktreeBase }
606
608
 
607
609
  // Resolve extensions/skills: isolated overrides to false
608
610
  const extensions = options.isolated ? false : config.extensions
@@ -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(cwd: string): Map<string, AgentConfig> {
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
- loadFromDir(globalDir, agents, "global") // lowest priority
30
- loadFromDir(workspaceProjectDir, agents, "project") // shared workspace
31
- loadFromDir(projectDir, agents, "project") // highest priority (overwrites)
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
- let content: string
54
- try {
55
- content = readFileSync(join(dir, file), "utf-8")
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,
@@ -1233,12 +1241,12 @@ Terse command-style prompts produce shallow, generic work.
1233
1241
  )
1234
1242
  },
1235
1243
 
1236
- renderResult(result, { expanded, isPartial }, theme) {
1244
+ renderResult(result, { expanded, isPartial }, theme, renderContext) {
1245
+ const resultText =
1246
+ result.content[0]?.type === "text" ? result.content[0].text : ""
1237
1247
  const details = result.details as AgentDetails | undefined
1238
- if (!details) {
1239
- const text =
1240
- result.content[0]?.type === "text" ? result.content[0].text : ""
1241
- return new Text(text, 0, 0)
1248
+ if (renderContext?.isError || !details?.status) {
1249
+ return new Text(resultText, 0, 0)
1242
1250
  }
1243
1251
 
1244
1252
  // Helper: build "haiku · thinking: high · ↻5≤30 · 3 tool uses · 33.8k tokens" stats string
@@ -1293,8 +1301,6 @@ Terse command-style prompts produce shallow, generic work.
1293
1301
  line += " " + theme.fg("dim", "·") + " " + theme.fg("dim", duration)
1294
1302
 
1295
1303
  if (expanded) {
1296
- const resultText =
1297
- result.content[0]?.type === "text" ? result.content[0].text : ""
1298
1304
  if (resultText) {
1299
1305
  const lines = resultText.split("\n").slice(0, 50)
1300
1306
  for (const l of lines) {
@@ -1324,6 +1330,10 @@ Terse command-style prompts produce shallow, generic work.
1324
1330
  return new Text(line, 0, 0)
1325
1331
  }
1326
1332
 
1333
+ if (details.status !== "error" && details.status !== "aborted") {
1334
+ return new Text(resultText, 0, 0)
1335
+ }
1336
+
1327
1337
  // ---- Error / Aborted (hard max_turns) ----
1328
1338
  const s = stats(details)
1329
1339
  let line = theme.fg("error", "✗") + (s ? " " + s : "")
@@ -1590,23 +1600,22 @@ Terse command-style prompts produce shallow, generic work.
1590
1600
  }
1591
1601
  }
1592
1602
 
1593
- try {
1594
- id = manager.spawn(pi, ctx, subagentType, params.prompt, {
1595
- description: params.description,
1596
- model,
1597
- maxTurns: effectiveMaxTurns,
1598
- isolated,
1599
- inheritContext,
1600
- thinkingLevel: thinking,
1601
- isBackground: true,
1602
- isolation,
1603
- invocation: agentInvocation,
1604
- rootSessionId: ctx.sessionManager.getSessionId(),
1605
- ...bgCallbacks,
1606
- })
1607
- } catch (err) {
1608
- return textResult(err instanceof Error ? err.message : String(err))
1609
- }
1603
+ // A throw here means the agent never started. Let it out: Pi marks a
1604
+ // tool call failed only when execute throws, while a returned message
1605
+ // reads to the model as a subagent that ran and reported this.
1606
+ id = manager.spawn(pi, ctx, subagentType, params.prompt, {
1607
+ description: params.description,
1608
+ model,
1609
+ maxTurns: effectiveMaxTurns,
1610
+ isolated,
1611
+ inheritContext,
1612
+ thinkingLevel: thinking,
1613
+ isBackground: true,
1614
+ isolation,
1615
+ invocation: agentInvocation,
1616
+ rootSessionId: ctx.sessionManager.getSessionId(),
1617
+ ...bgCallbacks,
1618
+ })
1610
1619
 
1611
1620
  // Set output file + join mode synchronously after spawn, before the
1612
1621
  // event loop yields — onSessionCreated is async so this is safe.
@@ -1766,18 +1775,15 @@ Terse command-style prompts produce shallow, generic work.
1766
1775
  },
1767
1776
  )
1768
1777
  record = fgResult.record
1769
- } catch (err) {
1778
+ } finally {
1779
+ // A startup throw propagates as a failed tool call without leaving
1780
+ // the spinner running or a finished agent in the widget.
1770
1781
  clearInterval(spinnerInterval)
1771
- return textResult(err instanceof Error ? err.message : String(err))
1772
- }
1773
-
1774
- clearInterval(spinnerInterval)
1775
-
1776
- // Clean up foreground agent from widget
1777
- if (fgId) {
1778
- agentActivity.delete(fgId)
1779
- widget.markFinished(fgId)
1780
- fleet.onAgentFinished(fgId)
1782
+ if (fgId) {
1783
+ agentActivity.delete(fgId)
1784
+ widget.markFinished(fgId)
1785
+ fleet.onAgentFinished(fgId)
1786
+ }
1781
1787
  }
1782
1788
 
1783
1789
  // Get final token count
@@ -2673,6 +2679,7 @@ ${systemPrompt}
2673
2679
  defaultJoinMode: getDefaultJoinMode(),
2674
2680
  schedulingEnabled: isSchedulingEnabled(),
2675
2681
  scopeModels: isScopeModelsEnabled(),
2682
+ strictAgentFiles,
2676
2683
  disableDefaultAgents: isDefaultsDisabled(),
2677
2684
  toolDescriptionMode: getToolDescriptionMode(),
2678
2685
  fleetView: isFleetViewEnabled(),
@@ -2762,6 +2769,14 @@ ${systemPrompt}
2762
2769
  currentValue: isScopeModelsEnabled() ? "on" : "off",
2763
2770
  values: ["on", "off"],
2764
2771
  },
2772
+ {
2773
+ id: "strictAgentFiles",
2774
+ label: "Strict agent files",
2775
+ description:
2776
+ "Fail startup on an unreadable or unparseable agent .md instead of skipping it with a warning",
2777
+ currentValue: strictAgentFiles ? "on" : "off",
2778
+ values: ["on", "off"],
2779
+ },
2765
2780
  {
2766
2781
  id: "disableDefaultAgents",
2767
2782
  label: "Disable defaults",
@@ -2867,6 +2882,13 @@ ${systemPrompt}
2867
2882
  const enabled = value === "on"
2868
2883
  setScopeModelsEnabled(enabled)
2869
2884
  notifyApplied(ctx, `Scope models ${enabled ? "enabled" : "disabled"}`)
2885
+ } else if (id === "strictAgentFiles") {
2886
+ const enabled = value === "on"
2887
+ strictAgentFiles = enabled
2888
+ notifyApplied(
2889
+ ctx,
2890
+ `Strict agent files ${enabled ? "enabled" : "disabled"}. Takes effect on next pi session.`,
2891
+ )
2870
2892
  } else if (id === "disableDefaultAgents") {
2871
2893
  const enabled = value === "on"
2872
2894
  setDisableDefaultAgents(enabled)
@@ -399,47 +399,38 @@ export function createNestedSubagentTools(
399
399
  // one earlier would silently give a grandchild the wrong worktree base, the
400
400
  // wrong conversation under inherit_context, and the wrong inherited model.
401
401
  //
402
- // spawn() throws on strict worktree-isolation failure and cwd validation
403
- // report it as a tool error, like the top-level Agent tool does, instead of
404
- // letting it escape into the child's turn.
405
- try {
406
- if (invocation.runInBackground) {
407
- const id = context.manager.spawn(
408
- context.pi,
409
- ctx,
410
- resolvedType,
411
- params.prompt,
412
- {
413
- ...options,
414
- isBackground: true,
415
- },
416
- )
417
- // Synchronous, before the event loop yields — onSessionCreated fires
418
- // asynchronously inside runAgent, so the file is attached in time.
419
- attachTranscript(id)
420
- return textResult(
421
- `Nested agent started in background. Agent ID: ${id}`,
422
- )
423
- }
424
-
425
- const { record } = await context.manager.spawnAndWait(
402
+ // Startup throws mean no child ran. Let them propagate so Pi marks the
403
+ // nested Agent tool call failed instead of presenting the message as a
404
+ // successful child result.
405
+ if (invocation.runInBackground) {
406
+ const id = context.manager.spawn(
426
407
  context.pi,
427
408
  ctx,
428
409
  resolvedType,
429
410
  params.prompt,
430
- { ...options, signal },
431
- attachTranscript,
432
- )
433
- return textResult(
434
- formatRecord(record, "inline"),
435
- record.status === "error",
436
- )
437
- } catch (err) {
438
- return textResult(
439
- err instanceof Error ? err.message : String(err),
440
- true,
411
+ {
412
+ ...options,
413
+ isBackground: true,
414
+ },
441
415
  )
416
+ // Synchronous, before the event loop yields — onSessionCreated fires
417
+ // asynchronously inside runAgent, so the file is attached in time.
418
+ attachTranscript(id)
419
+ return textResult(`Nested agent started in background. Agent ID: ${id}`)
442
420
  }
421
+
422
+ const { record } = await context.manager.spawnAndWait(
423
+ context.pi,
424
+ ctx,
425
+ resolvedType,
426
+ params.prompt,
427
+ { ...options, signal },
428
+ attachTranscript,
429
+ )
430
+ return textResult(
431
+ formatRecord(record, "inline"),
432
+ record.status === "error",
433
+ )
443
434
  },
444
435
  })
445
436
 
package/src/prompts.ts CHANGED
@@ -10,6 +10,8 @@ export interface PromptExtras {
10
10
  memoryBlock?: string
11
11
  /** Preloaded skill contents to inject. */
12
12
  skillBlocks?: { name: string; content: string }[]
13
+ /** Original checkout path when cwd is an isolated worktree copy. */
14
+ worktreeBase?: string
13
15
  }
14
16
 
15
17
  /**
@@ -55,6 +57,12 @@ Platform: ${env.platform}`
55
57
  }
56
58
  const extrasSuffix =
57
59
  extraSections.length > 0 ? "\n\n" + extraSections.join("\n") : ""
60
+ const worktreeSection = extras?.worktreeBase
61
+ ? `\n\n<worktree_isolation>
62
+ Your working directory is an isolated git worktree copy of ${extras.worktreeBase}.
63
+ Work only inside it — never in ${extras.worktreeBase}, even if other instructions name that path as your working directory.
64
+ </worktree_isolation>`
65
+ : ""
58
66
 
59
67
  if (config.promptMode === "append") {
60
68
  const identity = parentSystemPrompt || genericBase
@@ -88,6 +96,7 @@ You are operating as a sub-agent invoked to handle a specific task.
88
96
  "\n\n" +
89
97
  activeAgentTag +
90
98
  envBlock +
99
+ worktreeSection +
91
100
  customSection +
92
101
  extrasSuffix
93
102
  )
@@ -100,7 +109,12 @@ You have been invoked to handle a specific task autonomously.
100
109
  ${envBlock}`
101
110
 
102
111
  return (
103
- activeAgentTag + replaceHeader + "\n\n" + config.systemPrompt + extrasSuffix
112
+ activeAgentTag +
113
+ replaceHeader +
114
+ worktreeSection +
115
+ "\n\n" +
116
+ config.systemPrompt +
117
+ extrasSuffix
104
118
  )
105
119
  }
106
120
 
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"