@gotgenes/pi-subagents 8.0.0 → 9.0.1

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,34 @@ 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
+ ## [9.0.1](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v9.0.0...pi-subagents-v9.0.1) (2026-05-27)
9
+
10
+
11
+ ### Documentation
12
+
13
+ * **retro:** add retro notes for issue [#238](https://github.com/gotgenes/pi-packages/issues/238) ([84f2e49](https://github.com/gotgenes/pi-packages/commit/84f2e49c1c7a28e83ca556f377d7e3f970b8a5d2))
14
+
15
+ ## [9.0.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v8.0.0...pi-subagents-v9.0.0) (2026-05-27)
16
+
17
+
18
+ ### ⚠ BREAKING CHANGES
19
+
20
+ * extensions field in agent frontmatter no longer supports a comma-separated allowlist. Values like `extensions: pi-github-tools, colgrep` are coerced to `true` (inherit all). Users who relied on per-extension filtering should migrate to `permission:` frontmatter in pi-permission-system.
21
+
22
+ ### Features
23
+
24
+ * narrow extensions type from union to boolean ([90350ab](https://github.com/gotgenes/pi-packages/commit/90350abef259f5494990c5796a005f2edba2163d))
25
+
26
+
27
+ ### Documentation
28
+
29
+ * mark Phase 14 Step 2 complete in architecture ([fc8bc57](https://github.com/gotgenes/pi-packages/commit/fc8bc57788a3ade9ec2f74200973e5a811c48bb3))
30
+ * plan remove extensions filtering ([#238](https://github.com/gotgenes/pi-packages/issues/238)) ([1f4046a](https://github.com/gotgenes/pi-packages/commit/1f4046a84a308e80472425b5ca9e83263ecc310a))
31
+ * **retro:** add planning stage notes for issue [#238](https://github.com/gotgenes/pi-packages/issues/238) ([b2ce842](https://github.com/gotgenes/pi-packages/commit/b2ce842dcc999fd48f6aeb67c6d464750d6d87aa))
32
+ * **retro:** add retro notes for issue [#237](https://github.com/gotgenes/pi-packages/issues/237) ([6be215e](https://github.com/gotgenes/pi-packages/commit/6be215e6f4d9307f94b5bb61c024292a8eb77469))
33
+ * **retro:** add TDD stage notes for issue [#238](https://github.com/gotgenes/pi-packages/issues/238) ([87256db](https://github.com/gotgenes/pi-packages/commit/87256dbd97ba4a69267f2091896d959bef9ff9df))
34
+ * simplify extensions field to boolean in README ([b4028d7](https://github.com/gotgenes/pi-packages/commit/b4028d7c203fce792238acb147709ad3c3d4d28e))
35
+
8
36
  ## [8.0.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v7.8.1...pi-subagents-v8.0.0) (2026-05-27)
9
37
 
10
38
 
package/README.md CHANGED
@@ -175,7 +175,7 @@ All fields are optional — sensible defaults for everything.
175
175
  | `description` | filename | Agent description shown in tool listings |
176
176
  | `display_name` | — | Display name for UI (e.g. widget, agent list) |
177
177
  | `tools` | all 7 | Comma-separated built-in tools: read, bash, edit, write, grep, find, ls. `none` for no tools |
178
- | `extensions` | `true` | Inherit MCP/extension tools. `false` to disable |
178
+ | `extensions` | `true` | `true` to inherit all MCP/extension tools, `false` to disable |
179
179
  | `skills` | `true` | Inherit skills from parent. Can be a comma-separated list of skill names to preload (see [Skill Preloading](#skill-preloading) for discovery locations) |
180
180
  | `memory` | — | Persistent agent memory scope: `project`, `local`, or `user`. Auto-detects read-only agents |
181
181
  | `isolation` | — | Set to `worktree` to run in an isolated git worktree |
@@ -479,8 +479,7 @@ Each has a corresponding upstream PR:
479
479
  1. **Peer-dep migration to `@earendil-works/pi-*`** — `peerDependencies` and all imports point at `@earendil-works/pi-ai`, `@earendil-works/pi-coding-agent`, and `@earendil-works/pi-tui` (the active scope on npm) instead of the deprecated `@mariozechner/pi-*` scope.
480
480
  Also fixes a latent bug where `ThinkingLevel` was imported from `pi-agent-core` (an undeclared transitive dep that breaks under pnpm).
481
481
  Upstream PR: [tintinweb/pi-subagents#71](https://github.com/tintinweb/pi-subagents/pull/71).
482
- 2. **Post-`bindExtensions` active-tool re-filter** (`src/agent-runner.ts`) — `runAgent` re-runs its active-tool filter after `session.bindExtensions(...)` so extension-registered tools join the child's active tool set.
483
- Without this, the `extensions: string[]` allowlist branch was functionally dead for extension tools.
482
+ 2. **Post-`bindExtensions` active-tool re-filter** (`src/agent-runner.ts`) — `runAgent` re-runs its active-tool filter after `session.bindExtensions(...)` so the `EXCLUDED_TOOL_NAMES` recursion guard applies to extension-registered tools (which join the active set during `bindExtensions`).
484
483
  Upstream PR: [tintinweb/pi-subagents#72](https://github.com/tintinweb/pi-subagents/pull/72).
485
484
  3. **`<active_agent>` system-prompt tag** (`src/prompts.ts`) — `buildAgentPrompt` prepends `<active_agent name="${config.name}"/>` to every assembled child system prompt (both `replace` and `append` modes).
486
485
  Downstream extensions like [`@gotgenes/pi-permission-system`](https://github.com/gotgenes/pi-permission-system) parse this tag to resolve per-agent `permission:` frontmatter inside the child session.
@@ -697,7 +697,7 @@ Remove the `disallowedTools` field from `AgentConfig` and all code that processe
697
697
  - Smell: A (responsibility overlap with pi-permission-system)
698
698
  - Outcome: users migrate to `permission:` frontmatter for tool restrictions; single source of truth for access control
699
699
 
700
- ### Step 2: Remove `extensions` filtering — [#238]
700
+ ### Step 2: Remove `extensions` filtering — [#238] ✅ Complete
701
701
 
702
702
  Remove the `extensions: string[]` allowlist and simplify the field to a boolean.
703
703
  The `extensions: false` case (used by `isolated`) is retained in this step and removed in Phase 16.
@@ -0,0 +1,191 @@
1
+ ---
2
+ issue: 238
3
+ issue_title: "Remove extensions filtering from pi-subagents (Phase 14, Step 2)"
4
+ ---
5
+
6
+ # Remove `extensions` filtering from pi-subagents
7
+
8
+ ## Problem Statement
9
+
10
+ The `extensions: string[]` allowlist in agent configuration is tool filtering disguised as extension lifecycle control.
11
+ Extensions always initialize via `bindExtensions()` — the allowlist only hides their tools from the active set afterward, which is pi-permission-system's responsibility.
12
+ Simplifying `extensions` from `true | string[] | false` to `boolean` removes this overlap and makes pi-permission-system the sole authority for tool access control.
13
+
14
+ ## Goals
15
+
16
+ - Simplify `extensions` from `true | string[] | false` to `boolean` in `AgentConfig`, `ToolFilterConfig`, and all related code.
17
+ - Remove the `Array.isArray(extensions)` branch from `filterActiveTools` in `agent-runner.ts`.
18
+ - Remove extensions array handling from the agent config editor and creation wizard.
19
+ - Update custom agent frontmatter parsing to coerce array values to `true` (with a deprecation warning).
20
+ - Update tests to remove `extensions: string[]` assertions and fixture data.
21
+ - This is a **breaking change** — users with `extensions: <csv-list>` in agent frontmatter lose per-extension filtering.
22
+
23
+ ## Non-Goals
24
+
25
+ - Collapsing `filterActiveTools` to a recursion guard (Step 3, #239).
26
+ - Removing `extensions: false` — it is retained here (used by `isolated`) and dissolved in Phase 16.
27
+ - Changing any pi-permission-system code — this issue is purely pi-subagents.
28
+ - Removing Patch 2 (post-bind re-filter) — it still serves the `EXCLUDED_TOOL_NAMES` recursion guard when `extensions === true`.
29
+ Issue #239 collapses the two-pass dance.
30
+
31
+ ## Background
32
+
33
+ ### Phase 14 context
34
+
35
+ This is Phase 14, Step 2 of the architecture roadmap in `docs/architecture/architecture.md`.
36
+ Steps 1 (#237, completed) and 2 (#238) are independent; Step 3 (#239) depends on both.
37
+
38
+ ### Files involved
39
+
40
+ | File | Role |
41
+ | --------------------------------- | --------------------------------------------------------------------------- |
42
+ | `src/types.ts` | `AgentConfig.extensions` field type |
43
+ | `src/config/custom-agents.ts` | Parses `extensions` from YAML frontmatter via `inheritField()` |
44
+ | `src/session/session-config.ts` | `ToolFilterConfig.extensions` type + passthrough in `assembleSessionConfig` |
45
+ | `src/lifecycle/agent-runner.ts` | `filterActiveTools` array branch + Patch 2 comment |
46
+ | `src/ui/agent-config-editor.ts` | Serializes `extensions` array to frontmatter |
47
+ | `src/ui/agent-creation-wizard.ts` | Template text describing `extensions` field options |
48
+ | `README.md` | Frontmatter table row, Patch 2 notes |
49
+
50
+ ### `filterActiveTools` after this change
51
+
52
+ ```typescript
53
+ function filterActiveTools(
54
+ activeTools: string[],
55
+ config: ToolFilterConfig,
56
+ ): string[] {
57
+ const { toolNames, extensions } = config;
58
+ if (extensions === false) {
59
+ return activeTools;
60
+ }
61
+ const builtinToolNameSet = new Set(toolNames);
62
+ return activeTools.filter((t) => {
63
+ if (EXCLUDED_TOOL_NAMES.includes(t)) return false;
64
+ if (builtinToolNameSet.has(t)) return true;
65
+ return true;
66
+ });
67
+ }
68
+ ```
69
+
70
+ The `Array.isArray(extensions)` branch is gone.
71
+ The `builtinToolNameSet.has(t)` check becomes redundant (always returns true for builtins, falls through to `return true` for non-builtins) — but simplifying further is #239's scope.
72
+
73
+ ### Custom agent frontmatter migration
74
+
75
+ The `inheritField()` function currently parses CSV values into `string[]`.
76
+ After this change, it must still accept array values gracefully: coerce them to `true` and emit a warning so users know to update their frontmatter.
77
+
78
+ ## Design Overview
79
+
80
+ This is primarily a removal — no new types, no new modules.
81
+ The one piece of new behavior is the deprecation warning when `inheritField` encounters an array value for `extensions`.
82
+
83
+ ### `inheritField` change
84
+
85
+ Currently `inheritField` is shared between `extensions` and `skills` parsing.
86
+ Since `skills` still supports `string[]`, the function itself cannot change.
87
+ Instead, the call site for `extensions` will post-process: if `inheritField` returns `string[]`, coerce to `true` and log a warning.
88
+
89
+ ### Type change scope
90
+
91
+ `AgentConfig.extensions` changes from `true | string[] | false` to `boolean`.
92
+ `ToolFilterConfig.extensions` changes from `boolean | string[]` to `boolean`.
93
+ Both are narrowing changes — existing `true` and `false` values remain valid.
94
+
95
+ ## Module-Level Changes
96
+
97
+ 1. **`src/types.ts`** — Change `extensions` type from `true | string[] | false` to `boolean`.
98
+ Update JSDoc comment to remove the `string[] = only listed` description.
99
+ 2. **`src/config/custom-agents.ts`** — After calling `inheritField(fm.extensions ?? fm.inherit_extensions)`, coerce `string[]` results to `true` and emit a `debugLog` warning.
100
+ The warning informs users that `extensions: <csv-list>` is no longer supported and treated as `extensions: true`.
101
+ 3. **`src/session/session-config.ts`** — Change `ToolFilterConfig.extensions` type from `boolean | string[]` to `boolean`.
102
+ Update the JSDoc comment.
103
+ The `assembleSessionConfig` passthrough `const extensions = options.isolated ? false : agentConfig.extensions;` works without changes since the type narrows.
104
+ 4. **`src/lifecycle/agent-runner.ts`** — Remove the `Array.isArray(extensions)` branch (lines 47–49) from `filterActiveTools`.
105
+ Update the Patch 2 comment (line 366) that references `extensions: string[]`.
106
+ Update the comment at line 341 ("apply extension allowlist if specified").
107
+ 5. **`src/ui/agent-config-editor.ts`** — Remove the `else if (Array.isArray(cfg.extensions))` branch (lines 51–52) from `buildEjectContent`.
108
+ 6. **`src/ui/agent-creation-wizard.ts`** — Simplify the `extensions:` line in the template text from `<true (inherit all MCP/extension tools), false (none), or comma-separated names. Default: true>` to `<true (inherit all MCP/extension tools) or false (none). Default: true>`.
109
+ 7. **`README.md`** — Simplify the `extensions` row in the frontmatter table to document boolean-only.
110
+ Update Patch 2 notes to remove `extensions: string[]` reference.
111
+ 8. **`src/config/default-agents.ts`** — No changes needed; all defaults already use `extensions: true`.
112
+
113
+ ### Test files
114
+
115
+ 9. **`test/config/custom-agents.test.ts`** — Update the "handles extension allowlist" test: change expectation from `toEqual(["web-search", "mcp-server"])` to `toBe(true)` since CSV values now coerce to `true`.
116
+ Add a test verifying the deprecation warning is emitted.
117
+ 10. **`test/session/session-config.test.ts`** — Update the "isolated:true forces extensions to false even for string[] extension list" test: since `AgentConfig.extensions` no longer supports `string[]`, simplify this test to use `extensions: true` and verify `isolated` still forces it to `false`.
118
+ The test name changes to reflect the boolean type.
119
+ 11. **`test/lifecycle/agent-runner-extension-tools.test.ts`** — Remove the "post-bind re-filter respects extensions: string[] allowlist" test (line 139).
120
+ Change the `extensions` type annotation in the mutable config mock (line 26) from `boolean | string[]` to `boolean`.
121
+ Update the file-level JSDoc comment to remove `extensions: string[]` references.
122
+ 12. **`test/ui/agent-config-editor.test.ts`** — Remove the "emits 'extensions: \<list\>' when extensions is an array" test.
123
+ 13. **Test fixtures** — All test helper defaults already use `extensions: false` or `extensions: true`; no fixture changes needed beyond the tests listed above.
124
+
125
+ ### Architecture docs
126
+
127
+ 14. **`docs/architecture/architecture.md`** — Mark Step 2 as complete.
128
+
129
+ ## Test Impact Analysis
130
+
131
+ 1. **Tests removed** — 2 tests that directly assert `extensions: string[]` behavior:
132
+ - `agent-runner-extension-tools.test.ts`: "post-bind re-filter respects extensions: string[] allowlist"
133
+ - `agent-config-editor.test.ts`: "emits 'extensions: \<list\>' when extensions is an array"
134
+
135
+ 2. **Tests updated** — 3 tests that reference `extensions: string[]` in fixtures or assertions:
136
+ - `custom-agents.test.ts`: "handles extension allowlist" → expectation changes to `true`
137
+ - `session-config.test.ts`: "isolated:true forces extensions to false even for string[] extension list" → uses `true` instead of array
138
+ - `agent-runner-extension-tools.test.ts`: mock config type annotation narrows
139
+
140
+ 3. **Tests added** — 1 test:
141
+ - `custom-agents.test.ts`: verifies deprecation warning when array value is provided
142
+
143
+ 4. **Tests unchanged** — All other `extensions: true` and `extensions: false` tests remain valid since the boolean cases are preserved.
144
+
145
+ ## TDD Order
146
+
147
+ 1. **Narrow `extensions` type in `AgentConfig` and `ToolFilterConfig`** — Change `extensions` from `true | string[] | false` to `boolean` in `types.ts`.
148
+ Change `ToolFilterConfig.extensions` from `boolean | string[]` to `boolean` in `session-config.ts`.
149
+ Update JSDoc comments.
150
+ Run `pnpm run check` to surface all downstream type errors.
151
+ Commit: `feat!: narrow extensions type from union to boolean`
152
+
153
+ 2. **Remove array branch from `filterActiveTools`** — Remove the `Array.isArray(extensions)` branch.
154
+ Update the Patch 2 comment and the pre-bind filter comment.
155
+ Remove the "post-bind re-filter respects extensions: string[] allowlist" test.
156
+ Narrow the mock config type annotation in `agent-runner-extension-tools.test.ts`.
157
+ Update the file-level JSDoc.
158
+ Run tests: `pnpm vitest run`.
159
+ Commit: `refactor: remove extensions array branch from filterActiveTools`
160
+
161
+ 3. **Coerce array values in custom agent frontmatter** — Update the `extensions` assignment in `custom-agents.ts` to coerce `string[]` results from `inheritField` to `true` with a deprecation warning.
162
+ Update the "handles extension allowlist" test expectation from array to `true`.
163
+ Add a test for the deprecation warning.
164
+ Update the "isolated:true forces extensions to false even for string[] extension list" test in `session-config.test.ts` to use `extensions: true`.
165
+ Run tests: `pnpm vitest run`.
166
+ Commit: `refactor: coerce extensions array values to true with deprecation warning`
167
+
168
+ 4. **Remove extensions array from UI** — Remove the `Array.isArray(cfg.extensions)` branch from `buildEjectContent`.
169
+ Simplify the `extensions:` line in the creation wizard template.
170
+ Remove the "emits 'extensions: \<list\>'" test from `agent-config-editor.test.ts`.
171
+ Run tests: `pnpm vitest run`.
172
+ Commit: `refactor: remove extensions array from config editor and creation wizard`
173
+
174
+ 5. **Update README** — Simplify the `extensions` frontmatter table row to document boolean-only.
175
+ Update Patch 2 notes to remove `extensions: string[]` reference.
176
+ Commit: `docs: simplify extensions field to boolean in README`
177
+
178
+ 6. **Mark Phase 14 Step 2 complete** — Update `docs/architecture/architecture.md`.
179
+ Commit: `docs: mark Phase 14 Step 2 complete in architecture`
180
+
181
+ ## Risks and Mitigations
182
+
183
+ | Risk | Mitigation |
184
+ | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
185
+ | Users with `extensions: <csv-list>` silently lose per-extension filtering | `feat!:` commit triggers major version bump; frontmatter parser emits deprecation warning and coerces to `true`; README documents the change |
186
+ | `filterActiveTools` behavior regresses for boolean paths | Steps 1–4 each run type checks or tests; `extensions: true` and `extensions: false` tests are unchanged |
187
+ | Type annotation `boolean | string[]` in test mock causes tsc errors after step 1 | Step 2 narrows the annotation in the same commit that removes the array branch |
188
+
189
+ ## Open Questions
190
+
191
+ None — the issue's proposed change and the architecture doc's step description are unambiguous.
@@ -39,3 +39,40 @@ All checks (type check, lint, fallow dead-code gate) pass clean.
39
39
  - The `filterActiveTools` `extensions === false` branch simplified from "apply denylist to built-in tools" to `return activeTools`, exactly as the plan specified.
40
40
  Both guard conditions at the call sites simplified to `cfg.toolFilter.extensions !== false`.
41
41
  - No deviations from the plan's module-level changes list; all 7 source files and 4 test files were touched as specified.
42
+
43
+ ## Stage: Final Retrospective (2026-05-27T01:12:34Z)
44
+
45
+ ### Session summary
46
+
47
+ Three-stage lifecycle (planning → TDD → ship) completed in one session for Phase 14, Step 1.
48
+ All 6 TDD steps landed in 7 commits (6 planned + 1 fixup for dead `csvListOptional`).
49
+ Test count: 983 → 978 (−5).
50
+ Released as `pi-subagents-v8.0.0` (major bump from the `feat!:` breaking change).
51
+
52
+ ### Observations
53
+
54
+ #### What went well
55
+
56
+ - The plan's type-dependency-chain ordering (AgentConfig → ToolFilterConfig → `filterActiveTools` → UI → docs) meant each step produced the exact downstream type errors expected for the next step, with zero surprises.
57
+ This ordering pattern is worth preserving for future removal issues.
58
+ - Incremental verification ran after every step: `pnpm run check` after type changes, `pnpm vitest run <file>` after logic changes, full suite + lint + fallow at the end.
59
+ No regressions discovered at the final sweep.
60
+ - The reformulation of one deleted test into "extensions: false skips the filter entirely" added coverage for the simplified code path rather than just deleting it.
61
+ Good judgment call during implementation.
62
+
63
+ #### What caused friction (agent side)
64
+
65
+ - `missing-context` — The plan said "remove `disallowedTools: csvListOptional(fm.disallowed_tools)`" from `custom-agents.ts` but did not check whether `csvListOptional` had other callers.
66
+ It was the sole call site, so the function became dead code.
67
+ Biome caught it at the final lint sweep, requiring a separate `refactor:` commit (`f1ee7c1`).
68
+ Impact: one unplanned commit; no rework but added friction.
69
+ Self-identified at final lint.
70
+ Root cause: the `plan-issue.md` template had a rule for grepping removed *exports* but not for checking private-function orphans.
71
+
72
+ #### What caused friction (user side)
73
+
74
+ - No friction observed — the user's involvement was limited to confirming the target package (pi-subagents vs pi-permission-system) during planning, which was an appropriate clarification given the incorrect `pkg:` label on the issue.
75
+
76
+ ### Changes made
77
+
78
+ 1. `.pi/prompts/plan-issue.md` — Added rule: when a step removes a call to a private function, grep the file for other callers and list the function for removal if the removed call was the sole call site.
@@ -0,0 +1,72 @@
1
+ ---
2
+ issue: 238
3
+ issue_title: "Remove extensions filtering from pi-subagents (Phase 14, Step 2)"
4
+ ---
5
+
6
+ # Retro: #238 — Remove extensions filtering from pi-subagents
7
+
8
+ ## Stage: Planning (2026-05-27T01:31:01Z)
9
+
10
+ ### Session summary
11
+
12
+ Produced a 6-step TDD plan for narrowing `extensions` from `true | string[] | false` to `boolean` across `AgentConfig`, `ToolFilterConfig`, `filterActiveTools`, custom agent frontmatter parsing, UI serialization, and tests.
13
+ The plan mirrors the structure of the completed #237 plan (Step 1 of Phase 14) and explicitly defers `filterActiveTools` collapse to #239.
14
+
15
+ ### Observations
16
+
17
+ - The `pkg:pi-permission-system` label on this issue is intentional context (pi-permission-system becomes the sole tool-policy authority) but all code changes are in pi-subagents.
18
+ - The `inheritField()` helper is shared between `extensions` and `skills` parsing — it cannot be simplified since `skills` still supports `string[]`.
19
+ The plan coerces at the call site instead.
20
+ - After removing the `Array.isArray(extensions)` branch, the `builtinToolNameSet.has(t)` check in `filterActiveTools` becomes logically redundant (both branches return `true`), but simplifying further is #239's scope.
21
+ - Only 2 tests are removed and 3 updated; the boolean paths (`true`/`false`) are well-covered and unchanged.
22
+
23
+ ## Stage: Implementation — TDD (2026-05-27T01:39:43Z)
24
+
25
+ ### Session summary
26
+
27
+ All 6 TDD steps completed across 6 commits.
28
+ Test count went from 978 to 977 (net −1: 2 removed, 1 added for the deprecation warning).
29
+ Full suite: 62 files, 977 tests, all passing; type check and lint clean.
30
+
31
+ ### Observations
32
+
33
+ - **Plan deviation — step consolidation**: The type narrowing in step 1 caused immediate `tsc` errors in test files that the plan assigned to steps 2–4 (the `string[]`-specific tests in `agent-runner-extension-tools.test.ts`, `session-config.test.ts`, and `agent-config-editor.test.ts`).
34
+ All test fixes were folded into the step 1 commit to keep the build green at every commit.
35
+ Steps 2–4 became purely production-code changes.
36
+ - **Plan deviation — deprecation warning**: The plan said to use `debugLog` for the deprecation warning, but `debugLog` is debug-only (requires `PI_SUBAGENTS_DEBUG=1`) and its signature requires an error object as the second argument. `console.warn` was used instead, which is user-visible without special env vars and testable with `vi.spyOn`.
37
+ - **ESLint auto-fix in step 2**: ESLint rewrote `extensions === false` to `!extensions` and `cfg.toolFilter.extensions === false` to `!cfg.toolFilter.extensions` after the type narrowed to `boolean`.
38
+ Both are semantically identical; the changes were staged and included in the same commit.
39
+ - The `resolveBoolExtensions` helper in `custom-agents.ts` is a clean seam — if `skills` is ever also simplified to `boolean`, the same pattern applies.
40
+
41
+ ## Stage: Final Retrospective (2026-05-27T01:48:04Z)
42
+
43
+ ### Session summary
44
+
45
+ All three stages (planning, TDD, shipping) completed in a single continuous session.
46
+ Six implementation commits landed as `pi-subagents-v9.0.0` (major bump from `feat!:`).
47
+ Issue #238 closed; unblocks #239 (collapse `filterActiveTools`).
48
+
49
+ ### Observations
50
+
51
+ #### What went well
52
+
53
+ - The plan’s structure closely followed the successful #237 pattern, making execution predictable.
54
+ - The implementer caught the type-narrowing cascade on the first `pnpm run check` and adapted immediately — no user intervention needed.
55
+ - The `resolveBoolExtensions` helper extraction was a clean design choice that keeps `inheritField` reusable for `skills`.
56
+ - ESLint auto-fix of `=== false` → `!` after the type narrowed to `boolean` was handled smoothly within the same commit.
57
+
58
+ #### What caused friction (agent side)
59
+
60
+ - `instruction-violation` (self-identified) — The planner did not apply the testing skill’s existing rule: “When a TDD plan lists separate steps that share a type definition, changing that type in step N breaks steps N+1…N+k.”
61
+ The plan assigned test-fixture fixes (removing `extensions: string[]` values) to steps 2–4, but the type change in step 1 broke them all immediately.
62
+ Impact: no rework — the implementer folded fixes into step 1 — but the plan’s step boundaries were misleading.
63
+ - `missing-context` — The plan recommended `debugLog` for the deprecation warning without checking its function signature (`(context: string, err: unknown)`) or its debug-only behavior.
64
+ Impact: minor — switched to `console.warn` during implementation with no rework, but the plan was wrong.
65
+
66
+ #### What caused friction (user side)
67
+
68
+ - Nothing notable — no user corrections were needed across all three stages.
69
+
70
+ ### Changes made
71
+
72
+ 1. `.pi/skills/testing/SKILL.md` — Added type-narrowing-specific TDD planning rule: when a step narrows a union type, grep test files for fixtures using the removed variant and fold those fixes into the same step.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-subagents",
3
- "version": "8.0.0",
3
+ "version": "9.0.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/service.ts"
@@ -58,7 +58,7 @@ 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: inheritField(fm.extensions ?? fm.inherit_extensions),
61
+ extensions: resolveBoolExtensions(fm.extensions ?? fm.inherit_extensions),
62
62
  skills: inheritField(fm.skills ?? fm.inherit_skills),
63
63
  model: str(fm.model),
64
64
  thinking: str(fm.thinking) as ThinkingLevel | undefined,
@@ -109,6 +109,22 @@ function csvList(val: unknown, defaults: string[]): string[] {
109
109
  return parseCsvField(val) ?? [];
110
110
  }
111
111
 
112
+ /**
113
+ * Resolve the `extensions` field to a boolean.
114
+ * CSV/array values (legacy allowlist syntax) are coerced to `true` with a warning.
115
+ */
116
+ function resolveBoolExtensions(val: unknown): boolean {
117
+ const result = inheritField(val);
118
+ if (Array.isArray(result)) {
119
+ console.warn(
120
+ "[pi-subagents] extensions allowlist syntax is deprecated — treating as \"true\" (inherit all).\n" +
121
+ "Use \"permission:\" frontmatter in pi-permission-system for per-tool access control.",
122
+ );
123
+ return true;
124
+ }
125
+ return result;
126
+ }
127
+
112
128
  /**
113
129
  * Parse an inherit field (extensions, skills).
114
130
  * omitted/true → true (inherit all); false/"none"/empty → false; csv → listed names.
@@ -37,16 +37,13 @@ function filterActiveTools(
37
37
  config: ToolFilterConfig,
38
38
  ): string[] {
39
39
  const { toolNames, extensions } = config;
40
- if (extensions === false) {
40
+ if (!extensions) {
41
41
  return activeTools;
42
42
  }
43
43
  const builtinToolNameSet = new Set(toolNames);
44
44
  return activeTools.filter((t) => {
45
45
  if (EXCLUDED_TOOL_NAMES.includes(t)) return false;
46
46
  if (builtinToolNameSet.has(t)) return true;
47
- if (Array.isArray(extensions)) {
48
- return extensions.some((ext) => t.startsWith(ext) || t.includes(ext));
49
- }
50
47
  return true;
51
48
  });
52
49
  }
@@ -299,7 +296,7 @@ export async function runAgent(
299
296
 
300
297
  const agentDir = io.getAgentDir();
301
298
 
302
- // Load extensions/skills: true or string[] → load; false → don't.
299
+ // Load extensions/skills: true → load; false → don't.
303
300
  // Suppress AGENTS.md/CLAUDE.md and APPEND_SYSTEM.md - upstream's
304
301
  // buildSystemPrompt() re-appends both AFTER systemPromptOverride, which
305
302
  // would defeat prompt_mode: replace and isolated: true. Parent context, if
@@ -308,7 +305,7 @@ export async function runAgent(
308
305
  const loader = io.createResourceLoader({
309
306
  cwd: cfg.effectiveCwd,
310
307
  agentDir,
311
- noExtensions: cfg.toolFilter.extensions === false,
308
+ noExtensions: !cfg.toolFilter.extensions,
312
309
  noSkills: cfg.noSkills,
313
310
  noPromptTemplates: true,
314
311
  noThemes: true,
@@ -337,10 +334,9 @@ export async function runAgent(
337
334
  thinkingLevel: cfg.thinkingLevel,
338
335
  });
339
336
 
340
- // Filter active tools: remove our own tools to prevent nesting,
341
- // apply extension allowlist if specified.
337
+ // Filter active tools: remove our own tools to prevent nesting.
342
338
  // First pass - over built-in tools, before bindExtensions registers extension tools.
343
- if (cfg.toolFilter.extensions !== false) {
339
+ if (cfg.toolFilter.extensions) {
344
340
  const filtered = filterActiveTools(session.getActiveToolNames(), cfg.toolFilter);
345
341
  session.setActiveToolsByName(filtered);
346
342
  }
@@ -363,9 +359,9 @@ export async function runAgent(
363
359
  // Patch 2 (RepOne #443): re-filter active tools after bindExtensions.
364
360
  // Extension-registered tools (added during bindExtensions) are not in the
365
361
  // session's active set when the first filter pass runs above. Without this
366
- // re-filter, the `extensions: string[]` allowlist branch never matches any
367
- // extension tools. Run the same filter against the post-bind active set.
368
- if (cfg.toolFilter.extensions !== false) {
362
+ // re-filter, EXCLUDED_TOOL_NAMES would not be applied to extension-registered
363
+ // tools. Run the same filter against the post-bind active set.
364
+ if (cfg.toolFilter.extensions) {
369
365
  const refiltered = filterActiveTools(session.getActiveToolNames(), cfg.toolFilter);
370
366
  session.setActiveToolsByName(refiltered);
371
367
  }
@@ -27,8 +27,8 @@ import type { AgentPromptConfig, SubagentType, ThinkingLevel } from "#src/types"
27
27
  export interface ToolFilterConfig {
28
28
  /** Built-in tool name allowlist for this agent type. */
29
29
  toolNames: string[];
30
- /** Resolved extensions setting: false | true | string[] allowlist. */
31
- extensions: boolean | string[];
30
+ /** Resolved extensions setting: true = inherit all, false = none. */
31
+ extensions: boolean;
32
32
  }
33
33
 
34
34
  /**
package/src/types.ts CHANGED
@@ -42,8 +42,8 @@ export interface AgentPromptConfig {
42
42
  /** Unified agent configuration — used for both default and user-defined agents. */
43
43
  export interface AgentConfig extends AgentIdentity, AgentPromptConfig {
44
44
  builtinToolNames?: string[];
45
- /** true = inherit all, string[] = only listed, false = none */
46
- extensions: true | string[] | false;
45
+ /** true = inherit all extensions, false = none */
46
+ extensions: boolean;
47
47
  /** true = inherit all, string[] = only listed, false = none */
48
48
  skills: true | string[] | false;
49
49
  model?: string;
@@ -47,9 +47,7 @@ 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 === false) fmFields.push("extensions: false");
51
- else if (Array.isArray(cfg.extensions))
52
- fmFields.push(`extensions: ${cfg.extensions.join(", ")}`);
50
+ if (!cfg.extensions) fmFields.push("extensions: false");
53
51
  if (cfg.skills === false) fmFields.push("skills: false");
54
52
  else if (Array.isArray(cfg.skills))
55
53
  fmFields.push(`skills: ${cfg.skills.join(", ")}`);
@@ -104,7 +104,7 @@ 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), false (none), or comma-separated names. Default: true>
107
+ extensions: <true (inherit all MCP/extension tools) or false (none). Default: true>
108
108
  skills: <true (inherit all), false (none), or comma-separated skill names to preload into prompt. Default: true>
109
109
  inherit_context: <true to fork parent conversation into agent so it sees chat history. Default: false>
110
110
  run_in_background: <true to run in background by default. Default: false>