@gotgenes/pi-subagents 18.0.1 → 18.0.2

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.
Files changed (260) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/docs/architecture/architecture.md +157 -204
  3. package/docs/architecture/history/phase-19-implement-ui-decisions.md +282 -0
  4. package/package.json +5 -6
  5. package/src/lifecycle/subagent.ts +5 -9
  6. package/src/observation/notification.ts +23 -10
  7. package/src/observation/subagent-events-observer.ts +1 -3
  8. package/src/tools/get-result-report.ts +60 -0
  9. package/src/tools/get-result-tool.ts +25 -40
  10. package/src/types.ts +1 -1
  11. package/src/ui/session-navigator.ts +0 -2
  12. package/.prettierignore +0 -5
  13. package/AGENTS.md +0 -7
  14. package/docs/plans/0048-implement-subagents-api.md +0 -304
  15. package/docs/plans/0049-remove-group-join-output-file-rpc.md +0 -180
  16. package/docs/plans/0051-update-adr-0001-hard-fork.md +0 -77
  17. package/docs/plans/0052-remove-scheduled-subagents.md +0 -133
  18. package/docs/plans/0053-extract-model-resolution-from-execute.md +0 -181
  19. package/docs/plans/0054-decompose-index-into-modules.md +0 -302
  20. package/docs/plans/0057-structured-debug-logging.md +0 -154
  21. package/docs/plans/0061-session-format-transcript.md +0 -284
  22. package/docs/plans/0066-replace-as-any-with-sdk-types.md +0 -215
  23. package/docs/plans/0069-create-subagent-runtime.md +0 -345
  24. package/docs/plans/0070-extract-event-handlers.md +0 -306
  25. package/docs/plans/0071-extract-session-config-assembler.md +0 -362
  26. package/docs/plans/0072-inject-agent-manager-collaborators.md +0 -329
  27. package/docs/plans/0076-inject-cwd-into-agent-manager.md +0 -102
  28. package/docs/plans/0077-inject-project-agents-dir.md +0 -115
  29. package/docs/plans/0080-consolidate-agent-config-lookup.md +0 -247
  30. package/docs/plans/0084-extract-git-worktree-manager.md +0 -142
  31. package/docs/plans/0087-evolve-subagent-runtime-methods.md +0 -240
  32. package/docs/plans/0098-extract-agent-record-state-machine.md +0 -435
  33. package/docs/plans/0099-replace-ctx-with-parent-snapshot.md +0 -488
  34. package/docs/plans/0100-replace-callback-threading-with-session-subscription.md +0 -454
  35. package/docs/plans/0102-consolidate-test-record-factory.md +0 -176
  36. package/docs/plans/0108-extract-agent-type-registry.md +0 -322
  37. package/docs/plans/0109-extract-settings-manager.md +0 -276
  38. package/docs/plans/0110-agent-activity-tracker.md +0 -297
  39. package/docs/plans/0111-split-agent-record-lifecycle.md +0 -582
  40. package/docs/plans/0112-replace-agent-manager-callbacks.md +0 -241
  41. package/docs/plans/0113-disambiguate-spawn-options.md +0 -155
  42. package/docs/plans/0114-narrow-agent-tool-menu-deps.md +0 -279
  43. package/docs/plans/0115-decompose-agent-tool.md +0 -337
  44. package/docs/plans/0116-type-housekeeping.md +0 -351
  45. package/docs/plans/0118-settings-manager-apply-methods.md +0 -271
  46. package/docs/plans/0123-remove-vi-fn-cast-smell.md +0 -179
  47. package/docs/plans/0131-consolidate-shared-test-fixtures.md +0 -207
  48. package/docs/plans/0132-inject-io-into-session-config.md +0 -219
  49. package/docs/plans/0133-inject-sdk-boundary-into-agent-runner.md +0 -373
  50. package/docs/plans/0134-reduce-as-any-casts.md +0 -366
  51. package/docs/plans/0135-extract-display-helpers.md +0 -182
  52. package/docs/plans/0136-decompose-agent-menu.md +0 -300
  53. package/docs/plans/0144-consolidate-observation-model.md +0 -263
  54. package/docs/plans/0145-decompose-execute-push-ctx-to-boundary.md +0 -290
  55. package/docs/plans/0146-narrow-ui-context.md +0 -319
  56. package/docs/plans/0147-inject-wrap-text-into-conversation-viewer.md +0 -166
  57. package/docs/plans/0148-split-agent-widget-rendering.md +0 -255
  58. package/docs/plans/0152-add-prompt-snippet.md +0 -91
  59. package/docs/plans/0164-reorganize-into-domain-directories.md +0 -409
  60. package/docs/plans/0165-decompose-resolved-spawn-config.md +0 -157
  61. package/docs/plans/0166-extract-parent-session-info.md +0 -231
  62. package/docs/plans/0167-narrow-runner-io.md +0 -150
  63. package/docs/plans/0168-extract-tool-filter-config.md +0 -173
  64. package/docs/plans/0169-extract-run-context-from-run-options.md +0 -194
  65. package/docs/plans/0170-reduce-build-content-lines-complexity.md +0 -225
  66. package/docs/plans/0171-reduce-render-result-complexity.md +0 -200
  67. package/docs/plans/0172-extract-turn-formatting.md +0 -206
  68. package/docs/plans/0180-reorder-append-prompt-for-kv-cache.md +0 -100
  69. package/docs/plans/0185-remove-persistent-agent-memory.md +0 -161
  70. package/docs/plans/0188-replace-any-casts-with-sdk-types.md +0 -162
  71. package/docs/plans/0192-define-session-context-interface.md +0 -107
  72. package/docs/plans/0193-runtime-owns-context-queries.md +0 -245
  73. package/docs/plans/0194-align-tool-interfaces-for-structural-typing.md +0 -139
  74. package/docs/plans/0195-convert-tool-factories-to-classes.md +0 -264
  75. package/docs/plans/0196-convert-runner-menu-to-classes.md +0 -268
  76. package/docs/plans/0205-decompose-render-widget-lines.md +0 -140
  77. package/docs/plans/0206-decompose-show-agent-detail.md +0 -211
  78. package/docs/plans/0207-decompose-agent-widget-update.md +0 -242
  79. package/docs/plans/0208-extract-shared-test-fixtures.md +0 -298
  80. package/docs/plans/0214-convert-remaining-closure-factories-to-classes.md +0 -261
  81. package/docs/plans/0215-decompose-build-parent-context.md +0 -166
  82. package/docs/plans/0216-decompose-start-agent.md +0 -255
  83. package/docs/plans/0217-extract-overwrite-guard.md +0 -176
  84. package/docs/plans/0218-push-sdk-boundary-in-settings.md +0 -172
  85. package/docs/plans/0219-reduce-test-duplication-top-3-clone-families.md +0 -162
  86. package/docs/plans/0227-evolve-agent-record-into-agent.md +0 -322
  87. package/docs/plans/0228-async-start-agent-dissolve-run-handle.md +0 -288
  88. package/docs/plans/0229-agent-born-complete.md +0 -564
  89. package/docs/plans/0230-extract-concurrency-queue.md +0 -265
  90. package/docs/plans/0231-push-exec-registry-to-runner.md +0 -245
  91. package/docs/plans/0232-agent-resume-internal-observer-lifecycle.md +0 -180
  92. package/docs/plans/0237-remove-disallowed-tools.md +0 -180
  93. package/docs/plans/0238-remove-extensions-filtering.md +0 -191
  94. package/docs/plans/0239-collapse-filter-active-tools.md +0 -217
  95. package/docs/plans/0242-rename-agent-tool-to-subagent.md +0 -165
  96. package/docs/plans/0256-extract-worktree-isolation.md +0 -256
  97. package/docs/plans/0257-extract-child-session-factory.md +0 -285
  98. package/docs/plans/0262-add-workspace-provider-seam.md +0 -264
  99. package/docs/plans/0264-remove-extension-lifecycle-control.md +0 -277
  100. package/docs/plans/0265-born-complete-subagent-session.md +0 -333
  101. package/docs/plans/0270-type-consumable-public-surface.md +0 -204
  102. package/docs/plans/0272-export-workspace-collaborator-types.md +0 -147
  103. package/docs/plans/0277-encapsulate-agent-session.md +0 -304
  104. package/docs/plans/0280-rename-agent-to-subagent.md +0 -197
  105. package/docs/plans/0360-custom-agents-default-append.md +0 -132
  106. package/docs/plans/0373-extract-subagent-state.md +0 -250
  107. package/docs/plans/0374-encapsulate-subagent-start-notification.md +0 -268
  108. package/docs/plans/0375-extract-run-listener-workspace-bracket.md +0 -300
  109. package/docs/plans/0376-extract-manager-observer.md +0 -232
  110. package/docs/plans/0377-split-widget-delegation-from-runtime.md +0 -254
  111. package/docs/plans/0378-consolidate-lifecycle-test-fixtures.md +0 -243
  112. package/docs/plans/0379-consolidate-ui-tools-test-fixtures.md +0 -335
  113. package/docs/plans/0380-extract-layered-settings-loader.md +0 -223
  114. package/docs/plans/0381-replace-concurrency-queue-with-limiter.md +0 -267
  115. package/docs/plans/0400-include-parent-prompt-in-replace-mode.md +0 -199
  116. package/docs/plans/0403-abort-subagents-on-interrupt.md +0 -180
  117. package/docs/plans/0412-unify-session-mock-builders.md +0 -146
  118. package/docs/plans/0420-fold-run-metrics-live-activity-onto-record.md +0 -228
  119. package/docs/plans/0421-migrate-activity-readers-to-record-getters.md +0 -309
  120. package/docs/plans/0422-delete-activity-tracker-ui-observer.md +0 -204
  121. package/docs/plans/0423-widget-self-drive-from-lifecycle.md +0 -264
  122. package/docs/plans/0424-drop-widget-dep-from-subagent-tool.md +0 -166
  123. package/docs/plans/0425-reconcile-subagent-events-contract.md +0 -144
  124. package/docs/plans/0426-consolidate-residual-test-clone-families.md +0 -235
  125. package/docs/plans/0427-reconsider-ui-direction.md +0 -212
  126. package/docs/plans/0441-remove-agent-definition-management-subtree.md +0 -180
  127. package/docs/plans/0442-dissolve-agents-conversation-viewer.md +0 -195
  128. package/docs/plans/0443-consolidate-remaining-test-clone-families.md +0 -268
  129. package/docs/plans/0444-shrink-widget-to-background-agents.md +0 -156
  130. package/docs/plans/0445-native-session-navigation.md +0 -365
  131. package/docs/plans/0446-spike-session-navigation-entry-criteria.md +0 -171
  132. package/docs/plans/0447-extract-subagents-settings-command.md +0 -220
  133. package/docs/plans/0448-block-disabled-agent-spawn.md +0 -207
  134. package/docs/plans/0462-navigation-renderer-tui-components.md +0 -338
  135. package/docs/plans/0463-file-snapshot-source-evicted-agents.md +0 -306
  136. package/docs/plans/0470-refresh-readme-command-surface.md +0 -159
  137. package/docs/retro/0048-implement-subagents-api.md +0 -44
  138. package/docs/retro/0049-remove-group-join-output-file-rpc.md +0 -49
  139. package/docs/retro/0051-update-adr-0001-hard-fork.md +0 -39
  140. package/docs/retro/0053-extract-model-resolution-from-execute.md +0 -40
  141. package/docs/retro/0054-decompose-index-into-modules.md +0 -53
  142. package/docs/retro/0057-structured-debug-logging.md +0 -77
  143. package/docs/retro/0061-session-format-transcript.md +0 -41
  144. package/docs/retro/0066-replace-as-any-with-sdk-types.md +0 -44
  145. package/docs/retro/0069-create-subagent-runtime.md +0 -43
  146. package/docs/retro/0070-extract-event-handlers.md +0 -49
  147. package/docs/retro/0071-extract-session-config-assembler.md +0 -60
  148. package/docs/retro/0072-inject-agent-manager-collaborators.md +0 -46
  149. package/docs/retro/0076-inject-cwd-into-agent-manager.md +0 -35
  150. package/docs/retro/0077-inject-project-agents-dir.md +0 -33
  151. package/docs/retro/0080-consolidate-agent-config-lookup.md +0 -35
  152. package/docs/retro/0084-extract-git-worktree-manager.md +0 -37
  153. package/docs/retro/0087-evolve-subagent-runtime-methods.md +0 -42
  154. package/docs/retro/0098-extract-agent-record-state-machine.md +0 -46
  155. package/docs/retro/0099-replace-ctx-with-parent-snapshot.md +0 -37
  156. package/docs/retro/0100-replace-callback-threading-with-session-subscription.md +0 -36
  157. package/docs/retro/0102-consolidate-test-record-factory.md +0 -30
  158. package/docs/retro/0108-extract-agent-type-registry.md +0 -41
  159. package/docs/retro/0109-extract-settings-manager.md +0 -55
  160. package/docs/retro/0110-agent-activity-tracker.md +0 -44
  161. package/docs/retro/0111-split-agent-record-lifecycle.md +0 -61
  162. package/docs/retro/0112-replace-agent-manager-callbacks.md +0 -35
  163. package/docs/retro/0113-disambiguate-spawn-options.md +0 -29
  164. package/docs/retro/0114-narrow-agent-tool-menu-deps.md +0 -38
  165. package/docs/retro/0115-decompose-agent-tool.md +0 -51
  166. package/docs/retro/0116-type-housekeeping.md +0 -42
  167. package/docs/retro/0118-settings-manager-apply-methods.md +0 -40
  168. package/docs/retro/0123-remove-vi-fn-cast-smell.md +0 -49
  169. package/docs/retro/0131-consolidate-shared-test-fixtures.md +0 -46
  170. package/docs/retro/0132-inject-io-into-session-config.md +0 -33
  171. package/docs/retro/0133-inject-sdk-boundary-into-agent-runner.md +0 -45
  172. package/docs/retro/0134-reduce-as-any-casts.md +0 -56
  173. package/docs/retro/0135-extract-display-helpers.md +0 -38
  174. package/docs/retro/0136-decompose-agent-menu.md +0 -43
  175. package/docs/retro/0144-consolidate-observation-model.md +0 -39
  176. package/docs/retro/0145-decompose-execute-push-ctx-to-boundary.md +0 -56
  177. package/docs/retro/0146-narrow-ui-context.md +0 -70
  178. package/docs/retro/0147-inject-wrap-text-into-conversation-viewer.md +0 -90
  179. package/docs/retro/0148-split-agent-widget-rendering.md +0 -39
  180. package/docs/retro/0152-add-prompt-snippet.md +0 -34
  181. package/docs/retro/0164-reorganize-into-domain-directories.md +0 -82
  182. package/docs/retro/0165-decompose-resolved-spawn-config.md +0 -78
  183. package/docs/retro/0166-extract-parent-session-info.md +0 -66
  184. package/docs/retro/0167-narrow-runner-io.md +0 -63
  185. package/docs/retro/0168-extract-tool-filter-config.md +0 -76
  186. package/docs/retro/0169-extract-run-context-from-run-options.md +0 -68
  187. package/docs/retro/0170-reduce-build-content-lines-complexity.md +0 -72
  188. package/docs/retro/0171-reduce-render-result-complexity.md +0 -77
  189. package/docs/retro/0172-extract-turn-formatting.md +0 -80
  190. package/docs/retro/0180-reorder-append-prompt-for-kv-cache.md +0 -62
  191. package/docs/retro/0185-remove-persistent-agent-memory.md +0 -73
  192. package/docs/retro/0188-replace-any-casts-with-sdk-types.md +0 -69
  193. package/docs/retro/0192-define-session-context-interface.md +0 -59
  194. package/docs/retro/0193-runtime-owns-context-queries.md +0 -75
  195. package/docs/retro/0194-align-tool-interfaces-for-structural-typing.md +0 -62
  196. package/docs/retro/0195-convert-tool-factories-to-classes.md +0 -84
  197. package/docs/retro/0196-convert-runner-menu-to-classes.md +0 -73
  198. package/docs/retro/0205-decompose-render-widget-lines.md +0 -64
  199. package/docs/retro/0206-decompose-show-agent-detail.md +0 -67
  200. package/docs/retro/0207-decompose-agent-widget-update.md +0 -92
  201. package/docs/retro/0208-extract-shared-test-fixtures.md +0 -102
  202. package/docs/retro/0214-convert-remaining-closure-factories-to-classes.md +0 -66
  203. package/docs/retro/0215-decompose-build-parent-context.md +0 -61
  204. package/docs/retro/0216-decompose-start-agent.md +0 -80
  205. package/docs/retro/0217-extract-overwrite-guard.md +0 -63
  206. package/docs/retro/0218-push-sdk-boundary-in-settings.md +0 -72
  207. package/docs/retro/0219-reduce-test-duplication-top-3-clone-families.md +0 -64
  208. package/docs/retro/0227-evolve-agent-record-into-agent.md +0 -80
  209. package/docs/retro/0228-async-start-agent-dissolve-run-handle.md +0 -80
  210. package/docs/retro/0229-agent-born-complete.md +0 -89
  211. package/docs/retro/0230-extract-concurrency-queue.md +0 -38
  212. package/docs/retro/0231-push-exec-registry-to-runner.md +0 -71
  213. package/docs/retro/0232-agent-resume-internal-observer-lifecycle.md +0 -109
  214. package/docs/retro/0237-remove-disallowed-tools.md +0 -78
  215. package/docs/retro/0238-remove-extensions-filtering.md +0 -72
  216. package/docs/retro/0239-collapse-filter-active-tools.md +0 -70
  217. package/docs/retro/0242-rename-agent-tool-to-subagent.md +0 -82
  218. package/docs/retro/0256-extract-worktree-isolation.md +0 -89
  219. package/docs/retro/0257-extract-child-session-factory.md +0 -33
  220. package/docs/retro/0262-add-workspace-provider-seam.md +0 -89
  221. package/docs/retro/0264-remove-extension-lifecycle-control.md +0 -91
  222. package/docs/retro/0265-born-complete-subagent-session.md +0 -97
  223. package/docs/retro/0270-type-consumable-public-surface.md +0 -108
  224. package/docs/retro/0272-export-workspace-collaborator-types.md +0 -38
  225. package/docs/retro/0277-encapsulate-agent-session.md +0 -80
  226. package/docs/retro/0280-rename-agent-to-subagent.md +0 -96
  227. package/docs/retro/0360-custom-agents-default-append.md +0 -81
  228. package/docs/retro/0373-extract-subagent-state.md +0 -94
  229. package/docs/retro/0374-encapsulate-subagent-start-notification.md +0 -171
  230. package/docs/retro/0375-extract-run-listener-workspace-bracket.md +0 -104
  231. package/docs/retro/0376-extract-manager-observer.md +0 -81
  232. package/docs/retro/0377-split-widget-delegation-from-runtime.md +0 -105
  233. package/docs/retro/0378-consolidate-lifecycle-test-fixtures.md +0 -100
  234. package/docs/retro/0379-consolidate-ui-tools-test-fixtures.md +0 -52
  235. package/docs/retro/0380-extract-layered-settings-loader.md +0 -97
  236. package/docs/retro/0381-replace-concurrency-queue-with-limiter.md +0 -95
  237. package/docs/retro/0400-include-parent-prompt-in-replace-mode.md +0 -84
  238. package/docs/retro/0403-abort-subagents-on-interrupt.md +0 -90
  239. package/docs/retro/0412-unify-session-mock-builders.md +0 -80
  240. package/docs/retro/0420-fold-run-metrics-live-activity-onto-record.md +0 -90
  241. package/docs/retro/0421-migrate-activity-readers-to-record-getters.md +0 -105
  242. package/docs/retro/0422-delete-activity-tracker-ui-observer.md +0 -97
  243. package/docs/retro/0423-widget-self-drive-from-lifecycle.md +0 -112
  244. package/docs/retro/0424-drop-widget-dep-from-subagent-tool.md +0 -89
  245. package/docs/retro/0425-reconcile-subagent-events-contract.md +0 -101
  246. package/docs/retro/0426-consolidate-residual-test-clone-families.md +0 -100
  247. package/docs/retro/0427-reconsider-ui-direction.md +0 -102
  248. package/docs/retro/0441-remove-agent-definition-management-subtree.md +0 -97
  249. package/docs/retro/0442-dissolve-agents-conversation-viewer.md +0 -108
  250. package/docs/retro/0443-consolidate-remaining-test-clone-families.md +0 -28
  251. package/docs/retro/0444-shrink-widget-to-background-agents.md +0 -81
  252. package/docs/retro/0445-native-session-navigation.md +0 -122
  253. package/docs/retro/0446-spike-session-navigation-entry-criteria.md +0 -99
  254. package/docs/retro/0447-extract-subagents-settings-command.md +0 -91
  255. package/docs/retro/0448-block-disabled-agent-spawn.md +0 -75
  256. package/docs/retro/0462-navigation-renderer-tui-components.md +0 -105
  257. package/docs/retro/0463-file-snapshot-source-evicted-agents.md +0 -103
  258. package/docs/retro/0470-refresh-readme-command-surface.md +0 -40
  259. package/src/observation/notification-state.ts +0 -27
  260. package/vitest.config.ts +0 -14
