@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,250 +0,0 @@
1
- ---
2
- issue: 373
3
- issue_title: "Extract SubagentState; make Subagent execution deps mandatory"
4
- ---
5
-
6
- # Extract `SubagentState`; make `Subagent` execution deps mandatory
7
-
8
- ## Problem Statement
9
-
10
- `Subagent` is simultaneously a passive record and an executor.
11
- Its `SubagentInit` carries ~20 fields — nearly all optional with "required for `run()`, optional for tests" semantics — and `run()` compensates with two runtime guards that throw `"Subagent not configured for execution"`.
12
- This violates the construct-complete principle: one class is both a display-only snapshot (tests and the UI read `.status`/`.result`/stats) and an executor (production wires the session factory, observer, run config, and workspace provider).
13
-
14
- The earlier framing — split `SubagentInit` into a present-or-absent `SubagentRunSpec` + `SubagentExecutionDeps` pair — only *type-encodes* the duality; it does not remove it.
15
- The stronger move (captured in the architecture doc's "First-principles refinement") observes that the passive-record need is *test-only*: production constructs a `Subagent` in exactly one place (`SubagentManager.spawn`), always fully wired.
16
- So the readable state is the common denominator (extract it as a value object), and the execution deps are the optional-only-for-tests part (make them mandatory; push the passive case into the test factory).
17
-
18
- ## Goals
19
-
20
- - Extract `SubagentState` — a value object owning status, result, error, timestamps, and stats (`toolUses`, `lifetimeUsage`, `compactionCount`) plus their transition (`markRunning`, `markCompleted`, …) and accumulation (`incrementToolUses`, `addUsage`, `incrementCompactions`) methods.
21
- - `Subagent` holds one `SubagentState` privately; its existing getters and `markX`/`incrementX`/`addUsage` methods become one-line delegations, leaving the ~40 read sites and the external mutation callers (`markStopped` in the manager, `markCompleted` in `get-result-tool.test.ts`) unchanged.
22
- - Collapse the ~12 remaining execution inputs into a single **mandatory** `SubagentExecution` collaborator; `SubagentManager.spawn` always supplies it.
23
- - Delete the two `"not configured for execution"` throws in `run()` — impossible by construction.
24
- - Move the passive-record construction entirely into `test/helpers/make-subagent.ts`.
25
- - Retarget `subscribeSubagentObserver` at `SubagentState` so state-machine and observer tests need no stub execution.
26
-
27
- This change is **not breaking** for the published service surface.
28
- `src/service/service.ts` exposes `SubagentRecord`, `SubagentStatus`, and the spawn-config types — not `SubagentInit` or the `Subagent` constructor.
29
- The read API (getters, `SubagentStatus`) is unchanged; only the internal constructor signature changes.
30
-
31
- ## Non-Goals
32
-
33
- - Hoisting **metrics** (tool uses, token usage, compaction count) into a projection over the child session's event stream — stats stay on `SubagentState` for now (the third of four domains in the refinement, deferred).
34
- - Extracting **result delivery** (`notification` / `resultConsumed`) into its own domain — the fourth domain, deferred.
35
- - Encapsulating run start / making `promise` and `notification` read-only — that is Step 3 (#374); `notification` stays created in the constructor here.
36
- - Extracting run-listener / workspace-bracket collaborators — Step 4 (#375).
37
- - Renaming `record-observer.ts` — only its parameter type changes.
38
-
39
- ## Background
40
-
41
- - `src/lifecycle/subagent.ts` — the class under change.
42
- State fields (`_status`, `_result`, `_error`, `_startedAt`, `_completedAt`, `_toolUses`, `_lifetimeUsage`, `_compactionCount`) sit behind getters; transition/accumulation methods mutate them.
43
- `run()` reads the execution inputs and throws if `createSubagentSession`/`snapshot`/`prompt` are missing.
44
- `resume()` reads `observer` and throws only on a missing session (a genuine runtime condition — kept).
45
- - `src/lifecycle/subagent-manager.ts` — `spawn()` (line ~139) is the **only** production `new Subagent(...)` site; it sets the initial status (`queued` for background so the limiter thunk's `status !== "queued"` guard works, `running` for foreground).
46
- - `src/observation/record-observer.ts` — `subscribeSubagentObserver(session, record, { onCompact })` calls `record.incrementToolUses()`, `record.addUsage(…)`, `record.incrementCompactions()`, and forwards `onCompact(record, info)`.
47
- - `src/lifecycle/usage.ts` — `LifetimeUsage` type and the `addUsage(into, delta)` accumulator that `SubagentState` will own.
48
- - `test/helpers/make-subagent.ts` — `createTestSubagent` builds passive records; stat shorthands apply via mutation methods.
49
- - `test/lifecycle/subagent.test.ts` (~700 LOC) and `test/observation/record-observer.test.ts` — the test files that construct `Subagent` directly.
50
-
51
- AGENTS.md / code-design constraints that apply:
52
-
53
- - Keep Pi SDK imports out of `SubagentState` — it is a pure value object (imports only `LifetimeUsage`/`addUsage`).
54
- - `Subagent` is exported from the `types.ts` barrel; verify the barrel re-export still resolves after the file split.
55
- - `SubagentStatus` is re-exported by `src/service/service.ts` from `#src/lifecycle/subagent` — keep that import path valid (re-export `SubagentStatus` from `subagent.ts` even if its definition moves to `subagent-state.ts`), so the public type bundle path is unchanged.
56
-
57
- ## Design Overview
58
-
59
- ### `SubagentState` value object (`src/lifecycle/subagent-state.ts`)
60
-
61
- A pure, independently constructible value object. `SubagentStatus` moves here (its natural home) and is re-exported from `subagent.ts`.
62
-
63
- ```ts
64
- export type SubagentStatus =
65
- | "queued" | "running" | "completed" | "steered"
66
- | "aborted" | "stopped" | "error";
67
-
68
- export interface SubagentStateInit {
69
- status?: SubagentStatus;
70
- result?: string;
71
- error?: string;
72
- startedAt?: number;
73
- completedAt?: number;
74
- // stats always start at zero; callers accumulate via mutation methods
75
- }
76
-
77
- export class SubagentState {
78
- constructor(init?: SubagentStateInit); // status ?? "queued", startedAt ?? Date.now()
79
- // getters: status, result, error, startedAt, completedAt,
80
- // toolUses, lifetimeUsage (Readonly), compactionCount
81
- // transitions: markRunning, markCompleted, markAborted, markSteered,
82
- // markError, markStopped, resetForResume
83
- // accumulators: incrementToolUses, addUsage, incrementCompactions
84
- }
85
- ```
86
-
87
- It owns its mutations (no output arguments — methods write only its own private fields) and carries no upstream dependency beyond `usage.ts`.
88
-
89
- ### `SubagentExecution` collaborator (in `src/lifecycle/subagent.ts`)
90
-
91
- ```ts
92
- export interface SubagentExecution {
93
- createSubagentSession: (params: CreateSubagentSessionParams) => Promise<SubagentSession>;
94
- snapshot: ParentSnapshot;
95
- prompt: string;
96
- baseCwd: string;
97
- observer?: SubagentLifecycleObserver;
98
- getRunConfig?: () => RunConfig;
99
- getWorkspaceProvider?: () => WorkspaceProvider | undefined;
100
- model?: Model<any>;
101
- maxTurns?: number;
102
- thinkingLevel?: ThinkingLevel;
103
- parentSession?: ParentSessionInfo;
104
- signal?: AbortSignal;
105
- }
106
- ```
107
-
108
- The four fields the old `run()` guards required (`createSubagentSession`, `snapshot`, `prompt`, plus `baseCwd`) are mandatory; the genuinely-optional behavior knobs stay optional.
109
-
110
- ### `Subagent` constructor
111
-
112
- ```ts
113
- export interface SubagentInit {
114
- id: string;
115
- type: SubagentType;
116
- description: string;
117
- invocation?: AgentInvocation;
118
- execution: SubagentExecution; // mandatory — no more "optional for tests"
119
- state?: SubagentState; // defaults to new SubagentState() (fresh "queued")
120
- }
121
- ```
122
-
123
- `SubagentInit` drops from ~20 fields to 5.
124
- `Subagent` keeps `id`/`type`/`description`/`invocation`, `abortController`, `subagentSession?`, `notification?`, steer buffer, and per-run listener handles; it holds `private readonly state` and `private readonly execution`.
125
- Getters and mutation methods delegate one line to `this.state`.
126
- `notification` is still created in the constructor from `execution.parentSession?.toolCallId` (Step 3 moves it).
127
-
128
- ### Consumer call site — `SubagentManager.spawn`
129
-
130
- ```ts
131
- const execution: SubagentExecution = {
132
- createSubagentSession: this.createSubagentSession,
133
- snapshot, prompt, baseCwd: this.baseCwd,
134
- observer: this.buildObserver(options),
135
- getRunConfig: this.getRunConfig,
136
- getWorkspaceProvider: () => this._workspaceProvider,
137
- model: options.model, maxTurns: options.maxTurns,
138
- thinkingLevel: options.thinkingLevel,
139
- parentSession: options.parentSession, signal: options.signal,
140
- };
141
- const record = new Subagent({
142
- id, type, description: options.description, invocation: options.invocation,
143
- execution,
144
- state: new SubagentState({ status: options.isBackground ? "queued" : "running", startedAt: Date.now() }),
145
- });
146
- ```
147
-
148
- Tell-Don't-Ask: the execution bundle is assembled once and handed over; nothing reaches back into `spawn`'s locals afterward.
149
-
150
- ### Observer retarget (`src/observation/record-observer.ts`)
151
-
152
- The observer accumulates *stats*, not lifecycle — point it at `SubagentState` and drop the record from `onCompact`:
153
-
154
- ```ts
155
- export function subscribeSubagentObserver(
156
- session: SubscribableSession,
157
- state: SubagentState,
158
- options?: { onCompact?: (info: CompactionInfo) => void },
159
- ): () => void
160
- ```
161
-
162
- `Subagent.run()`/`resume()` wire it with `this.state` and close over `this` to forward itself to the lifecycle observer:
163
-
164
- ```ts
165
- this.attachObserver(subscribeSubagentObserver(this.subagentSession, this.state, {
166
- onCompact: (info) => this.execution.observer?.onCompacted?.(this, info),
167
- }));
168
- ```
169
-
170
- This removes the observer's only dependency on `Subagent`, so observer tests construct a `SubagentState` directly.
171
- `SubagentLifecycleObserver.onCompacted(agent, info)` is unchanged — it still receives the `Subagent`.
172
-
173
- Decision: the observer takes `SubagentState` concretely rather than a one-off narrow `incrementToolUses`/`addUsage`/`incrementCompactions` interface.
174
- `SubagentState` is a cohesive owned value object, not a wide dependency bag, and a single internal call site does not justify a speculative interface (recorded in Open Questions).
175
-
176
- ### Edge cases
177
-
178
- - **Initial status for the limiter guard** — production passes `state: new SubagentState({ status: queued|running })`; the background path still observes `queued` so `limiter.schedule(() => record.status !== "queued" ? … : record.run())` behaves identically.
179
- - **`resume()` without a prior run** — the `"not configured for resume — missing session"` throw stays; it guards a genuine runtime state (no session was ever created), not a construction concern.
180
- - **`make-subagent.ts` passive records** — built as `state: new SubagentState({ status: "completed", result, startedAt, completedAt })` plus a `makeStubExecution()`; stat shorthands keep delegating through `Subagent`'s methods.
181
-
182
- ## Module-Level Changes
183
-
184
- - **`src/lifecycle/subagent-state.ts`** (new) — `SubagentState`, `SubagentStateInit`, and the `SubagentStatus` type (moved from `subagent.ts`).
185
- - **`src/lifecycle/subagent.ts`** —
186
- - Re-export `SubagentStatus` from `subagent-state.ts` (keeps service.ts import path valid).
187
- - Add `SubagentExecution`; rewrite `SubagentInit` to the 5-field shape; remove the flat execution/run-config fields.
188
- - Remove the (unused) `isBackground?` field from `SubagentInit` — never read in this class.
189
- - Hold `private readonly state` / `private readonly execution`; convert getters and `markX`/`incrementX`/`addUsage`/`resetForResume` to delegations.
190
- - `run()`: delete the two `"not configured for execution"` throws; read inputs from `this.execution`; wire the observer at `this.state`.
191
- - `resume()`: read `observer` from `this.execution`; keep the missing-session throw.
192
- - **`src/lifecycle/subagent-manager.ts`** — `spawn()` builds the `SubagentExecution` bundle and the initial `SubagentState`; no other change (`markStopped` calls untouched).
193
- - **`src/observation/record-observer.ts`** — param `record: Subagent` → `state: SubagentState`; `onCompact` signature `(record, info)` → `(info)`; update the JSDoc bullet wording.
194
- - **`test/helpers/make-subagent.ts`** — add `makeStubExecution()`; build the passive record via `state` + `execution`.
195
- - **`test/lifecycle/subagent.test.ts`** — funnel constructions through a local helper (prep refactor); move the pure state-machine `describe` blocks to the new state test; supply `execution` everywhere; update the missing-factory test (the throw is gone — replace with a type-level/construction assertion or remove).
196
- - **`test/lifecycle/subagent-state.test.ts`** (new) — state-machine + accumulation tests targeting `SubagentState` directly.
197
- - **`test/observation/record-observer.test.ts`** — `makeRecord` → build a `SubagentState`; assert stats on it; `onCompact` test uses `(info)`.
198
- - **Docs** —
199
- - `docs/architecture/architecture.md`: add `subagent-state.ts` to the lifecycle directory listing; update the `Subagent` class diagram (state delegations, new collaborators); mark **Step 2 ✅ Complete**; refresh the Phase 17 problem-statement prose (line ~879) and the type-complexity table (`SubagentInit` row at line ~649, add `SubagentExecution`).
200
- - `.pi/skills/package-pi-subagents/SKILL.md`: Lifecycle domain `10 → 11` modules (add `subagent-state.ts`); total `56 → 57` files.
201
-
202
- Grep sweep confirmed no other `src/`, `test/`, or `SKILL.md` references to the removed flat fields or the `"not configured for execution"` string outside `subagent.ts`.
203
-
204
- ## Test Impact Analysis
205
-
206
- 1. **New tests the extraction enables** — `test/lifecycle/subagent-state.test.ts` exercises every transition and accumulator on `SubagentState` with no `Subagent`, no execution stub, no session.
207
- This is the construct-complete payoff: the state machine is now unit-testable in isolation.
208
- 2. **Tests that become redundant** — the state-machine `describe` blocks in `subagent.test.ts` (`markRunning`, `markCompleted`, `markAborted`, `markSteered`, `markError`, `markStopped`, `incrementToolUses`, `addUsage`, `incrementCompactions`, `resetForResume`, and the pure constructor-defaults cases) move out, leaving `subagent.test.ts` to cover identity, session-encapsulation delegation, abort, `run()`/`resume()`, `completeRun`/`failRun`, and listeners.
209
- `record-observer.test.ts` no longer constructs a `Subagent` — it builds a `SubagentState`.
210
- 3. **Tests that must stay as-is** — `run()`/`resume()` integration tests genuinely exercise the executor (factory wiring, observer lifecycle, workspace bracket); the `createTestSubagent` consumers across `test/ui/` and `test/tools/` exercise read sites and are unaffected (the helper absorbs the construction change).
211
-
212
- ## TDD Order
213
-
214
- 1. **Extract `SubagentState` (pure addition; lift-and-shift prep).**
215
- - Create `src/lifecycle/subagent-state.ts` and `test/lifecycle/subagent-state.test.ts` (red → green for every transition/accumulator).
216
- - Refactor `Subagent` to hold a `SubagentState` built from the *existing* `SubagentInit` fields; getters/mutators delegate. `SubagentStatus` moves to `subagent-state.ts`, re-exported from `subagent.ts`.
217
- - In `subagent.test.ts`, introduce a local construction helper and route call sites through it; move the pure state-machine `describe` blocks into `subagent-state.test.ts`.
218
- - No consumer breaks (init shape unchanged). `pnpm run check` + `test`.
219
- - Commit: `refactor: extract SubagentState value object (#373)`.
220
- 2. **Retarget the observer at `SubagentState`.**
221
- - Change `subscribeSubagentObserver` to accept `SubagentState` and an `onCompact(info)`; update the two `subagent.ts` call sites (pass `this.state`, close over `this`).
222
- - Rewrite `record-observer.test.ts` to build a `SubagentState`.
223
- - Commit: `refactor: target SubagentState in subagent observer (#373)`.
224
- 3. **Make execution mandatory (the atomic construction flip).**
225
- - Add `SubagentExecution`; rewrite `SubagentInit` to the 5-field shape; remove flat execution/run-config fields and `isBackground?`; delete the two `run()` throws; read from `this.execution`.
226
- - Update the single production site (`subagent-manager.ts spawn`) and `make-subagent.ts` (`makeStubExecution` + `state`) and the `subagent.test.ts` helper/`createRunnableAgent`/`createResumableAgent` in the **same commit** — removing the optional fields breaks every construction at the type level simultaneously.
227
- - Adjust the now-obsolete "missing session factory" test (throw is gone).
228
- - `pnpm run check`, `lint`, `test`, `fallow dead-code`.
229
- - Commit: `refactor: make Subagent execution deps mandatory (#373)`.
230
- 4. **Docs.**
231
- - Update `architecture.md` (file listing, class diagram, mark Step 2 complete, prose, type table) and `SKILL.md` (domain counts).
232
- - Commit: `docs: record SubagentState extraction in architecture and skill (#373)`.
233
-
234
- Optionally run `pnpm run verify:public-types` after Step 3 to confirm the public bundle is unaffected (expected: no change — the bundle does not reference `SubagentInit`).
235
-
236
- ## Risks and Mitigations
237
-
238
- - **Large test file churn (`subagent.test.ts`, ~700 LOC).**
239
- Mitigated by lift-and-shift: Step 1 funnels constructions through a local helper and moves state tests out, so Step 3's mandatory-execution flip edits the helper + two run/resume factories rather than rewriting the file.
240
- - **Initial-status regression for the limiter.**
241
- Production explicitly sets `state` status in `spawn`; a `subagent-manager` test should assert a background spawn reports `queued` before its slot opens (existing coverage; re-verify).
242
- - **`SubagentStatus` re-export path.**
243
- Keep `SubagentStatus` re-exported from `subagent.ts`; `verify:public-types` (and `pnpm run check`) confirm `service.ts` still resolves it.
244
- - **Circular import (`subagent.ts` ↔ `subagent-state.ts`).**
245
- Avoided: `subagent-state.ts` defines `SubagentStatus` and imports nothing from `subagent.ts`; `subagent.ts` imports `SubagentState`/`SubagentStatus` from `subagent-state.ts`.
246
-
247
- ## Open Questions
248
-
249
- - Whether the observer should accept a narrow `{ incrementToolUses; addUsage; incrementCompactions }` interface instead of `SubagentState` concretely — deferred; revisit if a second consumer of the stats sink appears (defer-until-needed, per the metrics-projection work in a later phase).
250
- - Whether the `make-subagent.ts` stat shorthands should construct `SubagentState` directly rather than delegating through `Subagent` — cosmetic; left until Step 7's fixture consolidation (#378).
@@ -1,268 +0,0 @@
1
- ---
2
- issue: 374
3
- issue_title: "Encapsulate run start and notification attachment on Subagent"
4
- ---
5
-
6
- # Encapsulate Subagent.start() and read-only promise/notification
7
-
8
- ## Problem Statement
9
-
10
- `Subagent.promise` is assigned from outside the class in three places — `SubagentManager.spawn()` (two sites: scheduled and immediate paths) — and `record.notification` is assigned from outside the class in seven test sites.
11
- Both are output-argument smells (design-review check 3): the object should own the state its own methods read.
12
- `Subagent.run()` already exists; the promise that tracks it lives outside the object purely so callers can `await record.promise`.
13
- `notification` was already moved to the constructor in Phase 17 Step 2 (wired from `execution.parentSession?.toolCallId`), but the field is still publicly writable, so tests bypass the constructor path with direct assignment.
14
-
15
- ## Goals
16
-
17
- - Add `Subagent.start()` that calls `run()`, stores the resulting promise internally, and returns it.
18
- - Fold the abort-while-queued status guard into `start()`, removing the inline check from `SubagentManager`.
19
- - Make `promise` externally read-only: private `_promise` field backed by a public `get promise()` accessor.
20
- - Make `notification` externally read-only: private `_notification` field backed by a public `get notification()` accessor.
21
- - Add `toolCallId?: string` to `TestSubagentOptions` so tests wire notification state via the constructor path without external writes.
22
- - Achieve grep-verifiable outcome: `\.promise =` and `\.notification =` appear only inside `subagent.ts`.
23
-
24
- ## Non-Goals
25
-
26
- - Extracting `RunListeners` or workspace-bracket collaborators from `Subagent` (Phase 17 Step 4, Issue [#375]).
27
- - Extracting the manager observer from `index.ts` (Phase 17 Step 5, Issue [#376]).
28
- - Any other Phase 17 step beyond Step 3.
29
-
30
- ## Background
31
-
32
- Phase 17 Step 1 ([#381]) replaced `ConcurrencyQueue` with a `ConcurrencyLimiter` — the manager now calls `this.limiter.schedule(thunk)` and stores the scheduled promise on `record.promise`.
33
- Phase 17 Step 2 ([#373]) extracted `SubagentState`, made `SubagentExecution` mandatory, and wired `notification` in the constructor via `execution.parentSession?.toolCallId`.
34
-
35
- Current external write sites after Step 2:
36
-
37
- | Field | Location | Count |
38
- | --------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
39
- | `record.promise` | `SubagentManager.spawn()` | 2 (scheduled + immediate) |
40
- | `record.promise` | Test files | 3 (`get-result-tool.test.ts`, `service-adapter.test.ts`, `make-subagent.test.ts`) |
41
- | `record.notification` | Test files | 7 (`get-result-tool.test.ts` ×2, `subagent-manager.test.ts` ×2, `service-adapter.test.ts` ×1, `notification.test.ts` ×2) |
42
-
43
- `SubagentManager.spawnAndWait()` and `waitForAll()` read `record.promise` via the public field — these become getter reads after the change.
44
- `get-result-tool.ts` reads `record.promise` to `await` it when `wait=true` — unchanged (getter).
45
-
46
- The `AGENTS.md` constraint that applies: **output arguments** — if a function sets a field on a received object, it is doing work that belongs inside the owning object.
47
-
48
- ## Design Overview
49
-
50
- ### `Subagent.start()` and the status guard
51
-
52
- ```typescript
53
- private _promise?: Promise<void>;
54
-
55
- /** Awaitable handle to the running promise. Set by start(). */
56
- get promise(): Promise<void> | undefined {
57
- return this._promise;
58
- }
59
-
60
- /**
61
- * Start execution: call run(), store the promise, and return it.
62
- * Guards against non-active states (e.g. abort-while-queued): if the agent
63
- * is neither queued nor running, the promise resolves immediately (no-op).
64
- */
65
- start(): Promise<void> {
66
- if (this.status !== "queued" && this.status !== "running") {
67
- this._promise = Promise.resolve();
68
- return this._promise;
69
- }
70
- this._promise = this.run();
71
- return this._promise;
72
- }
73
- ```
74
-
75
- The guard allows:
76
-
77
- - `"queued"` — background agent waiting in the limiter; `run()` proceeds normally.
78
- - `"running"` — foreground agent (status set to `"running"` at construction in the manager); `run()` proceeds normally.
79
- - Any terminal state (`"stopped"`, `"error"`, `"completed"`, etc.) — agent was aborted while queued; `start()` becomes a no-op returning an immediately-resolving promise.
80
-
81
- This folds the inline `if (record.status !== "queued") return Promise.resolve()` guard out of the `SubagentManager` limiter callback.
82
-
83
- ### `SubagentManager.spawn()` after the change
84
-
85
- ```typescript
86
- // Queued background path
87
- this.limiter.schedule(() => record.start());
88
-
89
- // Immediate path (foreground or bypassQueue)
90
- record.start();
91
- ```
92
-
93
- `spawnAndWait()` continues to `await record.promise` (now uses the getter, no behavior change).
94
- `waitForAll()`'s `pendingPromises()` continues to `r.promise` (getter — no behavior change).
95
-
96
- ### `notification` encapsulation
97
-
98
- The constructor already writes to `this.notification` internally.
99
- After the change, the constructor writes to `this._notification`:
100
-
101
- ```typescript
102
- private _notification?: NotificationState;
103
-
104
- get notification(): NotificationState | undefined {
105
- return this._notification;
106
- }
107
-
108
- // In constructor:
109
- const toolCallId = init.execution.parentSession?.toolCallId;
110
- if (toolCallId) {
111
- this._notification = new NotificationState(toolCallId);
112
- }
113
- ```
114
-
115
- No production writes to `notification` outside the constructor — only test sites need updating.
116
-
117
- ### `TestSubagentOptions` shorthand
118
-
119
- Add `toolCallId?: string` so tests that need a `NotificationState` use the constructor path:
120
-
121
- ```typescript
122
- // Before
123
- const record = createTestSubagent();
124
- record.notification = new NotificationState("tc-1");
125
-
126
- // After
127
- const record = createTestSubagent({ toolCallId: "tc-1" });
128
- ```
129
-
130
- In `createTestSubagent`, `toolCallId` routes through `makeStubExecution({ parentSession: { toolCallId } })`.
131
-
132
- ### Tests that write `record.promise`
133
-
134
- - **`service-adapter.test.ts`** ("strips promise from the record" tests): the test only needs `promise` to be absent from the serialized output.
135
- Since `toSubagentRecord()` already builds an explicit object without `promise`, these tests pass without any promise being set on the record.
136
- Remove the `record.promise = ...` setup.
137
- - **`make-subagent.test.ts`** ("allows setting promise directly"): the test's intent was to verify the field was settable.
138
- Replace with a test that `start()` sets `promise` internally via the stub execution.
139
- - **`get-result-tool.test.ts`** ("waits for promise when wait=true"): the test needs a running agent whose promise resolves and updates status to completed.
140
- Replace with an execution stub where `runTurnLoop` returns `{ responseText: "Finished after wait.", aborted: false, steered: false }` and call `record.start()`.
141
- The `createSubagentSessionStub()` default already resolves with `{ responseText: "done", ... }` — override `runTurnLoop` to return the expected text.
142
-
143
- ### `subagent-manager.test.ts` notification tests (lines 82, 100)
144
-
145
- Tests that reproduce the race-condition bug (notification set post-spawn) become:
146
-
147
- ```typescript
148
- const id = manager.spawn(STUB_SNAPSHOT, "general-purpose", "test", {
149
- description: "bg",
150
- isBackground: true,
151
- parentSession: { toolCallId: "tc-1" },
152
- });
153
- const record = manager.getRecord(id)!;
154
- // notification is already wired from the constructor
155
- await record.promise;
156
- record.notification?.markConsumed();
157
- ```
158
-
159
- The behavior under test (race: `markConsumed()` after `await` is too late) is unchanged.
160
-
161
- ## Module-Level Changes
162
-
163
- - `src/lifecycle/subagent.ts`
164
- - Remove public writable `promise?: Promise<void>` field.
165
- - Add `private _promise?: Promise<void>`.
166
- - Add `get promise(): Promise<void> | undefined`.
167
- - Add `start(): Promise<void>` with the status guard.
168
- - Rename `this.notification` write in constructor to `this._notification`.
169
- - Remove public writable `notification?: NotificationState` field.
170
- - Add `private _notification?: NotificationState`.
171
- - Add `get notification(): NotificationState | undefined`.
172
- - `src/lifecycle/subagent-manager.ts`
173
- - Replace `record.promise = this.limiter.schedule(() => { if (...) return ...; return record.run(); })` with `this.limiter.schedule(() => record.start())`.
174
- - Replace `record.promise = record.run()` with `record.start()`.
175
- - `test/helpers/make-subagent.ts`
176
- - Add `toolCallId?: string` to `TestSubagentOptions`.
177
- - In `createTestSubagent`, map `toolCallId` to `makeStubExecution({ parentSession: { toolCallId } })`.
178
- - `test/helpers/make-subagent.test.ts`
179
- - Replace "allows setting promise directly after construction" with a test that `start()` stores promise via the execution stub.
180
- - `test/tools/get-result-tool.test.ts`
181
- - Replace `record.promise = Promise.resolve().then(...)` setup with a stub execution + `record.start()`.
182
- - Replace `record.notification = new NotificationState("tc-1")` (×2) with `createTestSubagent({ toolCallId: "tc-1" })`.
183
- - `test/lifecycle/subagent-manager.test.ts`
184
- - Replace `record.notification = new NotificationState("tc-1")` (×2) with spawn options carrying `parentSession: { toolCallId: "tc-1" }`.
185
- - `test/service/service-adapter.test.ts`
186
- - Remove `record.promise = Promise.resolve()` setup (×2) from tests that only need to verify `toSubagentRecord()` strips the field.
187
- - Replace `record.notification = new NotificationState("tc-1")` with `createTestSubagent({ toolCallId: "tc-1" })`.
188
- - `test/observation/notification.test.ts`
189
- - Replace `record.notification = new NotificationState("tc-123/tc-1")` (×2) with `createTestSubagent({ toolCallId: "tc-123/tc-1" })`.
190
- - `docs/architecture/architecture.md`
191
- - Mark Step 3 `✅ Complete` and add a "Landed" note.
192
-
193
- ## Test Impact Analysis
194
-
195
- 1. **New unit tests enabled**: `start()` behavior (promise stored, status guard no-op) can be tested directly in `subagent.test.ts` without touching the manager.
196
- 2. **Existing tests simplified**: The 7 test sites that do `record.notification = ...` drop an artificial mutation and instead use the natural constructor path — the tests are shorter and closer to production semantics.
197
- 3. **Tests that must stay**: The manager's race-condition tests (lines 74–120) verify ordering of `markConsumed()` vs `await promise` — they change setup only (spawn with toolCallId), not intent.
198
- 4. **Tests removed**: The `make-subagent.test.ts` "allows setting promise" test is replaced, since direct write is no longer possible.
199
-
200
- ## TDD Order
201
-
202
- 1. **Add `Subagent.start()` alongside the existing public `promise?` field**
203
-
204
- In `test/lifecycle/subagent.test.ts`, add tests:
205
- - `start()` on a running agent returns a defined promise.
206
- - `start()` on a stopped agent returns a resolving promise immediately (no-op guard).
207
- - After `start()`, `record.promise` matches the returned promise.
208
-
209
- In `src/lifecycle/subagent.ts`: add `private _promise`, `get promise()` (shadowing the old field — TypeScript will require removing the duplicate; advance to step 2 immediately), and `start()`.
210
- Commit: `test: add Subagent.start() tests and initial implementation (#374)`
211
-
212
- 2. **Make `promise` read-only — remove public field, update all write sites**
213
-
214
- Breaking change at the type level.
215
- Atomic commit must include:
216
- - `src/lifecycle/subagent.ts` — remove `promise?: Promise<void>` public field (only `private _promise` + getter remain).
217
- - `src/lifecycle/subagent-manager.ts` — replace both `record.promise = ...` sites with `record.start()` calls; limiter thunk becomes `() => record.start()`.
218
- - `test/helpers/make-subagent.test.ts` — replace write-promise test with `start()` test.
219
- - `test/tools/get-result-tool.test.ts` — replace `record.promise = ...` setup; use execution stub + `record.start()`.
220
- - `test/service/service-adapter.test.ts` — remove `record.promise = Promise.resolve()` setup (×2).
221
-
222
- Run `pnpm --filter @gotgenes/pi-subagents run check` to verify.
223
- Commit: `feat: make Subagent.promise read-only, add start() (#374)`
224
-
225
- 3. **Make `notification` read-only — remove public field, update all write sites**
226
-
227
- Breaking change at the type level.
228
- Atomic commit must include:
229
- - `src/lifecycle/subagent.ts` — rename public `notification?` to `private _notification`; add `get notification()`; constructor write becomes `this._notification = ...`.
230
- - `test/helpers/make-subagent.ts` — add `toolCallId?: string` to `TestSubagentOptions`; route through `makeStubExecution`.
231
- - `test/tools/get-result-tool.test.ts` — replace `record.notification = new NotificationState(...)` (×2) with `createTestSubagent({ toolCallId: ... })`.
232
- - `test/lifecycle/subagent-manager.test.ts` — replace `record.notification = new NotificationState(...)` (×2) with spawn options carrying `parentSession: { toolCallId: ... }`.
233
- - `test/service/service-adapter.test.ts` — replace `record.notification = new NotificationState(...)` with `createTestSubagent({ toolCallId: ... })`.
234
- - `test/observation/notification.test.ts` — replace `record.notification = new NotificationState(...)` (×2) with `createTestSubagent({ toolCallId: ... })`.
235
-
236
- Run `pnpm --filter @gotgenes/pi-subagents exec vitest run` and `pnpm --filter @gotgenes/pi-subagents run check`.
237
- Commit: `feat: make Subagent.notification read-only, update tests (#374)`
238
-
239
- 4. **Update architecture doc**
240
-
241
- In `docs/architecture/architecture.md`, mark Step 3 `✅ Complete` and add a "Landed" note summarizing the outcome.
242
- Also update the note at line 943 that says "Step 3 later folds the guard into `Subagent.start()`" to reflect it is now done.
243
- Commit: `docs: mark Phase 17 Step 3 complete in architecture.md (#374)`
244
-
245
- ## Risks and Mitigations
246
-
247
- - **Risk**: Adding both `private _promise` and `get promise()` while the public `promise?` field still exists is a TypeScript error (duplicate identifier).
248
- **Mitigation**: Steps 1 and 2 are merged into one commit: introduce `start()`, remove the public writable field, and fix all consumers atomically.
249
- The TDD order describes testing `start()` first, but both the public field removal and the consumer updates land in the same `feat:` commit.
250
- - **Risk**: The status guard in `start()` allows `"running"` for foreground agents, which have `status = "running"` at construction.
251
- If a foreground agent is stopped before `start()` is called (edge case), `run()` would call `markRunning()` on an already-stopped agent.
252
- **Mitigation**: Foreground agents are started synchronously at the end of `spawn()` — there is no window between construction and `start()` during which the abort path can fire.
253
- The guard is conservative and causes no regression.
254
- - **Risk**: The race-condition test in `subagent-manager.test.ts` (lines 74–107) verifies that `markConsumed()` called after `await record.promise` is "too late" for the observer.
255
- Switching from `record.notification = new NotificationState("tc-1")` to the constructor path does not change timing semantics.
256
- **Mitigation**: The test body stays structurally identical; only the setup changes.
257
- - **Risk**: `service-adapter.test.ts` tests that call `record.promise = Promise.resolve()` might be testing that the field exists on the Subagent type.
258
- **Mitigation**: The tests are testing `toSubagentRecord()` output, not the field type.
259
- Removing the setup doesn't change the assertion.
260
-
261
- ## Open Questions
262
-
263
- - None.
264
- The design is fully specified by the Phase 17 Step 3 architecture note and the existing class structure.
265
-
266
- [#373]: https://github.com/gotgenes/pi-packages/issues/373
267
- [#375]: https://github.com/gotgenes/pi-packages/issues/375
268
- [#381]: https://github.com/gotgenes/pi-packages/issues/381