@gotgenes/pi-subagents 11.3.0 → 11.5.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 +16 -0
- package/docs/architecture/architecture.md +149 -249
- package/docs/decisions/0002-extensions-on-a-minimal-core.md +98 -0
- package/docs/plans/0257-extract-child-session-factory.md +283 -0
- package/docs/plans/0262-add-workspace-provider-seam.md +262 -0
- package/docs/retro/0256-extract-worktree-isolation.md +44 -0
- package/docs/retro/0257-extract-child-session-factory.md +31 -0
- package/docs/retro/0262-add-workspace-provider-seam.md +44 -0
- package/package.json +1 -1
- package/src/index.ts +3 -0
- package/src/lifecycle/agent-manager.ts +30 -0
- package/src/lifecycle/agent-runner.ts +14 -9
- package/src/lifecycle/agent.ts +44 -7
- package/src/lifecycle/child-lifecycle.ts +89 -0
- package/src/lifecycle/workspace.ts +47 -0
- package/src/service/service-adapter.ts +6 -0
- package/src/service/service.ts +13 -1
- package/src/lifecycle/permission-bridge.ts +0 -63
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ 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
|
+
## [11.5.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v11.4.0...pi-subagents-v11.5.0) (2026-05-29)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* add WorkspaceProvider registration seam to subagents service ([51a9970](https://github.com/gotgenes/pi-packages/commit/51a99701db214c11f08251e9ed5549d01c4d5839))
|
|
14
|
+
* consult workspace provider for child cwd and disposal ([32eeffc](https://github.com/gotgenes/pi-packages/commit/32eeffc1cc31bc7e403c25cdd116e2b351be4527))
|
|
15
|
+
|
|
16
|
+
## [11.4.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v11.3.0...pi-subagents-v11.4.0) (2026-05-29)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Features
|
|
20
|
+
|
|
21
|
+
* add child-execution lifecycle event publisher ([4d27c13](https://github.com/gotgenes/pi-packages/commit/4d27c130b4782b7fffb9b61a37e151f8500c55ea))
|
|
22
|
+
* emit child-execution lifecycle events and retire permission-bridge ([c8daee4](https://github.com/gotgenes/pi-packages/commit/c8daee4bcf21f6720d9dbc164282fb6a04e552b1))
|
|
23
|
+
|
|
8
24
|
## [11.3.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v11.2.0...pi-subagents-v11.3.0) (2026-05-29)
|
|
9
25
|
|
|
10
26
|
|
|
@@ -274,7 +274,8 @@ src/
|
|
|
274
274
|
│ ├── concurrency-queue.ts background agent scheduling with configurable concurrency limit
|
|
275
275
|
│ ├── parent-snapshot.ts immutable spawn-time parent state
|
|
276
276
|
│ ├── execution-state.ts session/output phase state
|
|
277
|
-
│ ├──
|
|
277
|
+
│ ├── child-lifecycle.ts child-execution lifecycle event publisher
|
|
278
|
+
│ ├── workspace.ts workspace provider seam (generative extension surface)
|
|
278
279
|
│ ├── worktree.ts git worktree isolation
|
|
279
280
|
│ ├── worktree-isolation.ts worktree lifecycle collaborator
|
|
280
281
|
│ └── usage.ts token usage tracking
|
|
@@ -352,15 +353,18 @@ They declare this package as an optional peer dependency and use dynamic import
|
|
|
352
353
|
- `AgentManager` — spawn, abort, resume, collection management, observer wiring.
|
|
353
354
|
- `ConcurrencyQueue` — background agent scheduling with configurable concurrency limit.
|
|
354
355
|
- `agent-runner` — session creation, turn loop, extension binding.
|
|
355
|
-
- `
|
|
356
|
-
|
|
356
|
+
- `child-lifecycle` — publishes the child-execution lifecycle (`spawning`, `session-created` before `bindExtensions()`, `completed`, `disposed`) on `pi.events`.
|
|
357
|
+
Reactive consumers subscribe: `@gotgenes/pi-permission-system` registers each child session on `session-created` and unregisters it on `disposed`.
|
|
358
|
+
This replaced the former outbound `permission-bridge` (#261, ADR 0002) — the core no longer looks up a named consumer.
|
|
359
|
+
- `workspace` — the single generative seam (#262, ADR 0002): a registered `WorkspaceProvider` supplies a child's cwd plus bracketed `dispose()` at run-start.
|
|
360
|
+
With no provider, children run in the parent cwd (default unchanged); the git worktree strategy moves behind this seam in #263.
|
|
357
361
|
- `session-config` — pure configuration assembler (extracted from `agent-runner`).
|
|
358
362
|
- `SubagentRuntime` — session-scoped state bag with methods.
|
|
359
363
|
- `ParentSnapshot` — immutable snapshot of parent session state, captured once at spawn time.
|
|
360
364
|
- `record-observer` — session-event observer that updates record statistics without callback threading.
|
|
361
365
|
- Agent type registry — default agents, custom `.md` file loading.
|
|
362
366
|
- Prompt assembly, context extraction, skills, environment.
|
|
363
|
-
- Worktree isolation.
|
|
367
|
+
- Worktree isolation — moving to `@gotgenes/pi-subagents-worktrees` via the workspace provider seam in Phase 16 (ADR 0002).
|
|
364
368
|
- Token usage tracking.
|
|
365
369
|
- Session directory derivation and persisted `SessionManager` for subagent transcripts.
|
|
366
370
|
- Settings persistence.
|
|
@@ -449,9 +453,34 @@ These are fire-and-forget broadcast events — no request IDs, no reply channels
|
|
|
449
453
|
|
|
450
454
|
## Target architecture
|
|
451
455
|
|
|
452
|
-
The long-term architectural direction is to make pi-subagents a **minimal
|
|
453
|
-
|
|
454
|
-
|
|
456
|
+
The long-term architectural direction is to make pi-subagents a **minimal orchestrator** with inverted dependencies.
|
|
457
|
+
The core spawns a child session derived from the parent, runs the turn loop, tracks and streams and collects the result, gates concurrency, supports resume, and **publishes its lifecycle**.
|
|
458
|
+
Everything else — permissions, worktree/workspace isolation, UI, telemetry — is an extension that attaches through one of two surfaces and never reaches into the core.
|
|
459
|
+
|
|
460
|
+
The rationale and the full reasoning chain that led here are recorded in [`docs/decisions/0002-extensions-on-a-minimal-core.md`](../decisions/0002-extensions-on-a-minimal-core.md).
|
|
461
|
+
|
|
462
|
+
### Two extension surfaces
|
|
463
|
+
|
|
464
|
+
Extensions attach through exactly two surfaces, distinguished by the direction of information flow.
|
|
465
|
+
|
|
466
|
+
1. **Lifecycle events (observational) — unlimited.**
|
|
467
|
+
The core emits awaited, ordered events for the child-execution lifecycle (`spawning`, `session-created` pre-`bindExtensions`, `completed`, `disposed`).
|
|
468
|
+
Any number of extensions subscribe; handlers return nothing.
|
|
469
|
+
Reactive concerns live here: permission detection, telemetry, UI, notifications.
|
|
470
|
+
Adding a reactive concern never modifies the core.
|
|
471
|
+
2. **Provider seams (generative) — rationed.**
|
|
472
|
+
The rare concern that must *inject* a value the core consumes synchronously registers a provider the core consults.
|
|
473
|
+
Today there is exactly one: the **workspace provider** (returns the child's working directory plus bracketed setup/teardown).
|
|
474
|
+
A provider seam is the only place the core is "open," so the list is kept as small as possible.
|
|
475
|
+
|
|
476
|
+
The discriminator when deciding how a concern attaches:
|
|
477
|
+
|
|
478
|
+
- It only needs to **know** what happened → subscribe to a lifecycle event (observational, unlimited).
|
|
479
|
+
- It must **return a value the core consumes** → register a provider (generative, rationed).
|
|
480
|
+
|
|
481
|
+
The governing rule — **no vacant hooks**: the architecture must *admit* a seam without *shipping* it until a concrete consumer exists.
|
|
482
|
+
A provider seam with no consumer is a speculative abstraction that taxes every reader and that `fallow` flags as dead.
|
|
483
|
+
Latent extensibility is the deliverable; a vacant hook is not.
|
|
455
484
|
|
|
456
485
|
### Core responsibilities (keep)
|
|
457
486
|
|
|
@@ -460,27 +489,34 @@ The target state eliminates this overlap and flips the dependency direction.
|
|
|
460
489
|
- **Session lifecycle** — create child sessions, bind extensions, run conversation loop, track results.
|
|
461
490
|
- **Concurrency management** — queue, abort, resume, max concurrency.
|
|
462
491
|
- **Recursion guard** — remove pi-subagents' own three tools from child sessions (prevent infinite nesting).
|
|
463
|
-
|
|
492
|
+
With `isolated` removed, children always load the parent's resources, so the guard becomes unconditional rather than gated on `cfg.extensions`.
|
|
493
|
+
This is the core defending its own invariant, keyed off its own tool names — not policy.
|
|
494
|
+
- **Lifecycle events** — emit awaited, ordered events when child sessions spawn, are created, complete, and are disposed.
|
|
495
|
+
- **Workspace provider seam** — accept a registered `WorkspaceProvider` and consult it for the child's cwd; default to the parent's cwd when none is registered.
|
|
464
496
|
- **Service API** — publish `SubagentsService` via `Symbol.for()` for cross-extension access.
|
|
465
497
|
|
|
466
498
|
### Responsibilities to remove
|
|
467
499
|
|
|
468
500
|
- **Tool policy** (`disallowed_tools`) — access control belongs in pi-permission-system's `permission:` frontmatter.
|
|
469
501
|
- **Extension filtering** (`extensions: string[]` allowlist) — tool visibility is pi-permission-system's job.
|
|
470
|
-
- **
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
502
|
+
- **Worktree isolation** (`worktree.ts`, `worktree-isolation.ts`, `GitWorktreeManager`, the `isolation: "worktree"` spawn mode) — environment policy, not core.
|
|
503
|
+
Git worktrees are one *strategy* for choosing the child's working directory; containers, throwaway tmpdirs, and remote sandboxes are others.
|
|
504
|
+
These move to `@gotgenes/pi-subagents-worktrees`, the first consumer of the workspace provider seam.
|
|
505
|
+
- **Extension lifecycle control** (`extensions: false`, `isolated`, `noSkills`) — deny-at-use (the in-child permission layer blocking disallowed tool calls) covers what `isolated` pretended to do for tools.
|
|
506
|
+
Prevent-load (refusing to bind an extension because of load-time side effects, cost, or true sandboxing) is genuinely generative and is left as a *latent* (un-built) provider seam, added only if a real consumer needs it.
|
|
474
507
|
|
|
475
508
|
### Composition model
|
|
476
509
|
|
|
477
|
-
In the target state, pi-subagents publishes events and other packages hook in:
|
|
510
|
+
In the target state, pi-subagents publishes events and a provider seam; other packages hook in:
|
|
478
511
|
|
|
479
|
-
- **pi-permission-system**
|
|
512
|
+
- **pi-permission-system** (observational) subscribes to child-session lifecycle events, detects subagent execution context in the child, and gates tool calls at runtime.
|
|
513
|
+
- **pi-subagents-worktrees** (generative) registers a `WorkspaceProvider` that prepares a git worktree at run-start and tears it down after, supplying the child's cwd.
|
|
480
514
|
- **pi-subagents-ui** (future) subscribes to the service API, renders the widget, conversation viewer, and `/agents` menu.
|
|
481
|
-
- **Any future extension** (OTel, auditing, cost tracking)
|
|
515
|
+
- **Any future extension** (OTel, auditing, cost tracking) subscribes to the same events without pi-subagents knowing.
|
|
482
516
|
|
|
483
|
-
|
|
517
|
+
Composition test: install neither extension, only permissions, only workspaces, or both — the core is byte-for-byte identical in all four cases, and the two extensions never reference each other.
|
|
518
|
+
|
|
519
|
+
This is achieved across phases: Phase 14 (strip policy), Phase 16 (invert dependencies — extensions on a minimal core), and Phase 17 (extract UI).
|
|
484
520
|
|
|
485
521
|
## Current structural analysis
|
|
486
522
|
|
|
@@ -686,252 +722,109 @@ See [phase-14-strip-policy.md](history/phase-14-strip-policy.md) for details.
|
|
|
686
722
|
[#239]: https://github.com/gotgenes/pi-packages/issues/239
|
|
687
723
|
[#242]: https://github.com/gotgenes/pi-packages/issues/242
|
|
688
724
|
|
|
689
|
-
##
|
|
690
|
-
|
|
691
|
-
Phase 16 gives Agent proper collaborators so it can do its work without accumulating raw materials.
|
|
692
|
-
|
|
693
|
-
Phase 15 established the principle: Agent owns its lifecycle, not a manager.
|
|
694
|
-
But in practice, Agent received 9 raw config fields and a shared generic runner, then assembled the runner call itself.
|
|
695
|
-
The runner (`ConcreteAgentRunner`) is a stateless service — one instance shared across all agents — so every per-agent concern (snapshot, prompt, model, maxTurns, etc.) had to live on Agent as private fields.
|
|
696
|
-
The result: `AgentInit` has ~20 optional fields, and Agent stores ~87 `this._` references.
|
|
697
|
-
|
|
698
|
-
The deeper issue: the "runner" conflates two concerns.
|
|
699
|
-
Session *creation* (platform plumbing — resource loaders, extension binding, tool filtering, env detection) is genuinely separate from session *interaction* (prompt, steer, abort, resume).
|
|
700
|
-
Pi's own `Agent` class (in `packages/agent/`) already handles the interaction — it owns the transcript, runs the turn loop, executes tools, manages steering queues.
|
|
701
|
-
Our extension's novel value is **child session orchestration within a parent session**: creating child sessions with config derived from the parent, managing concurrency, wiring lifecycle across sessions, and enabling resume.
|
|
702
|
-
We should leverage the Pi session for interaction and focus on what's novel.
|
|
703
|
-
|
|
704
|
-
### Target architecture
|
|
705
|
-
|
|
706
|
-
Agent receives three collaborators at construction, each ready to go:
|
|
707
|
-
|
|
708
|
-
| Collaborator | Absorbs | Agent tells it |
|
|
709
|
-
| ---------------------- | ------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------- |
|
|
710
|
-
| Session factory | runner + snapshot + prompt + model + maxTurns + isolated + thinkingLevel + parentSession + getRunConfig (9 fields) | "create me a configured child session" |
|
|
711
|
-
| WorktreeIsolation | worktrees + isolation + worktreeState (3 fields) | `setup()`, `cleanup(description)` |
|
|
712
|
-
| AgentLifecycleObserver | (already exists, 0 new fields) | `onStarted`, `onSessionCreated`, `onRunFinished`, `onCompacted` |
|
|
713
|
-
|
|
714
|
-
After the session factory creates a session, Agent owns it directly — prompt, steer, abort, resume are Agent's verbs, not a collaborator's.
|
|
715
|
-
The shared `ConcreteAgentRunner` becomes a factory that produces per-agent session factories.
|
|
716
|
-
The "runner" concept dissolves.
|
|
717
|
-
|
|
718
|
-
`AgentInit` shrinks from ~20 to ~10 fields:
|
|
719
|
-
|
|
720
|
-
- 4 identity (`id`, `type`, `description`, `invocation`)
|
|
721
|
-
- 2 status (`status`, `startedAt` — for tests/restore)
|
|
722
|
-
- 3 collaborators (`sessionFactory`, `worktree`, `observer`)
|
|
723
|
-
- 1 wiring (`signal`)
|
|
724
|
-
|
|
725
|
-
Agent's `run()` becomes coordination, not assembly:
|
|
726
|
-
|
|
727
|
-
```text
|
|
728
|
-
mark running → notify observer → wire signal
|
|
729
|
-
→ tell worktree to setup
|
|
730
|
-
→ tell session factory to create session
|
|
731
|
-
→ own the session: flush steers, subscribe observers, prompt, track turns
|
|
732
|
-
→ on completion: tell worktree to cleanup, transition status, notify observer
|
|
733
|
-
```
|
|
734
|
-
|
|
735
|
-
Agent's `resume()` is trivially Agent's work — it already has the session:
|
|
736
|
-
|
|
737
|
-
```text
|
|
738
|
-
reset status → re-subscribe observer → prompt the existing session → transition status
|
|
739
|
-
```
|
|
740
|
-
|
|
741
|
-
### What we can commit to
|
|
742
|
-
|
|
743
|
-
1. **The runner is not a collaborator — it's Agent's core behavior conflated with a session factory.**
|
|
744
|
-
The shared `ConcreteAgentRunner` becomes a factory.
|
|
745
|
-
Each agent receives a per-agent session factory with config already bound.
|
|
746
|
-
Once the session exists, Agent interacts with it directly.
|
|
747
|
-
|
|
748
|
-
2. **WorktreeIsolation is a genuine collaborator.**
|
|
749
|
-
Created by the factory (AgentManager) only when `isolation === "worktree"`.
|
|
750
|
-
Agent tells it `setup()` and `cleanup()` instead of managing worktree internals.
|
|
751
|
-
The null check (`this.worktree?.setup()`) replaces the mode check (`this._isolation !== "worktree"`).
|
|
725
|
+
## Phase 15 (complete)
|
|
752
726
|
|
|
753
|
-
|
|
754
|
-
|
|
727
|
+
Phase 15 evolved `Agent` from a passive state machine (`AgentRecord`) into an object that owns its entire execution lifecycle.
|
|
728
|
+
Before Phase 15, `AgentManager` orchestrated everything: calling the runner, handling session creation, wiring observers, and cleaning up worktrees — reaching into Agent 10+ times across `spawn()` and `startAgent()`.
|
|
729
|
+
After Phase 15, Agent is born complete with all dependencies and configuration, owns `run()` and `resume()`, and manages its own observer and worktree lifecycle.
|
|
730
|
+
All six steps are closed: [#227], [#228], [#231], [#229], [#230], [#232].
|
|
731
|
+
See [phase-15-domain-model-evolution.md](history/phase-15-domain-model-evolution.md) for details.
|
|
755
732
|
|
|
756
|
-
|
|
757
|
-
~20 optional fields → ~10, with clear grouping: identity + collaborators + wiring.
|
|
733
|
+
## Improvement roadmap (Phase 16 — invert dependencies: extensions on a minimal core)
|
|
758
734
|
|
|
759
|
-
|
|
735
|
+
Phase 16 reclaims its original intent — invert the core's outbound dependencies — and extends it: worktree isolation joins permissions as an *extension* on a minimal core, leaving pi-subagents a pure child-session orchestrator.
|
|
736
|
+
The decision and the full reasoning chain are recorded in [ADR 0002](../decisions/0002-extensions-on-a-minimal-core.md); the two-surface extension model is described under [Target architecture](#target-architecture).
|
|
760
737
|
|
|
761
|
-
|
|
738
|
+
### Abandoned exploration: agent collaborator architecture
|
|
762
739
|
|
|
763
|
-
|
|
740
|
+
An earlier Phase 16 plan ("agent collaborator architecture") proposed giving `Agent` three collaborators — a session factory, a `WorktreeIsolation`, and a lifecycle observer — and dissolving the runner.
|
|
741
|
+
That framing was abandoned.
|
|
742
|
+
Pulling on a single late-bound `create(cwd?)` parameter on the planned `ChildSessionFactory` exposed deeper problems:
|
|
764
743
|
|
|
765
|
-
|
|
744
|
+
- `WorktreeIsolation.setup()` is a two-phase `construct-then-setup()` that violates "Construct complete" (principle 8) — the worktree is only *ready* at dequeue.
|
|
745
|
+
- The worktree and the child session share one lifespan, so they are one run-scoped resource, not sibling collaborators that `Agent` must sequence; the `cwd` parameter only existed because the worktree was split out and `Agent` relayed its output back in.
|
|
746
|
+
- Worktrees are not intrinsic to subagents — they are one *workspace strategy* and belong outside the core, exactly as Phase 14 evicted tool/extension policy.
|
|
766
747
|
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
| Prompt (initial + resume) | `session.prompt(text)` — works for both; calling it again on an existing session IS resume |
|
|
770
|
-
| Steer | `session.steer(text)` |
|
|
771
|
-
| Abort | `session.abort()` — async, waits for idle |
|
|
772
|
-
| Subscribe to events | `session.subscribe(listener)` — turn_end, message_end, tool_execution_end, compaction_end |
|
|
773
|
-
| Read messages | `session.messages` |
|
|
774
|
-
| Get session file | `session.sessionManager.getSessionFile()` |
|
|
775
|
-
| Dispose | `session.dispose()` |
|
|
776
|
-
|
|
777
|
-
Key finding: `session.prompt(text)` handles both initial run and resume — our current `resumeAgent()` already just calls this.
|
|
778
|
-
The core Pi `Agent` (accessible via `session.agent`) owns the transcript, turn loop, tool execution, and steering/follow-up queues.
|
|
779
|
-
Our Agent should call `session.prompt()` directly and subscribe to events for turn-limit enforcement.
|
|
780
|
-
|
|
781
|
-
#### 2. Session factory boundary — resolved
|
|
782
|
-
|
|
783
|
-
The factory encapsulates everything *before* Agent starts using the session.
|
|
784
|
-
The seam is clean: factory produces a ready-to-use `AgentSession`, Agent operates it.
|
|
785
|
-
|
|
786
|
-
```text
|
|
787
|
-
Factory creates (platform plumbing):
|
|
788
|
-
detect env → assemble config → create resource loader → reload
|
|
789
|
-
→ create session manager → new session
|
|
790
|
-
→ createAgentSession() → bindExtensions() → filter tools (recursion guard)
|
|
791
|
-
→ register with permission bridge
|
|
792
|
-
→ return { session, outputFile, cleanup }
|
|
793
|
-
|
|
794
|
-
Agent takes over (novel orchestration):
|
|
795
|
-
→ subscribe for turn tracking (maxTurns + graceTurns)
|
|
796
|
-
→ session.prompt(text)
|
|
797
|
-
→ collect response from session.messages
|
|
798
|
-
→ session.steer() / session.abort() for turn limits
|
|
799
|
-
→ call cleanup() when done
|
|
800
|
-
```
|
|
801
|
-
|
|
802
|
-
Factory input: per-agent config (snapshot, prompt, model, maxTurns, isolated, thinkingLevel, parentSession) bound at construction, plus per-call `cwd` from worktree.
|
|
803
|
-
Factory output: `{ session: AgentSession, outputFile?: string, cleanup: () => void }`.
|
|
804
|
-
|
|
805
|
-
#### 3. Turn-limit enforcement — Agent's job via session subscription
|
|
806
|
-
|
|
807
|
-
Agent subscribes to session events and enforces turn limits — this is novel orchestration that Pi's Agent doesn't provide:
|
|
808
|
-
|
|
809
|
-
```typescript
|
|
810
|
-
session.subscribe((event) => {
|
|
811
|
-
if (event.type === "turn_end") {
|
|
812
|
-
turnCount++;
|
|
813
|
-
if (turnCount >= maxTurns) session.steer("wrap up");
|
|
814
|
-
if (turnCount >= maxTurns + graceTurns) session.abort();
|
|
815
|
-
}
|
|
816
|
-
});
|
|
817
|
-
```
|
|
818
|
-
|
|
819
|
-
This uses `session.subscribe()`, `session.steer()`, and `session.abort()` directly.
|
|
820
|
-
No runner involvement needed.
|
|
821
|
-
|
|
822
|
-
#### 4. Response collection — Agent's job, simplified
|
|
823
|
-
|
|
824
|
-
Agent collects the response directly from `session.messages` after `prompt()` completes.
|
|
825
|
-
The existing `getLastAssistantText()` helper (which reads `session.messages`) already works as a fallback.
|
|
826
|
-
The streaming `collectResponseText()` subscriber can move onto Agent for real-time text collection during the run.
|
|
827
|
-
|
|
828
|
-
#### 5. Permission bridge — factory-internal
|
|
829
|
-
|
|
830
|
-
The bridge calls (`registerChildSession` / `unregisterChildSession`) bracket `bindExtensions()` inside the factory.
|
|
831
|
-
Since the factory owns `createAgentSession()` and `bindExtensions()`, both bridge calls become factory-internal.
|
|
832
|
-
The factory returns a `cleanup()` function that Agent calls on completion; `cleanup()` handles `unregisterChildSession()` along with any other teardown.
|
|
833
|
-
Agent never sees or imports the permission bridge.
|
|
834
|
-
This naturally resolves the original Phase 16 dependency-inversion concern.
|
|
748
|
+
Issue #256 (`WorktreeIsolation` as a collaborator) shipped under the abandoned plan and is now superseded by #263; issue #257 (`ChildSessionFactory` extraction) was parked.
|
|
749
|
+
The structural win the collaborator plan chased — a born-complete child execution and the dissolution of the runner — is recovered by a cleaner route once the workspace seam exists (Step 5).
|
|
835
750
|
|
|
836
751
|
### Steps
|
|
837
752
|
|
|
838
|
-
#### Step 1:
|
|
753
|
+
#### Step 1: Child-execution lifecycle events; retire permission-bridge — [#261] ✅ Delivered
|
|
839
754
|
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
The null check (`this.worktree?.setup()`) replaces the mode check (`this._isolation !== "worktree"`).
|
|
843
|
-
AgentManager creates the collaborator only when `isolation === "worktree"` and passes it to Agent ready to go.
|
|
755
|
+
Emit ordered child-execution events (`spawning`, `session-created` before `bindExtensions()`, `completed`, `disposed`) carrying child identity (session directory, agent name, parent session id).
|
|
756
|
+
Migrate `@gotgenes/pi-permission-system` to subscribe to `session-created`/`disposed` for registration instead of being looked up by the core; delete `permission-bridge.ts`.
|
|
844
757
|
|
|
845
|
-
-
|
|
846
|
-
-
|
|
847
|
-
|
|
758
|
+
- Cross-package: pi-subagents (emit + remove bridge) and pi-permission-system (subscribe).
|
|
759
|
+
- Investigation (resolved): `pi.events` is a Node `EventEmitter`, so `emit()` dispatches listeners synchronously on the same call stack — a synchronous subscriber completes before `emit()` returns.
|
|
760
|
+
Emitting `session-created` immediately before `bindExtensions()` therefore guarantees the registry entry lands pre-bind, with no new SDK hook.
|
|
761
|
+
The synchronous-handler constraint is encoded as a real-bus test in pi-permission-system.
|
|
762
|
+
- Outcome: the core stops reaching out to a named consumer; permission detection rides events.
|
|
763
|
+
- Deferred: removing the now-caller-less `registerSubagentSession`/`unregisterSubagentSession` from `PermissionsService` → #267; registry-detected resume ("executing now" → "exists" semantics) → #265.
|
|
848
764
|
|
|
849
|
-
#### Step 2:
|
|
765
|
+
#### Step 2: Define the `WorkspaceProvider` seam — [#262] ✅ Delivered
|
|
850
766
|
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
`
|
|
855
|
-
Permission bridge calls (`registerChildSession` / `unregisterChildSession`) move inside the factory.
|
|
767
|
+
Added the `WorkspaceProvider` / `Workspace` interfaces (`src/lifecycle/workspace.ts`) and `SubagentsService.registerWorkspaceProvider` (single provider, throws on duplicate, returns an unregister disposer).
|
|
768
|
+
Only `WorkspaceProvider` is named-re-exported from `service.ts`; `Workspace` and the context types resolve via inference when a consumer assigns to `WorkspaceProvider` (the worktrees package adds named re-exports in #263 when it imports them by name).
|
|
769
|
+
At run-start `Agent.run()` consults the registered provider (provider-first precedence) for the child's cwd and a disposal handle; with no provider it falls back to the legacy worktree collaborator, and with neither the child runs in the parent's cwd.
|
|
770
|
+
On completion the core calls `Workspace.dispose({ status, description })` and appends the returned `resultAddendum` verbatim — the provider owns the wording.
|
|
856
771
|
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
772
|
+
- The seam is additive and non-breaking: the existing `isolation: "worktree"` path is untouched (its eviction is Step 3).
|
|
773
|
+
- Land alongside its first consumer (Step 3) to avoid a vacant hook — the "no vacant hooks" rule.
|
|
774
|
+
Within #262 the seam is exercised only by test fakes; do not cut a release containing the seam without `@gotgenes/pi-subagents-worktrees`.
|
|
775
|
+
- Outcome: a single generative seam; the core no longer knows what an "isolation strategy" is.
|
|
861
776
|
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
}
|
|
867
|
-
```
|
|
777
|
+
#### Step 3: Extract worktrees to `@gotgenes/pi-subagents-worktrees` — [#263]
|
|
778
|
+
|
|
779
|
+
New package implementing `WorkspaceProvider`: prepares a git worktree at run-start (born complete), tears it down after (saving the branch), and owns the "changes saved to branch" result.
|
|
780
|
+
Remove `worktree.ts`, `worktree-isolation.ts`, `GitWorktreeManager`, and the `isolation: "worktree"` mode from the core; drop `isolation` from the spawn API and `SubagentsService`.
|
|
868
781
|
|
|
869
|
-
-
|
|
870
|
-
|
|
871
|
-
- Outcome:
|
|
782
|
+
- Supersedes #256.
|
|
783
|
+
New package registered in `release-please-config.json`; peer-depends on `@gotgenes/pi-subagents`.
|
|
784
|
+
- Outcome: git leaves the core; worktree users install one package, everyone else pays nothing.
|
|
872
785
|
|
|
873
|
-
#### Step
|
|
786
|
+
#### Step 4: Remove `isolated` / `extensions: false` / `noSkills` — [#264]
|
|
874
787
|
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
Agent's `resume()` calls `session.prompt()` directly — the session already exists from the initial run.
|
|
878
|
-
`AgentInit` shrinks: loses `_runner`, `_snapshot`, `_prompt`, `_model`, `_maxTurns`, `_isolated`, `_thinkingLevel`, `_parentSession`, `_getRunConfig` (9 fields); gains `factory` (1 field).
|
|
879
|
-
Combined with Step 1, AgentInit goes from ~20 to ~10 fields.
|
|
788
|
+
Children always load the parent's extensions and skills; the recursion guard becomes unconditional.
|
|
789
|
+
Deny-at-use (the in-child permission layer) covers tool restriction; prevent-load is left as a latent provider seam (not shipped).
|
|
880
790
|
|
|
881
|
-
- Depends on: Step 1 (
|
|
882
|
-
-
|
|
883
|
-
- Smell: C (Agent assembles 9 raw fields into a runner call instead of telling a collaborator) + B (runner conflates creation and interaction)
|
|
884
|
-
- Outcome: Agent owns session interaction; `run()` is coordination not assembly; `resume()` is trivially `session.prompt()`; AgentInit has ~10 fields
|
|
791
|
+
- Depends on: Step 1 (deny-at-use over events).
|
|
792
|
+
- Outcome: the `isolated`/`extensions`/`noSkills` axis is gone; one fewer conditional in the guard.
|
|
885
793
|
|
|
886
|
-
#### Step
|
|
794
|
+
#### Step 5: Born-complete child execution; dissolve the runner — [#265]
|
|
887
795
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
Retain `getAgentConversation()` (used by conversation viewer) and `normalizeMaxTurns()` (used by spawn-config).
|
|
796
|
+
With the cwd resolved through the provider seam (not relayed by `Agent`), child-session creation produces a born-complete execution (`{ session, outputFile?, dispose() }`).
|
|
797
|
+
`Agent` owns session interaction directly (prompt, steer, abort, resume, turn limits, response collection); `runAgent` / `resumeAgent` / `ConcreteAgentRunner` / `RunOptions` / `RunResult` dissolve.
|
|
798
|
+
Retain `getAgentConversation()` and `normalizeMaxTurns()`.
|
|
892
799
|
|
|
893
|
-
- Depends on:
|
|
894
|
-
-
|
|
895
|
-
- Smell: A (dead code after runner dissolution)
|
|
896
|
-
- Outcome: `agent-runner.ts` shrinks from 467 to ~50 lines (retained helpers only) or is deleted with helpers relocated; the "runner" concept is gone from the architecture
|
|
800
|
+
- Depends on: Steps 2–4.
|
|
801
|
+
- Outcome: the "runner" concept is gone; `Agent.run()` is coordination, not assembly — the structural goal of the abandoned collaborator plan, reached cleanly.
|
|
897
802
|
|
|
898
803
|
### Step dependency diagram
|
|
899
804
|
|
|
900
805
|
```mermaid
|
|
901
806
|
flowchart LR
|
|
902
|
-
S1["Step 1<br/>
|
|
903
|
-
S2["Step 2<br/>
|
|
904
|
-
S3["Step 3<br/>
|
|
905
|
-
S4["Step 4<br/>
|
|
807
|
+
S1["Step 1<br/>Lifecycle events<br/>(retire bridge)"]
|
|
808
|
+
S2["Step 2<br/>WorkspaceProvider seam"]
|
|
809
|
+
S3["Step 3<br/>Extract worktrees pkg"]
|
|
810
|
+
S4["Step 4<br/>Remove isolated"]
|
|
811
|
+
S5["Step 5<br/>Born-complete execution"]
|
|
906
812
|
|
|
907
|
-
S1 --> S3
|
|
908
813
|
S2 --> S3
|
|
909
|
-
|
|
814
|
+
S1 --> S4
|
|
815
|
+
S2 --> S5
|
|
816
|
+
S3 --> S5
|
|
817
|
+
S4 --> S5
|
|
910
818
|
```
|
|
911
819
|
|
|
912
820
|
### Tracks
|
|
913
821
|
|
|
914
|
-
1. **Track A —
|
|
822
|
+
1. **Track A — Inversion seams** (Steps 1, 2): lifecycle events and the workspace seam.
|
|
915
823
|
Independent of each other — can proceed in parallel.
|
|
916
|
-
2. **Track B —
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
The original Phase 16 ("invert dependencies") targeted permission-bridge removal, `extensions: false` removal, and `isolated` dissolution.
|
|
922
|
-
The permission-bridge concern is resolved by Step 2 — the factory handles registration internally, and Agent never imports the bridge.
|
|
923
|
-
The `extensions`/`isolated` concerns are secondary and may move to a later phase once the collaborator architecture is in place.
|
|
924
|
-
|
|
925
|
-
### Fallow health snapshot (2026-05-28)
|
|
926
|
-
|
|
927
|
-
| Metric | Value |
|
|
928
|
-
| ---------------------- | ------------------------------------------------------------------- |
|
|
929
|
-
| Health score | 78/100 (B) — deductions: hotspots -10, unit size -10, coupling -2.5 |
|
|
930
|
-
| Dead code | 0 files, 0 exports |
|
|
931
|
-
| Production duplication | 11 lines (1 internal clone in `agent-config-editor.ts`) |
|
|
932
|
-
| Test duplication | 42 clone groups, 661 lines (3.1%) |
|
|
933
|
-
| Hotspot #1 | `index.ts` — 70.0, accelerating (128 commits) |
|
|
934
|
-
| Refactoring targets | 0 |
|
|
824
|
+
2. **Track B — Eviction** (Steps 3, 4): worktrees and `isolated` leave the core.
|
|
825
|
+
Step 3 depends on Step 2.
|
|
826
|
+
3. **Track C — Consolidation** (Step 5): dissolve the runner around the new seam.
|
|
827
|
+
Depends on Tracks A and B.
|
|
935
828
|
|
|
936
829
|
## Improvement roadmap (Phase 17 — extract UI)
|
|
937
830
|
|
|
@@ -962,28 +855,29 @@ Detailed records are preserved in per-phase history files:
|
|
|
962
855
|
| 13 | Remaining structural smells | Complete | [phase-13-remaining-smells.md](history/phase-13-remaining-smells.md) |
|
|
963
856
|
| 14 | Strip policy from core | Complete | [phase-14-strip-policy.md](history/phase-14-strip-policy.md) |
|
|
964
857
|
| 15 | Domain model evolution | Complete | [phase-15-domain-model-evolution.md](history/phase-15-domain-model-evolution.md) |
|
|
965
|
-
| 16 |
|
|
858
|
+
| 16 | Invert dependencies (extensions on a minimal core) | Planned | [ADR 0002](../decisions/0002-extensions-on-a-minimal-core.md) |
|
|
966
859
|
| 17 | Extract UI to separate package | Planned | — |
|
|
967
860
|
|
|
968
861
|
### Structural refactoring issues
|
|
969
862
|
|
|
970
|
-
| Phase
|
|
971
|
-
|
|
|
972
|
-
| Foundation
|
|
973
|
-
| Core decomposition
|
|
974
|
-
| Interface polish
|
|
975
|
-
| Features
|
|
976
|
-
| AgentManager
|
|
977
|
-
| Encapsulation
|
|
978
|
-
| Testability
|
|
979
|
-
| Observation/ctx
|
|
980
|
-
| Phase 10
|
|
981
|
-
| Phase 11
|
|
982
|
-
| Phase 12
|
|
983
|
-
| Phase 13
|
|
984
|
-
| Phase 14
|
|
985
|
-
| Phase 15
|
|
986
|
-
| Phase 16
|
|
863
|
+
| Phase | Issue | Summary |
|
|
864
|
+
| -------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
865
|
+
| Foundation | #69, #71, #76, #80 | SubagentRuntime, pure assembler, cwd injection, config consolidation |
|
|
866
|
+
| Core decomposition | #84, #72, #87, #70 | WorktreeManager, AgentManager DI, runtime methods, handler extraction |
|
|
867
|
+
| Interface polish | #66, #77 | SDK types, projectAgentsDir |
|
|
868
|
+
| Features | #61 | JSONL session transcripts |
|
|
869
|
+
| AgentManager | #98, #99, #100, #102 | Record state machine, ParentSnapshot, session-event observation, test factory |
|
|
870
|
+
| Encapsulation | #108, #109, #110, #111, #112, #113, #114, #115, #116, #118 | Registry, settings, activity tracker, record lifecycle, observer, spawn options, deps narrowing, tool split, type housekeeping |
|
|
871
|
+
| Testability | #131, #132, #133, #134, #135, #136 | Shared fixtures, session-config IO, runner SDK boundary, as-any reduction, display extraction, menu decomposition |
|
|
872
|
+
| Observation/ctx | #144, #145, #146, #147, #148 | Observation consolidation, execute decomposition, UI context, text wrapping injection, widget rendering split |
|
|
873
|
+
| Phase 10 | #164, #165, #166, #167, #168, #169, #170, #171, #172 | Domain directories, ResolvedSpawnConfig, ParentSessionInfo, RunnerIO split, ToolFilterConfig, RunContext, buildContentLines, renderResult, content-items |
|
|
874
|
+
| Phase 11 | #192, #193, #194, #195, #196 | SessionContext, runtime queries, interface alignment, tool classes, runner/menu classes, index.ts simplification |
|
|
875
|
+
| Phase 12 | #205, #206, #207, #208 | renderWidgetLines, showAgentDetail, widget update, shared test fixtures |
|
|
876
|
+
| Phase 13 | #214, #215, #216, #217, #218, #219 | Closure-to-class, buildParentContext, startAgent decomp, overwrite guard, settings SDK, test duplication |
|
|
877
|
+
| Phase 14 | #237, #238, #239, #242 | Remove disallowed_tools, remove extensions filtering, collapse filterActiveTools, rename Agent to subagent |
|
|
878
|
+
| Phase 15 | #227, #228, #231, #229, #230, #232 | Agent domain model, async startAgent, runner self-contained, Agent.run(), ConcurrencyQueue, Agent.resume() |
|
|
879
|
+
| Phase 16 | #261, #262, #263, #264, #265 | Lifecycle events (retire permission-bridge), WorkspaceProvider seam, extract worktrees package, remove isolated, born-complete execution / dissolve runner |
|
|
880
|
+
| Phase 16 (abandoned) | #256 (superseded), #257 (parked) | Agent collaborator architecture — replaced by the inversion approach above (ADR 0002) |
|
|
987
881
|
|
|
988
882
|
The remaining open issue is #22 (parent-session resolution), a cross-extension track that does not gate the structural work.
|
|
989
883
|
|
|
@@ -1016,8 +910,14 @@ The upstream test suite is run periodically as a regression canary for the agent
|
|
|
1016
910
|
[#217]: https://github.com/gotgenes/pi-packages/issues/217
|
|
1017
911
|
[#218]: https://github.com/gotgenes/pi-packages/issues/218
|
|
1018
912
|
[#219]: https://github.com/gotgenes/pi-packages/issues/219
|
|
913
|
+
[#227]: https://github.com/gotgenes/pi-packages/issues/227
|
|
914
|
+
[#228]: https://github.com/gotgenes/pi-packages/issues/228
|
|
915
|
+
[#229]: https://github.com/gotgenes/pi-packages/issues/229
|
|
916
|
+
[#230]: https://github.com/gotgenes/pi-packages/issues/230
|
|
1019
917
|
[#231]: https://github.com/gotgenes/pi-packages/issues/231
|
|
1020
|
-
[#
|
|
1021
|
-
[#
|
|
1022
|
-
[#
|
|
1023
|
-
[#
|
|
918
|
+
[#232]: https://github.com/gotgenes/pi-packages/issues/232
|
|
919
|
+
[#261]: https://github.com/gotgenes/pi-packages/issues/261
|
|
920
|
+
[#262]: https://github.com/gotgenes/pi-packages/issues/262
|
|
921
|
+
[#263]: https://github.com/gotgenes/pi-packages/issues/263
|
|
922
|
+
[#264]: https://github.com/gotgenes/pi-packages/issues/264
|
|
923
|
+
[#265]: https://github.com/gotgenes/pi-packages/issues/265
|