@@ -1,300 +0,0 @@
1
- ---
2
- issue: 375
3
- issue_title: "Extract run-listener and workspace-bracket collaborators from Subagent"
4
- ---
5
-
6
- # Extract run-listener and workspace-bracket collaborators from Subagent
7
-
8
- ## Problem Statement
9
-
10
- `subagent.ts` is the largest source file in `pi-subagents` and an accelerating churn hotspot.
11
- Two concerns inflate the `Subagent` class beyond its core responsibility (own a child's execution lifecycle):
12
-
13
- 1. The per-run listener handles (`_unsub`, `_detachFn`) are raw nullable fields declared mid-class, with `wireSignal`/`attachObserver`/`releaseListeners` scattered around them.
14
- 2. Workspace teardown logic appears at two run-completion call sites — `completeRun` (success/abort/steer) and `failRun` (error) — each re-deriving the `if (this._workspace) … dispose() … resultAddendum` shape.
15
-
16
- This is Phase 17 Step 4 (core consolidation).
17
- The goal is to lift both concerns into small owned collaborators so the class shrinks and the dispose logic lives in one place.
18
-
19
- ## Goals
20
-
21
- - Extract a `RunListeners` collaborator that owns the observer-unsubscribe and signal-detach handles, so the two listener handles stop being raw nullable fields on `Subagent`.
22
- - Extract a `WorkspaceBracket` collaborator that owns prepare-at-run-start and dispose-with-result-addendum, so the workspace-disposal *logic* lives in exactly one place.
23
- - Bring `subagent.ts` to ≤ 450 LOC (currently 488).
24
- - Preserve all observable run/resume behavior exactly: observer-callback order, listener release on complete/fail/resume, workspace dispose status mapping, addendum folding, and the distinct error-handling semantics of the two dispose call sites.
25
-
26
- This change is **not breaking**: it is a pure internal restructuring with no change to observable behavior, output shape, public service surface, or defaults.
27
- `RunListeners` and `WorkspaceBracket` are internal lifecycle collaborators, not part of the published `dist/public.d.ts` surface.
28
-
29
- ## Non-Goals
30
-
31
- - No change to `SubagentState`, `SubagentExecution`, the `WorkspaceProvider`/`Workspace` seam interfaces (`src/lifecycle/workspace.ts`), or `subagent-manager.ts` wiring.
32
- - No change to the run/resume control flow, the abort path, the steer buffer, or the notification field.
33
- - No unification of the two dispose call sites' *error-handling* semantics (see Design Overview — they are intentionally different lifecycle contexts).
34
- - Phase 17 Steps 5–9 (manager observer extraction, widget delegation split, test-fixture consolidation, cross-package settings-loader duplication) are separate issues.
35
-
36
- ## Background
37
-
38
- Relevant modules:
39
-
40
- - `src/lifecycle/subagent.ts` — the `Subagent` class.
41
- After Phase 17 Step 2 ([#373]) extracted `SubagentState` and Step 3 ([#374]) encapsulated `start`/`promise`/`notification`, the remaining structural debt is the listener fields and the workspace dispose duplication.
42
- - `src/lifecycle/workspace.ts` — the generative workspace seam (ADR 0002): `WorkspaceProvider.prepare(ctx)` returns a `Workspace` (a `cwd` plus a `dispose(outcome)` hook returning an optional `resultAddendum`).
43
- - `src/lifecycle/subagent-manager.ts` — the only production constructor of `Subagent` (`spawn`); it resolves the registered provider lazily via `getWorkspaceProvider: () => this._workspaceProvider`.
44
- It calls `record.disposeSession()` but none of the listener/workspace methods being moved.
45
- - `test/lifecycle/subagent.test.ts` — directly tests `wireSignal`, `attachObserver`, `releaseListeners` (3 `describe` blocks) and the workspace dispose paths via `run()`.
46
-
47
- Current listener fields and methods on `Subagent`:
48
-
49
- ```typescript
50
- private _unsub?: () => void;
51
- private _detachFn?: () => void;
52
- wireSignal(signal: AbortSignal | undefined, onAbort: () => void): void { … } // sets _detachFn
53
- attachObserver(unsub: () => void): void { … } // sets _unsub
54
- releaseListeners(): void { … } // clears both
55
- ```
56
-
57
- The two handles are attached at different moments: `wireSignal` at run start, `attachObserver` after the session is created; `resume()` only attaches the observer.
58
- So a single combined `attach(unsub, detach)` (the issue's first-cut sketch) does not match the real call pattern — the collaborator must expose the two attach points separately.
59
-
60
- Current workspace state and dispose call sites:
61
-
62
- ```typescript
63
- private _workspace?: Workspace;
64
- // run(): const provider = this.execution.getWorkspaceProvider?.(); if (provider) { this._workspace = await provider.prepare({…}); cwd = this._workspace?.cwd; }
65
- // completeRun(): if (this._workspace) { finalStatus = …; const r = this._workspace.dispose({status, description}); if (r?.resultAddendum) finalResult += r.resultAddendum; }
66
- // failRun(): try { if (this._workspace) this._workspace.dispose({status:"error", description}); } catch (e) { debugLog(…); }
67
- ```
68
-
69
- Note: the issue's "three places" wording counts `run()`'s prepare-failure catch as a teardown path, but that catch has no prepared workspace to dispose — it only releases listeners and notifies.
70
- The actual `dispose()` call appears at **two** sites (`completeRun`, `failRun`).
71
-
72
- Constraint from AGENTS.md / code-design: business-logic modules stay SDK-free.
73
- Both new collaborators use only globals (`AbortSignal`) and the local `workspace.ts` types — no Pi SDK imports.
74
- Import siblings via `#src/...` aliases.
75
-
76
- ### Invariants from prior Phase 17 steps (must not regress)
77
-
78
- Per the [#374] retro lesson — a later phase step must not regress an earlier step's documented `Outcome:` with a green suite.
79
- This step touches the `Subagent` run/resume surface, so the at-risk invariants are:
80
-
81
- - **Step 1 ([#381]) — "every spawned agent has a `promise` at spawn."**
82
- Pinned by the regression test in `test/lifecycle/subagent-manager.test.ts` (queued agent has a `promise` at spawn) and `scheduleVia` unit tests in `subagent.test.ts`.
83
- This step does **not** touch `start`/`scheduleVia`/`guardedRun`/`_promise` — low risk, but the suite pins it.
84
- - **Step 2 ([#373]) — "`Subagent` is construct-complete; no 'not configured for execution' throws."**
85
- Pinned by the grep check (no "not configured for execution" in `subagent.ts`) and the constructor tests.
86
- Both new collaborators are constructed **inside** the `Subagent` constructor (no new optional init fields), so construct-completeness is preserved.
87
- - **Step 3 ([#374]) — "zero external writes to `Subagent` fields outside its own methods."**
88
- Pinned by the grep check (`\.promise =` / `\.notification =` only in `subagent.ts`).
89
- The removed `_unsub`/`_detachFn`/`_workspace` fields had no external writers; removing the public `wireSignal`/`attachObserver`/`releaseListeners` methods reduces the surface further.
90
-
91
- ## Design Overview
92
-
93
- Two small owned value objects, each owning state plus the behavior that reads/writes it (principle 9 — state and behavior in a class, not raw fields scattered through a host).
94
- Neither is procedure-splitting: each owns mutable state and returns a value (`prepare` → cwd, `dispose` → addendum) or encapsulates lifecycle handles.
95
-
96
- ### `RunListeners` — `src/lifecycle/run-listeners.ts`
97
-
98
- Owns the two per-run teardown handles and the wire/release behavior.
99
-
100
- ```typescript
101
- export class RunListeners {
102
- private unsub?: () => void;
103
- private detach?: () => void;
104
-
105
- /** Wire a parent AbortSignal so it stops the run when fired. No-op when no signal. */
106
- wireSignal(signal: AbortSignal | undefined, onAbort: () => void): void {
107
- if (!signal) return;
108
- const listener = () => onAbort();
109
- signal.addEventListener("abort", listener, { once: true });
110
- this.detach = () => signal.removeEventListener("abort", listener);
111
- }
112
-
113
- /** Store the record-observer unsubscribe handle. */
114
- attachObserver(unsub: () => void): void {
115
- this.unsub = unsub;
116
- }
117
-
118
- /** Release the observer + signal handles. Idempotent. */
119
- release(): void {
120
- this.unsub?.();
121
- this.unsub = undefined;
122
- this.detach?.();
123
- this.detach = undefined;
124
- }
125
- }
126
- ```
127
-
128
- ### `WorkspaceBracket` — `src/lifecycle/workspace-bracket.ts`
129
-
130
- Owns the prepared `Workspace` and the prepare/dispose logic.
131
- It captures the provider *resolver* (not the provider) so resolution stays at run-start, matching today's `getWorkspaceProvider?.()` timing.
132
-
133
- ```typescript
134
- import type {
135
- Workspace,
136
- WorkspaceDisposeOutcome,
137
- WorkspacePrepareContext,
138
- WorkspaceProvider,
139
- } from "#src/lifecycle/workspace";
140
-
141
- export class WorkspaceBracket {
142
- private prepared?: Workspace;
143
-
144
- constructor(private readonly resolveProvider: () => WorkspaceProvider | undefined) {}
145
-
146
- /** Resolve the registered provider and prepare the child workspace; returns its cwd (undefined when none). */
147
- async prepare(ctx: WorkspacePrepareContext): Promise<string | undefined> {
148
- const provider = this.resolveProvider();
149
- if (!provider) return undefined;
150
- this.prepared = await provider.prepare(ctx);
151
- return this.prepared?.cwd;
152
- }
153
-
154
- /** Dispose the prepared workspace (if any); returns the result addendum verbatim ("" when none). */
155
- dispose(outcome: WorkspaceDisposeOutcome): string {
156
- if (!this.prepared) return "";
157
- return this.prepared.dispose(outcome)?.resultAddendum ?? "";
158
- }
159
- }
160
- ```
161
-
162
- ### `Subagent` interaction with the collaborators
163
-
164
- Constructed in the `Subagent` constructor (construct-complete):
165
-
166
- ```typescript
167
- private readonly listeners = new RunListeners();
168
- private readonly workspaceBracket: WorkspaceBracket;
169
- // in constructor:
170
- this.workspaceBracket = new WorkspaceBracket(this.execution.getWorkspaceProvider ?? (() => undefined));
171
- ```
172
-
173
- Call sites in `run()` / `resume()` / `completeRun()` / `failRun()`:
174
-
175
- ```typescript
176
- // run() start:
177
- this.listeners.wireSignal(this.execution.signal, () => this.abort());
178
- const cwd = await this.workspaceBracket.prepare({ agentId: this.id, agentType: this.type, baseCwd: this.execution.baseCwd, invocation: this.invocation });
179
- // run() after session created:
180
- this.listeners.attachObserver(subscribeSubagentObserver(this.subagentSession, this.state, { onCompact: … }));
181
-
182
- // completeRun():
183
- const finalStatus: SubagentStatus = result.aborted ? "aborted" : result.steered ? "steered" : "completed";
184
- let finalResult = result.responseText + this.workspaceBracket.dispose({ status: finalStatus, description: this.description });
185
- // failRun():
186
- try { this.workspaceBracket.dispose({ status: "error", description: this.description }); }
187
- catch (cleanupErr) { debugLog("workspace dispose on agent error", cleanupErr); }
188
- ```
189
-
190
- This follows Tell-Don't-Ask: callers tell the bracket to `dispose(outcome)` and receive the addendum string; they do not reach through to `workspace.dispose(…).resultAddendum` (the prior Law-of-Demeter reach-through is absorbed into the bracket).
191
-
192
- ### Why the two dispose call sites stay separate (structural-duplication check)
193
-
194
- The issue asks to "collapse the three dispose paths into one."
195
- Tracing why the two `dispose()` calls differ (per the code-design "structural reasons before extracting duplication" heuristic) shows they are **different lifecycle contexts**, not incidental duplication:
196
-
197
- | Call site | Status source | Addendum | Error handling |
198
- | ------------- | ------------------------------------------------- | --------------------------- | ----------------------------------------------------------------- |
199
- | `completeRun` | derived from `result` (completed/aborted/steered) | folded into the result text | propagates (a dispose throw flows to `run()`'s catch → `failRun`) |
200
- | `failRun` | hardcoded `"error"` | discarded | best-effort `try/catch` + `debugLog` |
201
-
202
- So the resolution is: the dispose **logic** (the `if (prepared)` guard, the `.dispose()` call, the addendum unwrap) centralizes into `WorkspaceBracket.dispose()` — exactly one place, satisfying the issue's "disposal logic in exactly one place" outcome.
203
- The two callers retain their distinct status derivation and error handling, because forcing them into one call would require a discriminator parameter that papers over a real lifecycle difference (Sandi Metz: "duplication is cheaper than the wrong abstraction").
204
- `WorkspaceBracket.dispose()` deliberately does **not** wrap in `try/catch` — the best-effort behavior stays at `failRun`'s call site, preserving the existing semantics line-for-line (including the pre-existing success-path-throw → `failRun` re-dispose behavior).
205
-
206
- ## Module-Level Changes
207
-
208
- - **Added** `src/lifecycle/run-listeners.ts` — `RunListeners` class (`wireSignal`, `attachObserver`, `release`).
209
- - **Added** `src/lifecycle/workspace-bracket.ts` — `WorkspaceBracket` class (`prepare`, `dispose`).
210
- - **Added** `test/lifecycle/run-listeners.test.ts` — direct unit tests for `RunListeners`.
211
- - **Added** `test/lifecycle/workspace-bracket.test.ts` — direct unit tests for `WorkspaceBracket`.
212
- - **Changed** `src/lifecycle/subagent.ts`:
213
- - Remove fields `_unsub`, `_detachFn`, `_workspace` and the public methods `wireSignal`, `attachObserver`, `releaseListeners`.
214
- - Add `private readonly listeners = new RunListeners()` and `private readonly workspaceBracket: WorkspaceBracket` (constructed from `execution.getWorkspaceProvider`).
215
- - Rewire `run()`, `resume()`, `resetForResume()`, `completeRun()`, `failRun()` to call `this.listeners.*` and `this.workspaceBracket.*`.
216
- - Add imports for the two new modules; drop the now-unused `Workspace` type import if no longer referenced (keep `WorkspaceProvider` only if still referenced — verify with the type checker).
217
- - **Changed** `test/lifecycle/subagent.test.ts`:
218
- - Remove the `wireSignal`, `attachObserver / releaseListeners`, and `resetForResume releases listeners` `describe` blocks (their coverage moves to `run-listeners.test.ts`); the `run()`/`completeRun`/`failRun`/`resume` behavioral tests stay and continue to exercise the wired collaborators.
219
- - Remove the `attachObserver(unsub)` calls inside the `completeRun`/`failRun` "releases listeners" tests — assert listener release via the run/resume path instead, or via a `RunListeners` unit test.
220
-
221
- Doc updates (architecture references the moved symbols and module count):
222
-
223
- - **Changed** `docs/architecture/architecture.md`:
224
- - File-tree listing (`lifecycle/` block) — add `run-listeners.ts` and `workspace-bracket.ts` entries.
225
- - `Subagent` class diagram (key domain types) — remove `+wireSignal`, `+attachObserver`, `+releaseListeners`; optionally add `RunListeners` / `WorkspaceBracket` classes with composition edges.
226
- - Findings/health tables — bump `57 files` → `59 files` (lines ~650 and ~897).
227
- - Phase 17 Step 4 entry — append a `Landed:` note recording the two collaborators and the final `subagent.ts` LOC.
228
- - **Changed** `.pi/skills/package-pi-subagents/SKILL.md` — "seven domains (57 files)" → "(59 files)"; Lifecycle domain module count `11` → `13` and add the two modules to the directory list; update the test count if it is cited.
229
-
230
- Grep confirmation done while planning: `wireSignal`/`attachObserver`/`releaseListeners`/`_workspace`/`_unsub`/`_detachFn` appear only in `subagent.ts` and `subagent.test.ts` (plus the architecture class diagram); the SKILL does not name them.
231
- `disposeSession` (the one listener-adjacent method used by `subagent-manager.ts`) is **not** part of this change.
232
-
233
- ## Test Impact Analysis
234
-
235
- 1. **New unit tests the extraction enables.**
236
- `RunListeners` and `WorkspaceBracket` become directly constructible and testable without booting a `Subagent` or a full `run()`:
237
- - `run-listeners.test.ts` — `wireSignal` attaches and `release()` detaches the abort listener; `wireSignal(undefined, …)` is a no-op; `attachObserver` + `release()` calls and clears the unsub; `release()` is idempotent (double-call safe).
238
- - `workspace-bracket.test.ts` — `prepare` with no provider returns `undefined`; with a provider returns `workspace.cwd`; with a provider resolving `undefined` returns `undefined`; `dispose` with no prepared workspace returns `""`; `dispose` returns the `resultAddendum` verbatim; `dispose` returns `""` when the workspace returns no addendum; a throwing `dispose` propagates (not swallowed).
239
- 2. **Existing tests that become redundant.**
240
- The `wireSignal`, `attachObserver / releaseListeners`, and `resetForResume releases listeners` `describe` blocks in `subagent.test.ts` (~7 tests) duplicate what `run-listeners.test.ts` now covers at a lower level — remove them.
241
- 3. **Existing tests that must stay as-is.**
242
- The `run()` workspace tests (prepare threads cwd into the factory, dispose status mapping for completed/error, addendum folding, no-provider path, prepare-failure path) genuinely exercise the *integration* of the bracket into the run lifecycle — they stay and verify the wiring preserved behavior.
243
- The `completeRun`/`failRun`/`resume`/`abort` behavioral tests stay.
244
-
245
- ## Invariants at risk
246
-
247
- | Invariant (prior step) | Pinning test | Risk from this step |
248
- | ------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
249
- | Every spawned agent has a `promise` at spawn ([#381]) | `subagent-manager.test.ts` queued-agent regression test; `scheduleVia` unit tests | None — `start`/`scheduleVia`/`_promise` untouched |
250
- | `Subagent` construct-complete; no "not configured for execution" throws ([#373]) | `subagent.test.ts` constructor tests + grep check | None — both collaborators constructed in the constructor, no new optional init fields |
251
- | Zero external writes to `Subagent` fields ([#374]) | grep check (`\.promise =` / `\.notification =` only in `subagent.ts`) | None — removes fields/methods, adds no external writers |
252
- | Listener release on complete/fail/resume; observer-callback order; dispose status mapping + addendum folding | `subagent.test.ts` `run()`/`completeRun`/`failRun`/`resume` blocks | Preserved by keeping the call sequence identical; run-suite pins it |
253
-
254
- No invariant lives only in prose here — each is already pinned by a named test, and the run-lifecycle tests stay green through the wiring step.
255
-
256
- ## TDD Order
257
-
258
- 1. **Add `RunListeners` (red → green → commit).**
259
- Write `test/lifecycle/run-listeners.test.ts` (fails — module absent), then implement `src/lifecycle/run-listeners.ts`.
260
- Pure addition; `Subagent` not yet touched, suite otherwise unchanged.
261
- Commit: `test: add RunListeners with wire/attach/release behavior (#375)` (or a single `refactor:` if test+impl land together — prefer the red/green split).
262
- 2. **Add `WorkspaceBracket` (red → green → commit).**
263
- Write `test/lifecycle/workspace-bracket.test.ts` (fails — module absent), then implement `src/lifecycle/workspace-bracket.ts`.
264
- Pure addition.
265
- Commit: `test: add WorkspaceBracket prepare/dispose behavior (#375)` then `refactor: add WorkspaceBracket collaborator (#375)` — or one `refactor:` commit for the red/green pair.
266
- 3. **Wire both into `Subagent`; remove the raw fields/methods (atomic).**
267
- Rewire `run`/`resume`/`resetForResume`/`completeRun`/`failRun` to the collaborators; remove `_unsub`/`_detachFn`/`_workspace` and the public `wireSignal`/`attachObserver`/`releaseListeners`; construct `listeners` and `workspaceBracket` in the constructor.
268
- In the **same commit**, update `test/lifecycle/subagent.test.ts`: removing the three public methods breaks that file at the type level, so the deletion of the redundant `describe` blocks and the construction/wiring change must land together.
269
- Run `pnpm run check` immediately after (interface-shape change).
270
- Commit: `refactor: extract RunListeners and WorkspaceBracket from Subagent (#375)`.
271
- 4. **Update docs (commit).**
272
- `architecture.md` (file tree, class diagram, `57 → 59` file counts, Step 4 `Landed:` note with final LOC) and `package-pi-subagents` SKILL.md (file count, Lifecycle module list/count, test count if cited).
273
- Commit: `docs: record run-listener and workspace-bracket extraction (#375)`.
274
-
275
- Verification gates after step 3 and before review: `pnpm run check`, `pnpm run lint`, `pnpm -r run test`, `pnpm fallow dead-code` (confirm no orphaned imports in `subagent.test.ts` after the `describe` removals), and `wc -l src/lifecycle/subagent.ts` (assert ≤ 450).
276
-
277
- ## Risks and Mitigations
278
-
279
- - **Risk: the wiring step silently changes dispose error semantics.**
280
- Mitigation: `WorkspaceBracket.dispose()` deliberately does not `try/catch`; the best-effort wrapper stays at `failRun`'s call site, preserving the success-path-throw → `failRun` behavior line-for-line.
281
- A `workspace-bracket.test.ts` case asserts a throwing `dispose` propagates.
282
- - **Risk: `subagent.ts` does not reach ≤ 450 LOC.**
283
- Mitigation: removing 3 fields + 3 methods (≈ 25 lines incl. comments) and simplifying the two dispose blocks nets ≈ 40 lines below the current 488; `wc -l` is a gate.
284
- If it lands at 451–455, that is an acceptable near-miss to note, not a blocker.
285
- - **Risk: orphaned imports in `subagent.test.ts` after removing `describe` blocks.**
286
- Mitigation: Biome `noUnusedImports` is warning-level (exit 0), so run `pnpm fallow dead-code` and re-check imports manually as part of step 3.
287
- - **Risk: release cadence.**
288
- This issue is internal-only; with `refactor:`/`docs:`/`test:` commits it will not trigger a release-please version bump — it ships bundled with the next feature release.
289
- If a standalone release is desired (matching prior Phase 17 per-step cadence), that is a ship-time decision, not a planning one.
290
-
291
- ## Open Questions
292
-
293
- - Whether to add `RunListeners` / `WorkspaceBracket` as classes with composition edges to the architecture class diagram, or only update the file tree.
294
- Defer to the doc step — add them if the diagram stays readable.
295
- - The pre-existing untested success-path-`dispose`-throw → `failRun` re-dispose behavior is preserved but remains unpinned by a `Subagent`-level test.
296
- Defer pinning it unless the implementer finds it cheap to add — the extraction does not change it.
297
-
298
- [#373]: https://github.com/gotgenes/pi-packages/issues/373
299
- [#374]: https://github.com/gotgenes/pi-packages/issues/374
300
- [#381]: https://github.com/gotgenes/pi-packages/issues/381
@@ -1,232 +0,0 @@
1
- ---
2
- issue: 376
3
- issue_title: "Extract the manager observer from index.ts into a class"
4
- ---
5
-
6
- # Extract the manager observer from index.ts into a class
7
-
8
- ## Problem Statement
9
-
10
- `index.ts` constructs an inline `SubagentManagerObserver` object literal (~50 lines, four methods) at the composition root and hands it to `SubagentManager`.
11
- The literal mixes three concerns: emitting `pi.events` lifecycle events, persisting the final record via `pi.appendEntry`, and dispatching completion notifications through the `NotificationManager`.
12
- Per code-design principle 9 (state and behavior belong in a class, not a closure-captured literal), these three concerns cannot be unit-tested today without booting the entire extension.
13
- `index.ts` is the package's dominant churn hotspot (31.3, 91 commits); shrinking it is the goal of Phase 17 Track B.
14
-
15
- ## Goals
16
-
17
- - Extract the inline observer literal into a `SubagentEventsObserver` class under `src/observation/`, constructed with narrow deps: an `emit` function, an `appendEntry` function, and the `NotificationSystem`.
18
- - `index.ts` instantiates the class and passes it to `SubagentManager` in place of the literal.
19
- - Unit-test the observer's three concerns directly (event shapes, record persistence payload, notification dispatch branching) without booting the extension.
20
- - Preserve every emitted event payload and `appendEntry` shape byte-for-byte — this is a pure extraction with no observable behavior change (not breaking).
21
- - Bring `index.ts` below 170 lines.
22
-
23
- ## Non-Goals
24
-
25
- - Splitting widget delegation out of `SubagentRuntime` (Phase 17 Step 6, Issue [#377]) — Step 5 is its prerequisite but lands separately.
26
- - Moving `buildEventData` out of `notification.ts` — it stays where it is tested and is imported by the new observer.
27
- - Pushing event-payload construction onto `Subagent` (a Tell-Don't-Ask improvement) — tracked as an Open Question, out of scope here.
28
- - Narrowing `NotificationSystem` to a two-method interface — see Open Questions.
29
- - Any other Phase 17 step.
30
-
31
- ## Background
32
-
33
- Relevant modules:
34
-
35
- - `src/index.ts` (226 lines) — composition root.
36
- Defines `const observer: SubagentManagerObserver = { … }` at lines 79–134 and passes it to `new SubagentManager({ …, observer, … })`.
37
- - `src/lifecycle/subagent-manager.ts` — owns the `SubagentManagerObserver` interface (four methods: `onSubagentStarted`, `onSubagentCompleted`, `onSubagentCompacted`, `onSubagentCreated`).
38
- This interface is the manager's contract and stays here.
39
- - `src/observation/notification.ts` — exports `buildEventData(record)` (pure, tested in `notification.test.ts`), the `NotificationSystem` interface (`cancelNudge`, `sendCompletion`, `cleanupCompleted`, `dispose`), and `NotificationManager`.
40
- - `src/observation/record-observer.ts` — the established pattern for an observation-domain module that subscribes/dispatches with narrow deps; the new class is a sibling.
41
-
42
- The current literal's four methods:
43
-
44
- - `onSubagentStarted(record)` → `emit("subagents:started", { id, type, description })`.
45
- - `onSubagentCompleted(record)` → branch on terminal status to `emit("subagents:failed" | "subagents:completed", buildEventData(record))`; `appendEntry("subagents:record", { …8 fields… })`; then either `notifications.cleanupCompleted(record.id)` (when `record.notification?.resultConsumed`) or `notifications.sendCompletion(record)`.
46
- - `onSubagentCompacted(record, info)` → `emit("subagents:compacted", { id, type, description, reason, tokensBefore, compactionCount })`.
47
- - `onSubagentCreated(record)` → `emit("subagents:created", { id, type, description, isBackground: true })`.
48
-
49
- SDK signatures the narrow deps mirror:
50
-
51
- - `EventBus.emit(channel: string, data: unknown): void`.
52
- - `ExtensionAPI.appendEntry<T = unknown>(customType: string, data?: T): void`.
53
-
54
- The applicable AGENTS.md constraints: Pi SDK imports stay out of the observation module — the class accepts `emit`/`appendEntry` as injected callbacks (the same pattern `SettingsManager` uses with `SettingsEmit`), and `index.ts` wires them to `pi.events.emit`/`pi.appendEntry` via arrows (avoids `@typescript-eslint/unbound-method`).
55
-
56
- ## Design Overview
57
-
58
- ### Class shape
59
-
60
- ```typescript
61
- /** Emit callback — a subset of `pi.events.emit`. */
62
- export type EventEmit = (channel: string, data: unknown) => void;
63
-
64
- /** Append callback — a subset of `pi.appendEntry`. */
65
- export type AppendEntry = (customType: string, data: unknown) => void;
66
-
67
- export interface SubagentEventsObserverDeps {
68
- emit: EventEmit;
69
- appendEntry: AppendEntry;
70
- notifications: NotificationSystem;
71
- }
72
-
73
- export class SubagentEventsObserver implements SubagentManagerObserver {
74
- private readonly emit: EventEmit;
75
- private readonly appendEntry: AppendEntry;
76
- private readonly notifications: NotificationSystem;
77
-
78
- constructor(deps: SubagentEventsObserverDeps) {
79
- this.emit = deps.emit;
80
- this.appendEntry = deps.appendEntry;
81
- this.notifications = deps.notifications;
82
- }
83
-
84
- onSubagentStarted(record: Subagent): void { /* emit started */ }
85
- onSubagentCompleted(record: Subagent): void { /* emit failed|completed, appendEntry, dispatch */ }
86
- onSubagentCompacted(record: Subagent, info: CompactionInfo): void { /* emit compacted */ }
87
- onSubagentCreated(record: Subagent): void { /* emit created */ }
88
- }
89
- ```
90
-
91
- The four method bodies are moved verbatim from the literal — same event channels, same payload fields, same branching.
92
- `buildEventData` is imported from `#src/observation/notification`.
93
-
94
- ### Call site in index.ts
95
-
96
- ```typescript
97
- const observer = new SubagentEventsObserver({
98
- emit: (channel, data) => pi.events.emit(channel, data),
99
- appendEntry: (customType, data) => pi.appendEntry(customType, data),
100
- notifications,
101
- });
102
-
103
- const manager = new SubagentManager({
104
- createSubagentSession: (params) => createSubagentSession(params, subagentSessionDeps),
105
- baseCwd: process.cwd(),
106
- observer,
107
- limiter,
108
- getRunConfig: () => settings,
109
- });
110
- ```
111
-
112
- `notifications` (the `NotificationManager`) is already constructed earlier in `index.ts`, so the observer construction slots in where the literal was.
113
-
114
- ### Extracted-module interaction with upstream deps
115
-
116
- The observer only *reads* from its inputs and *tells* its collaborators — no output-argument mutation, no reverse-search, no reach-back into the manager:
117
-
118
- ```text
119
- onSubagentCompleted(record):
120
- reads record.status / record.id / … (Subagent getters)
121
- calls buildEventData(record) (pure helper)
122
- tells this.emit("subagents:completed", data) (injected callback)
123
- tells this.appendEntry("subagents:record", {…}) (injected callback)
124
- reads record.notification?.resultConsumed (LoD chain — pre-existing)
125
- tells this.notifications.cleanupCompleted | sendCompletion
126
- ```
127
-
128
- The one Law-of-Demeter chain (`record.notification?.resultConsumed`) is carried over unchanged from the literal; it is pre-existing and out of scope (see Open Questions).
129
-
130
- ### Design-review checklist result
131
-
132
- | Smell | Location | Evidence | Disposition |
133
- | ---------------- | ------------------------------------- | -------------------------------------------------------- | -------------------------------------------- |
134
- | Dependency width | `SubagentEventsObserverDeps` (3 deps) | all methods use `emit`; `onSubagentCompleted` uses all 3 | OK — narrow |
135
- | LoD violation | `onSubagentCompleted` | `record.notification?.resultConsumed` | Track and watch — pre-existing, out of scope |
136
- | Output argument | — | none | Clean |
137
- | ISP | `NotificationSystem` (4 methods) | observer uses `sendCompletion`, `cleanupCompleted` | Accept per issue spec — see Open Questions |
138
-
139
- The extraction introduces a real collaborator (a class that owns three behaviors and is independently testable), not a procedure split — each method returns no value but performs a distinct side-effecting concern that was previously untestable in isolation.
140
-
141
- ## Module-Level Changes
142
-
143
- - `src/observation/subagent-events-observer.ts` (new)
144
- - Export `SubagentEventsObserver implements SubagentManagerObserver`, plus `EventEmit`, `AppendEntry`, and `SubagentEventsObserverDeps` types.
145
- - Imports: `SubagentManagerObserver` from `#src/lifecycle/subagent-manager`, `buildEventData` and `NotificationSystem` from `#src/observation/notification`, `Subagent` and `CompactionInfo` from `#src/types`.
146
- - `src/index.ts` (changed)
147
- - Remove the inline `const observer: SubagentManagerObserver = { … }` literal (lines 79–134).
148
- - Add `import { SubagentEventsObserver } from "#src/observation/subagent-events-observer"`.
149
- - Construct `const observer = new SubagentEventsObserver({ emit, appendEntry, notifications })`.
150
- - Remove now-unused imports: `buildEventData` (moves to the new module) and the `type SubagentManagerObserver` import (the literal annotation is gone).
151
- - Keep `NotificationDetails` and `NotificationManager` imports (still used by `registerMessageRenderer` and the `notifications` construction).
152
- - `test/observation/subagent-events-observer.test.ts` (new)
153
- - Unit tests for all four methods (see TDD Order).
154
- - `docs/architecture/architecture.md` (changed)
155
- - Mark Phase 17 Step 5 `✅ Complete` and add a "Landed" note (index.ts line count, file/test counts).
156
- - Update the "Total LOC … (60 files …)" listings to 61 files and refresh the test count.
157
- - The churn-hotspot note for `index.ts` stays — Step 5 continues the shrink trend.
158
-
159
- Grep confirms no `package-*/SKILL.md` documents the inline observer literal or `SubagentEventsObserver` by name; no skill-doc update needed.
160
-
161
- ## Test Impact Analysis
162
-
163
- 1. **New unit tests enabled.**
164
- All four observer methods become directly testable with `vi.fn()` stubs for `emit`/`appendEntry` and a stubbed `NotificationSystem`.
165
- Previously the only way to exercise these paths was through `SubagentManager` integration tests that drove the manager's `buildObserver` forwarding — the index-level observer body itself had zero coverage.
166
- 2. **No existing tests become redundant.**
167
- The `subagent-manager.test.ts` observer tests (lines 76–148) exercise the manager's *forwarding* (`buildObserver` → `this.observer?.onSubagent…`), a different layer that stays.
168
- `notification.test.ts` keeps the `buildEventData` tests.
169
- `agent-tool.test.ts:153` keeps its note that `subagents:created` is delegated to the observer.
170
- 3. **Tests that must stay as-is.**
171
- The manager forwarding tests and the `buildEventData` purity tests genuinely exercise layers the extraction does not touch.
172
-
173
- ## Invariants at risk
174
-
175
- This step touches `index.ts`, which prior Phase 17 steps shrank, and the event/notification dispatch surface.
176
- Step 6 ([#377]) depends on this step; it must not regress the index.ts shrink.
177
-
178
- | Invariant | Pinned by |
179
- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- |
180
- | Completion of a successful background agent emits `subagents:completed` (not `subagents:failed`) with `buildEventData` shape | new `onSubagentCompleted` success test |
181
- | Terminal `error`/`stopped`/`aborted` status emits `subagents:failed` | new `onSubagentCompleted` error test |
182
- | `appendEntry("subagents:record", …)` persists the eight-field record on every completion | new `onSubagentCompleted` appendEntry test |
183
- | Already-consumed result skips `sendCompletion` and calls `cleanupCompleted` | new `onSubagentCompleted` resultConsumed test |
184
- | `subagents:created` carries `isBackground: true` | new `onSubagentCreated` test |
185
-
186
- These invariants lived only in the untested inline literal before; the new tests pin them for the first time.
187
-
188
- ## TDD Order
189
-
190
- 1. **Red → Green → Commit: extract `SubagentEventsObserver` and wire `index.ts`.**
191
-
192
- Write `test/observation/subagent-events-observer.test.ts` first (red), covering:
193
- - `onSubagentStarted` emits `subagents:started` with `{ id, type, description }`.
194
- - `onSubagentCompleted` (success status) emits `subagents:completed` with `buildEventData(record)`, calls `appendEntry("subagents:record", …)` with the eight fields, and calls `notifications.sendCompletion(record)`.
195
- - `onSubagentCompleted` (error/stopped/aborted) emits `subagents:failed`.
196
- - `onSubagentCompleted` with `record.notification?.resultConsumed` calls `notifications.cleanupCompleted(record.id)` and does *not* call `sendCompletion`.
197
- - `onSubagentCompacted` emits `subagents:compacted` with `{ id, type, description, reason, tokensBefore, compactionCount }`.
198
- - `onSubagentCreated` emits `subagents:created` with `{ id, type, description, isBackground: true }`.
199
-
200
- Use `createTestSubagent({ … })` from `#test/helpers/make-subagent` for records (it provides status, result, error, `toolCallId` for `NotificationState`, `compactionCount`); `vi.fn()` for `emit`/`appendEntry`; a stub object for `NotificationSystem`.
201
-
202
- Then create `src/observation/subagent-events-observer.ts` (green) and update `index.ts` in the **same commit**: replace the literal with `new SubagentEventsObserver({ … })` and drop the now-unused `buildEventData` / `type SubagentManagerObserver` imports.
203
- The class and the index.ts swap are coupled — index.ts is the sole call site of the literal being replaced, and the new class needs a consumer to satisfy `pnpm fallow dead-code`.
204
-
205
- Run `pnpm --filter @gotgenes/pi-subagents run check` and `pnpm --filter @gotgenes/pi-subagents exec vitest run`.
206
- Commit: `refactor: extract SubagentEventsObserver from index.ts (#376)`
207
-
208
- 2. **Commit: mark Phase 17 Step 5 complete in the architecture doc.**
209
-
210
- In `docs/architecture/architecture.md`, mark Step 5 `✅ Complete`, add a "Landed" note (new index.ts line count, file count 60 → 61, refreshed test count), and update the two "(60 files …)" LOC listings.
211
- Commit: `docs: mark Phase 17 Step 5 complete in architecture.md (#376)`
212
-
213
- ## Risks and Mitigations
214
-
215
- - **Risk:** the extracted methods drift from the literal's exact event payloads or `appendEntry` shape.
216
- **Mitigation:** move the bodies verbatim; the new tests assert each payload field explicitly, pinning the shapes.
217
- - **Risk:** passing `pi.events.emit` / `pi.appendEntry` as bare values trips `@typescript-eslint/unbound-method`.
218
- **Mitigation:** wire them as arrow callbacks in `index.ts` (`(channel, data) => pi.events.emit(channel, data)`), matching the existing `SettingsManager` emit wiring.
219
- - **Risk:** the new class is flagged as dead code by `pnpm fallow dead-code` if it lands before its consumer.
220
- **Mitigation:** the class and the `index.ts` wiring land in one commit (TDD step 1).
221
- - **Risk:** `index.ts` does not drop below 170 lines.
222
- **Mitigation:** the literal is ~50 lines and the replacement is ~6; 226 − ~50 + ~6 ≈ 182 minus the removed import lines lands it near/below 170 — verify with `wc -l` during implementation and, if just over, the net is still a clear shrink (the Outcome target is directional, not a hard gate).
223
-
224
- ## Open Questions
225
-
226
- - Should `NotificationSystem` be narrowed to a two-method `CompletionNotifier` (`sendCompletion`, `cleanupCompleted`) for the observer per ISP?
227
- Deferred: the issue prescribes passing `NotificationSystem`, and the interface is already cohesive.
228
- Revisit only if a third consumer wants the narrow slice.
229
- - Should event-payload construction (and the `record.notification?.resultConsumed` read) move onto `Subagent` as Tell-Don't-Ask methods (`record.toEventData()`, `record.isResultConsumed()`)?
230
- Deferred: out of scope for a faithful extraction; `buildEventData` already exists as a pure helper.
231
-
232
- [#377]: https://github.com/gotgenes/pi-packages/issues/377