@gotgenes/pi-subagents 18.0.1 → 18.0.3

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 (268) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/docs/architecture/architecture.md +165 -203
  3. package/docs/architecture/history/phase-19-implement-ui-decisions.md +282 -0
  4. package/package.json +6 -7
  5. package/src/config/default-agents.ts +3 -0
  6. package/src/lifecycle/subagent.ts +25 -14
  7. package/src/observation/notification.ts +23 -10
  8. package/src/observation/subagent-events-observer.ts +1 -3
  9. package/src/runtime.ts +2 -1
  10. package/src/service/service-adapter.ts +20 -17
  11. package/src/session/model-resolver.ts +31 -30
  12. package/src/tools/agent-tool.ts +17 -13
  13. package/src/tools/get-result-report.ts +60 -0
  14. package/src/tools/get-result-tool.ts +25 -40
  15. package/src/tools/helpers.ts +21 -5
  16. package/src/tools/spawn-config.ts +5 -5
  17. package/src/tools/steer-tool.ts +37 -27
  18. package/src/types.ts +4 -1
  19. package/src/ui/session-navigator.ts +0 -2
  20. package/.prettierignore +0 -5
  21. package/AGENTS.md +0 -7
  22. package/docs/plans/0048-implement-subagents-api.md +0 -304
  23. package/docs/plans/0049-remove-group-join-output-file-rpc.md +0 -180
  24. package/docs/plans/0051-update-adr-0001-hard-fork.md +0 -77
  25. package/docs/plans/0052-remove-scheduled-subagents.md +0 -133
  26. package/docs/plans/0053-extract-model-resolution-from-execute.md +0 -181
  27. package/docs/plans/0054-decompose-index-into-modules.md +0 -302
  28. package/docs/plans/0057-structured-debug-logging.md +0 -154
  29. package/docs/plans/0061-session-format-transcript.md +0 -284
  30. package/docs/plans/0066-replace-as-any-with-sdk-types.md +0 -215
  31. package/docs/plans/0069-create-subagent-runtime.md +0 -345
  32. package/docs/plans/0070-extract-event-handlers.md +0 -306
  33. package/docs/plans/0071-extract-session-config-assembler.md +0 -362
  34. package/docs/plans/0072-inject-agent-manager-collaborators.md +0 -329
  35. package/docs/plans/0076-inject-cwd-into-agent-manager.md +0 -102
  36. package/docs/plans/0077-inject-project-agents-dir.md +0 -115
  37. package/docs/plans/0080-consolidate-agent-config-lookup.md +0 -247
  38. package/docs/plans/0084-extract-git-worktree-manager.md +0 -142
  39. package/docs/plans/0087-evolve-subagent-runtime-methods.md +0 -240
  40. package/docs/plans/0098-extract-agent-record-state-machine.md +0 -435
  41. package/docs/plans/0099-replace-ctx-with-parent-snapshot.md +0 -488
  42. package/docs/plans/0100-replace-callback-threading-with-session-subscription.md +0 -454
  43. package/docs/plans/0102-consolidate-test-record-factory.md +0 -176
  44. package/docs/plans/0108-extract-agent-type-registry.md +0 -322
  45. package/docs/plans/0109-extract-settings-manager.md +0 -276
  46. package/docs/plans/0110-agent-activity-tracker.md +0 -297
  47. package/docs/plans/0111-split-agent-record-lifecycle.md +0 -582
  48. package/docs/plans/0112-replace-agent-manager-callbacks.md +0 -241
  49. package/docs/plans/0113-disambiguate-spawn-options.md +0 -155
  50. package/docs/plans/0114-narrow-agent-tool-menu-deps.md +0 -279
  51. package/docs/plans/0115-decompose-agent-tool.md +0 -337
  52. package/docs/plans/0116-type-housekeeping.md +0 -351
  53. package/docs/plans/0118-settings-manager-apply-methods.md +0 -271
  54. package/docs/plans/0123-remove-vi-fn-cast-smell.md +0 -179
  55. package/docs/plans/0131-consolidate-shared-test-fixtures.md +0 -207
  56. package/docs/plans/0132-inject-io-into-session-config.md +0 -219
  57. package/docs/plans/0133-inject-sdk-boundary-into-agent-runner.md +0 -373
  58. package/docs/plans/0134-reduce-as-any-casts.md +0 -366
  59. package/docs/plans/0135-extract-display-helpers.md +0 -182
  60. package/docs/plans/0136-decompose-agent-menu.md +0 -300
  61. package/docs/plans/0144-consolidate-observation-model.md +0 -263
  62. package/docs/plans/0145-decompose-execute-push-ctx-to-boundary.md +0 -290
  63. package/docs/plans/0146-narrow-ui-context.md +0 -319
  64. package/docs/plans/0147-inject-wrap-text-into-conversation-viewer.md +0 -166
  65. package/docs/plans/0148-split-agent-widget-rendering.md +0 -255
  66. package/docs/plans/0152-add-prompt-snippet.md +0 -91
  67. package/docs/plans/0164-reorganize-into-domain-directories.md +0 -409
  68. package/docs/plans/0165-decompose-resolved-spawn-config.md +0 -157
  69. package/docs/plans/0166-extract-parent-session-info.md +0 -231
  70. package/docs/plans/0167-narrow-runner-io.md +0 -150
  71. package/docs/plans/0168-extract-tool-filter-config.md +0 -173
  72. package/docs/plans/0169-extract-run-context-from-run-options.md +0 -194
  73. package/docs/plans/0170-reduce-build-content-lines-complexity.md +0 -225
  74. package/docs/plans/0171-reduce-render-result-complexity.md +0 -200
  75. package/docs/plans/0172-extract-turn-formatting.md +0 -206
  76. package/docs/plans/0180-reorder-append-prompt-for-kv-cache.md +0 -100
  77. package/docs/plans/0185-remove-persistent-agent-memory.md +0 -161
  78. package/docs/plans/0188-replace-any-casts-with-sdk-types.md +0 -162
  79. package/docs/plans/0192-define-session-context-interface.md +0 -107
  80. package/docs/plans/0193-runtime-owns-context-queries.md +0 -245
  81. package/docs/plans/0194-align-tool-interfaces-for-structural-typing.md +0 -139
  82. package/docs/plans/0195-convert-tool-factories-to-classes.md +0 -264
  83. package/docs/plans/0196-convert-runner-menu-to-classes.md +0 -268
  84. package/docs/plans/0205-decompose-render-widget-lines.md +0 -140
  85. package/docs/plans/0206-decompose-show-agent-detail.md +0 -211
  86. package/docs/plans/0207-decompose-agent-widget-update.md +0 -242
  87. package/docs/plans/0208-extract-shared-test-fixtures.md +0 -298
  88. package/docs/plans/0214-convert-remaining-closure-factories-to-classes.md +0 -261
  89. package/docs/plans/0215-decompose-build-parent-context.md +0 -166
  90. package/docs/plans/0216-decompose-start-agent.md +0 -255
  91. package/docs/plans/0217-extract-overwrite-guard.md +0 -176
  92. package/docs/plans/0218-push-sdk-boundary-in-settings.md +0 -172
  93. package/docs/plans/0219-reduce-test-duplication-top-3-clone-families.md +0 -162
  94. package/docs/plans/0227-evolve-agent-record-into-agent.md +0 -322
  95. package/docs/plans/0228-async-start-agent-dissolve-run-handle.md +0 -288
  96. package/docs/plans/0229-agent-born-complete.md +0 -564
  97. package/docs/plans/0230-extract-concurrency-queue.md +0 -265
  98. package/docs/plans/0231-push-exec-registry-to-runner.md +0 -245
  99. package/docs/plans/0232-agent-resume-internal-observer-lifecycle.md +0 -180
  100. package/docs/plans/0237-remove-disallowed-tools.md +0 -180
  101. package/docs/plans/0238-remove-extensions-filtering.md +0 -191
  102. package/docs/plans/0239-collapse-filter-active-tools.md +0 -217
  103. package/docs/plans/0242-rename-agent-tool-to-subagent.md +0 -165
  104. package/docs/plans/0256-extract-worktree-isolation.md +0 -256
  105. package/docs/plans/0257-extract-child-session-factory.md +0 -285
  106. package/docs/plans/0262-add-workspace-provider-seam.md +0 -264
  107. package/docs/plans/0264-remove-extension-lifecycle-control.md +0 -277
  108. package/docs/plans/0265-born-complete-subagent-session.md +0 -333
  109. package/docs/plans/0270-type-consumable-public-surface.md +0 -204
  110. package/docs/plans/0272-export-workspace-collaborator-types.md +0 -147
  111. package/docs/plans/0277-encapsulate-agent-session.md +0 -304
  112. package/docs/plans/0280-rename-agent-to-subagent.md +0 -197
  113. package/docs/plans/0360-custom-agents-default-append.md +0 -132
  114. package/docs/plans/0373-extract-subagent-state.md +0 -250
  115. package/docs/plans/0374-encapsulate-subagent-start-notification.md +0 -268
  116. package/docs/plans/0375-extract-run-listener-workspace-bracket.md +0 -300
  117. package/docs/plans/0376-extract-manager-observer.md +0 -232
  118. package/docs/plans/0377-split-widget-delegation-from-runtime.md +0 -254
  119. package/docs/plans/0378-consolidate-lifecycle-test-fixtures.md +0 -243
  120. package/docs/plans/0379-consolidate-ui-tools-test-fixtures.md +0 -335
  121. package/docs/plans/0380-extract-layered-settings-loader.md +0 -223
  122. package/docs/plans/0381-replace-concurrency-queue-with-limiter.md +0 -267
  123. package/docs/plans/0400-include-parent-prompt-in-replace-mode.md +0 -199
  124. package/docs/plans/0403-abort-subagents-on-interrupt.md +0 -180
  125. package/docs/plans/0412-unify-session-mock-builders.md +0 -146
  126. package/docs/plans/0420-fold-run-metrics-live-activity-onto-record.md +0 -228
  127. package/docs/plans/0421-migrate-activity-readers-to-record-getters.md +0 -309
  128. package/docs/plans/0422-delete-activity-tracker-ui-observer.md +0 -204
  129. package/docs/plans/0423-widget-self-drive-from-lifecycle.md +0 -264
  130. package/docs/plans/0424-drop-widget-dep-from-subagent-tool.md +0 -166
  131. package/docs/plans/0425-reconcile-subagent-events-contract.md +0 -144
  132. package/docs/plans/0426-consolidate-residual-test-clone-families.md +0 -235
  133. package/docs/plans/0427-reconsider-ui-direction.md +0 -212
  134. package/docs/plans/0441-remove-agent-definition-management-subtree.md +0 -180
  135. package/docs/plans/0442-dissolve-agents-conversation-viewer.md +0 -195
  136. package/docs/plans/0443-consolidate-remaining-test-clone-families.md +0 -268
  137. package/docs/plans/0444-shrink-widget-to-background-agents.md +0 -156
  138. package/docs/plans/0445-native-session-navigation.md +0 -365
  139. package/docs/plans/0446-spike-session-navigation-entry-criteria.md +0 -171
  140. package/docs/plans/0447-extract-subagents-settings-command.md +0 -220
  141. package/docs/plans/0448-block-disabled-agent-spawn.md +0 -207
  142. package/docs/plans/0462-navigation-renderer-tui-components.md +0 -338
  143. package/docs/plans/0463-file-snapshot-source-evicted-agents.md +0 -306
  144. package/docs/plans/0470-refresh-readme-command-surface.md +0 -159
  145. package/docs/retro/0048-implement-subagents-api.md +0 -44
  146. package/docs/retro/0049-remove-group-join-output-file-rpc.md +0 -49
  147. package/docs/retro/0051-update-adr-0001-hard-fork.md +0 -39
  148. package/docs/retro/0053-extract-model-resolution-from-execute.md +0 -40
  149. package/docs/retro/0054-decompose-index-into-modules.md +0 -53
  150. package/docs/retro/0057-structured-debug-logging.md +0 -77
  151. package/docs/retro/0061-session-format-transcript.md +0 -41
  152. package/docs/retro/0066-replace-as-any-with-sdk-types.md +0 -44
  153. package/docs/retro/0069-create-subagent-runtime.md +0 -43
  154. package/docs/retro/0070-extract-event-handlers.md +0 -49
  155. package/docs/retro/0071-extract-session-config-assembler.md +0 -60
  156. package/docs/retro/0072-inject-agent-manager-collaborators.md +0 -46
  157. package/docs/retro/0076-inject-cwd-into-agent-manager.md +0 -35
  158. package/docs/retro/0077-inject-project-agents-dir.md +0 -33
  159. package/docs/retro/0080-consolidate-agent-config-lookup.md +0 -35
  160. package/docs/retro/0084-extract-git-worktree-manager.md +0 -37
  161. package/docs/retro/0087-evolve-subagent-runtime-methods.md +0 -42
  162. package/docs/retro/0098-extract-agent-record-state-machine.md +0 -46
  163. package/docs/retro/0099-replace-ctx-with-parent-snapshot.md +0 -37
  164. package/docs/retro/0100-replace-callback-threading-with-session-subscription.md +0 -36
  165. package/docs/retro/0102-consolidate-test-record-factory.md +0 -30
  166. package/docs/retro/0108-extract-agent-type-registry.md +0 -41
  167. package/docs/retro/0109-extract-settings-manager.md +0 -55
  168. package/docs/retro/0110-agent-activity-tracker.md +0 -44
  169. package/docs/retro/0111-split-agent-record-lifecycle.md +0 -61
  170. package/docs/retro/0112-replace-agent-manager-callbacks.md +0 -35
  171. package/docs/retro/0113-disambiguate-spawn-options.md +0 -29
  172. package/docs/retro/0114-narrow-agent-tool-menu-deps.md +0 -38
  173. package/docs/retro/0115-decompose-agent-tool.md +0 -51
  174. package/docs/retro/0116-type-housekeeping.md +0 -42
  175. package/docs/retro/0118-settings-manager-apply-methods.md +0 -40
  176. package/docs/retro/0123-remove-vi-fn-cast-smell.md +0 -49
  177. package/docs/retro/0131-consolidate-shared-test-fixtures.md +0 -46
  178. package/docs/retro/0132-inject-io-into-session-config.md +0 -33
  179. package/docs/retro/0133-inject-sdk-boundary-into-agent-runner.md +0 -45
  180. package/docs/retro/0134-reduce-as-any-casts.md +0 -56
  181. package/docs/retro/0135-extract-display-helpers.md +0 -38
  182. package/docs/retro/0136-decompose-agent-menu.md +0 -43
  183. package/docs/retro/0144-consolidate-observation-model.md +0 -39
  184. package/docs/retro/0145-decompose-execute-push-ctx-to-boundary.md +0 -56
  185. package/docs/retro/0146-narrow-ui-context.md +0 -70
  186. package/docs/retro/0147-inject-wrap-text-into-conversation-viewer.md +0 -90
  187. package/docs/retro/0148-split-agent-widget-rendering.md +0 -39
  188. package/docs/retro/0152-add-prompt-snippet.md +0 -34
  189. package/docs/retro/0164-reorganize-into-domain-directories.md +0 -82
  190. package/docs/retro/0165-decompose-resolved-spawn-config.md +0 -78
  191. package/docs/retro/0166-extract-parent-session-info.md +0 -66
  192. package/docs/retro/0167-narrow-runner-io.md +0 -63
  193. package/docs/retro/0168-extract-tool-filter-config.md +0 -76
  194. package/docs/retro/0169-extract-run-context-from-run-options.md +0 -68
  195. package/docs/retro/0170-reduce-build-content-lines-complexity.md +0 -72
  196. package/docs/retro/0171-reduce-render-result-complexity.md +0 -77
  197. package/docs/retro/0172-extract-turn-formatting.md +0 -80
  198. package/docs/retro/0180-reorder-append-prompt-for-kv-cache.md +0 -62
  199. package/docs/retro/0185-remove-persistent-agent-memory.md +0 -73
  200. package/docs/retro/0188-replace-any-casts-with-sdk-types.md +0 -69
  201. package/docs/retro/0192-define-session-context-interface.md +0 -59
  202. package/docs/retro/0193-runtime-owns-context-queries.md +0 -75
  203. package/docs/retro/0194-align-tool-interfaces-for-structural-typing.md +0 -62
  204. package/docs/retro/0195-convert-tool-factories-to-classes.md +0 -84
  205. package/docs/retro/0196-convert-runner-menu-to-classes.md +0 -73
  206. package/docs/retro/0205-decompose-render-widget-lines.md +0 -64
  207. package/docs/retro/0206-decompose-show-agent-detail.md +0 -67
  208. package/docs/retro/0207-decompose-agent-widget-update.md +0 -92
  209. package/docs/retro/0208-extract-shared-test-fixtures.md +0 -102
  210. package/docs/retro/0214-convert-remaining-closure-factories-to-classes.md +0 -66
  211. package/docs/retro/0215-decompose-build-parent-context.md +0 -61
  212. package/docs/retro/0216-decompose-start-agent.md +0 -80
  213. package/docs/retro/0217-extract-overwrite-guard.md +0 -63
  214. package/docs/retro/0218-push-sdk-boundary-in-settings.md +0 -72
  215. package/docs/retro/0219-reduce-test-duplication-top-3-clone-families.md +0 -64
  216. package/docs/retro/0227-evolve-agent-record-into-agent.md +0 -80
  217. package/docs/retro/0228-async-start-agent-dissolve-run-handle.md +0 -80
  218. package/docs/retro/0229-agent-born-complete.md +0 -89
  219. package/docs/retro/0230-extract-concurrency-queue.md +0 -38
  220. package/docs/retro/0231-push-exec-registry-to-runner.md +0 -71
  221. package/docs/retro/0232-agent-resume-internal-observer-lifecycle.md +0 -109
  222. package/docs/retro/0237-remove-disallowed-tools.md +0 -78
  223. package/docs/retro/0238-remove-extensions-filtering.md +0 -72
  224. package/docs/retro/0239-collapse-filter-active-tools.md +0 -70
  225. package/docs/retro/0242-rename-agent-tool-to-subagent.md +0 -82
  226. package/docs/retro/0256-extract-worktree-isolation.md +0 -89
  227. package/docs/retro/0257-extract-child-session-factory.md +0 -33
  228. package/docs/retro/0262-add-workspace-provider-seam.md +0 -89
  229. package/docs/retro/0264-remove-extension-lifecycle-control.md +0 -91
  230. package/docs/retro/0265-born-complete-subagent-session.md +0 -97
  231. package/docs/retro/0270-type-consumable-public-surface.md +0 -108
  232. package/docs/retro/0272-export-workspace-collaborator-types.md +0 -38
  233. package/docs/retro/0277-encapsulate-agent-session.md +0 -80
  234. package/docs/retro/0280-rename-agent-to-subagent.md +0 -96
  235. package/docs/retro/0360-custom-agents-default-append.md +0 -81
  236. package/docs/retro/0373-extract-subagent-state.md +0 -94
  237. package/docs/retro/0374-encapsulate-subagent-start-notification.md +0 -171
  238. package/docs/retro/0375-extract-run-listener-workspace-bracket.md +0 -104
  239. package/docs/retro/0376-extract-manager-observer.md +0 -81
  240. package/docs/retro/0377-split-widget-delegation-from-runtime.md +0 -105
  241. package/docs/retro/0378-consolidate-lifecycle-test-fixtures.md +0 -100
  242. package/docs/retro/0379-consolidate-ui-tools-test-fixtures.md +0 -52
  243. package/docs/retro/0380-extract-layered-settings-loader.md +0 -97
  244. package/docs/retro/0381-replace-concurrency-queue-with-limiter.md +0 -95
  245. package/docs/retro/0400-include-parent-prompt-in-replace-mode.md +0 -84
  246. package/docs/retro/0403-abort-subagents-on-interrupt.md +0 -90
  247. package/docs/retro/0412-unify-session-mock-builders.md +0 -80
  248. package/docs/retro/0420-fold-run-metrics-live-activity-onto-record.md +0 -90
  249. package/docs/retro/0421-migrate-activity-readers-to-record-getters.md +0 -105
  250. package/docs/retro/0422-delete-activity-tracker-ui-observer.md +0 -97
  251. package/docs/retro/0423-widget-self-drive-from-lifecycle.md +0 -112
  252. package/docs/retro/0424-drop-widget-dep-from-subagent-tool.md +0 -89
  253. package/docs/retro/0425-reconcile-subagent-events-contract.md +0 -101
  254. package/docs/retro/0426-consolidate-residual-test-clone-families.md +0 -100
  255. package/docs/retro/0427-reconsider-ui-direction.md +0 -102
  256. package/docs/retro/0441-remove-agent-definition-management-subtree.md +0 -97
  257. package/docs/retro/0442-dissolve-agents-conversation-viewer.md +0 -108
  258. package/docs/retro/0443-consolidate-remaining-test-clone-families.md +0 -28
  259. package/docs/retro/0444-shrink-widget-to-background-agents.md +0 -81
  260. package/docs/retro/0445-native-session-navigation.md +0 -122
  261. package/docs/retro/0446-spike-session-navigation-entry-criteria.md +0 -99
  262. package/docs/retro/0447-extract-subagents-settings-command.md +0 -91
  263. package/docs/retro/0448-block-disabled-agent-spawn.md +0 -75
  264. package/docs/retro/0462-navigation-renderer-tui-components.md +0 -105
  265. package/docs/retro/0463-file-snapshot-source-evicted-agents.md +0 -103
  266. package/docs/retro/0470-refresh-readme-command-surface.md +0 -40
  267. package/src/observation/notification-state.ts +0 -27
  268. 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