@gotgenes/pi-subagents 7.8.1 → 8.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,31 @@ 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
+ ## [8.0.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v7.8.1...pi-subagents-v8.0.0) (2026-05-27)
9
+
10
+
11
+ ### ⚠ BREAKING CHANGES
12
+
13
+ * The `disallowed_tools` frontmatter field is no longer supported. Users should migrate to pi-permission-system's `permission:`
14
+
15
+ ### Features
16
+
17
+ * remove disallowedTools from AgentConfig ([6044552](https://github.com/gotgenes/pi-packages/commit/6044552db68180bf8a151c0f64c13f264f3dacb9))
18
+
19
+
20
+ ### Documentation
21
+
22
+ * add Phase 14 issue links, anemic-model and lifecycle-object smells to discovery skill ([9831176](https://github.com/gotgenes/pi-packages/commit/983117652937dce08e61a6cf7624e35ccf8a0dcb))
23
+ * mark Phase 14 Step 1 complete in architecture ([d24e3dd](https://github.com/gotgenes/pi-packages/commit/d24e3dd5c664e63a372f399f9e325f1bdf877022))
24
+ * **pi-subagents:** add Phase 14 issue links ([#237](https://github.com/gotgenes/pi-packages/issues/237), [#238](https://github.com/gotgenes/pi-packages/issues/238), [#239](https://github.com/gotgenes/pi-packages/issues/239)) ([b152a75](https://github.com/gotgenes/pi-packages/commit/b152a75dcd6596468c5c0aeca1b36ac33216dfa8))
25
+ * **pi-subagents:** add target architecture vision and renumber phases 14-17 ([f7e5315](https://github.com/gotgenes/pi-packages/commit/f7e5315bbfb088d7a40637deead1811691dec1bd))
26
+ * **pi-subagents:** archive Phase 13, update metrics for Phase 14 ([9d473db](https://github.com/gotgenes/pi-packages/commit/9d473db1b0e420fa52ce9d09bea7afff75331e58))
27
+ * plan removal of disallowed_tools ([#237](https://github.com/gotgenes/pi-packages/issues/237)) ([9cb600c](https://github.com/gotgenes/pi-packages/commit/9cb600cad9c8cc7b1081b021f039b0f36b9f9a44))
28
+ * remove disallowed_tools from README and add migration note ([9b3905f](https://github.com/gotgenes/pi-packages/commit/9b3905fbaa81db47633c0c8edf21ed3b766d5507))
29
+ * **retro:** add planning stage notes for issue [#237](https://github.com/gotgenes/pi-packages/issues/237) ([85460dd](https://github.com/gotgenes/pi-packages/commit/85460dd72949d7da83eaf9ed2196ff5c41d92f7f))
30
+ * **retro:** add retro notes for issue [#219](https://github.com/gotgenes/pi-packages/issues/219) ([2d92af5](https://github.com/gotgenes/pi-packages/commit/2d92af577c0cfb71c575a5c334df2347840b4a8d))
31
+ * **retro:** add TDD stage notes for issue [#237](https://github.com/gotgenes/pi-packages/issues/237) ([f334538](https://github.com/gotgenes/pi-packages/commit/f3345383a76975f09c0ee689edcb5df286b76fc7))
32
+
8
33
  ## [7.8.1](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v7.8.0...pi-subagents-v7.8.1) (2026-05-26)
9
34
 
10
35
 
package/README.md CHANGED
@@ -31,7 +31,6 @@ Run them in foreground or background, steer them mid-run, resume completed sessi
31
31
  - **Persistent agent memory** — three scopes (project, local, user) with automatic read-only fallback for agents without write tools
32
32
  - **Git worktree isolation** — run agents in isolated repo copies; changes auto-committed to branches on completion
33
33
  - **Skill preloading** — inject named skills into agent system prompts, discovered from `.pi/skills/`, `.agents/skills/`, and global locations (Pi-standard `<name>/SKILL.md` directory layout supported)
34
- - **Tool denylist** — block specific tools via `disallowed_tools` frontmatter
35
34
  - **Styled completion notifications** — background agent results render as themed, compact notification boxes (icon, stats, result preview) instead of raw XML.
36
35
  Expandable to show full output
37
36
  - **Event bus** — lifecycle events (`subagents:created`, `started`, `completed`, `failed`, `steered`, `compacted`) emitted via `pi.events`, enabling other extensions to react to sub-agent activity
@@ -179,7 +178,6 @@ All fields are optional — sensible defaults for everything.
179
178
  | `extensions` | `true` | Inherit MCP/extension tools. `false` to disable |
180
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) |
181
180
  | `memory` | — | Persistent agent memory scope: `project`, `local`, or `user`. Auto-detects read-only agents |
182
- | `disallowed_tools` | — | Comma-separated tools to deny even if extensions provide them |
183
181
  | `isolation` | — | Set to `worktree` to run in an isolated git worktree |
184
182
  | `model` | inherit parent | Model — `provider/modelId` or fuzzy name (`"haiku"`, `"sonnet"`) |
185
183
  | `thinking` | inherit | off, minimal, low, medium, high, xhigh |
@@ -356,8 +354,6 @@ Agents with write tools get full read-write access.
356
354
  **Read-only agents** (no `write`/`edit` tools) automatically get read-only memory — they can consume memories written by other agents but cannot modify them.
357
355
  This prevents unintended tool escalation.
358
356
 
359
- The `disallowed_tools` field is respected when determining write capability — an agent with `tools: write` + `disallowed_tools: write` correctly gets read-only memory.
360
-
361
357
  ## Worktree Isolation
362
358
 
363
359
  Set `isolation: worktree` to run an agent in a temporary git worktree:
@@ -408,18 +404,19 @@ Traversal is byte-order sorted for deterministic resolution across filesystems.
408
404
  **Security:** symlinks are rejected at every layer (root, flat file, skill directory, `SKILL.md` inside a skill directory) — intentional deviation from Pi, which follows symlinks.
409
405
  Skill names with path-traversal characters (`..`, `/`, `\`, spaces, leading dot, >128 chars) are rejected.
410
406
 
411
- ## Tool Denylist
407
+ ## Migrating from `disallowed_tools`
412
408
 
413
- Block specific tools from an agent even if extensions provide them:
409
+ The `disallowed_tools` frontmatter field has been removed.
410
+ Use [`@gotgenes/pi-permission-system`](https://github.com/gotgenes/pi-permission-system)'s `permission:` frontmatter instead — it provides richer semantics (allow/ask/deny vs. binary hide):
414
411
 
415
412
  ```yaml
416
- ---
417
- tools: read, bash, grep, write
418
- disallowed_tools: write, edit
419
- ---
420
- ```
413
+ # Before (no longer supported)
414
+ disallowed_tools: bash
421
415
 
422
- This is useful for creating agents that inherit extension tools but should not have write access.
416
+ # After
417
+ permission:
418
+ bash: deny
419
+ ```
423
420
 
424
421
  ## Permission System Integration
425
422
 
@@ -483,7 +480,7 @@ Each has a corresponding upstream PR:
483
480
  Also fixes a latent bug where `ThinkingLevel` was imported from `pi-agent-core` (an undeclared transitive dep that breaks under pnpm).
484
481
  Upstream PR: [tintinweb/pi-subagents#71](https://github.com/tintinweb/pi-subagents/pull/71).
485
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.
486
- Without this, the `extensions: string[]` allowlist branch was functionally dead for extension tools, and `extensions: true` with a `disallowedTools` denylist let denylisted extension tools slip through.
483
+ Without this, the `extensions: string[]` allowlist branch was functionally dead for extension tools.
487
484
  Upstream PR: [tintinweb/pi-subagents#72](https://github.com/tintinweb/pi-subagents/pull/72).
488
485
  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).
489
486
  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.
@@ -22,6 +22,9 @@ This document describes the architecture of the pi-subagents fork: a focused, co
22
22
  No post-construction field writes from external code — if an object can't be instantiated ready-to-go, the prep work hasn't been done and the right dependencies haven't been identified.
23
23
  9. **State owns its mutations** — mutable state lives in a class whose methods enforce valid transitions and invariants.
24
24
  Free functions that mutate module-scoped variables, closure-captured bags-of-functions, and external writes to shared interfaces are replaced by classes that encapsulate the state they manage.
25
+ 10. **Open for extension, closed for modification** — pi-subagents is a minimal core that publishes events and a service API.
26
+ Other packages (pi-permission-system, a future UI extension, hypothetical OTel integration) hook into these events to add permissions, rendering, or telemetry.
27
+ Pi-subagents has zero knowledge of its consumers — dependency arrows point inward, never outward.
25
28
 
26
29
  ## Domain model
27
30
 
@@ -222,7 +225,7 @@ sequenceDiagram
222
225
 
223
226
  ## Module organization
224
227
 
225
- The extension has 53 source files organized into six domains plus entry-point wiring.
228
+ The extension has 56 source files organized into six domains plus entry-point wiring.
226
229
  All eight domains have directories: `config/`, `session/`, `lifecycle/`, `observation/`, `service/`, `tools/`, `ui/`, and `handlers/`.
227
230
  Issue #164 moved the 26 previously flat root-level files into five new domain directories, reducing the root to 5 files + 8 directories.
228
231
 
@@ -260,6 +263,7 @@ src/
260
263
  │ ├── parent-snapshot.ts immutable spawn-time parent state
261
264
  │ ├── execution-state.ts session/output phase state
262
265
  │ ├── permission-bridge.ts optional bridge to pi-permission-system registry
266
+ │ ├── run-handle.ts per-run cleanup lifecycle
263
267
  │ ├── worktree.ts git worktree isolation
264
268
  │ ├── worktree-state.ts worktree phase state
265
269
  │ └── usage.ts token usage tracking
@@ -272,7 +276,7 @@ src/
272
276
 
273
277
  ├── service/ cross-extension API boundary
274
278
  │ ├── service.ts SubagentsService interface + Symbol.for() accessors
275
- │ └── service-adapter.ts SubagentsService wrapper around AgentManager
279
+ │ └── service-adapter.ts SubagentsServiceAdapter class wrapping AgentManager
276
280
 
277
281
  ├── tools/ LLM-facing tool implementations
278
282
  │ ├── agent-tool.ts Agent tool definition, validation, dispatch
@@ -288,8 +292,8 @@ src/
288
292
  │ ├── agent-widget.ts above-editor live status widget
289
293
  │ ├── widget-renderer.ts pure rendering for widget
290
294
  │ ├── agent-menu.ts /agents slash command menu
291
- │ ├── agent-config-editor.ts agent detail/edit view
292
- │ ├── agent-creation-wizard.ts agent creation (AI + manual)
295
+ │ ├── agent-config-editor.ts agent detail/edit view (AgentConfigEditor class)
296
+ │ ├── agent-creation-wizard.ts agent creation (AgentCreationWizard class)
293
297
  │ ├── conversation-viewer.ts scrollable session overlay
294
298
  │ ├── message-formatters.ts pure per-message-type formatters (extracted from conversation-viewer)
295
299
  │ ├── agent-activity-tracker.ts live activity state tracker
@@ -335,8 +339,9 @@ They declare this package as an optional peer dependency and use dynamic import
335
339
 
336
340
  - The three tools: `Agent`, `get_subagent_result`, `steer_subagent`.
337
341
  - `AgentManager` — spawn, queue, abort, resume, concurrency control.
338
- - `agent-runner` — session creation, turn loop, tool filtering, extension binding (Patches 2 and 3), permission-system registration.
342
+ - `agent-runner` — session creation, turn loop, extension binding.
339
343
  - `permission-bridge` — optional cross-extension bridge to `@gotgenes/pi-permission-system`; registers each child session with `SubagentSessionRegistry` before `bindExtensions()` so the permission system detects in-process children deterministically.
344
+ Scheduled for removal in Phase 16 — replaced by lifecycle events that consumers listen for.
340
345
  - `session-config` — pure configuration assembler (extracted from `agent-runner`).
341
346
  - `SubagentRuntime` — session-scoped state bag with methods.
342
347
  - `ParentSnapshot` — immutable snapshot of parent session state, captured once at spawn time.
@@ -430,22 +435,56 @@ The core emits events on `pi.events` that any extension can observe:
430
435
 
431
436
  These are fire-and-forget broadcast events — no request IDs, no reply channels.
432
437
 
438
+ ## Target architecture
439
+
440
+ The long-term architectural direction is to make pi-subagents a **minimal core** with inverted dependencies.
441
+ Today, pi-subagents reaches outward to pi-permission-system via a bridge module and owns tool/extension filtering logic that duplicates permission-system responsibilities.
442
+ The target state eliminates this overlap and flips the dependency direction.
443
+
444
+ ### Core responsibilities (keep)
445
+
446
+ - **Agent definitions** — name, model, thinking, system prompt, tools list.
447
+ - **Prompt composition** — system prompt assembly, skill preloading into prompt.
448
+ - **Session lifecycle** — create child sessions, bind extensions, run conversation loop, track results.
449
+ - **Concurrency management** — queue, abort, resume, max concurrency.
450
+ - **Recursion guard** — remove pi-subagents' own three tools from child sessions (prevent infinite nesting).
451
+ - **Lifecycle events** — emit events on `pi.events` when child sessions are created, completed, etc.
452
+ - **Service API** — publish `SubagentsService` via `Symbol.for()` for cross-extension access.
453
+
454
+ ### Responsibilities to remove
455
+
456
+ - **Tool policy** (`disallowed_tools`, `ToolFilterConfig.disallowedSet`) — access control belongs in pi-permission-system's `permission:` frontmatter.
457
+ - **Extension filtering** (`extensions: string[]` allowlist) — tool visibility is pi-permission-system's job.
458
+ - **Permission bridge** (`permission-bridge.ts`) — outbound coupling to pi-permission-system.
459
+ Replaced by lifecycle events that pi-permission-system listens for.
460
+ - **Extension lifecycle control** (`extensions: false`, `isolated`) — extensions provide behavioral layers (permissions, formatting, context management) that benefit all agents.
461
+ Blanket-disabling them is a blunt instrument with no clear use case; tool restrictions belong in the permission system.
462
+
463
+ ### Composition model
464
+
465
+ In the target state, pi-subagents publishes events and other packages hook in:
466
+
467
+ - **pi-permission-system** listens for child session lifecycle events, applies per-agent policy (allow/ask/deny), gates tool calls at runtime.
468
+ - **pi-subagents-ui** (future) subscribes to the service API, renders the widget, conversation viewer, and `/agents` menu.
469
+ - **Any future extension** (OTel, auditing, cost tracking) hooks into the same events without pi-subagents knowing.
470
+
471
+ This is achieved across three phases: Phase 14 (strip policy), Phase 16 (invert dependencies), and Phase 17 (extract UI).
472
+
433
473
  ## Current structural analysis
434
474
 
435
475
  ### Health metrics
436
476
 
437
- | Metric | Value |
438
- | -------------------------- | ------------------------------------ |
439
- | Health score | 78/100 (B) |
440
- | Total LOC | 8,180 (53 files) |
441
- | Test LOC | 12,026 |
442
- | Dead code | 0 files, 0 exports |
443
- | Maintainability index | 90.7 (good) |
444
- | Avg cyclomatic complexity | 1.5 |
445
- | P90 cyclomatic complexity | 2 |
446
- | Production duplication | 20 lines (1 clone group) |
447
- | Test duplication | 59 clone groups, 1,046 lines |
448
- | Fallow refactoring targets | 1 (buildParentContext, cognitive 30) |
477
+ | Metric | Value |
478
+ | -------------------------- | --------------------------------- |
479
+ | Health score | 78/100 (B) |
480
+ | Total LOC | 8,382 (56 files) |
481
+ | Dead code | 0 files, 0 exports |
482
+ | Maintainability index | 90.8 (good) |
483
+ | Avg cyclomatic complexity | 1.4 |
484
+ | P90 cyclomatic complexity | 2 |
485
+ | Production duplication | 11 lines (1 internal clone group) |
486
+ | Test duplication | 38 clone groups, 634 lines |
487
+ | Fallow refactoring targets | 0 |
449
488
 
450
489
  ### Dependency bag inventory
451
490
 
@@ -471,28 +510,29 @@ Bags with 10+ fields are the highest priority for decomposition.
471
510
 
472
511
  Functions with cyclomatic complexity ≥ 21 (critical threshold):
473
512
 
474
- No functions remain above the critical threshold — all hotspots resolved in Phase 12.
513
+ No functions remain above the critical threshold — all hotspots resolved in Phase 12. 6 functions remain at HIGH severity (CRAP ≥ 65); 13 at moderate.
475
514
 
476
515
  ### Churn hotspots
477
516
 
478
- Files with highest commit frequency × complexity (accelerating trend):
517
+ Files with highest commit frequency × complexity:
479
518
 
480
519
  | Score | File | Commits | Trend |
481
520
  | ----- | --------------------------- | ------- | -------------- |
482
- | 43.3 | `index.ts` | 81 | ▲ accelerating |
483
- | 26.0 | `ui/agent-menu.ts` | 33 | accelerating |
484
- | 13.6 | `tools/agent-tool.ts` | 41 | accelerating |
485
- | 13.3 | `ui/conversation-viewer.ts` | 16 | accelerating |
486
- | 12.6 | `ui/agent-config-editor.ts` | 10 | cooling |
487
- | 11.7 | `ui/agent-widget.ts` | 14 | accelerating |
521
+ | 65.0 | `index.ts` | 128 | ▲ accelerating |
522
+ | 9.1 | `ui/agent-widget.ts` | 13 | cooling |
523
+ | 8.4 | `ui/conversation-viewer.ts` | 11 | stable |
524
+ | 6.4 | `runtime.ts` | 12 | stable |
525
+ | 3.3 | `settings.ts` | 4 | stable |
526
+ | 2.9 | `handlers/lifecycle.ts` | 11 | stable |
488
527
 
489
- Note: accelerating trends reflect recent refactoring phases, not feature churn.
490
- Once structural work stabilizes, these are expected to cool.
528
+ Most files have cooled to stable after 13 phases of structural work.
529
+ `index.ts` remains the sole accelerating hotspot expected as the wiring entry point for each refactoring phase.
491
530
 
492
531
  ### Production duplication
493
532
 
494
533
  The prior clone group between `agent-runner.ts` and `message-formatters.ts` was resolved in #172.
495
- The 20-line clone group between `agent-config-editor.ts` and `agent-creation-wizard.ts` was resolved in #217 — extracted into `ui/agent-file-writer.ts` (`writeAgentFile`). 0 production clone groups remain.
534
+ The 20-line clone group between `agent-config-editor.ts` and `agent-creation-wizard.ts` was resolved in #217 — extracted into `ui/agent-file-writer.ts` (`writeAgentFile`).
535
+ One 11-line internal clone group remains within `agent-config-editor.ts` (lines 135–145 / 173–183).
496
536
 
497
537
  ### Proposed bag decompositions
498
538
 
@@ -619,123 +659,97 @@ See [phase-11-closure-to-class.md](history/phase-11-closure-to-class.md) for det
619
659
  Phase 12 decomposed the three remaining high-complexity UI functions and extracted shared test fixtures.
620
660
  All four steps are closed: [#205], [#206], [#207], [#208].
621
661
 
622
- ## Improvement roadmap (Phase 13)
623
-
624
- Phase 13 addresses the remaining fallow refactoring target, `agent-manager.ts` oversized method, production duplication, SDK boundary coupling, and the heaviest test clone families.
625
- Health score target: 80+ (A).
626
-
627
- ### Findings summary
628
-
629
- | Finding | Category | Impact | Risk | Priority |
630
- | ---------------------------------------------------------------- | -------------- | ------ | ---- | -------- |
631
- | 3 remaining closure factories (Phase 11 survivors) | C: Coupling | 4 | 2 | 16 |
632
- | `buildParentContext` cognitive 30 (only fallow target) | B: Oversized | 3 | 1 | 15 |
633
- | `startAgent` in agent-manager.ts (~130 LOC method) | B: Oversized | 4 | 3 | 12 |
634
- | Test duplication: 59 clone groups, 1,046 lines | D: Testability | 3 | 2 | 12 |
635
- | Overwrite guard duplicated across UI modules (20 lines) | A: Redundant | 2 | 1 | 10 |
636
- | `settings.ts` calls SDK function `getAgentDir()` at module level | C: Coupling | 2 | 1 | 10 |
637
-
638
- ### Step 1: Convert remaining closure factories to classes — [#214] ✓
662
+ ## Phase 13 (complete)
639
663
 
640
- Three closure factories converted to classes in [#214].
664
+ Phase 13 addressed remaining closure factories, the last fallow refactoring target, oversized methods, production duplication, SDK boundary coupling, and test clone families.
665
+ All six steps are closed: [#214], [#215], [#216], [#217], [#218], [#219].
666
+ See [phase-13-remaining-smells.md](history/phase-13-remaining-smells.md) for details.
641
667
 
642
- | Factory Class | File | Captures |
643
- | ------------------------------------------------------ | ----------------------------- | ---------------------------------------- |
644
- | `createAgentConfigEditor()` → `AgentConfigEditor` | `ui/agent-config-editor.ts` | `fileOps`, `registry`, 2 dirs |
645
- | `createAgentCreationWizard()` → `AgentCreationWizard` | `ui/agent-creation-wizard.ts` | `fileOps`, `manager`, `registry`, 2 dirs |
646
- | `createSubagentsService()` → `SubagentsServiceAdapter` | `service/service-adapter.ts` | `manager`, `resolveModel`, `runtime` |
668
+ ## Improvement roadmap (Phase 14 — strip policy from core)
647
669
 
648
- - Smell: C (coupling deps hidden in closure scope instead of explicit on class)
649
- - Outcome: 0 remaining closure factories (excluding pure-function factories), deps visible as constructor parameters
670
+ Phase 14 removes tool and extension policy enforcement from pi-subagents.
671
+ This code duplicates what pi-permission-system already provides with richer semantics (allow/ask/deny vs. binary hide).
672
+ Removing it simplifies `runAgent`, shrinks `AgentConfig` and `SessionConfig`, and makes the Phase 15 domain-model work operate on a cleaner codebase.
650
673
 
651
- ### Step 2: Decompose `buildParentContext` (cognitive 30) — [#215] ✓
652
-
653
- `buildParentContext` in `session/context.ts` is the only remaining fallow refactoring target.
654
- The function loops over branch entries with 3 type-check branches, each with sub-branches for role or summary.
655
- Extract per-entry-type formatters: `formatMessageEntry(entry)` and `formatCompactionEntry(entry)`.
656
-
657
- - Target: `src/session/context.ts`
658
- - Smell: B (oversized function)
659
- - Outcome: cognitive complexity < 10, function < 15 LOC
660
-
661
- ### Step 3: Decompose `startAgent` in `agent-manager.ts` — [#216] ✓
662
-
663
- `startAgent` had two mutable closure variables (`unsubRecordObserver`, `detachParentSignal`) shared across three callbacks with duplicated finalization logic in `.then()`/`.catch()`.
664
- The fix introduced a `RunHandle` lifecycle object (private to `agent-manager.ts`) that owns the per-run cleanup state and exposes `complete()`/`fail()` as Tell-Don't-Ask methods.
665
- `WorktreeState` gained `performCleanup(worktrees, description)` to eliminate the ask-tell dance at cleanup sites.
674
+ ### Findings summary
666
675
 
667
- Extracted:
676
+ | Finding | Category | Impact | Risk | Priority |
677
+ | ----------------------------------------------------------------------------------------- | ------------- | ------ | ---- | -------- |
678
+ | `disallowed_tools` duplicates pi-permission-system's `permission:` frontmatter | A: Overlap | 4 | 2 | 12 |
679
+ | `extensions: string[]` allowlist is tool filtering disguised as lifecycle control | A: Overlap | 3 | 2 | 9 |
680
+ | `filterActiveTools` runs twice (pre-bind + post-bind) to catch extension-registered tools | B: Complexity | 3 | 1 | 6 |
681
+ | `ToolFilterConfig` exists solely to carry filtering state through the runner | C: Accidental | 2 | 1 | 4 |
668
682
 
669
- 1. `RunHandle` class owns `unsub`/`detachFn`, `wireSignal()`, `attachObserver()`, `complete()`, `fail()`, idempotent `fireOnFinished()`.
670
- 2. `finalizeBackgroundRun(record)` — shared `runningBackground--`, crash-safe observer notification, `drainQueue()`.
671
- 3. `setupWorktree(id, record, isolation)` — worktree creation with strict failure.
672
- 4. `flushPendingSteers(id, session)` — drain buffered steers on session creation.
673
- 5. `WorktreeState.performCleanup(worktrees, description)` — self-cleanup eliminating ask-tell.
683
+ ### Step 1: Remove `disallowed_tools` [#237] Complete
674
684
 
675
- - Target: `src/lifecycle/agent-manager.ts`, `src/lifecycle/worktree-state.ts`
676
- - Smell: B (oversized method) + A (duplicated finalization logic in then/catch)
677
- - Outcome: `startAgent` reduced to ~40 LOC coordinator with zero mutable `let` bindings; `.then()`/`.catch()` are one-liners
685
+ Remove the `disallowedTools` field from `AgentConfig` and all code that processes it.
678
686
 
679
- ### Step 4: Extract overwrite guard from UI — [#217] ✓
687
+ 1. Remove `disallowedTools` from the `AgentConfig` interface in `types.ts`.
688
+ 2. Remove `disallowed_tools` parsing from custom agent frontmatter in `custom-agents.ts`.
689
+ 3. Remove `disallowedSet` from `ToolFilterConfig` in `session-config.ts`.
690
+ 4. Remove the `disallowedSet` construction in `assembleSessionConfig`.
691
+ 5. Remove the `disallowedSet` branch from `filterActiveTools` in `agent-runner.ts`.
692
+ 6. Remove `disallowed_tools` from the agent config editor UI.
693
+ 7. Remove `disallowed_tools` from the agent creation wizard.
694
+ 8. Update tests.
680
695
 
681
- The 20-line pattern duplicated between `agent-config-editor.ts:138–151` and `agent-creation-wizard.ts:231–250` checks file existence, prompts for confirmation, writes the file, reloads the registry, and notifies the user.
682
- Extract a shared `writeAgentFile(fileOps, ui, registry, targetPath, content, label)` function.
696
+ - Target: `types.ts`, `custom-agents.ts`, `session-config.ts`, `agent-runner.ts`, `ui/agent-config-editor.ts`, `ui/agent-creation-wizard.ts`
697
+ - Smell: A (responsibility overlap with pi-permission-system)
698
+ - Outcome: users migrate to `permission:` frontmatter for tool restrictions; single source of truth for access control
683
699
 
684
- - Target: new `src/ui/agent-file-writer.ts`, consumers `src/ui/agent-config-editor.ts` and `src/ui/agent-creation-wizard.ts`
685
- - Smell: A (production duplication)
686
- - Outcome: 0 production clone groups
700
+ ### Step 2: Remove `extensions` filtering [#238]
687
701
 
688
- ### Step 5: Push SDK boundary in `settings.ts` [#218]
702
+ Remove the `extensions: string[]` allowlist and simplify the field to a boolean.
703
+ The `extensions: false` case (used by `isolated`) is retained in this step and removed in Phase 16.
689
704
 
690
- `globalPath()` calls `getAgentDir()` (a Pi SDK function) at invocation time.
691
- This hides a platform dependency inside a module that is otherwise pure configuration logic.
692
- Inject `agentDir: string` as a constructor parameter to `SettingsManager` and pass the global settings path from the boundary in `index.ts`.
705
+ 1. Change `extensions` type from `true | string[] | false` to `boolean` in `AgentConfig`.
706
+ 2. Remove the `extensions` array branch from `filterActiveTools` in `agent-runner.ts`.
707
+ 3. Remove `extensions` from the agent config editor and creation wizard.
708
+ 4. Update custom agent frontmatter parsing to treat array values as `true` (with a warning).
709
+ 5. Update tests.
693
710
 
694
- - Target: `src/settings.ts`, `src/index.ts`
695
- - Smell: C (platform type threading)
696
- - Outcome: `settings.ts` has 0 Pi SDK imports, `loadSettings`/`saveSettings` become fully testable without SDK stubs
711
+ - Target: `types.ts`, `agent-runner.ts`, `session-config.ts`, `ui/agent-config-editor.ts`, `ui/agent-creation-wizard.ts`
712
+ - Smell: A (tool filtering disguised as extension lifecycle control)
713
+ - Outcome: `filterActiveTools` reduces to two concerns: recursion guard and `extensions: false` passthrough
697
714
 
698
- ### Step 6: Reduce test duplication top 3 clone families — [#219]
715
+ ### Step 3: Collapse `filterActiveTools` to recursion guard — [#239]
699
716
 
700
- The three heaviest remaining clone families after Phase 12:
717
+ With Steps 1–2 complete, `filterActiveTools` has only two remaining branches: the `EXCLUDED_TOOL_NAMES` recursion guard and the `extensions === false` passthrough.
718
+ Inline the `extensions === false` passthrough into the callsite and reduce the function to its essential purpose.
701
719
 
702
- 1. `agent-manager.test.ts` 16 clone groups, 160 duplicated lines.
703
- Extract shared setup/assertion helpers into `test/helpers/manager-stubs.ts`.
704
- 2. `conversation-viewer.test.ts`8 clone groups, 91 duplicated lines.
705
- Extract entry-builder helpers into existing `test/helpers/` or inline factory.
706
- 3. `agent-config-editor.test.ts` — 5 clone groups, 42 duplicated lines.
707
- Extract shared setup helpers.
720
+ 1. Simplify `filterActiveTools` to filter only `EXCLUDED_TOOL_NAMES`.
721
+ 2. Remove `ToolFilterConfig` the function no longer needs a config bag.
722
+ 3. Remove the pre-bind filter call extension tools aren't in the active set yet, so filtering built-in tools pre-bind is only needed for denylist/allowlist logic that no longer exists.
723
+ 4. Keep a single post-bind filter call for the recursion guard.
724
+ 5. Simplify `SessionConfig` — remove the `toolFilter` field, keep `toolNames` as a flat field.
725
+ 6. Update tests.
708
726
 
709
- - Target: `test/lifecycle/agent-manager.test.ts`, `test/conversation-viewer.test.ts`, `test/ui/agent-config-editor.test.ts`
710
- - Smell: D (test duplication)
711
- - Outcome: test duplication reduced by ~200 lines (from 1,046 to < 850)
727
+ - Target: `agent-runner.ts`, `session-config.ts`
728
+ - Smell: B (accidental complexity), C (two-pass filter dance)
729
+ - Outcome: `filterActiveTools` is a one-liner; `SessionConfig` loses one nested type; the pre-bind/post-bind dance is gone
712
730
 
713
731
  ### Step dependency diagram
714
732
 
715
733
  ```mermaid
716
734
  flowchart LR
717
- S1["Step 1\nclosure to class"]
718
- S2["Step 2\nbuildParentContext"]
719
- S3["Step 3\nstartAgent decomp"]
720
- S4["Step 4\noverwrite guard"]
721
- S5["Step 5\nsettings SDK"]
722
- S6["Step 6\ntest duplication"]
723
-
724
- S1 --> S4
725
- S1 --> S6
726
- S3 --> S6
727
- S2 ~~~ S5
735
+ S1["Step 1\nRemove disallowed_tools"]
736
+ S2["Step 2\nRemove extensions filtering"]
737
+ S3["Step 3\nCollapse filterActiveTools"]
738
+
739
+ S1 --> S3
740
+ S2 --> S3
728
741
  ```
729
742
 
730
- ### Tracks
743
+ Steps 1 and 2 are independent and can proceed in parallel.
744
+ Step 3 depends on both.
731
745
 
732
- 1. **Track A — Structural** (Steps 1, 3): closure-to-class conversions and method decomposition.
733
- 2. **Track B — Complexity and coupling** (Steps 2, 5): independent, can proceed in parallel with Track A.
734
- 3. **Track C — Duplication** (Steps 4, 6): Step 4 depends on Step 1 (overwrite guard lives in files being converted); Step 6 depends on Steps 1 and 3 (production code they test changes first).
746
+ [#237]: https://github.com/gotgenes/pi-packages/issues/237
747
+ [#238]: https://github.com/gotgenes/pi-packages/issues/238
748
+ [#239]: https://github.com/gotgenes/pi-packages/issues/239
735
749
 
736
- ## Improvement roadmap (Phase 14)
750
+ ## Improvement roadmap (Phase 15 — domain model evolution)
737
751
 
738
- Phase 14 addresses the anemic domain model in the lifecycle layer.
752
+ Phase 15 addresses the anemic domain model in the lifecycle layer.
739
753
  `AgentRecord` is a data bag — identity, status transitions, and stats — but no behavior.
740
754
  `AgentManager` reaches into records 37 times, doing work that belongs on the agent.
741
755
  Per-agent state (pending steers, abort logic, run lifecycle) is scattered across the manager, `RunHandle`, and a manager-level Map.
@@ -839,26 +853,51 @@ flowchart LR
839
853
  Sequential — each depends on the previous.
840
854
  2. **Track B — Decoupling** (Steps 3, 4, 5): independent, can proceed in parallel with Track A.
841
855
 
856
+ ## Improvement roadmap (Phase 16 — invert dependencies)
857
+
858
+ Phase 16 completes the architectural inversion by removing the outbound permission bridge and the `extensions: false` / `isolated` concepts.
859
+ It depends on Phase 15's observer pattern (#229) as the replacement mechanism.
860
+
861
+ Phase 16 is scoped but not yet broken into steps.
862
+ Key changes:
863
+
864
+ 1. Remove `permission-bridge.ts` — the outbound coupling to pi-permission-system.
865
+ 2. Emit child session lifecycle events via the observer — pi-permission-system and other consumers listen for these events instead of being called.
866
+ 3. Remove `extensions: false` — all child sessions load all extensions.
867
+ 4. Dissolve or redefine `isolated` — without extension control and tool filtering, the concept either disappears or becomes purely about prompt composition (no skill preloading, no parent context inheritance).
868
+ 5. Update pi-permission-system to listen for child session events instead of being registered by the bridge.
869
+
870
+ ## Improvement roadmap (Phase 17 — extract UI)
871
+
872
+ Phase 17 is the long-deferred UI extraction (originally Phase 6).
873
+ The widget, conversation viewer, and `/agents` command menu move to a separate `pi-subagents-ui` extension that consumes the `SubagentsService` API.
874
+ By this point the core is minimal and stable — the API boundary has been proven across Phases 14–16.
875
+
842
876
  ## Refactoring history
843
877
 
844
878
  Phases 1–5 and 7–12 are complete.
845
879
  Phase 6 (UI extraction to a separate package) is deferred.
846
880
  Detailed records are preserved in per-phase history files:
847
881
 
848
- | Phase | Title | Status | History |
849
- | ----- | --------------------------------------------------- | -------- | ------------------------------------------------------------------------------------ |
850
- | 1 | Export SubagentsService API boundary | Complete | [phase-1-api-boundary.md](history/phase-1-api-boundary.md) |
851
- | 2 | Remove scheduling subsystem | Complete | [phase-2-remove-scheduling.md](history/phase-2-remove-scheduling.md) |
852
- | 3 | Remove group-join, RPC; replace output-file | Complete | [phase-3-remove-rpc-groupjoin.md](history/phase-3-remove-rpc-groupjoin.md) |
853
- | 4 | Implement and publish SubagentsService | Complete | [phase-4-implement-service.md](history/phase-4-implement-service.md) |
854
- | 5 | Decompose index.ts | Complete | [phase-5-decompose-index.md](history/phase-5-decompose-index.md) |
855
- | 6 | Extract UI to separate package | Deferred | — |
856
- | 7 | Encapsulation and dependency narrowing | Complete | [phase-7-encapsulation.md](history/phase-7-encapsulation.md) |
857
- | 8 | Testability, display extraction, menu decomposition | Complete | [phase-8-testability.md](history/phase-8-testability.md) |
858
- | 9 | Observation consolidation, ctx elimination | Complete | [phase-9-observation-ctx.md](history/phase-9-observation-ctx.md) |
859
- | 10 | Domain organization, bag decomposition, complexity | Complete | [phase-10-structural-decomposition.md](history/phase-10-structural-decomposition.md) |
860
- | 11 | Closure factories to classes | Complete | [phase-11-closure-to-class.md](history/phase-11-closure-to-class.md) |
861
- | 12 | Complexity reduction and test fixture extraction | Complete | [phase-12-complexity-test-fixtures.md](history/phase-12-complexity-test-fixtures.md) |
882
+ | Phase | Title | Status | History |
883
+ | -------- | --------------------------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
884
+ | 1 | Export SubagentsService API boundary | Complete | [phase-1-api-boundary.md](history/phase-1-api-boundary.md) |
885
+ | 2 | Remove scheduling subsystem | Complete | [phase-2-remove-scheduling.md](history/phase-2-remove-scheduling.md) |
886
+ | 3 | Remove group-join, RPC; replace output-file | Complete | [phase-3-remove-rpc-groupjoin.md](history/phase-3-remove-rpc-groupjoin.md) |
887
+ | 4 | Implement and publish SubagentsService | Complete | [phase-4-implement-service.md](history/phase-4-implement-service.md) |
888
+ | 5 | Decompose index.ts | Complete | [phase-5-decompose-index.md](history/phase-5-decompose-index.md) |
889
+ | 6 | Extract UI to separate package | Deferred → Phase 17 | — |
890
+ | 7 | Encapsulation and dependency narrowing | Complete | [phase-7-encapsulation.md](history/phase-7-encapsulation.md) |
891
+ | 8 | Testability, display extraction, menu decomposition | Complete | [phase-8-testability.md](history/phase-8-testability.md) |
892
+ | 9 | Observation consolidation, ctx elimination | Complete | [phase-9-observation-ctx.md](history/phase-9-observation-ctx.md) |
893
+ | 10 | Domain organization, bag decomposition, complexity | Complete | [phase-10-structural-decomposition.md](history/phase-10-structural-decomposition.md) |
894
+ | 11 | Closure factories to classes | Complete | [phase-11-closure-to-class.md](history/phase-11-closure-to-class.md) |
895
+ | 12 | Complexity reduction and test fixture extraction | Complete | [phase-12-complexity-test-fixtures.md](history/phase-12-complexity-test-fixtures.md) |
896
+ | 13 | Remaining structural smells | Complete | [phase-13-remaining-smells.md](history/phase-13-remaining-smells.md) |
897
+ | 14 | Strip policy from core | Planned | — |
898
+ | 15 | Domain model evolution | Planned | — |
899
+ | 16 | Invert dependencies | Planned | — |
900
+ | 17 | Extract UI to separate package | Planned | — |
862
901
 
863
902
  ### Structural refactoring issues
864
903
 
@@ -876,7 +915,8 @@ Detailed records are preserved in per-phase history files:
876
915
  | Phase 11 | #192, #193, #194, #195, #196 | SessionContext, runtime queries, interface alignment, tool classes, runner/menu classes, index.ts simplification |
877
916
  | Phase 12 | #205, #206, #207, #208 | renderWidgetLines, showAgentDetail, widget update, shared test fixtures |
878
917
  | Phase 13 | #214, #215, #216, #217, #218, #219 | Closure-to-class, buildParentContext, startAgent decomp, overwrite guard, settings SDK, test duplication |
879
- | Phase 14 | #227, #228, #229, #230, #231, #232 | Agent domain model, async startAgent, onSessionCreated observer, ConcurrencyQueue, relay deps, resume unification |
918
+ | Phase 14 | #237, #238, #239 | Remove disallowed_tools, remove extensions filtering, collapse filterActiveTools |
919
+ | Phase 15 | #227, #228, #229, #230, #231, #232 | Agent domain model, async startAgent, onSessionCreated observer, ConcurrencyQueue, relay deps, resume unification |
880
920
 
881
921
  The remaining open issue is #22 (parent-session resolution), a cross-extension track that does not gate the structural work.
882
922
 
@@ -909,3 +949,9 @@ The upstream test suite is run periodically as a regression canary for the agent
909
949
  [#217]: https://github.com/gotgenes/pi-packages/issues/217
910
950
  [#218]: https://github.com/gotgenes/pi-packages/issues/218
911
951
  [#219]: https://github.com/gotgenes/pi-packages/issues/219
952
+ [#227]: https://github.com/gotgenes/pi-packages/issues/227
953
+ [#228]: https://github.com/gotgenes/pi-packages/issues/228
954
+ [#229]: https://github.com/gotgenes/pi-packages/issues/229
955
+ [#230]: https://github.com/gotgenes/pi-packages/issues/230
956
+ [#231]: https://github.com/gotgenes/pi-packages/issues/231
957
+ [#232]: https://github.com/gotgenes/pi-packages/issues/232
@@ -0,0 +1,88 @@
1
+ # Phase 13: Remaining structural smells
2
+
3
+ ## Summary
4
+
5
+ Phase 13 addressed the remaining fallow refactoring target, oversized methods, production duplication, SDK boundary coupling, and test clone families.
6
+ All six steps are closed: [#214], [#215], [#216], [#217], [#218], [#219].
7
+
8
+ ## Steps
9
+
10
+ ### Step 1: Convert remaining closure factories to classes — [#214]
11
+
12
+ Three closure factories converted to classes, making their dependencies explicit as constructor parameters.
13
+
14
+ | Factory → Class | File |
15
+ | ------------------------------------------------------ | ----------------------------- |
16
+ | `createAgentConfigEditor()` → `AgentConfigEditor` | `ui/agent-config-editor.ts` |
17
+ | `createAgentCreationWizard()` → `AgentCreationWizard` | `ui/agent-creation-wizard.ts` |
18
+ | `createSubagentsService()` → `SubagentsServiceAdapter` | `service/service-adapter.ts` |
19
+
20
+ - Outcome: 0 remaining closure factories (excluding pure-function factories)
21
+
22
+ ### Step 2: Decompose `buildParentContext` (cognitive 30) — [#215]
23
+
24
+ `buildParentContext` in `session/context.ts` was the only remaining fallow refactoring target.
25
+ Extracted per-entry-type formatters: `formatMessageEntry()` and `formatCompactionEntry()`.
26
+
27
+ - Target: `src/session/context.ts`
28
+ - Outcome: cognitive complexity < 10, function < 15 LOC, 0 fallow refactoring targets
29
+
30
+ ### Step 3: Decompose `startAgent` in `agent-manager.ts` — [#216]
31
+
32
+ `startAgent` had two mutable closure variables and duplicated finalization logic in `.then()`/`.catch()`.
33
+ Introduced `RunHandle` lifecycle object (private to `agent-manager.ts`) that owns per-run cleanup state.
34
+ `WorktreeState` gained `performCleanup()` to eliminate ask-tell at cleanup sites.
35
+
36
+ Extracted:
37
+
38
+ 1. `RunHandle` class — owns `unsub`/`detachFn`, `complete()`, `fail()`, idempotent `fireOnFinished()`.
39
+ 2. `finalizeBackgroundRun()` — shared background finalization.
40
+ 3. `setupWorktree()` — worktree creation with strict failure.
41
+ 4. `flushPendingSteers()` — drain buffered steers on session creation.
42
+ 5. `WorktreeState.performCleanup()` — self-cleanup eliminating ask-tell.
43
+
44
+ - Target: `src/lifecycle/agent-manager.ts`, `src/lifecycle/worktree-state.ts`
45
+ - Outcome: `startAgent` reduced to ~40 LOC; zero mutable `let` bindings in `.then()`/`.catch()`
46
+
47
+ ### Step 4: Extract overwrite guard from UI — [#217]
48
+
49
+ Extracted the 20-line pattern duplicated between `agent-config-editor.ts` and `agent-creation-wizard.ts` into `writeAgentFile()` in `src/ui/agent-file-writer.ts`.
50
+
51
+ - Target: new `src/ui/agent-file-writer.ts`
52
+ - Outcome: 0 production clone groups (at the time; one internal group re-emerged later in `agent-config-editor.ts`)
53
+
54
+ ### Step 5: Push SDK boundary in `settings.ts` — [#218]
55
+
56
+ Injected `agentDir: string` as a constructor parameter to `SettingsManager`, replacing the module-level `getAgentDir()` SDK call.
57
+
58
+ - Target: `src/settings.ts`, `src/index.ts`
59
+ - Outcome: `settings.ts` has 0 Pi SDK imports; `loadSettings`/`saveSettings` fully testable without SDK stubs
60
+
61
+ ### Step 6: Reduce test duplication — top 3 clone families — [#219]
62
+
63
+ Extracted shared setup/assertion helpers for the three heaviest test clone families.
64
+
65
+ - Target: `test/lifecycle/agent-manager.test.ts`, `test/conversation-viewer.test.ts`, `test/ui/agent-config-editor.test.ts`
66
+ - Outcome: test duplication significantly reduced
67
+
68
+ ## Metrics change
69
+
70
+ | Metric | Before | After |
71
+ | -------------------------- | ---------------------- | --------------------------------- |
72
+ | Health score | 78/100 (B) | 78/100 (B) |
73
+ | Source files | 53 | 56 |
74
+ | Total LOC | 8,180 | 8,382 |
75
+ | Dead code | 0 files, 0 exports | 0 files, 0 exports |
76
+ | Maintainability index | 90.7 | 90.8 |
77
+ | Avg cyclomatic complexity | 1.5 | 1.4 |
78
+ | Fallow refactoring targets | 1 | 0 |
79
+ | Production duplication | 0 clone groups | 1 internal clone group (11 lines) |
80
+ | Test duplication | 59 groups, 1,046 lines | 38 groups, 645 lines (overall) |
81
+ | Churn hotspots | 6 accelerating | 1 accelerating (`index.ts`) |
82
+
83
+ [#214]: https://github.com/gotgenes/pi-packages/issues/214
84
+ [#215]: https://github.com/gotgenes/pi-packages/issues/215
85
+ [#216]: https://github.com/gotgenes/pi-packages/issues/216
86
+ [#217]: https://github.com/gotgenes/pi-packages/issues/217
87
+ [#218]: https://github.com/gotgenes/pi-packages/issues/218
88
+ [#219]: https://github.com/gotgenes/pi-packages/issues/219
@@ -0,0 +1,180 @@
1
+ ---
2
+ issue: 237
3
+ issue_title: "Remove disallowed_tools from pi-subagents (Phase 14, Step 1)"
4
+ ---
5
+
6
+ # Remove `disallowed_tools` from pi-subagents
7
+
8
+ ## Problem Statement
9
+
10
+ The `disallowed_tools` frontmatter field and its runtime counterpart `disallowedSet` duplicate what pi-permission-system already provides via the `permission:` frontmatter with richer semantics (allow/ask/deny vs. binary hide).
11
+ Removing it establishes a single source of truth for tool access control in pi-permission-system and simplifies `filterActiveTools`, `ToolFilterConfig`, and `AgentConfig`.
12
+
13
+ ## Goals
14
+
15
+ - Remove the `disallowedTools` field from `AgentConfig` and all code that reads, parses, or serializes it.
16
+ - Remove the `disallowedSet` field from `ToolFilterConfig` and the construction logic in `assembleSessionConfig`.
17
+ - Remove the `disallowedSet` branch from `filterActiveTools` in `agent-runner.ts`.
18
+ - Remove `disallowed_tools` from the agent config editor UI and the agent creation wizard.
19
+ - Update README.md to remove all `disallowed_tools` references and add a migration note.
20
+ - Update tests to remove `disallowedTools`/`disallowedSet` assertions and fixture data.
21
+ - This is a **breaking change** — users with `disallowed_tools` in agent frontmatter must migrate to `permission:` frontmatter.
22
+
23
+ ## Non-Goals
24
+
25
+ - Removing `extensions` filtering (Step 2, #238).
26
+ - Collapsing `filterActiveTools` to a recursion guard (Step 3, #239).
27
+ - Changing any pi-permission-system code — this issue is purely pi-subagents.
28
+ - Adding deprecation warnings or runtime migration helpers — the architecture doc calls for clean removal.
29
+
30
+ ## Background
31
+
32
+ ### Phase 14 context
33
+
34
+ This is Phase 14, Step 1 of the architecture roadmap in `docs/architecture/architecture.md`.
35
+ Steps 1 and 2 (#237, #238) are independent; Step 3 (#239) depends on both.
36
+
37
+ ### Files involved
38
+
39
+ | File | Role |
40
+ | --------------------------------- | -------------------------------------------------------------------------- |
41
+ | `src/types.ts` | `AgentConfig.disallowedTools` field definition |
42
+ | `src/config/custom-agents.ts` | Parses `disallowed_tools` from YAML frontmatter |
43
+ | `src/session/session-config.ts` | `ToolFilterConfig.disallowedSet` + construction in `assembleSessionConfig` |
44
+ | `src/lifecycle/agent-runner.ts` | `filterActiveTools` denylist branch + guard conditions at two call sites |
45
+ | `src/ui/agent-config-editor.ts` | Serializes `disallowedTools` to `disallowed_tools` frontmatter |
46
+ | `src/ui/agent-creation-wizard.ts` | Template text showing `disallowed_tools` as an available field |
47
+ | `README.md` | Feature list, frontmatter table, memory detection note, patch notes |
48
+
49
+ ### `filterActiveTools` after this change
50
+
51
+ With `disallowedSet` removed, the function simplifies:
52
+
53
+ ```typescript
54
+ function filterActiveTools(
55
+ activeTools: string[],
56
+ config: ToolFilterConfig,
57
+ ): string[] {
58
+ const { toolNames, extensions } = config;
59
+ if (extensions === false) {
60
+ return activeTools;
61
+ }
62
+ const builtinToolNameSet = new Set(toolNames);
63
+ return activeTools.filter((t) => {
64
+ if (EXCLUDED_TOOL_NAMES.includes(t)) return false;
65
+ if (builtinToolNameSet.has(t)) return true;
66
+ if (Array.isArray(extensions)) {
67
+ return extensions.some((ext) => t.startsWith(ext) || t.includes(ext));
68
+ }
69
+ return true;
70
+ });
71
+ }
72
+ ```
73
+
74
+ The `extensions === false` branch becomes a trivial passthrough — no filtering at all.
75
+ The guard condition at both call sites simplifies from `cfg.toolFilter.extensions !== false || cfg.toolFilter.disallowedSet` to `cfg.toolFilter.extensions !== false`.
76
+
77
+ ## Design Overview
78
+
79
+ This is a pure removal — no new types, no new modules, no new behavior.
80
+ Every change deletes or simplifies existing code.
81
+
82
+ ### Migration path
83
+
84
+ Users currently using `disallowed_tools` in agent frontmatter migrate to pi-permission-system's `permission:` frontmatter:
85
+
86
+ ```yaml
87
+ # Before
88
+ disallowed_tools: bash
89
+
90
+ # After
91
+ permission:
92
+ bash: deny
93
+ ```
94
+
95
+ ## Module-Level Changes
96
+
97
+ 1. **`src/types.ts`** — Remove the `disallowedTools` field and its JSDoc comment from `AgentConfig`.
98
+ 2. **`src/config/custom-agents.ts`** — Remove the `disallowedTools: csvListOptional(fm.disallowed_tools)` line from the agent config construction.
99
+ 3. **`src/session/session-config.ts`** — Remove `disallowedSet` from the `ToolFilterConfig` interface.
100
+ Remove the `disallowedSet` construction block and its comment in `assembleSessionConfig`.
101
+ Remove `disallowedSet` from the `toolFilter` object literal.
102
+ 4. **`src/lifecycle/agent-runner.ts`** — Remove the `disallowedSet` destructure and denylist branches from `filterActiveTools`.
103
+ Simplify the `extensions === false` branch to `return activeTools`.
104
+ Simplify both guard conditions from `cfg.toolFilter.extensions !== false || cfg.toolFilter.disallowedSet` to `cfg.toolFilter.extensions !== false`.
105
+ Update comments that mention `disallowedTools denylist`.
106
+ 5. **`src/ui/agent-config-editor.ts`** — Remove the two lines that serialize `disallowedTools` to `disallowed_tools` frontmatter.
107
+ 6. **`src/ui/agent-creation-wizard.ts`** — Remove the `disallowed_tools:` line from the template text.
108
+ 7. **`README.md`** — Remove the "Tool denylist" feature bullet, the `disallowed_tools` row from the frontmatter table, the memory write-capability paragraph (memory was removed in #185; this reference is already stale), the `disallowed_tools` example in the memory section, and the `disallowedTools` mention in the Patch 2 notes.
109
+ Add a migration note under a "Migration" heading or in the breaking changes section.
110
+
111
+ ### Test files
112
+
113
+ 8. **`test/config/custom-agents.test.ts`** — Remove the two `disallowed_tools` tests ("parses disallowed_tools as csv list" and "disallowed_tools defaults to undefined when omitted").
114
+ 9. **`test/session/session-config.test.ts`** — Remove the `disallowedSet` assertion from the default config test and remove the entire "builds disallowedSet from agentConfig.disallowedTools" test.
115
+ 10. **`test/lifecycle/agent-runner-extension-tools.test.ts`** — Remove `disallowedTools` from both mock agent config objects.
116
+ Remove the three `disallowedTools`-specific tests ("post-bind re-filter respects disallowedTools denylist", "extensions: false still applies disallowedTools", "extensions: false with no disallowedTools skips the filter").
117
+ Update the file-level JSDoc comment.
118
+ 11. **`test/ui/agent-config-editor.test.ts`** — Remove `disallowedTools` from the test fixture and remove the `disallowed_tools` assertion.
119
+
120
+ ### Architecture docs
121
+
122
+ 12. **`docs/architecture/architecture.md`** — Mark Step 1 as complete (update the step heading or add a completion note).
123
+
124
+ ## Test Impact Analysis
125
+
126
+ 1. **Tests removed** — 7 tests that directly assert `disallowedTools`/`disallowedSet` behavior become meaningless and are deleted:
127
+ - `custom-agents.test.ts`: 2 tests (csv parsing, undefined default)
128
+ - `session-config.test.ts`: 1 test (`disallowedSet` construction) + 1 assertion in default config test
129
+ - `agent-runner-extension-tools.test.ts`: 3 tests (denylist for extension tools, denylist for built-in tools with `extensions: false`, filter skip with `extensions: false` + no denylist)
130
+ - `agent-config-editor.test.ts`: 1 assertion in the serialization test
131
+
132
+ 2. **Tests updated** — Several tests have `disallowedTools` in their mock fixtures but don't test it directly; those fixtures lose the field:
133
+ - `agent-runner-extension-tools.test.ts`: both mock config objects drop `disallowedTools`
134
+
135
+ 3. **Tests unchanged** — The remaining `agent-runner-extension-tools.test.ts` tests (`extensions: true`, `extensions: string[]` allowlist, `extensions: false` passthrough) remain valid — they test extension filtering, not the denylist.
136
+
137
+ ## TDD Order
138
+
139
+ 1. **Remove `disallowedTools` from `AgentConfig`** — Delete the field and JSDoc from `types.ts`.
140
+ Remove `disallowedTools` parsing from `custom-agents.ts`.
141
+ Remove the two `disallowed_tools` tests from `custom-agents.test.ts`.
142
+ Run `pnpm run check` to surface all downstream type errors.
143
+ Commit: `feat!: remove disallowedTools from AgentConfig`
144
+
145
+ 2. **Remove `disallowedSet` from `ToolFilterConfig` and `assembleSessionConfig`** — Delete the field from `ToolFilterConfig`, the construction block in `assembleSessionConfig`, and the `disallowedSet` key from the return literal.
146
+ Remove the `disallowedSet`-specific test and assertion from `session-config.test.ts`.
147
+ Run `pnpm run check`.
148
+ Commit: `refactor: remove disallowedSet from ToolFilterConfig`
149
+
150
+ 3. **Simplify `filterActiveTools` and guard conditions** — Remove the `disallowedSet` destructure and denylist branches from `filterActiveTools`.
151
+ Simplify the `extensions === false` branch to `return activeTools`.
152
+ Simplify both guard conditions to `cfg.toolFilter.extensions !== false`.
153
+ Update comments.
154
+ Remove the three `disallowedTools`-specific tests and update mock fixtures in `agent-runner-extension-tools.test.ts`.
155
+ Run tests: `pnpm vitest run`.
156
+ Commit: `refactor: remove disallowedSet branch from filterActiveTools`
157
+
158
+ 4. **Remove `disallowed_tools` from UI** — Remove serialization from `agent-config-editor.ts` and the template line from `agent-creation-wizard.ts`.
159
+ Remove `disallowedTools` from the test fixture and assertion in `agent-config-editor.test.ts`.
160
+ Run tests: `pnpm vitest run`.
161
+ Commit: `refactor: remove disallowed_tools from config editor and creation wizard`
162
+
163
+ 5. **Update README.md** — Remove all `disallowed_tools` references (feature bullet, frontmatter table row, stale memory paragraph, example, patch notes mention).
164
+ Add a migration note directing users to `permission:` frontmatter.
165
+ Commit: `docs: remove disallowed_tools from README and add migration note`
166
+
167
+ 6. **Mark Phase 14 Step 1 complete in architecture doc** — Update `docs/architecture/architecture.md` to reflect completion.
168
+ Commit: `docs: mark Phase 14 Step 1 complete in architecture`
169
+
170
+ ## Risks and Mitigations
171
+
172
+ | Risk | Mitigation |
173
+ | ---------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
174
+ | Users with existing `disallowed_tools` frontmatter silently lose tool restrictions | Breaking change is intentional and documented; `feat!:` commit triggers a major version bump via release-please; README migration note directs to `permission:` frontmatter |
175
+ | `filterActiveTools` behavior regresses for non-denylist paths | Steps 1–3 each run `pnpm run check` or `pnpm vitest run`; existing extension-filtering tests remain unchanged |
176
+ | Stale references to `disallowed_tools` in docs or plans | Grep sweep in step 5 covers README; architecture doc updated in step 6; historical plan/retro files are left as-is (they document past state) |
177
+
178
+ ## Open Questions
179
+
180
+ None — the issue's proposed change is unambiguous and the architecture doc provides detailed step-by-step guidance.
@@ -34,3 +34,31 @@ All 4 commits landed cleanly; full suite green at every step.
34
34
  - The `createSessionRunner` + `createRunResult` chain required careful identity-check verification: `createRunResult(sess)` calls `toAgentSession(sess)` which casts without creating a new object, so `toBe(session)` assertions in the execution-state tests still pass. ✓
35
35
  - ESLint auto-fixed two cosmetic issues on commit (`activity = undefined` → `activity` destructuring, `session as unknown` cast removal) — caught by pre-commit hooks, not a problem in practice.
36
36
  - The `assertRenderFitsWidths` helper in `conversation-viewer.test.ts` reduced the 10 render-safety tests from ~8 lines each to 1–4 lines each; the `setupDetail` helper in `agent-config-editor.test.ts` eliminated 3 repeated setup lines per test across 18 `showAgentDetail` tests.
37
+
38
+ ## Stage: Final Retrospective (2026-05-26T17:50:31Z)
39
+
40
+ ### Session summary
41
+
42
+ All three stages (planning, TDD, ship) completed in a single session with zero user corrections.
43
+ Four TDD commits landed cleanly, CI passed on first push, issue #219 closed, and `pi-subagents-v7.8.1` released.
44
+
45
+ ### Observations
46
+
47
+ #### What went well
48
+
49
+ - Clean incremental verification: `pnpm vitest run <file>` ran after every code change, full suite at the end.
50
+ No regressions at any step.
51
+ - The plan's decision to keep gated runners inline proved correct — tests with `Promise.withResolvers` flow control stayed readable without abstraction.
52
+ - The `write` tool was the right choice for `conversation-viewer.test.ts` and `agent-config-editor.test.ts` (pervasive changes), while `edit` with 16 targeted replacements worked cleanly for `agent-manager.test.ts` (scattered but formulaic substitutions).
53
+ - Ship stage used `deepseek-v4-flash` for purely mechanical CI/release steps — appropriate model-task alignment.
54
+
55
+ #### What caused friction (agent side)
56
+
57
+ - `other` — ESLint pre-commit hooks auto-fixed two cosmetic patterns (`activity = undefined` → bare destructuring; `session as unknown` cast removal), requiring re-stage + re-commit.
58
+ Impact: ~10 seconds per occurrence, no rework.
59
+ This is routine and inherent to the workflow.
60
+
61
+ #### What caused friction (user side)
62
+
63
+ - None observed.
64
+ The user's three-stage prompt chain (`/plan-issue` → `/tdd-plan` → `/ship-issue`) provided sufficient context at each stage with no manual intervention needed.
@@ -0,0 +1,41 @@
1
+ ---
2
+ issue: 237
3
+ issue_title: "Remove disallowed_tools from pi-subagents (Phase 14, Step 1)"
4
+ ---
5
+
6
+ # Retro: #237 — Remove disallowed_tools from pi-subagents
7
+
8
+ ## Stage: Planning (2026-05-27T00:52:26Z)
9
+
10
+ ### Session summary
11
+
12
+ Produced a 6-step TDD plan to remove `disallowedTools` from `AgentConfig`, `disallowedSet` from `ToolFilterConfig`, and all parsing/serialization/UI/test code that references them.
13
+ The plan covers 7 source files, 4 test files, README, and the architecture doc.
14
+
15
+ ### Observations
16
+
17
+ - The issue label `pkg:pi-permission-system` was incorrect — all target files live in `packages/pi-subagents`.
18
+ Confirmed with the user that the plan targets pi-subagents.
19
+ - The README still references `disallowed_tools` in the context of memory write-capability detection, but memory was already removed in #185.
20
+ The plan treats this as a stale reference to clean up.
21
+ - After removing `disallowedSet`, the `filterActiveTools` `extensions === false` branch simplifies to a trivial passthrough (`return activeTools`), and both guard conditions at the call sites drop the `|| cfg.toolFilter.disallowedSet` arm.
22
+ This leaves the function in the exact shape that Step 3 (#239) expects.
23
+ - The plan orders steps to follow the type dependency chain: `AgentConfig` first (surfaces all downstream errors), then `ToolFilterConfig`, then `filterActiveTools`, then UI, then docs.
24
+
25
+ ## Stage: Implementation — TDD (2026-05-27T00:59:19Z)
26
+
27
+ ### Session summary
28
+
29
+ All 6 TDD steps completed in one session, producing 7 commits (6 planned + 1 fixup for dead code).
30
+ Test count dropped from 983 to 978 (5 tests removed: 2 from `custom-agents.test.ts`, 1 from `session-config.test.ts`, 2 from `agent-runner-extension-tools.test.ts` after renaming one deleted test into a retained form).
31
+ All checks (type check, lint, fallow dead-code gate) pass clean.
32
+
33
+ ### Observations
34
+
35
+ - `csvListOptional` in `custom-agents.ts` was left dead after removing the `disallowedTools` parsing call; Biome flagged it as unused.
36
+ Removed as a separate `refactor:` commit since it couldn't be amended into the `feat!:` commit (later commits already on top of it).
37
+ - One of the three deleted `agent-runner-extension-tools.test.ts` denylist tests ("extensions: false with no disallowedTools skips the filter") was reformulated into a new test ("extensions: false skips the filter entirely") rather than simply deleted, because it covers genuinely different behavior after the simplification: `extensions: false` now always skips the filter, not just when no denylist is present.
38
+ This adds coverage for the simplified code path.
39
+ - The `filterActiveTools` `extensions === false` branch simplified from "apply denylist to built-in tools" to `return activeTools`, exactly as the plan specified.
40
+ Both guard conditions at the call sites simplified to `cfg.toolFilter.extensions !== false`.
41
+ - No deviations from the plan's module-level changes list; all 7 source files and 4 test files were touched as specified.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-subagents",
3
- "version": "7.8.1",
3
+ "version": "8.0.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/service.ts"
@@ -58,7 +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
- disallowedTools: csvListOptional(fm.disallowed_tools),
62
61
  extensions: inheritField(fm.extensions ?? fm.inherit_extensions),
63
62
  skills: inheritField(fm.skills ?? fm.inherit_skills),
64
63
  model: str(fm.model),
@@ -110,14 +109,6 @@ function csvList(val: unknown, defaults: string[]): string[] {
110
109
  return parseCsvField(val) ?? [];
111
110
  }
112
111
 
113
- /**
114
- * Parse an optional comma-separated list field.
115
- * omitted → undefined; "none"/empty → undefined; csv → listed items.
116
- */
117
- function csvListOptional(val: unknown): string[] | undefined {
118
- return parseCsvField(val);
119
- }
120
-
121
112
  /**
122
113
  * Parse an inherit field (extensions, skills).
123
114
  * omitted/true → true (inherit all); false/"none"/empty → false; csv → listed names.
@@ -22,7 +22,7 @@ import type { ShellExec, SubagentType, ThinkingLevel } from "#src/types";
22
22
  const EXCLUDED_TOOL_NAMES = ["Agent", "get_subagent_result", "steer_subagent"];
23
23
 
24
24
  /**
25
- * Filter the session's active tool names according to extension/denylist rules.
25
+ * Filter the session's active tool names according to extension rules.
26
26
  *
27
27
  * Run twice - once before `bindExtensions` (filters built-in tools) and once after
28
28
  * (filters extension-registered tools, which only join the active set during
@@ -36,16 +36,13 @@ function filterActiveTools(
36
36
  activeTools: string[],
37
37
  config: ToolFilterConfig,
38
38
  ): string[] {
39
- const { toolNames, extensions, disallowedSet } = config;
39
+ const { toolNames, extensions } = config;
40
40
  if (extensions === false) {
41
- // Extensions disabled: only apply the denylist to built-in tools.
42
- if (!disallowedSet) return activeTools;
43
- return activeTools.filter((t) => !disallowedSet.has(t));
41
+ return activeTools;
44
42
  }
45
43
  const builtinToolNameSet = new Set(toolNames);
46
44
  return activeTools.filter((t) => {
47
45
  if (EXCLUDED_TOOL_NAMES.includes(t)) return false;
48
- if (disallowedSet?.has(t)) return false;
49
46
  if (builtinToolNameSet.has(t)) return true;
50
47
  if (Array.isArray(extensions)) {
51
48
  return extensions.some((ext) => t.startsWith(ext) || t.includes(ext));
@@ -341,9 +338,9 @@ export async function runAgent(
341
338
  });
342
339
 
343
340
  // Filter active tools: remove our own tools to prevent nesting,
344
- // apply extension allowlist if specified, and apply disallowedTools denylist.
341
+ // apply extension allowlist if specified.
345
342
  // First pass - over built-in tools, before bindExtensions registers extension tools.
346
- if (cfg.toolFilter.extensions !== false || cfg.toolFilter.disallowedSet) {
343
+ if (cfg.toolFilter.extensions !== false) {
347
344
  const filtered = filterActiveTools(session.getActiveToolNames(), cfg.toolFilter);
348
345
  session.setActiveToolsByName(filtered);
349
346
  }
@@ -367,9 +364,8 @@ export async function runAgent(
367
364
  // Extension-registered tools (added during bindExtensions) are not in the
368
365
  // session's active set when the first filter pass runs above. Without this
369
366
  // re-filter, the `extensions: string[]` allowlist branch never matches any
370
- // extension tools and `extensions: true` lets non-allowlisted denylist
371
- // entries slip in. Run the same filter against the post-bind active set.
372
- if (cfg.toolFilter.extensions !== false || cfg.toolFilter.disallowedSet) {
367
+ // extension tools. Run the same filter against the post-bind active set.
368
+ if (cfg.toolFilter.extensions !== false) {
373
369
  const refiltered = filterActiveTools(session.getActiveToolNames(), cfg.toolFilter);
374
370
  session.setActiveToolsByName(refiltered);
375
371
  }
@@ -21,14 +21,12 @@ import type { AgentPromptConfig, SubagentType, ThinkingLevel } from "#src/types"
21
21
  /**
22
22
  * Tool filtering configuration — consumed by `filterActiveTools` in `agent-runner.ts`.
23
23
  *
24
- * Groups the three fields that travel together through the assembly→runner boundary:
25
- * the built-in tool allowlist, the optional denylist, and the extensions setting.
24
+ * Groups the two fields that travel together through the assembly→runner boundary:
25
+ * the built-in tool allowlist and the extensions setting.
26
26
  */
27
27
  export interface ToolFilterConfig {
28
28
  /** Built-in tool name allowlist for this agent type. */
29
29
  toolNames: string[];
30
- /** Disallowed tool set from agentConfig. undefined when empty. */
31
- disallowedSet: Set<string> | undefined;
32
30
  /** Resolved extensions setting: false | true | string[] allowlist. */
33
31
  extensions: boolean | string[];
34
32
  }
@@ -210,11 +208,6 @@ export function assembleSessionConfig(
210
208
  // tell the resource loader not to load them again.
211
209
  const noSkills = skills === false || Array.isArray(skills);
212
210
 
213
- // Disallowed tools set (for filterActiveTools in runAgent)
214
- const disallowedSet = agentConfig.disallowedTools
215
- ? new Set(agentConfig.disallowedTools)
216
- : undefined;
217
-
218
211
  // Model resolution: explicit option > config model string > parent model
219
212
  const model =
220
213
  options.model ??
@@ -229,7 +222,7 @@ export function assembleSessionConfig(
229
222
  return {
230
223
  effectiveCwd,
231
224
  systemPrompt,
232
- toolFilter: { toolNames, disallowedSet, extensions },
225
+ toolFilter: { toolNames, extensions },
233
226
  model,
234
227
  thinkingLevel,
235
228
  noSkills,
package/src/types.ts CHANGED
@@ -42,8 +42,6 @@ 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
- /** Tool denylist — these tools are removed even if `builtinToolNames` or extensions include them. */
46
- disallowedTools?: string[];
47
45
  /** true = inherit all, string[] = only listed, false = none */
48
46
  extensions: true | string[] | false;
49
47
  /** true = inherit all, string[] = only listed, false = none */
@@ -53,8 +53,6 @@ export function buildEjectContent(cfg: AgentConfig): string {
53
53
  if (cfg.skills === false) fmFields.push("skills: false");
54
54
  else if (Array.isArray(cfg.skills))
55
55
  fmFields.push(`skills: ${cfg.skills.join(", ")}`);
56
- if (cfg.disallowedTools?.length)
57
- fmFields.push(`disallowed_tools: ${cfg.disallowedTools.join(", ")}`);
58
56
  if (cfg.inheritContext) fmFields.push("inherit_context: true");
59
57
  if (cfg.runInBackground) fmFields.push("run_in_background: true");
60
58
  if (cfg.isolated) fmFields.push("isolated: true");
@@ -106,7 +106,6 @@ 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
107
  extensions: <true (inherit all MCP/extension tools), false (none), or comma-separated names. Default: true>
108
108
  skills: <true (inherit all), false (none), or comma-separated skill names to preload into prompt. Default: true>
109
- disallowed_tools: <comma-separated tool names to block, even if otherwise available. Omit for none>
110
109
  inherit_context: <true to fork parent conversation into agent so it sees chat history. Default: false>
111
110
  run_in_background: <true to run in background by default. Default: false>
112
111
  isolated: <true for no extension/MCP tools, only built-in tools. Default: false>