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