@gotgenes/pi-subagents 12.1.0 → 13.0.0

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
@@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [13.0.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v12.1.0...pi-subagents-v13.0.0) (2026-05-30)
9
+
10
+
11
+ ### ⚠ BREAKING CHANGES
12
+
13
+ * the `skills:` custom-agent frontmatter key and skill preloading are removed; children always load the parent's skills.
14
+ * the `extensions:` custom-agent frontmatter key is removed; children always load the parent's extensions.
15
+ * `SpawnOptions.isolated` and the `isolated:` custom-agent frontmatter key are removed. Children always load the parent's extensions.
16
+
17
+ ### Features
18
+
19
+ * always inherit extensions; make the recursion guard unconditional ([#264](https://github.com/gotgenes/pi-packages/issues/264)) ([3cc682e](https://github.com/gotgenes/pi-packages/commit/3cc682ec401167f922a1f892f6260a10f9fa99f2))
20
+ * always inherit skills; remove noSkills and the skill-preload path ([#264](https://github.com/gotgenes/pi-packages/issues/264)) ([93266ff](https://github.com/gotgenes/pi-packages/commit/93266ff4a204d154b357efac57912330fad240be))
21
+ * remove isolated from the subagent spawn API and lifecycle ([#264](https://github.com/gotgenes/pi-packages/issues/264)) ([d08f340](https://github.com/gotgenes/pi-packages/commit/d08f34066ea472d850423e67349b7a623ca72f42))
22
+
8
23
  ## [12.1.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v12.0.0...pi-subagents-v12.1.0) (2026-05-29)
9
24
 
10
25
 
package/dist/public.d.ts CHANGED
@@ -25,7 +25,6 @@ interface AgentInvocation {
25
25
  modelName?: string;
26
26
  thinking?: ThinkingLevel;
27
27
  maxTurns?: number;
28
- isolated?: boolean;
29
28
  inheritContext?: boolean;
30
29
  runInBackground?: boolean;
31
30
  }
@@ -125,7 +124,6 @@ interface SpawnOptions {
125
124
  model?: string;
126
125
  maxTurns?: number;
127
126
  thinkingLevel?: string;
128
- isolated?: boolean;
129
127
  inheritContext?: boolean;
130
128
  foreground?: boolean;
131
129
  bypassQueue?: boolean;
@@ -45,8 +45,6 @@ flowchart TB
45
45
  SessionConfig["assembleSessionConfig<br/>(pure assembler)"]
46
46
  Prompts["prompts<br/>(system prompt)"]
47
47
  Context["context<br/>(parent history)"]
48
- SafeFs["safe-fs<br/>(symlink/name guards)"]
49
- SkillLoader["skill-loader<br/>(preload skills)"]
50
48
  Env["env<br/>(git/platform)"]
51
49
  ModelResolver["model-resolver<br/>(fuzzy match)"]
52
50
  end
@@ -90,8 +88,7 @@ flowchart TB
90
88
  AgentManager --> AgentRunner
91
89
  AgentRunner --> SessionConfig
92
90
  SessionConfig --> AgentTypeRegistry
93
- SessionConfig --> Prompts & SkillLoader & Env
94
- SkillLoader --> SafeFs
91
+ SessionConfig --> Prompts & Env
95
92
  AgentTypeRegistry --> DefaultAgents & CustomAgents
96
93
  RecordObserver -.->|subscribes| AgentRunner
97
94
  UIObserver -.->|subscribes| AgentRunner
@@ -260,8 +257,6 @@ src/
260
257
  │ ├── prompts.ts system prompt building
261
258
  │ ├── content-items.ts shared message content parsing (tool-call names, assistant content)
262
259
  │ ├── context.ts parent conversation extraction
263
- │ ├── safe-fs.ts symlink rejection and safe file reads
264
- │ ├── skill-loader.ts skill preloading
265
260
  │ ├── env.ts git/platform detection
266
261
  │ ├── model-resolver.ts fuzzy model name resolution
267
262
  │ └── session-dir.ts session directory derivation
@@ -415,7 +410,7 @@ Key types:
415
410
 
416
411
  - `SubagentsService` — `spawn`, `getRecord`, `listAgents`, `abort`, `steer`, `waitForAll`, `hasRunning`.
417
412
  - `SubagentRecord` — serializable agent snapshot (no live session objects).
418
- - `SpawnOptions` — `description`, `model`, `maxTurns`, `thinkingLevel`, `isolated`, `inheritContext`, `foreground`, `bypassQueue`.
413
+ - `SpawnOptions` — `description`, `model`, `maxTurns`, `thinkingLevel`, `inheritContext`, `foreground`, `bypassQueue`.
419
414
  - `SUBAGENT_EVENTS` — channel constants for `pi.events` subscriptions.
420
415
 
421
416
  ### Accessor pattern
@@ -482,11 +477,11 @@ Latent extensibility is the deliverable; a vacant hook is not.
482
477
  ### Core responsibilities (keep)
483
478
 
484
479
  - **Agent definitions** — name, model, thinking, system prompt, tools list.
485
- - **Prompt composition** — system prompt assembly, skill preloading into prompt.
480
+ - **Prompt composition** — system prompt assembly.
486
481
  - **Session lifecycle** — create child sessions, bind extensions, run conversation loop, track results.
487
482
  - **Concurrency management** — queue, abort, resume, max concurrency.
488
483
  - **Recursion guard** — remove pi-subagents' own three tools from child sessions (prevent infinite nesting).
489
- With `isolated` removed, children always load the parent's resources, so the guard becomes unconditional rather than gated on `cfg.extensions`.
484
+ With `isolated` removed (#264), children always load the parent's resources, so the guard is unconditional rather than gated on `cfg.extensions`.
490
485
  This is the core defending its own invariant, keyed off its own tool names — not policy.
491
486
  - **Lifecycle events** — emit awaited, ordered events when child sessions spawn, are created, complete, and are disposed.
492
487
  - **Workspace provider seam** — accept a registered `WorkspaceProvider` and consult it for the child's cwd; default to the parent's cwd when none is registered.
@@ -499,7 +494,8 @@ Latent extensibility is the deliverable; a vacant hook is not.
499
494
  - **Worktree isolation** (`worktree.ts`, `worktree-isolation.ts`, `GitWorktreeManager`, the `isolation: "worktree"` spawn mode) — environment policy, not core.
500
495
  Git worktrees are one *strategy* for choosing the child's working directory; containers, throwaway tmpdirs, and remote sandboxes are others.
501
496
  Evicted to `@gotgenes/pi-subagents-worktrees` (#263), the first consumer of the workspace provider seam.
502
- - **Extension lifecycle control** (`extensions: false`, `isolated`, `noSkills`) — deny-at-use (the in-child permission layer blocking disallowed tool calls) covers what `isolated` pretended to do for tools.
497
+ - **Extension lifecycle control** (`extensions: false`, `isolated`, `noSkills`) — removed in #264.
498
+ Deny-at-use (the in-child permission layer blocking disallowed tool calls) covers what `isolated` pretended to do for tools.
503
499
  Prevent-load (refusing to bind an extension because of load-time side effects, cost, or true sandboxing) is genuinely generative and is left as a *latent* (un-built) provider seam, added only if a real consumer needs it.
504
500
 
505
501
  ### Composition model
@@ -536,20 +532,20 @@ This is achieved across phases: Phase 14 (strip policy), Phase 16 (invert depend
536
532
  These interfaces carry hidden dependencies that obscure true coupling.
537
533
  Bags with 10+ fields are the highest priority for decomposition.
538
534
 
539
- | Interface | Fields | Consumers | Severity |
540
- | --------------------------- | ------------------------------------------------------ | ------------------------------------------------- | --------- |
541
- | `ResolvedSpawnConfig` | 3 nested | foreground-runner, background-spawner, agent-tool | ✓ done |
542
- | `AgentSpawnConfig` | 13 → 13 (ParentSessionInfo nested) | agent-manager (internal) | ✓ done |
543
- | `RunOptions` | 9 (`RunContext` nested) | agent-runner | ✓ done |
544
- | `SessionConfig` | 8 (flat fields, ToolFilterConfig removed) | agent-runner (output of assembler) | ✓ done |
545
- | `NotificationDetails` | 10 | notification | Low (DTO) |
546
- | `ResourceLoaderOptions` | 10 | agent-runner (SDK bridge) | Low (SDK) |
547
- | `RunnerIO` | split → `EnvironmentIO` (3) + `SessionFactoryIO` (5+1) | agent-runner | ✓ done |
548
- | `CreateSessionOptions` | 9 | agent-runner (SDK bridge) | Low (SDK) |
549
- | `AgentToolDeps` | 8 | agent-tool | ✓ done |
550
- | `AgentMenuDeps` | 8 | agent-menu | ✓ done |
551
- | `ConversationViewerOptions` | 8 | conversation-viewer | Low |
552
- | `AgentInit` | 8 | agent | Low |
535
+ | Interface | Fields | Consumers | Severity |
536
+ | --------------------------- | ----------------------------------------------------------- | ------------------------------------------------- | --------- |
537
+ | `ResolvedSpawnConfig` | 3 nested | foreground-runner, background-spawner, agent-tool | ✓ done |
538
+ | `AgentSpawnConfig` | 13 → 13 (ParentSessionInfo nested) | agent-manager (internal) | ✓ done |
539
+ | `RunOptions` | 9 (`RunContext` nested) | agent-runner | ✓ done |
540
+ | `SessionConfig` | 6 (flat fields; extensions/noSkills/extras removed in #264) | agent-runner (output of assembler) | ✓ done |
541
+ | `NotificationDetails` | 10 | notification | Low (DTO) |
542
+ | `ResourceLoaderOptions` | 10 | agent-runner (SDK bridge) | Low (SDK) |
543
+ | `RunnerIO` | split → `EnvironmentIO` (3) + `SessionFactoryIO` (5+1) | agent-runner | ✓ done |
544
+ | `CreateSessionOptions` | 9 | agent-runner (SDK bridge) | Low (SDK) |
545
+ | `AgentToolDeps` | 8 | agent-tool | ✓ done |
546
+ | `AgentMenuDeps` | 8 | agent-menu | ✓ done |
547
+ | `ConversationViewerOptions` | 8 | conversation-viewer | Low |
548
+ | `AgentInit` | 8 | agent | Low |
553
549
 
554
550
  ### Complexity hotspots
555
551
 
@@ -604,7 +600,6 @@ interface SpawnExecution {
604
600
  thinking: ThinkingLevel | undefined;
605
601
  inheritContext: boolean;
606
602
  runInBackground: boolean;
607
- isolated: boolean;
608
603
  agentInvocation: AgentInvocation;
609
604
  }
610
605
 
@@ -648,7 +643,7 @@ export interface RunContext {
648
643
  }
649
644
  ```
650
645
 
651
- The remaining `RunOptions` fields (`model`, `maxTurns`, `signal`, `isolated`, `thinkingLevel`, `defaultMaxTurns`, `graceTurns`, `onSessionCreated`) are genuine execution parameters.
646
+ The remaining `RunOptions` fields (`model`, `maxTurns`, `signal`, `thinkingLevel`, `defaultMaxTurns`, `graceTurns`, `onSessionCreated`) are genuine execution parameters.
652
647
  `RunOptions` now has 9 fields: 1 nested `context: RunContext` (2 per-call fields) plus 8 flat execution fields.
653
648
 
654
649
  #### SessionConfig (11 fields → extract ToolFilterConfig) — done ([#168][168])
@@ -781,13 +776,14 @@ Removed `worktree.ts`, `worktree-isolation.ts`, `GitWorktreeManager`, and the `i
781
776
  From the release carrying #272, all five workspace types are importable by name: `WorkspaceProvider`, `Workspace`, `WorkspacePrepareContext`, `WorkspaceDisposeOutcome`, and `WorkspaceDisposeResult`.
782
777
  - Outcome: git leaves the core; worktree users install one package, everyone else pays nothing.
783
778
 
784
- #### Step 4: Remove `isolated` / `extensions: false` / `noSkills` — [#264]
779
+ #### Step 4: Remove `isolated` / `extensions: false` / `noSkills` — [#264] ✅ Delivered
785
780
 
786
- Children always load the parent's extensions and skills; the recursion guard becomes unconditional.
781
+ Children always load the parent's extensions and skills; the recursion guard is now unconditional.
787
782
  Deny-at-use (the in-child permission layer) covers tool restriction; prevent-load is left as a latent provider seam (not shipped).
783
+ The `skills` curation axis collapsed symmetrically with `extensions`: `AgentConfig.skills`, the skill-preload path (`skill-loader.ts`, `safe-fs.ts`, `preloadSkills`, `PromptExtras`), `SessionConfig.{extensions,noSkills,extras}`, and the `isolated:` / `extensions:` / `skills:` custom-agent frontmatter keys are all gone.
788
784
 
789
- - Depends on: Step 1 (deny-at-use over events).
790
- - Outcome: the `isolated`/`extensions`/`noSkills` axis is gone; one fewer conditional in the guard.
785
+ - Depended on: Step 1 (deny-at-use over events).
786
+ - Outcome: the `isolated`/`extensions`/`noSkills`/`skills` axis is gone; the guard is unconditional.
791
787
 
792
788
  #### Step 5: Born-complete child execution; dissolve the runner — [#265]
793
789
 
@@ -0,0 +1,275 @@
1
+ ---
2
+ issue: 264
3
+ issue_title: "Remove isolated / extensions:false / noSkills from core"
4
+ ---
5
+
6
+ # Remove the extension-lifecycle-control axis from the core
7
+
8
+ ## Problem Statement
9
+
10
+ The core still carries an extension-lifecycle-control axis — `isolated`, `extensions: false`, and `noSkills` — that lets a spawn blanket-disable a child's extensions and skills.
11
+ Per ADR 0002, this is policy that does not belong in a minimal orchestrator.
12
+ Deny-at-use (the in-child permission layer, shipped in Step 1 / #261) already covers what `isolated` pretended to do for tools.
13
+ Prevent-load (refusing to bind an extension for true sandboxing) is genuinely generative and is deliberately left as a *latent, un-built* provider seam — we do not ship a vacant hook.
14
+
15
+ This is Phase 16, Step 4.
16
+ With the axis gone, children always load the parent's extensions and skills, and the recursion guard — which currently gates on `cfg.extensions` — becomes unconditional.
17
+
18
+ ## Goals
19
+
20
+ - Remove `isolated` from the spawn API, `SubagentsService`, the lifecycle plumbing, and the config assembler.
21
+ - Remove the `extensions` boolean from `AgentConfig` and the assembler (children always inherit extensions).
22
+ - Remove `noSkills` from the assembler and the resource-loader options.
23
+ - Collapse the skill-curation axis symmetrically: remove `AgentConfig.skills` and the skill-**preload** path (`skill-loader.ts`, `safe-fs.ts`, `preloadSkills`, `PromptExtras`, `extras.skillBlocks`).
24
+ Children always load Pi's full skill system, exactly as `skills: true` does today.
25
+ - Make the recursion guard unconditional — it always strips `subagent` / `get_subagent_result` / `steer_subagent` from children, keyed off the core's own tool names.
26
+ - This is a **breaking** change: the public `SpawnOptions.isolated` field and the `isolated:` / `extensions:` / `skills:` custom-agent frontmatter keys are removed.
27
+ Suggested commits use `feat!:`.
28
+
29
+ ## Non-Goals
30
+
31
+ - Born-complete child execution / dissolving the runner — that is Step 5 (#265) and depends on this step.
32
+ `RunOptions`, `runAgent`, `ConcreteAgentRunner`, and `Agent.run()` survive this issue (with `isolated` removed from them).
33
+ - Shipping a prevent-load provider seam — ADR 0002 leaves it latent until a real consumer needs it.
34
+ - Changing `builtinToolNames` (the `tools:` frontmatter allowlist) — that is a separate, surviving concern.
35
+ - Changing deny-at-use behavior in `@gotgenes/pi-permission-system` — already in place from #261.
36
+
37
+ ## Background
38
+
39
+ Relevant modules and how they relate:
40
+
41
+ - `src/types.ts` — declares `AgentConfig` (`extensions`, `skills`, `isolated`) and `AgentInvocation` (`isolated`).
42
+ - `src/config/default-agents.ts` — the three embedded agents set `extensions: true`, `skills: true`.
43
+ - `src/config/custom-agents.ts` — parses `extensions` / `skills` / `isolated` from `.md` frontmatter via `resolveBoolExtensions` and `inheritField`.
44
+ - `src/config/invocation-config.ts` — merges `isolated` from agent config + tool params.
45
+ - `src/config/agent-types.ts` — an absolute-fallback `AgentConfig` literal sets `extensions: true`, `skills: true`.
46
+ - `src/session/session-config.ts` — `assembleSessionConfig` derives `extensions`/`skills` from `options.isolated`, calls `io.preloadSkills` when `skills` is `string[]`, and computes `noSkills`.
47
+ Returns `SessionConfig.{ extensions, noSkills, extras }`.
48
+ - `src/session/prompts.ts` — `buildAgentPrompt` injects `extras.skillBlocks` as `# Preloaded Skill:` sections; `PromptExtras` carries only `skillBlocks` (memory was removed in #185).
49
+ - `src/session/skill-loader.ts` — `preloadSkills` reads named skill files from disk; consumes `safe-fs.ts`.
50
+ - `src/session/safe-fs.ts` — symlink/path-traversal guards; **only consumer is `skill-loader.ts`**.
51
+ - `src/lifecycle/agent-runner.ts` — `RunOptions.isolated` flows into the assembler; `createResourceLoader` is called with `noExtensions: !cfg.extensions` and `noSkills: cfg.noSkills`; the recursion guard runs only `if (cfg.extensions)`.
52
+ `ResourceLoaderOptions` (a local narrow interface, not the SDK type) declares `noExtensions?` / `noSkills?`.
53
+ - `src/lifecycle/agent.ts` — `AgentInit.isolated`, the `_isolated` field, and `isolated:` in the `runner.run(...)` call.
54
+ - `src/lifecycle/agent-manager.ts` — `AgentSpawnConfig.isolated` and its pass-through to `new Agent(...)`.
55
+ - `src/tools/{agent-tool,spawn-config,foreground-runner,background-spawner}.ts` — tool schema `isolated`, `SpawnExecution.isolated`, and pass-through.
56
+ - `src/service/{service,service-adapter}.ts` — public `SpawnOptions.isolated` and its pass-through to the manager.
57
+ - `src/ui/{display,agent-config-editor,agent-creation-wizard}.ts` — `isolated` tag, eject-content emission, and generation-prompt template text.
58
+ - `src/index.ts` — wires `preloadSkills` and `buildAgentPrompt` into `assemblerIO`.
59
+
60
+ AGENTS.md constraints that apply:
61
+
62
+ - Conventional Commits; breaking changes use `feat!:`.
63
+ - Do not edit `CHANGELOG.md` (release-please owns it).
64
+ - When removing an export, grep all `src/` and `test/` for the symbol before finalizing (done below).
65
+ - When adding/removing a module, check `docs/architecture/` for layout listings and complexity tables that reference it (done below — Mermaid + tree + field tables).
66
+ - One-sentence-per-line markdown; sequential numbering restarting per heading.
67
+
68
+ ## Design Overview
69
+
70
+ This is a **symmetric collapse**, not a refactor.
71
+ Two parallel axes leave the core together:
72
+
73
+ | Axis | Today | After |
74
+ | ---------- | -------------------------------------------------------------- | --------------------------------------------- |
75
+ | Extensions | `extensions: true \| false`; `isolated` forces `false` | always inherit (field gone) |
76
+ | Skills | `skills: true \| string[] \| false`; `isolated` forces `false` | always inherit full skill system (field gone) |
77
+
78
+ The `skills` field collapses for the same reason `extensions` does: `noSkills` is the single mechanism behind **both** restriction modes (`skills: false` → no skills; `skills: string[]` → only those, preloaded into the prompt with the SDK loader suppressed).
79
+ Removing `noSkills` without removing `AgentConfig.skills` would leave a field that silently stops restricting — a `string[]` agent would get its baked-in skills *plus* the full system.
80
+ ADR 0002 says children always load the parent's skills, which is exactly the `skills: true` path; the other two values are skill curation/policy and leave the core.
81
+
82
+ ### Assembler after collapse
83
+
84
+ `assembleSessionConfig` loses the `isolated` branch, the `preloadSkills` block, and the `noSkills`/`extensions`/`extras` outputs:
85
+
86
+ ```typescript
87
+ export interface AssemblerOptions {
88
+ cwd?: string;
89
+ model?: unknown;
90
+ thinkingLevel?: ThinkingLevel;
91
+ // isolated removed
92
+ }
93
+
94
+ export interface SessionConfig {
95
+ effectiveCwd: string;
96
+ systemPrompt: string;
97
+ toolNames: string[];
98
+ model: unknown;
99
+ thinkingLevel: ThinkingLevel | undefined;
100
+ agentMaxTurns: number | undefined;
101
+ // extensions, noSkills, extras removed
102
+ }
103
+ ```
104
+
105
+ `AssemblerIO` drops `preloadSkills`, keeping only `buildAgentPrompt` (now called without `extras`).
106
+
107
+ ### Runner after collapse
108
+
109
+ The resource-loader call drops the two suppression flags, and the guard runs unconditionally:
110
+
111
+ ```typescript
112
+ const loader = deps.io.createResourceLoader({
113
+ cwd: cfg.effectiveCwd,
114
+ agentDir,
115
+ noPromptTemplates: true,
116
+ noThemes: true,
117
+ noContextFiles: true,
118
+ systemPromptOverride: () => cfg.systemPrompt,
119
+ appendSystemPromptOverride: () => [],
120
+ // noExtensions, noSkills removed
121
+ });
122
+ // ...
123
+ await session.bindExtensions({});
124
+ // Recursion guard — now unconditional (children always load extensions).
125
+ const filtered = filterActiveTools(session.getActiveToolNames());
126
+ session.setActiveToolsByName(filtered);
127
+ ```
128
+
129
+ `ResourceLoaderOptions` drops `noExtensions?` / `noSkills?` (a local narrow interface — removing the latent fields keeps us honest per ADR 0002's "no vacant hooks").
130
+
131
+ ### Call-site sketch — recursion guard (Tell-Don't-Ask check)
132
+
133
+ The guard already asks the session for its active tools and tells it the filtered set — unchanged except for removing the `if`.
134
+ No new collaborator, no reach-through; the only structural change is that `cfg.extensions` is no longer consulted, so `SessionConfig` no longer needs to expose it.
135
+ This is a narrowing of the assembler's output contract, which is the desired direction.
136
+
137
+ ### Edge cases
138
+
139
+ - Append-mode agents: `parentSystemPrompt` still embeds parent context via `systemPromptOverride`; nothing about that path depends on skills/extensions flags.
140
+ - Custom agents with legacy `extensions:` / `skills:` / `isolated:` frontmatter: the keys are silently ignored after this change (no parse, no warning).
141
+ `resolveBoolExtensions` already warned on the deprecated allowlist syntax; that warning path is removed with the parser.
142
+ - The absolute-fallback `AgentConfig` in `agent-types.ts` drops `extensions`/`skills` like every other construction site.
143
+
144
+ ## Module-Level Changes
145
+
146
+ ### Source — `isolated` axis
147
+
148
+ - `src/types.ts` — remove `AgentConfig.isolated`, `AgentInvocation.isolated`.
149
+ - `src/config/invocation-config.ts` — remove `isolated` from `AgentInvocationParams` and the return object.
150
+ - `src/config/custom-agents.ts` — remove the `isolated:` frontmatter parse.
151
+ - `src/session/session-config.ts` — remove `AssemblerOptions.isolated` and the `options.isolated ? false : ...` derivation (read `agentConfig.extensions`/`agentConfig.skills` directly, for now).
152
+ - `src/lifecycle/agent-runner.ts` — remove `RunOptions.isolated` and the `isolated:` argument passed to `assembleSessionConfig`.
153
+ - `src/lifecycle/agent.ts` — remove `AgentInit.isolated`, the `_isolated` field + constructor assignment, and `isolated:` in the `runner.run(...)` call.
154
+ - `src/lifecycle/agent-manager.ts` — remove `AgentSpawnConfig.isolated` and its pass-through to `new Agent(...)`.
155
+ - `src/tools/agent-tool.ts` — remove the `isolated` schema property.
156
+ - `src/tools/spawn-config.ts` — remove `SpawnExecution.isolated`, the `const isolated = resolvedConfig.isolated`, `isolated` in `agentInvocation`, and `isolated` in the `execution` return.
157
+ - `src/tools/foreground-runner.ts` / `src/tools/background-spawner.ts` — remove `isolated: execution.isolated`.
158
+ - `src/service/service.ts` — remove `SpawnOptions.isolated`.
159
+ - `src/service/service-adapter.ts` — remove `isolated: options?.isolated` from the `manager.spawn(...)` call.
160
+ - `src/ui/display.ts` — remove `if (invocation.isolated) tags.push("isolated")` and the `isolated` mention in the JSDoc example.
161
+ - `src/ui/agent-config-editor.ts` — remove the `isolated: true` line from `buildEjectContent`.
162
+ - `src/ui/agent-creation-wizard.ts` — remove the `isolated:` template line and the "Set isolated: true …" guideline.
163
+
164
+ ### Source — `extensions` axis + unconditional guard
165
+
166
+ - `src/types.ts` — remove `AgentConfig.extensions`.
167
+ - `src/config/default-agents.ts` — remove `extensions: true` from all three agents.
168
+ - `src/config/agent-types.ts` — remove `extensions: true` from the absolute-fallback literal.
169
+ - `src/config/custom-agents.ts` — remove the `extensions:` frontmatter parse and delete `resolveBoolExtensions`.
170
+ - `src/session/session-config.ts` — remove `SessionConfig.extensions` and stop assigning it.
171
+ - `src/lifecycle/agent-runner.ts` — remove `ResourceLoaderOptions.noExtensions`, drop `noExtensions` from the `createResourceLoader` call, and make the recursion guard unconditional (delete `if (cfg.extensions)`); update the explanatory comment.
172
+ - `src/ui/agent-config-editor.ts` — remove the `extensions: false` line from `buildEjectContent`.
173
+ - `src/ui/agent-creation-wizard.ts` — remove the `extensions:` template line.
174
+
175
+ ### Source — `skills` axis + preload path
176
+
177
+ - `src/types.ts` — remove `AgentConfig.skills`.
178
+ - `src/config/default-agents.ts` — remove `skills: true` from all three agents.
179
+ - `src/config/agent-types.ts` — remove `skills: true` from the absolute-fallback literal.
180
+ - `src/config/custom-agents.ts` — remove the `skills:` frontmatter parse and delete `inheritField` (its only remaining callers are `skills`/`extensions`; `csvList`, `parseCsvField`, `str`, `nonNegativeInt` stay — `csvList` still serves `tools:`).
181
+ - `src/session/session-config.ts` — remove `AssemblerIO.preloadSkills`, `SessionConfig.noSkills`, `SessionConfig.extras`, the `extras`/`preloadSkills` block, and the `extras` argument to `buildAgentPrompt`.
182
+ - `src/session/prompts.ts` — remove `PromptExtras`, the `extras` parameter, and the `extrasSuffix` logic.
183
+ - `src/session/skill-loader.ts` — **delete** (export `preloadSkills`, `PreloadedSkill`).
184
+ - `src/session/safe-fs.ts` — **delete** (sole consumer was `skill-loader.ts`).
185
+ - `src/lifecycle/agent-runner.ts` — remove `ResourceLoaderOptions.noSkills` and drop `noSkills` from the `createResourceLoader` call.
186
+ - `src/index.ts` — remove the `preloadSkills` import and its `assemblerIO.preloadSkills` wiring.
187
+ - `src/ui/agent-config-editor.ts` — remove the `skills: false` / `skills: <list>` lines from `buildEjectContent`.
188
+ - `src/ui/agent-creation-wizard.ts` — remove the `skills:` template line.
189
+
190
+ ### Tests
191
+
192
+ - `test/session/skill-loader.test.ts` — **delete**.
193
+ - `test/session/safe-fs.test.ts` — **delete**.
194
+ - `test/session/session-config.test.ts` — remove the `isolated`-mode `describe`, the `noSkills` assertions, and the preload tests; keep model-resolution and prompt-assembly assertions.
195
+ - `test/session/prompts.test.ts` — remove `isolated`/`extensions`/`skills` from `AgentConfig` fixtures and any `skillBlocks`/`extras` cases.
196
+ - `test/config/invocation-config.test.ts` — remove `isolated` cases and fixture fields.
197
+ - `test/config/custom-agents.test.ts` — remove `extensions` / `skills` / `isolated` frontmatter-parsing tests.
198
+ - `test/config/agent-types.test.ts` — remove `extensions` / `skills` / `isolated` from fixtures.
199
+ - `test/lifecycle/agent-runner-extension-tools.test.ts` — remove `extensions`/`skills`/`isolated` from the mock config; delete the "extensions: false skips the filter entirely" test; keep/adjust the post-bind guard tests to assert the guard runs unconditionally.
200
+ - `test/tools/spawn-config.test.ts` — remove the "sets isolated from params" test and `isolated` fixture fields.
201
+ - `test/tools/background-spawner.test.ts` / `test/tools/foreground-runner.test.ts` — remove `isolated` from fixtures and `agentInvocation` assertions.
202
+ - `test/tools/result-renderer.test.ts` — remove the `"isolated"` tag case.
203
+ - `test/ui/agent-config-editor.test.ts` — remove the `isolated` / `extensions: false` eject-emission tests.
204
+ - `test/display.test.ts` — remove `extensions`/`skills` from `AgentConfig` fixtures.
205
+ - `test/helpers/runner-io.ts` — remove `extensions`/`skills`/`isolated` from `DEFAULT_AGENT_CONFIG`.
206
+ - `test/helpers/runner-io.test.ts` — remove the `config.extensions`/`config.skills` assertions and the `extensions: true` override case.
207
+ - `test/helpers/ui-stubs.ts` — remove `extensions: true` / `skills: true` from the stub `AgentConfig`.
208
+
209
+ ### Docs
210
+
211
+ - `docs/architecture/architecture.md` —
212
+ remove the `SafeFs` and `SkillLoader` nodes from the session-domain Mermaid subgraph;
213
+ remove `safe-fs.ts` and `skill-loader.ts` from the directory-tree listing;
214
+ drop `isolated` from the `SpawnOptions` field list (≈ line 418) and the `RunOptions` field list (≈ line 651);
215
+ mark Phase 16 Step 4 (#264) done in the roadmap and update the "Children always load the parent's extensions and skills" note;
216
+ reflect the smaller session domain (8 → 6 modules).
217
+ - `.pi/skills/package-pi-subagents/SKILL.md` — update the Session domain row (module count 8 → 6, drop `safe-fs.ts` / `skill-loader.ts` from the file list).
218
+
219
+ ## Test Impact Analysis
220
+
221
+ 1. New coverage enabled — minimal (this is a removal).
222
+ The one behavioral assertion worth strengthening: the post-bind recursion guard now runs **unconditionally**.
223
+ Update `agent-runner-extension-tools.test.ts` so a case that previously relied on `extensions: true` instead asserts the guard always calls `setActiveToolsByName` and always excludes `EXCLUDED_TOOL_NAMES`, with no config dependence.
224
+ 2. Tests that become redundant and are removed —
225
+ `skill-loader.test.ts` and `safe-fs.test.ts` (modules deleted);
226
+ the `isolated`-mode `describe` in `session-config.test.ts`;
227
+ the "extensions: false skips the filter entirely" case;
228
+ `isolated` parameter tests in `spawn-config.test.ts` / `invocation-config.test.ts`;
229
+ `extensions` / `skills` frontmatter-parsing tests in `custom-agents.test.ts`.
230
+ 3. Tests that must stay (genuinely exercise surviving layers) —
231
+ prompt assembly (`prompts.test.ts`, minus `extras`);
232
+ model resolution in `session-config.test.ts`;
233
+ the post-bind guard ordering tests (adjusted, not removed);
234
+ custom-agent `tools:` parsing (`csvList` survives).
235
+
236
+ ## TDD Order
237
+
238
+ Each cycle ends green (`pnpm run check` + `pnpm -r run test`).
239
+ Because removing a field from `AgentConfig` / `AgentInvocation` breaks every reader and every object-literal construction site at once (TS excess-property + property-access), each cycle folds its test updates into the same commit (per the testing skill's removal rule).
240
+ The three axes are split so each commit stays reviewable and leaves the repo compiling.
241
+
242
+ 1. Remove the `isolated` axis end-to-end.
243
+ Surface: `types.ts` (`AgentConfig.isolated`, `AgentInvocation.isolated`), `invocation-config.ts`, `custom-agents.ts` (parse only), `session-config.ts` (`AssemblerOptions.isolated` + derivation), `agent-runner.ts` (`RunOptions.isolated`), `agent.ts`, `agent-manager.ts`, `tools/*`, `service*.ts`, `ui/*` (isolated bits), plus all listed tests.
244
+ After: `extensions`/`skills`/`noSkills` still function; the assembler reads `agentConfig.extensions`/`agentConfig.skills` directly.
245
+ Commit: `feat!: remove isolated from the subagent spawn API and lifecycle (#264)`.
246
+ 2. Remove the `extensions` axis; make the recursion guard unconditional.
247
+ Surface: `types.ts` (`AgentConfig.extensions`), `default-agents.ts`, `agent-types.ts` (fallback), `custom-agents.ts` (parse + delete `resolveBoolExtensions`), `session-config.ts` (`SessionConfig.extensions`), `agent-runner.ts` (`ResourceLoaderOptions.noExtensions`, drop `noExtensions` arg, unconditional guard), `ui/agent-config-editor.ts` + `ui/agent-creation-wizard.ts` (extensions bits), plus tests (including the guard-always-runs update and deleting the "skips filter" case).
248
+ Commit: `feat!: always inherit extensions; make the recursion guard unconditional (#264)`.
249
+ 3. Remove the `skills` axis, `noSkills`, and the skill-preload path.
250
+ Surface: `types.ts` (`AgentConfig.skills`), `default-agents.ts`, `agent-types.ts` (fallback), `custom-agents.ts` (parse + delete `inheritField`), `session-config.ts` (`AssemblerIO.preloadSkills`, `SessionConfig.noSkills`/`extras`, preload block, `buildAgentPrompt` call), `prompts.ts` (`PromptExtras`/`extras`/`extrasSuffix`), delete `skill-loader.ts` + `safe-fs.ts`, `agent-runner.ts` (`ResourceLoaderOptions.noSkills`, drop arg), `index.ts` (wiring), `ui/*` (skills bits), plus tests (delete `skill-loader.test.ts` + `safe-fs.test.ts`).
251
+ Commit: `feat!: always inherit skills; remove noSkills and the skill-preload path (#264)`.
252
+ 4. Update the architecture doc and package skill.
253
+ Surface: `docs/architecture/architecture.md` (Mermaid session subgraph, directory tree, `SpawnOptions`/`RunOptions` field lists, roadmap status), `.pi/skills/package-pi-subagents/SKILL.md` (session domain row).
254
+ Commit: `docs: record removal of the extension-lifecycle-control axis (#264)`.
255
+
256
+ ## Risks and Mitigations
257
+
258
+ - Risk: `resolveBoolExtensions` / `inheritField` deletion removes a helper still used elsewhere.
259
+ Mitigation: greps confirm `resolveBoolExtensions` is used only by the removed `extensions` parse, and `inheritField` only by `extensions`/`skills`; `csvList`/`parseCsvField` (used by `tools:`) stay.
260
+ - Risk: deleting `safe-fs.ts` orphans an import.
261
+ Mitigation: grep confirms `skill-loader.ts` is its sole consumer; `safe-fs.test.ts` is deleted in the same cycle.
262
+ - Risk: removing `SpawnOptions.isolated` breaks a published consumer of the service.
263
+ Mitigation: this is an intentional breaking change (ADR 0002); `feat!:` triggers a major bump via release-please.
264
+ The public type surface is verified by `pnpm run verify:public-types` after Step 3.
265
+ - Risk: skill behavior silently changes for agents that relied on `skills: string[]` curation.
266
+ Mitigation: documented in Goals as breaking; children now inherit the full skill system, which is strictly more capable, and deny-at-use governs what they may act on.
267
+ - Risk: a test file is touched in multiple cycles (e.g. `runner-io.ts`).
268
+ Mitigation: each cycle removes only the field it owns; ordering is fixed (isolated → extensions → skills) so no cycle leaves a dangling reference.
269
+
270
+ ## Open Questions
271
+
272
+ - Should custom-agent `.md` files with now-defunct `extensions:` / `skills:` / `isolated:` frontmatter emit a one-time deprecation warning, or be silently ignored?
273
+ Deferred: silent-ignore matches the Phase 14 precedent for the removed `disallowed_tools` field; revisit only if users report confusion.
274
+ - Does `verify:public-types` need a new negative assertion that `isolated` is absent from `SpawnOptions`?
275
+ Deferred to Step 3 implementation — the existing consumer type-check will fail if a stale field lingers.
@@ -0,0 +1,48 @@
1
+ ---
2
+ issue: 264
3
+ issue_title: "Remove isolated / extensions:false / noSkills from core"
4
+ ---
5
+
6
+ # Retro: #264 — Remove isolated / extensions:false / noSkills from core
7
+
8
+ ## Stage: Planning (2026-05-30T00:09:21Z)
9
+
10
+ ### Session summary
11
+
12
+ Planned Phase 16, Step 4: removing the extension-lifecycle-control axis (`isolated`, `extensions: false`, `noSkills`) from the pi-subagents core per ADR 0002.
13
+ Confirmed all three prerequisite Phase 16 steps (#261, #262, #263) are closed, so the explicit "deny-at-use" dependency is satisfied.
14
+ Produced a four-cycle TDD plan (`isolated` → `extensions` → `skills`/`noSkills`/preload → docs) and committed it.
15
+
16
+ ### Observations
17
+
18
+ - Scope expansion decided with the user: the issue names only `isolated` / `extensions: false` / `noSkills`, but `noSkills` is the single mechanism behind **both** skill-restriction modes (`skills: false` and `skills: string[]` preload).
19
+ Removing `noSkills` without also removing `AgentConfig.skills` would leave a field that silently stops restricting.
20
+ Chose the **collapse-skills-fully** option (symmetric with `extensions`): retire `AgentConfig.skills`, `skill-loader.ts`, `safe-fs.ts` (sole consumer was the skill loader), `preloadSkills`, `PromptExtras`, and `extras.skillBlocks`.
21
+ Children always inherit Pi's full skill system — the `skills: true` path.
22
+ - The recursion guard's `if (cfg.extensions)` gate is removed in the `extensions` cycle (cycle 2), since `SessionConfig.extensions` disappears there.
23
+ A guard-always-runs assertion replaces the deleted "extensions: false skips the filter entirely" test.
24
+ - This is a **breaking** change (`feat!:`): public `SpawnOptions.isolated` and the `isolated:` / `extensions:` / `skills:` custom-agent frontmatter keys are removed.
25
+ Custom agents with legacy frontmatter will silently ignore those keys (matches the Phase 14 precedent for `disallowed_tools`).
26
+ - Sequencing note surfaced to the user: some `isolated`-threading removed here (`RunOptions.isolated`, `Agent.run()` plumbing) is structure that Step 5 (#265, dissolve the runner) will delete anyway — small, unavoidable, and #265 depends on this step, so no reordering benefit.
27
+ - Helper-file churn accepted: `test/helpers/runner-io.ts` is touched in all three removal cycles (one field per cycle); ordering is fixed (`isolated` → `extensions` → `skills`) so no cycle leaves a dangling reference.
28
+ - Doc updates identified: `docs/architecture/architecture.md` (Mermaid session subgraph, directory tree, `SpawnOptions`/`RunOptions` field lists, roadmap status) and the `package-pi-subagents` SKILL.md session-domain row (8 → 6 modules).
29
+
30
+ ## Stage: Implementation — TDD (2026-05-30T00:38:00Z)
31
+
32
+ ### Session summary
33
+
34
+ Executed all four plan cycles (`isolated` → `extensions` + unconditional guard → `skills`/`noSkills`/preload → docs) as three `feat!:` commits plus one `docs:` commit.
35
+ The extension-lifecycle-control axis and the `skills` curation axis are gone; children always inherit the parent's extensions and full skill system, and the recursion guard is unconditional.
36
+ Test count went 1016 → 951 (−65): deleted `skill-loader.test.ts` and `safe-fs.test.ts`, plus removed isolated/extensions/skills/preload-specific cases; `check`, `lint`, `fallow dead-code`, and `verify:public-types` all green.
37
+
38
+ ### Observations
39
+
40
+ - The plan's cycle split held up cleanly.
41
+ The only interleaving friction was `test/config/custom-agents.test.ts`, where `extensions` and `skills` assertions shared the same `it` blocks.
42
+ Handled it by retitling the shared tests to skills-only in cycle 2 (keeping skills compiling/passing), then deleting them in cycle 3 — no dangling references between commits.
43
+ - BSD `sed` (macOS) does not support `\|` alternation in basic regex; the standalone-fixture-line deletions needed `sed -E`.
44
+ Worth remembering for future bulk fixture removals.
45
+ - Two in-scope judgment calls beyond the literal plan: (1) kept the generic tag-rendering test in `result-renderer.test.ts` by swapping the example tag `"isolated"` → `"inherit context"` rather than deleting coverage; (2) removed the now-unused `vi` import from `custom-agents.test.ts` after dropping the extensions-deprecation-warning test.
46
+ - The `spawn-config.test.ts` `agentInvocation` snapshot carried a stale `isolation: undefined` leftover (from the #263 worktree eviction) that `toEqual` had been silently ignoring; removed it alongside `isolated: false` for a clean exact-match assertion.
47
+ - `verify:public-types` confirmed the breaking `SpawnOptions.isolated` removal type-checks against an external consumer; no lockfile changes; `dist/` correctly gitignored after the type-bundle build.
48
+ - Pre-completion reviewer: **PASS** — both acceptance criteria code-verified, all deterministic checks green, 6 Mermaid diagrams render, docs accurate, zero dead code.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-subagents",
3
- "version": "12.1.0",
3
+ "version": "13.0.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -114,8 +114,6 @@ export class AgentTypeRegistry implements AgentConfigLookup {
114
114
  displayName: "Agent",
115
115
  description: "General-purpose agent for complex, multi-step tasks",
116
116
  builtinToolNames: BUILTIN_TOOL_NAMES,
117
- extensions: true,
118
- skills: true,
119
117
  systemPrompt: "",
120
118
  promptMode: "append",
121
119
  };
@@ -58,8 +58,6 @@ function loadFromDir(dir: string, agents: Map<string, AgentConfig>, source: "pro
58
58
  displayName: str(fm.display_name),
59
59
  description: str(fm.description) ?? name,
60
60
  builtinToolNames: csvList(fm.tools, BUILTIN_TOOL_NAMES),
61
- extensions: resolveBoolExtensions(fm.extensions ?? fm.inherit_extensions),
62
- skills: inheritField(fm.skills ?? fm.inherit_skills),
63
61
  model: str(fm.model),
64
62
  thinking: str(fm.thinking) as ThinkingLevel | undefined,
65
63
  maxTurns: nonNegativeInt(fm.max_turns),
@@ -67,7 +65,6 @@ function loadFromDir(dir: string, agents: Map<string, AgentConfig>, source: "pro
67
65
  promptMode: fm.prompt_mode === "append" ? "append" : "replace",
68
66
  inheritContext: fm.inherit_context != null ? fm.inherit_context === true : undefined,
69
67
  runInBackground: fm.run_in_background != null ? fm.run_in_background === true : undefined,
70
- isolated: fm.isolated != null ? fm.isolated === true : undefined,
71
68
  enabled: fm.enabled !== false, // default true; explicitly false disables
72
69
  source,
73
70
  });
@@ -107,30 +104,3 @@ function csvList(val: unknown, defaults: string[]): string[] {
107
104
  if (val === undefined || val === null) return defaults;
108
105
  return parseCsvField(val) ?? [];
109
106
  }
110
-
111
- /**
112
- * Resolve the `extensions` field to a boolean.
113
- * CSV/array values (legacy allowlist syntax) are coerced to `true` with a warning.
114
- */
115
- function resolveBoolExtensions(val: unknown): boolean {
116
- const result = inheritField(val);
117
- if (Array.isArray(result)) {
118
- console.warn(
119
- "[pi-subagents] extensions allowlist syntax is deprecated — treating as \"true\" (inherit all).\n" +
120
- "Use \"permission:\" frontmatter in pi-permission-system for per-tool access control.",
121
- );
122
- return true;
123
- }
124
- return result;
125
- }
126
-
127
- /**
128
- * Parse an inherit field (extensions, skills).
129
- * omitted/true → true (inherit all); false/"none"/empty → false; csv → listed names.
130
- */
131
- function inheritField(val: unknown): true | string[] | false {
132
- if (val === undefined || val === null || val === true) return true;
133
- if (val === false || val === "none") return false;
134
- const items = csvList(val, []);
135
- return items.length > 0 ? items : false;
136
- }
@@ -16,10 +16,8 @@ export const DEFAULT_AGENTS: Map<string, AgentConfig> = new Map([
16
16
  displayName: "Agent",
17
17
  description: "General-purpose agent for complex, multi-step tasks",
18
18
  // builtinToolNames omitted — means "all available tools" (resolved at lookup time)
19
- // inheritContext / runInBackground / isolated omitted — strategy fields, callers decide per-call.
19
+ // inheritContext / runInBackground omitted — strategy fields, callers decide per-call.
20
20
  // Setting them to false would lock callsite intent (see resolveAgentInvocationConfig in invocation-config.ts).
21
- extensions: true,
22
- skills: true,
23
21
  systemPrompt: "",
24
22
  promptMode: "append",
25
23
  isDefault: true,
@@ -32,8 +30,6 @@ export const DEFAULT_AGENTS: Map<string, AgentConfig> = new Map([
32
30
  displayName: "Explore",
33
31
  description: "Fast codebase exploration agent (read-only)",
34
32
  builtinToolNames: READ_ONLY_TOOLS,
35
- extensions: true,
36
- skills: true,
37
33
  model: "anthropic/claude-haiku-4-5-20251001",
38
34
  systemPrompt: `# CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS
39
35
  You are a file search specialist. You excel at thoroughly navigating and exploring codebases.
@@ -74,8 +70,6 @@ Use Bash ONLY for read-only operations: ls, git status, git log, git diff, find,
74
70
  displayName: "Plan",
75
71
  description: "Software architect for implementation planning (read-only)",
76
72
  builtinToolNames: READ_ONLY_TOOLS,
77
- extensions: true,
78
- skills: true,
79
73
  systemPrompt: `# CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS
80
74
  You are a software architect and planning specialist.
81
75
  Your role is EXCLUSIVELY to explore the codebase and design implementation plans.
@@ -6,7 +6,6 @@ interface AgentInvocationParams {
6
6
  max_turns?: number;
7
7
  run_in_background?: boolean;
8
8
  inherit_context?: boolean;
9
- isolated?: boolean;
10
9
  }
11
10
 
12
11
  export function resolveAgentInvocationConfig(
@@ -19,7 +18,6 @@ export function resolveAgentInvocationConfig(
19
18
  maxTurns?: number;
20
19
  inheritContext: boolean;
21
20
  runInBackground: boolean;
22
- isolated: boolean;
23
21
  } {
24
22
  return {
25
23
  modelInput: agentConfig?.model ?? params.model,
@@ -28,6 +26,5 @@ export function resolveAgentInvocationConfig(
28
26
  maxTurns: agentConfig?.maxTurns ?? params.max_turns,
29
27
  inheritContext: agentConfig?.inheritContext ?? params.inherit_context ?? false,
30
28
  runInBackground: agentConfig?.runInBackground ?? params.run_in_background ?? false,
31
- isolated: agentConfig?.isolated ?? params.isolated ?? false,
32
29
  };
33
30
  }
package/src/index.ts CHANGED
@@ -38,7 +38,6 @@ import { detectEnv } from "#src/session/env";
38
38
  import { resolveModel } from "#src/session/model-resolver";
39
39
  import { buildAgentPrompt } from "#src/session/prompts";
40
40
  import { deriveSubagentSessionDir } from "#src/session/session-dir";
41
- import { preloadSkills } from "#src/session/skill-loader";
42
41
  import { SettingsManager } from "#src/settings";
43
42
  import { AgentTool } from "#src/tools/agent-tool";
44
43
  import { GetResultTool } from "#src/tools/get-result-tool";
@@ -143,7 +142,6 @@ export default function (pi: ExtensionAPI) {
143
142
  createSettingsManager: (cwd, dir) => SdkSettingsManager.create(cwd, dir),
144
143
  createSession: (opts) => createAgentSession(opts as any),
145
144
  assemblerIO: {
146
- preloadSkills,
147
145
  buildAgentPrompt,
148
146
  },
149
147
  },
@@ -41,7 +41,6 @@ export interface AgentSpawnConfig {
41
41
  description: string;
42
42
  model?: Model<any>;
43
43
  maxTurns?: number;
44
- isolated?: boolean;
45
44
  inheritContext?: boolean;
46
45
  thinkingLevel?: ThinkingLevel;
47
46
  isBackground?: boolean;
@@ -149,7 +148,6 @@ export class AgentManager {
149
148
  prompt,
150
149
  model: options.model,
151
150
  maxTurns: options.maxTurns,
152
- isolated: options.isolated,
153
151
  thinkingLevel: options.thinkingLevel,
154
152
  parentSession: options.parentSession,
155
153
  signal: options.signal,
@@ -55,8 +55,6 @@ export interface SessionManagerLike {
55
55
  export interface ResourceLoaderOptions {
56
56
  cwd: string;
57
57
  agentDir: string;
58
- noExtensions?: boolean;
59
- noSkills?: boolean;
60
58
  noPromptTemplates?: boolean;
61
59
  noThemes?: boolean;
62
60
  noContextFiles?: boolean;
@@ -148,7 +146,6 @@ export interface RunOptions {
148
146
  model?: Model<any>;
149
147
  maxTurns?: number;
150
148
  signal?: AbortSignal;
151
- isolated?: boolean;
152
149
  thinkingLevel?: ThinkingLevel;
153
150
  /** Called once after session creation - session delivery mechanism. */
154
151
  onSessionCreated?: (session: AgentSession) => void;
@@ -283,7 +280,6 @@ export async function runAgent(
283
280
  },
284
281
  {
285
282
  cwd: options.context.cwd,
286
- isolated: options.isolated,
287
283
  model: options.model,
288
284
  thinkingLevel: options.thinkingLevel,
289
285
  },
@@ -294,17 +290,15 @@ export async function runAgent(
294
290
 
295
291
  const agentDir = deps.io.getAgentDir();
296
292
 
297
- // Load extensions/skills: true load; false don't.
293
+ // Children always load the parent's extensions and skills.
298
294
  // Suppress AGENTS.md/CLAUDE.md and APPEND_SYSTEM.md - upstream's
299
295
  // buildSystemPrompt() re-appends both AFTER systemPromptOverride, which
300
- // would defeat prompt_mode: replace and isolated: true. Parent context, if
296
+ // would defeat prompt_mode: replace. Parent context, if
301
297
  // wanted, reaches the subagent via prompt_mode: append (parentSystemPrompt
302
298
  // is embedded in systemPromptOverride) or inherit_context (conversation).
303
299
  const loader = deps.io.createResourceLoader({
304
300
  cwd: cfg.effectiveCwd,
305
301
  agentDir,
306
- noExtensions: !cfg.extensions,
307
- noSkills: cfg.noSkills,
308
302
  noPromptTemplates: true,
309
303
  noThemes: true,
310
304
  noContextFiles: true,
@@ -348,12 +342,10 @@ export async function runAgent(
348
342
 
349
343
  // Apply recursion guard: remove our own tools from the child's active set.
350
344
  // Runs after bindExtensions so extension-registered tools are included in the
351
- // post-bind active set. Only needed when extensions are loaded (extensions: false
352
- // means no extension tools were registered, so the guard is a no-op).
353
- if (cfg.extensions) {
354
- const filtered = filterActiveTools(session.getActiveToolNames());
355
- session.setActiveToolsByName(filtered);
356
- }
345
+ // post-bind active set. Unconditional: children always load the parent's
346
+ // extensions, so the guard must always strip our dispatch tools.
347
+ const filtered = filterActiveTools(session.getActiveToolNames());
348
+ session.setActiveToolsByName(filtered);
357
349
 
358
350
  options.onSessionCreated?.(session);
359
351
 
@@ -81,7 +81,6 @@ export interface AgentInit {
81
81
  prompt?: string;
82
82
  model?: Model<any>;
83
83
  maxTurns?: number;
84
- isolated?: boolean;
85
84
  thinkingLevel?: ThinkingLevel;
86
85
  parentSession?: ParentSessionInfo;
87
86
  isBackground?: boolean;
@@ -140,7 +139,6 @@ export class Agent {
140
139
  private readonly _prompt?: string;
141
140
  private readonly _model?: Model<any>;
142
141
  private readonly _maxTurns?: number;
143
- private readonly _isolated?: boolean;
144
142
  private readonly _thinkingLevel?: ThinkingLevel;
145
143
  private readonly _parentSession?: ParentSessionInfo;
146
144
  private readonly _signal?: AbortSignal;
@@ -198,7 +196,6 @@ export class Agent {
198
196
  this._prompt = init.prompt;
199
197
  this._model = init.model;
200
198
  this._maxTurns = init.maxTurns;
201
- this._isolated = init.isolated;
202
199
  this._thinkingLevel = init.thinkingLevel;
203
200
  this._parentSession = init.parentSession;
204
201
  this._signal = init.signal;
@@ -261,7 +258,6 @@ export class Agent {
261
258
  maxTurns: this._maxTurns,
262
259
  defaultMaxTurns: runConfig?.defaultMaxTurns,
263
260
  graceTurns: runConfig?.graceTurns,
264
- isolated: this._isolated,
265
261
  thinkingLevel: this._thinkingLevel,
266
262
  signal: this.abortController.signal,
267
263
  onSessionCreated: (session) => {
@@ -66,7 +66,6 @@ export class SubagentsServiceAdapter implements SubagentsService {
66
66
  model,
67
67
  maxTurns: options?.maxTurns,
68
68
  thinkingLevel: options?.thinkingLevel,
69
- isolated: options?.isolated,
70
69
  inheritContext: options?.inheritContext,
71
70
  bypassQueue: options?.bypassQueue,
72
71
  isBackground,
@@ -61,7 +61,6 @@ export interface SpawnOptions {
61
61
  model?: string;
62
62
  maxTurns?: number;
63
63
  thinkingLevel?: string;
64
- isolated?: boolean;
65
64
  inheritContext?: boolean;
66
65
  foreground?: boolean;
67
66
  bypassQueue?: boolean;
@@ -5,12 +5,6 @@
5
5
  import type { EnvInfo } from "#src/session/env";
6
6
  import type { AgentPromptConfig } from "#src/types";
7
7
 
8
- /** Extra sections to inject into the system prompt (skills, etc.). */
9
- export interface PromptExtras {
10
- /** Preloaded skill contents to inject. */
11
- skillBlocks?: { name: string; content: string }[];
12
- }
13
-
14
8
  /**
15
9
  * Build the system prompt for an agent from its config.
16
10
  *
@@ -25,14 +19,12 @@ export interface PromptExtras {
25
19
  * inherited content so the stable prefix is cacheable by the LLM's KV cache.
26
20
  *
27
21
  * @param parentSystemPrompt The parent agent's effective system prompt (for append mode).
28
- * @param extras Optional extra sections to inject (memory, preloaded skills).
29
22
  */
30
23
  export function buildAgentPrompt(
31
24
  config: AgentPromptConfig,
32
25
  cwd: string,
33
26
  env: EnvInfo,
34
27
  parentSystemPrompt?: string,
35
- extras?: PromptExtras,
36
28
  ): string {
37
29
  const activeAgentTag = `<active_agent name="${config.name}"/>\n\n`;
38
30
 
@@ -41,18 +33,6 @@ Working directory: ${cwd}
41
33
  ${env.isGitRepo ? `Git repository: yes\nBranch: ${env.branch}` : "Not a git repository"}
42
34
  Platform: ${env.platform}`;
43
35
 
44
- // Build optional extras suffix
45
- const extraSections: string[] = [];
46
- if (extras?.skillBlocks?.length) {
47
- for (const skill of extras.skillBlocks) {
48
- extraSections.push(
49
- `\n# Preloaded Skill: ${skill.name}\n${skill.content}`,
50
- );
51
- }
52
- }
53
- const extrasSuffix =
54
- extraSections.length > 0 ? "\n\n" + extraSections.join("\n") : "";
55
-
56
36
  if (config.promptMode === "append") {
57
37
  const identity = parentSystemPrompt ?? genericBase;
58
38
 
@@ -85,8 +65,7 @@ You are operating as a sub-agent invoked to handle a specific task.
85
65
  "\n\n" +
86
66
  activeAgentTag +
87
67
  envBlock +
88
- customSection +
89
- extrasSuffix
68
+ customSection
90
69
  );
91
70
  }
92
71
 
@@ -97,7 +76,7 @@ You have been invoked to handle a specific task autonomously.
97
76
  ${envBlock}`;
98
77
 
99
78
  return (
100
- activeAgentTag + replaceHeader + "\n\n" + config.systemPrompt + extrasSuffix
79
+ activeAgentTag + replaceHeader + "\n\n" + config.systemPrompt
101
80
  );
102
81
  }
103
82
 
@@ -12,8 +12,6 @@
12
12
 
13
13
  import type { AgentConfigLookup } from "#src/config/agent-types";
14
14
  import type { EnvInfo } from "#src/session/env";
15
- import type { PromptExtras } from "#src/session/prompts";
16
- import type { PreloadedSkill } from "#src/session/skill-loader";
17
15
  import type { AgentPromptConfig, SubagentType, ThinkingLevel } from "#src/types";
18
16
 
19
17
  // ── Public interfaces ────────────────────────────────────────────────────────
@@ -21,19 +19,17 @@ import type { AgentPromptConfig, SubagentType, ThinkingLevel } from "#src/types"
21
19
  /**
22
20
  * IO collaborators injected into `assembleSessionConfig`.
23
21
  *
24
- * Bundling the four IO-touching (or promptly testable) functions into a single
22
+ * Bundling the IO-touching (or promptly testable) function into a single
25
23
  * interface keeps the assembler free of direct module imports and makes it
26
24
  * trivially testable without `vi.mock()` — callers inject real implementations
27
25
  * at the edge (`agent-runner.ts`) or stubs in tests.
28
26
  */
29
27
  export interface AssemblerIO {
30
- preloadSkills: (skills: string[], cwd: string) => PreloadedSkill[];
31
28
  buildAgentPrompt: (
32
29
  config: AgentPromptConfig,
33
30
  cwd: string,
34
31
  env: EnvInfo,
35
32
  parentPrompt?: string,
36
- extras?: PromptExtras,
37
33
  ) => string;
38
34
  }
39
35
 
@@ -67,8 +63,6 @@ export interface AssemblerContext {
67
63
  export interface AssemblerOptions {
68
64
  /** Override working directory (e.g. for worktree isolation). */
69
65
  cwd?: string;
70
- /** When true, forces extensions and skills to false. */
71
- isolated?: boolean;
72
66
  /** Explicit model override — wins over agentConfig.model and parent model. */
73
67
  model?: unknown;
74
68
  /** Explicit thinking level — wins over agentConfig.thinking. */
@@ -87,8 +81,6 @@ export interface SessionConfig {
87
81
  systemPrompt: string;
88
82
  /** Built-in tool name allowlist for this agent type. */
89
83
  toolNames: string[];
90
- /** Resolved extensions setting: true = inherit all, false = none. */
91
- extensions: boolean;
92
84
  /**
93
85
  * Resolved model instance (undefined → use parent model as passed to SDK).
94
86
  * Opaque handle — the assembler passes it through without inspection.
@@ -97,10 +89,6 @@ export interface SessionConfig {
97
89
  model: unknown;
98
90
  /** Resolved thinking level (undefined → inherit from session). */
99
91
  thinkingLevel: ThinkingLevel | undefined;
100
- /** Whether to skip skill loading in the resource loader (`noSkills` flag). */
101
- noSkills: boolean;
102
- /** Prompt extras (memory block, preloaded skill blocks) — for transparency. */
103
- extras: PromptExtras;
104
92
  /** Per-agent configured max turns (from agentConfig.maxTurns). */
105
93
  agentMaxTurns: number | undefined;
106
94
  }
@@ -150,7 +138,7 @@ function resolveDefaultModel(
150
138
  *
151
139
  * @param type The subagent type name (case-insensitive registry lookup).
152
140
  * @param ctx Narrow context from the parent session.
153
- * @param options Per-call overrides (cwd, isolated, model, thinkingLevel).
141
+ * @param options Per-call overrides (cwd, model, thinkingLevel).
154
142
  * @param env Pre-resolved environment info from `detectEnv()`.
155
143
  * @param registry Agent config lookup — provides resolveAgentConfig and getToolNamesForType.
156
144
  * @param io IO collaborators (skill loader, memory builder, prompt builder).
@@ -167,21 +155,6 @@ export function assembleSessionConfig(
167
155
 
168
156
  const effectiveCwd = options.cwd ?? ctx.cwd;
169
157
 
170
- // Resolve extensions/skills: isolated overrides to false
171
- const extensions = options.isolated ? false : agentConfig.extensions;
172
- const skills = options.isolated ? false : agentConfig.skills;
173
-
174
- // Build prompt extras (memory, preloaded skills)
175
- const extras: PromptExtras = {};
176
-
177
- // Skill preloading: when skills is string[], preload their content into the prompt
178
- if (Array.isArray(skills)) {
179
- const loaded = io.preloadSkills(skills, effectiveCwd);
180
- if (loaded.length > 0) {
181
- extras.skillBlocks = loaded;
182
- }
183
- }
184
-
185
158
  const toolNames = registry.getToolNamesForType(type);
186
159
 
187
160
  // Build system prompt from the resolved agent config
@@ -190,13 +163,8 @@ export function assembleSessionConfig(
190
163
  effectiveCwd,
191
164
  env,
192
165
  ctx.parentSystemPrompt,
193
- extras,
194
166
  );
195
167
 
196
- // noSkills: when we've already preloaded skills into the prompt, or skills = false,
197
- // tell the resource loader not to load them again.
198
- const noSkills = skills === false || Array.isArray(skills);
199
-
200
168
  // Model resolution: explicit option > config model string > parent model
201
169
  const model =
202
170
  options.model ??
@@ -212,11 +180,8 @@ export function assembleSessionConfig(
212
180
  effectiveCwd,
213
181
  systemPrompt,
214
182
  toolNames,
215
- extensions,
216
183
  model,
217
184
  thinkingLevel,
218
- noSkills,
219
- extras,
220
185
  agentMaxTurns,
221
186
  };
222
187
  }
@@ -220,11 +220,6 @@ Guidelines:
220
220
  description: "Optional agent ID to resume from. Continues from previous context.",
221
221
  }),
222
222
  ),
223
- isolated: Type.Optional(
224
- Type.Boolean({
225
- description: "If true, agent gets no extension/MCP tools — only built-in tools.",
226
- }),
227
- ),
228
223
  inherit_context: Type.Optional(
229
224
  Type.Boolean({
230
225
  description:
@@ -48,7 +48,6 @@ export function spawnBackground(
48
48
  description: execution.description,
49
49
  model: execution.model,
50
50
  maxTurns: execution.effectiveMaxTurns,
51
- isolated: execution.isolated,
52
51
  inheritContext: execution.inheritContext,
53
52
  thinkingLevel: execution.thinking,
54
53
  isBackground: true,
@@ -102,7 +102,6 @@ export async function runForeground(
102
102
  description: execution.description,
103
103
  model: execution.model,
104
104
  maxTurns: execution.effectiveMaxTurns,
105
- isolated: execution.isolated,
106
105
  inheritContext: execution.inheritContext,
107
106
  thinkingLevel: execution.thinking,
108
107
  invocation: execution.agentInvocation,
@@ -43,7 +43,6 @@ export interface SpawnExecution {
43
43
  thinking: ThinkingLevel | undefined;
44
44
  inheritContext: boolean;
45
45
  runInBackground: boolean;
46
- isolated: boolean;
47
46
  agentInvocation: AgentInvocation;
48
47
  }
49
48
 
@@ -102,7 +101,6 @@ export function resolveSpawnConfig(
102
101
  const thinking = resolvedConfig.thinking;
103
102
  const inheritContext = resolvedConfig.inheritContext;
104
103
  const runInBackground = resolvedConfig.runInBackground;
105
- const isolated = resolvedConfig.isolated;
106
104
 
107
105
  // Compute display model name (only shown when different from parent)
108
106
  const parentModelId = modelInfo.parentModel?.id;
@@ -120,7 +118,6 @@ export function resolveSpawnConfig(
120
118
  modelName,
121
119
  thinking,
122
120
  maxTurns: normalizeMaxTurns(resolvedConfig.maxTurns),
123
- isolated,
124
121
  inheritContext,
125
122
  runInBackground,
126
123
  };
@@ -147,7 +144,6 @@ export function resolveSpawnConfig(
147
144
  thinking,
148
145
  inheritContext,
149
146
  runInBackground,
150
- isolated,
151
147
  agentInvocation,
152
148
  },
153
149
  presentation: { modelName, agentTags, detailBase },
package/src/types.ts CHANGED
@@ -39,10 +39,6 @@ export interface AgentPromptConfig {
39
39
  /** Unified agent configuration — used for both default and user-defined agents. */
40
40
  export interface AgentConfig extends AgentIdentity, AgentPromptConfig {
41
41
  builtinToolNames?: string[];
42
- /** true = inherit all extensions, false = none */
43
- extensions: boolean;
44
- /** true = inherit all, string[] = only listed, false = none */
45
- skills: true | string[] | false;
46
42
  model?: string;
47
43
  thinking?: ThinkingLevel;
48
44
  maxTurns?: number;
@@ -50,8 +46,6 @@ export interface AgentConfig extends AgentIdentity, AgentPromptConfig {
50
46
  inheritContext?: boolean;
51
47
  /** Default for spawn: run in background. undefined = caller decides. */
52
48
  runInBackground?: boolean;
53
- /** Default for spawn: no extension tools. undefined = caller decides. */
54
- isolated?: boolean;
55
49
  /** true = this is an embedded default agent (informational) */
56
50
  isDefault?: boolean;
57
51
  /** false = agent is hidden from the registry */
@@ -65,7 +59,6 @@ export interface AgentInvocation {
65
59
  modelName?: string;
66
60
  thinking?: ThinkingLevel;
67
61
  maxTurns?: number;
68
- isolated?: boolean;
69
62
  inheritContext?: boolean;
70
63
  runInBackground?: boolean;
71
64
  }
@@ -47,13 +47,8 @@ export function buildEjectContent(cfg: AgentConfig): string {
47
47
  if (cfg.thinking) fmFields.push(`thinking: ${cfg.thinking}`);
48
48
  if (cfg.maxTurns) fmFields.push(`max_turns: ${cfg.maxTurns}`);
49
49
  fmFields.push(`prompt_mode: ${cfg.promptMode}`);
50
- if (!cfg.extensions) fmFields.push("extensions: false");
51
- if (cfg.skills === false) fmFields.push("skills: false");
52
- else if (Array.isArray(cfg.skills))
53
- fmFields.push(`skills: ${cfg.skills.join(", ")}`);
54
50
  if (cfg.inheritContext) fmFields.push("inherit_context: true");
55
51
  if (cfg.runInBackground) fmFields.push("run_in_background: true");
56
- if (cfg.isolated) fmFields.push("isolated: true");
57
52
  return `---\n${fmFields.join("\n")}\n---\n\n${cfg.systemPrompt}\n`;
58
53
  }
59
54
 
@@ -104,11 +104,8 @@ model: <optional model as "provider/modelId", e.g. "anthropic/claude-haiku-4-5-2
104
104
  thinking: <optional thinking level: off, minimal, low, medium, high, xhigh. Omit to inherit>
105
105
  max_turns: <optional max agentic turns. 0 or omit for unlimited (default)>
106
106
  prompt_mode: <"replace" (body IS the full system prompt) or "append" (body is appended to default prompt). Default: replace>
107
- extensions: <true (inherit all MCP/extension tools) or false (none). Default: true>
108
- skills: <true (inherit all), false (none), or comma-separated skill names to preload into prompt. Default: true>
109
107
  inherit_context: <true to fork parent conversation into agent so it sees chat history. Default: false>
110
108
  run_in_background: <true to run in background by default. Default: false>
111
- isolated: <true for no extension/MCP tools, only built-in tools. Default: false>
112
109
  ---
113
110
 
114
111
  <system prompt body — instructions for the agent>
@@ -120,7 +117,6 @@ Guidelines for choosing settings:
120
117
  - Use prompt_mode: append if the agent should keep the default system prompt and add specialization on top
121
118
  - Use prompt_mode: replace for fully custom agents with their own personality/instructions
122
119
  - Set inherit_context: true if the agent needs to know what was discussed in the parent conversation
123
- - Set isolated: true if the agent should NOT have access to MCP servers or other extensions
124
120
  - Only include frontmatter fields that differ from defaults — omit fields where the default is fine
125
121
 
126
122
  Write the file using the write tool. Only write the file, nothing else.`;
package/src/ui/display.ts CHANGED
@@ -30,7 +30,7 @@ export interface AgentDetails {
30
30
  spinnerFrame?: number;
31
31
  /** Short model name if different from parent (e.g. "haiku", "sonnet"). */
32
32
  modelName?: string;
33
- /** Notable config tags (e.g. ["thinking: high", "isolated"]). */
33
+ /** Notable config tags (e.g. ["thinking: high", "inherit context"]). */
34
34
  tags?: string[];
35
35
  /** Current turn count. */
36
36
  turnCount?: number;
@@ -135,7 +135,6 @@ export function buildInvocationTags(
135
135
  const tags: string[] = [];
136
136
  if (!invocation) return { tags };
137
137
  if (invocation.thinking) tags.push(`thinking: ${invocation.thinking}`);
138
- if (invocation.isolated) tags.push("isolated");
139
138
  if (invocation.inheritContext) tags.push("inherit context");
140
139
  if (invocation.runInBackground) tags.push("background");
141
140
  if (invocation.maxTurns != null) tags.push(`max turns: ${invocation.maxTurns}`);
@@ -1,45 +0,0 @@
1
- /**
2
- * safe-fs.ts — Filesystem safety utilities for reading untrusted paths.
3
- *
4
- * Used by skill-loader.ts to reject symlinks and path-traversal names
5
- * before reading skill files from disk.
6
- */
7
-
8
- import { existsSync, lstatSync, readFileSync } from "node:fs";
9
- import { debugLog } from "#src/debug";
10
-
11
- /**
12
- * Returns true if a name contains characters not allowed in agent/skill names.
13
- * Uses a whitelist: only alphanumeric, hyphens, underscores, and dots (no leading dot).
14
- */
15
- export function isUnsafeName(name: string): boolean {
16
- if (!name || name.length > 128) return true;
17
- return !/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(name);
18
- }
19
-
20
- /**
21
- * Returns true if the given path is a symlink (defense against symlink attacks).
22
- */
23
- export function isSymlink(filePath: string): boolean {
24
- try {
25
- return lstatSync(filePath).isSymbolicLink();
26
- } catch (err) {
27
- debugLog("lstatSync", err);
28
- return false;
29
- }
30
- }
31
-
32
- /**
33
- * Safely read a file, rejecting symlinks.
34
- * Returns undefined if the file doesn't exist, is a symlink, or can't be read.
35
- */
36
- export function safeReadFile(filePath: string): string | undefined {
37
- if (!existsSync(filePath)) return undefined;
38
- if (isSymlink(filePath)) return undefined;
39
- try {
40
- return readFileSync(filePath, "utf-8");
41
- } catch (err) {
42
- debugLog("readFileSync", err);
43
- return undefined;
44
- }
45
- }
@@ -1,104 +0,0 @@
1
- /**
2
- * skill-loader.ts — Preload named skills.
3
- *
4
- * Roots, in precedence order:
5
- * - <cwd>/.pi/skills (project, Pi's standard)
6
- * - <cwd>/.agents/skills (project, cross-tool Agent Skills spec — https://agentskills.io)
7
- * - getAgentDir()/skills (user, default ~/.pi/agent/skills — Pi's standard)
8
- * - ~/.agents/skills (user, cross-tool Agent Skills spec)
9
- * - ~/.pi/skills (legacy global, pre-Pi)
10
- *
11
- * Layout per root:
12
- * - <root>/<name>.md (flat file at the top level)
13
- * - <root>/.../<name>/SKILL.md (directory skill, may be nested — Pi's standard)
14
- *
15
- * Recursion skips dotfile entries and node_modules. A directory that itself contains
16
- * SKILL.md is a skill — we don't descend into it (Pi: skills don't nest).
17
- *
18
- * Symlinks are rejected for security (deviation from Pi, which follows them).
19
- */
20
-
21
- import type { Dirent } from "node:fs";
22
- import { existsSync, readdirSync } from "node:fs";
23
- import { homedir } from "node:os";
24
- import { join } from "node:path";
25
- import { getAgentDir } from "@earendil-works/pi-coding-agent";
26
- import { debugLog } from "#src/debug";
27
- import { isSymlink, isUnsafeName, safeReadFile } from "#src/session/safe-fs";
28
-
29
- export interface PreloadedSkill {
30
- name: string;
31
- content: string;
32
- }
33
-
34
- export function preloadSkills(skillNames: string[], cwd: string): PreloadedSkill[] {
35
- return skillNames.map((name) => ({ name, content: loadSkillContent(name, cwd) }));
36
- }
37
-
38
- function loadSkillContent(name: string, cwd: string): string {
39
- if (isUnsafeName(name)) {
40
- return `(Skill "${name}" skipped: name contains path traversal characters)`;
41
- }
42
- const roots = [
43
- join(cwd, ".pi", "skills"), // project — Pi standard
44
- join(cwd, ".agents", "skills"), // project — Agent Skills spec
45
- join(getAgentDir(), "skills"), // user — Pi standard
46
- join(homedir(), ".agents", "skills"), // user — Agent Skills spec
47
- join(homedir(), ".pi", "skills"), // legacy global, pre-Pi
48
- ];
49
- for (const root of roots) {
50
- const content = findInRoot(root, name);
51
- if (content !== undefined) return content;
52
- }
53
- return `(Skill "${name}" not found in .pi/skills/, .agents/skills/, or global skill locations)`;
54
- }
55
-
56
- function findInRoot(root: string, name: string): string | undefined {
57
- if (isSymlink(root)) return undefined; // reject symlinked roots entirely
58
- const flat = safeReadFile(join(root, `${name}.md`))?.trim();
59
- if (flat !== undefined) return flat;
60
- return findSkillDirectory(root, name);
61
- }
62
-
63
- /** BFS under `root` for a directory named `name` containing `SKILL.md`. Pi-conforming filters. */
64
- function findSkillDirectory(root: string, name: string): string | undefined {
65
- if (!existsSync(root)) return undefined;
66
- const queue: string[] = [root];
67
-
68
- while (queue.length > 0) {
69
- const current = queue.shift();
70
- if (current === undefined) continue;
71
-
72
- let entries: Dirent[];
73
- try {
74
- entries = readdirSync(current, { withFileTypes: true });
75
- } catch (err) {
76
- debugLog("readdirSync skill root", err);
77
- continue;
78
- }
79
-
80
- // Deterministic byte-order traversal — locale-independent.
81
- entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
82
-
83
- for (const entry of entries) {
84
- if (!entry.isDirectory()) continue;
85
- if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
86
-
87
- // Symlinked dirs already filtered by entry.isDirectory() — Dirent uses lstat semantics.
88
- const path = join(current, entry.name);
89
- const skillMd = join(path, "SKILL.md");
90
- const isSkillDir = existsSync(skillMd);
91
-
92
- if (isSkillDir) {
93
- if (entry.name === name) {
94
- const content = safeReadFile(skillMd)?.trim();
95
- if (content !== undefined) return content;
96
- }
97
- continue; // Pi rule: skills don't nest — don't descend into a skill dir
98
- }
99
-
100
- queue.push(path);
101
- }
102
- }
103
- return undefined;
104
- }