@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.
- package/CHANGELOG.md +14 -0
- package/README.md +22 -31
- package/docs/architecture/architecture.md +157 -204
- package/docs/architecture/history/phase-19-implement-ui-decisions.md +282 -0
- package/package.json +5 -6
- package/src/lifecycle/subagent.ts +5 -9
- package/src/observation/notification.ts +23 -10
- package/src/observation/subagent-events-observer.ts +1 -3
- package/src/tools/get-result-report.ts +60 -0
- package/src/tools/get-result-tool.ts +25 -40
- package/src/types.ts +1 -1
- package/src/ui/session-navigator.ts +0 -2
- package/.prettierignore +0 -5
- package/AGENTS.md +0 -7
- package/docs/plans/0048-implement-subagents-api.md +0 -304
- package/docs/plans/0049-remove-group-join-output-file-rpc.md +0 -180
- package/docs/plans/0051-update-adr-0001-hard-fork.md +0 -77
- package/docs/plans/0052-remove-scheduled-subagents.md +0 -133
- package/docs/plans/0053-extract-model-resolution-from-execute.md +0 -181
- package/docs/plans/0054-decompose-index-into-modules.md +0 -302
- package/docs/plans/0057-structured-debug-logging.md +0 -154
- package/docs/plans/0061-session-format-transcript.md +0 -284
- package/docs/plans/0066-replace-as-any-with-sdk-types.md +0 -215
- package/docs/plans/0069-create-subagent-runtime.md +0 -345
- package/docs/plans/0070-extract-event-handlers.md +0 -306
- package/docs/plans/0071-extract-session-config-assembler.md +0 -362
- package/docs/plans/0072-inject-agent-manager-collaborators.md +0 -329
- package/docs/plans/0076-inject-cwd-into-agent-manager.md +0 -102
- package/docs/plans/0077-inject-project-agents-dir.md +0 -115
- package/docs/plans/0080-consolidate-agent-config-lookup.md +0 -247
- package/docs/plans/0084-extract-git-worktree-manager.md +0 -142
- package/docs/plans/0087-evolve-subagent-runtime-methods.md +0 -240
- package/docs/plans/0098-extract-agent-record-state-machine.md +0 -435
- package/docs/plans/0099-replace-ctx-with-parent-snapshot.md +0 -488
- package/docs/plans/0100-replace-callback-threading-with-session-subscription.md +0 -454
- package/docs/plans/0102-consolidate-test-record-factory.md +0 -176
- package/docs/plans/0108-extract-agent-type-registry.md +0 -322
- package/docs/plans/0109-extract-settings-manager.md +0 -276
- package/docs/plans/0110-agent-activity-tracker.md +0 -297
- package/docs/plans/0111-split-agent-record-lifecycle.md +0 -582
- package/docs/plans/0112-replace-agent-manager-callbacks.md +0 -241
- package/docs/plans/0113-disambiguate-spawn-options.md +0 -155
- package/docs/plans/0114-narrow-agent-tool-menu-deps.md +0 -279
- package/docs/plans/0115-decompose-agent-tool.md +0 -337
- package/docs/plans/0116-type-housekeeping.md +0 -351
- package/docs/plans/0118-settings-manager-apply-methods.md +0 -271
- package/docs/plans/0123-remove-vi-fn-cast-smell.md +0 -179
- package/docs/plans/0131-consolidate-shared-test-fixtures.md +0 -207
- package/docs/plans/0132-inject-io-into-session-config.md +0 -219
- package/docs/plans/0133-inject-sdk-boundary-into-agent-runner.md +0 -373
- package/docs/plans/0134-reduce-as-any-casts.md +0 -366
- package/docs/plans/0135-extract-display-helpers.md +0 -182
- package/docs/plans/0136-decompose-agent-menu.md +0 -300
- package/docs/plans/0144-consolidate-observation-model.md +0 -263
- package/docs/plans/0145-decompose-execute-push-ctx-to-boundary.md +0 -290
- package/docs/plans/0146-narrow-ui-context.md +0 -319
- package/docs/plans/0147-inject-wrap-text-into-conversation-viewer.md +0 -166
- package/docs/plans/0148-split-agent-widget-rendering.md +0 -255
- package/docs/plans/0152-add-prompt-snippet.md +0 -91
- package/docs/plans/0164-reorganize-into-domain-directories.md +0 -409
- package/docs/plans/0165-decompose-resolved-spawn-config.md +0 -157
- package/docs/plans/0166-extract-parent-session-info.md +0 -231
- package/docs/plans/0167-narrow-runner-io.md +0 -150
- package/docs/plans/0168-extract-tool-filter-config.md +0 -173
- package/docs/plans/0169-extract-run-context-from-run-options.md +0 -194
- package/docs/plans/0170-reduce-build-content-lines-complexity.md +0 -225
- package/docs/plans/0171-reduce-render-result-complexity.md +0 -200
- package/docs/plans/0172-extract-turn-formatting.md +0 -206
- package/docs/plans/0180-reorder-append-prompt-for-kv-cache.md +0 -100
- package/docs/plans/0185-remove-persistent-agent-memory.md +0 -161
- package/docs/plans/0188-replace-any-casts-with-sdk-types.md +0 -162
- package/docs/plans/0192-define-session-context-interface.md +0 -107
- package/docs/plans/0193-runtime-owns-context-queries.md +0 -245
- package/docs/plans/0194-align-tool-interfaces-for-structural-typing.md +0 -139
- package/docs/plans/0195-convert-tool-factories-to-classes.md +0 -264
- package/docs/plans/0196-convert-runner-menu-to-classes.md +0 -268
- package/docs/plans/0205-decompose-render-widget-lines.md +0 -140
- package/docs/plans/0206-decompose-show-agent-detail.md +0 -211
- package/docs/plans/0207-decompose-agent-widget-update.md +0 -242
- package/docs/plans/0208-extract-shared-test-fixtures.md +0 -298
- package/docs/plans/0214-convert-remaining-closure-factories-to-classes.md +0 -261
- package/docs/plans/0215-decompose-build-parent-context.md +0 -166
- package/docs/plans/0216-decompose-start-agent.md +0 -255
- package/docs/plans/0217-extract-overwrite-guard.md +0 -176
- package/docs/plans/0218-push-sdk-boundary-in-settings.md +0 -172
- package/docs/plans/0219-reduce-test-duplication-top-3-clone-families.md +0 -162
- package/docs/plans/0227-evolve-agent-record-into-agent.md +0 -322
- package/docs/plans/0228-async-start-agent-dissolve-run-handle.md +0 -288
- package/docs/plans/0229-agent-born-complete.md +0 -564
- package/docs/plans/0230-extract-concurrency-queue.md +0 -265
- package/docs/plans/0231-push-exec-registry-to-runner.md +0 -245
- package/docs/plans/0232-agent-resume-internal-observer-lifecycle.md +0 -180
- package/docs/plans/0237-remove-disallowed-tools.md +0 -180
- package/docs/plans/0238-remove-extensions-filtering.md +0 -191
- package/docs/plans/0239-collapse-filter-active-tools.md +0 -217
- package/docs/plans/0242-rename-agent-tool-to-subagent.md +0 -165
- package/docs/plans/0256-extract-worktree-isolation.md +0 -256
- package/docs/plans/0257-extract-child-session-factory.md +0 -285
- package/docs/plans/0262-add-workspace-provider-seam.md +0 -264
- package/docs/plans/0264-remove-extension-lifecycle-control.md +0 -277
- package/docs/plans/0265-born-complete-subagent-session.md +0 -333
- package/docs/plans/0270-type-consumable-public-surface.md +0 -204
- package/docs/plans/0272-export-workspace-collaborator-types.md +0 -147
- package/docs/plans/0277-encapsulate-agent-session.md +0 -304
- package/docs/plans/0280-rename-agent-to-subagent.md +0 -197
- package/docs/plans/0360-custom-agents-default-append.md +0 -132
- package/docs/plans/0373-extract-subagent-state.md +0 -250
- package/docs/plans/0374-encapsulate-subagent-start-notification.md +0 -268
- package/docs/plans/0375-extract-run-listener-workspace-bracket.md +0 -300
- package/docs/plans/0376-extract-manager-observer.md +0 -232
- package/docs/plans/0377-split-widget-delegation-from-runtime.md +0 -254
- package/docs/plans/0378-consolidate-lifecycle-test-fixtures.md +0 -243
- package/docs/plans/0379-consolidate-ui-tools-test-fixtures.md +0 -335
- package/docs/plans/0380-extract-layered-settings-loader.md +0 -223
- package/docs/plans/0381-replace-concurrency-queue-with-limiter.md +0 -267
- package/docs/plans/0400-include-parent-prompt-in-replace-mode.md +0 -199
- package/docs/plans/0403-abort-subagents-on-interrupt.md +0 -180
- package/docs/plans/0412-unify-session-mock-builders.md +0 -146
- package/docs/plans/0420-fold-run-metrics-live-activity-onto-record.md +0 -228
- package/docs/plans/0421-migrate-activity-readers-to-record-getters.md +0 -309
- package/docs/plans/0422-delete-activity-tracker-ui-observer.md +0 -204
- package/docs/plans/0423-widget-self-drive-from-lifecycle.md +0 -264
- package/docs/plans/0424-drop-widget-dep-from-subagent-tool.md +0 -166
- package/docs/plans/0425-reconcile-subagent-events-contract.md +0 -144
- package/docs/plans/0426-consolidate-residual-test-clone-families.md +0 -235
- package/docs/plans/0427-reconsider-ui-direction.md +0 -212
- package/docs/plans/0441-remove-agent-definition-management-subtree.md +0 -180
- package/docs/plans/0442-dissolve-agents-conversation-viewer.md +0 -195
- package/docs/plans/0444-shrink-widget-to-background-agents.md +0 -156
- package/docs/plans/0445-native-session-navigation.md +0 -365
- package/docs/plans/0446-spike-session-navigation-entry-criteria.md +0 -171
- package/docs/plans/0447-extract-subagents-settings-command.md +0 -220
- package/docs/plans/0448-block-disabled-agent-spawn.md +0 -207
- package/docs/plans/0462-navigation-renderer-tui-components.md +0 -338
- package/docs/plans/0463-file-snapshot-source-evicted-agents.md +0 -306
- package/docs/retro/0048-implement-subagents-api.md +0 -44
- package/docs/retro/0049-remove-group-join-output-file-rpc.md +0 -49
- package/docs/retro/0051-update-adr-0001-hard-fork.md +0 -39
- package/docs/retro/0053-extract-model-resolution-from-execute.md +0 -40
- package/docs/retro/0054-decompose-index-into-modules.md +0 -53
- package/docs/retro/0057-structured-debug-logging.md +0 -77
- package/docs/retro/0061-session-format-transcript.md +0 -41
- package/docs/retro/0066-replace-as-any-with-sdk-types.md +0 -44
- package/docs/retro/0069-create-subagent-runtime.md +0 -43
- package/docs/retro/0070-extract-event-handlers.md +0 -49
- package/docs/retro/0071-extract-session-config-assembler.md +0 -60
- package/docs/retro/0072-inject-agent-manager-collaborators.md +0 -46
- package/docs/retro/0076-inject-cwd-into-agent-manager.md +0 -35
- package/docs/retro/0077-inject-project-agents-dir.md +0 -33
- package/docs/retro/0080-consolidate-agent-config-lookup.md +0 -35
- package/docs/retro/0084-extract-git-worktree-manager.md +0 -37
- package/docs/retro/0087-evolve-subagent-runtime-methods.md +0 -42
- package/docs/retro/0098-extract-agent-record-state-machine.md +0 -46
- package/docs/retro/0099-replace-ctx-with-parent-snapshot.md +0 -37
- package/docs/retro/0100-replace-callback-threading-with-session-subscription.md +0 -36
- package/docs/retro/0102-consolidate-test-record-factory.md +0 -30
- package/docs/retro/0108-extract-agent-type-registry.md +0 -41
- package/docs/retro/0109-extract-settings-manager.md +0 -55
- package/docs/retro/0110-agent-activity-tracker.md +0 -44
- package/docs/retro/0111-split-agent-record-lifecycle.md +0 -61
- package/docs/retro/0112-replace-agent-manager-callbacks.md +0 -35
- package/docs/retro/0113-disambiguate-spawn-options.md +0 -29
- package/docs/retro/0114-narrow-agent-tool-menu-deps.md +0 -38
- package/docs/retro/0115-decompose-agent-tool.md +0 -51
- package/docs/retro/0116-type-housekeeping.md +0 -42
- package/docs/retro/0118-settings-manager-apply-methods.md +0 -40
- package/docs/retro/0123-remove-vi-fn-cast-smell.md +0 -49
- package/docs/retro/0131-consolidate-shared-test-fixtures.md +0 -46
- package/docs/retro/0132-inject-io-into-session-config.md +0 -33
- package/docs/retro/0133-inject-sdk-boundary-into-agent-runner.md +0 -45
- package/docs/retro/0134-reduce-as-any-casts.md +0 -56
- package/docs/retro/0135-extract-display-helpers.md +0 -38
- package/docs/retro/0136-decompose-agent-menu.md +0 -43
- package/docs/retro/0144-consolidate-observation-model.md +0 -39
- package/docs/retro/0145-decompose-execute-push-ctx-to-boundary.md +0 -56
- package/docs/retro/0146-narrow-ui-context.md +0 -70
- package/docs/retro/0147-inject-wrap-text-into-conversation-viewer.md +0 -90
- package/docs/retro/0148-split-agent-widget-rendering.md +0 -39
- package/docs/retro/0152-add-prompt-snippet.md +0 -34
- package/docs/retro/0164-reorganize-into-domain-directories.md +0 -82
- package/docs/retro/0165-decompose-resolved-spawn-config.md +0 -78
- package/docs/retro/0166-extract-parent-session-info.md +0 -66
- package/docs/retro/0167-narrow-runner-io.md +0 -63
- package/docs/retro/0168-extract-tool-filter-config.md +0 -76
- package/docs/retro/0169-extract-run-context-from-run-options.md +0 -68
- package/docs/retro/0170-reduce-build-content-lines-complexity.md +0 -72
- package/docs/retro/0171-reduce-render-result-complexity.md +0 -77
- package/docs/retro/0172-extract-turn-formatting.md +0 -80
- package/docs/retro/0180-reorder-append-prompt-for-kv-cache.md +0 -62
- package/docs/retro/0185-remove-persistent-agent-memory.md +0 -73
- package/docs/retro/0188-replace-any-casts-with-sdk-types.md +0 -69
- package/docs/retro/0192-define-session-context-interface.md +0 -59
- package/docs/retro/0193-runtime-owns-context-queries.md +0 -75
- package/docs/retro/0194-align-tool-interfaces-for-structural-typing.md +0 -62
- package/docs/retro/0195-convert-tool-factories-to-classes.md +0 -84
- package/docs/retro/0196-convert-runner-menu-to-classes.md +0 -73
- package/docs/retro/0205-decompose-render-widget-lines.md +0 -64
- package/docs/retro/0206-decompose-show-agent-detail.md +0 -67
- package/docs/retro/0207-decompose-agent-widget-update.md +0 -92
- package/docs/retro/0208-extract-shared-test-fixtures.md +0 -102
- package/docs/retro/0214-convert-remaining-closure-factories-to-classes.md +0 -66
- package/docs/retro/0215-decompose-build-parent-context.md +0 -61
- package/docs/retro/0216-decompose-start-agent.md +0 -80
- package/docs/retro/0217-extract-overwrite-guard.md +0 -63
- package/docs/retro/0218-push-sdk-boundary-in-settings.md +0 -72
- package/docs/retro/0219-reduce-test-duplication-top-3-clone-families.md +0 -64
- package/docs/retro/0227-evolve-agent-record-into-agent.md +0 -80
- package/docs/retro/0228-async-start-agent-dissolve-run-handle.md +0 -80
- package/docs/retro/0229-agent-born-complete.md +0 -89
- package/docs/retro/0230-extract-concurrency-queue.md +0 -38
- package/docs/retro/0231-push-exec-registry-to-runner.md +0 -71
- package/docs/retro/0232-agent-resume-internal-observer-lifecycle.md +0 -109
- package/docs/retro/0237-remove-disallowed-tools.md +0 -78
- package/docs/retro/0238-remove-extensions-filtering.md +0 -72
- package/docs/retro/0239-collapse-filter-active-tools.md +0 -70
- package/docs/retro/0242-rename-agent-tool-to-subagent.md +0 -82
- package/docs/retro/0256-extract-worktree-isolation.md +0 -89
- package/docs/retro/0257-extract-child-session-factory.md +0 -33
- package/docs/retro/0262-add-workspace-provider-seam.md +0 -89
- package/docs/retro/0264-remove-extension-lifecycle-control.md +0 -91
- package/docs/retro/0265-born-complete-subagent-session.md +0 -97
- package/docs/retro/0270-type-consumable-public-surface.md +0 -108
- package/docs/retro/0272-export-workspace-collaborator-types.md +0 -38
- package/docs/retro/0277-encapsulate-agent-session.md +0 -80
- package/docs/retro/0280-rename-agent-to-subagent.md +0 -96
- package/docs/retro/0360-custom-agents-default-append.md +0 -81
- package/docs/retro/0373-extract-subagent-state.md +0 -94
- package/docs/retro/0374-encapsulate-subagent-start-notification.md +0 -171
- package/docs/retro/0375-extract-run-listener-workspace-bracket.md +0 -104
- package/docs/retro/0376-extract-manager-observer.md +0 -81
- package/docs/retro/0377-split-widget-delegation-from-runtime.md +0 -105
- package/docs/retro/0378-consolidate-lifecycle-test-fixtures.md +0 -100
- package/docs/retro/0379-consolidate-ui-tools-test-fixtures.md +0 -52
- package/docs/retro/0380-extract-layered-settings-loader.md +0 -97
- package/docs/retro/0381-replace-concurrency-queue-with-limiter.md +0 -95
- package/docs/retro/0400-include-parent-prompt-in-replace-mode.md +0 -84
- package/docs/retro/0403-abort-subagents-on-interrupt.md +0 -90
- package/docs/retro/0412-unify-session-mock-builders.md +0 -80
- package/docs/retro/0420-fold-run-metrics-live-activity-onto-record.md +0 -90
- package/docs/retro/0421-migrate-activity-readers-to-record-getters.md +0 -105
- package/docs/retro/0422-delete-activity-tracker-ui-observer.md +0 -97
- package/docs/retro/0423-widget-self-drive-from-lifecycle.md +0 -112
- package/docs/retro/0424-drop-widget-dep-from-subagent-tool.md +0 -89
- package/docs/retro/0425-reconcile-subagent-events-contract.md +0 -101
- package/docs/retro/0426-consolidate-residual-test-clone-families.md +0 -100
- package/docs/retro/0427-reconsider-ui-direction.md +0 -102
- package/docs/retro/0441-remove-agent-definition-management-subtree.md +0 -45
- package/docs/retro/0442-dissolve-agents-conversation-viewer.md +0 -108
- package/docs/retro/0444-shrink-widget-to-background-agents.md +0 -81
- package/docs/retro/0445-native-session-navigation.md +0 -122
- package/docs/retro/0446-spike-session-navigation-entry-criteria.md +0 -99
- package/docs/retro/0447-extract-subagents-settings-command.md +0 -91
- package/docs/retro/0448-block-disabled-agent-spawn.md +0 -75
- package/docs/retro/0462-navigation-renderer-tui-components.md +0 -105
- package/docs/retro/0463-file-snapshot-source-evicted-agents.md +0 -103
- package/src/observation/notification-state.ts +0 -27
- package/vitest.config.ts +0 -14
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
issue: 215
|
|
3
|
-
issue_title: "Decompose buildParentContext (cognitive 30) (Phase 13, Step 2)"
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Decompose `buildParentContext`
|
|
7
|
-
|
|
8
|
-
## Problem Statement
|
|
9
|
-
|
|
10
|
-
`buildParentContext` in `src/session/context.ts` is the only remaining fallow refactoring target in the package.
|
|
11
|
-
The function has a cognitive complexity of 30, driven by a loop with three type-check branches (`message`, `compaction`, default), each with sub-branches for role (`user` vs `assistant`) and content type (`string` vs array).
|
|
12
|
-
The architecture roadmap (Phase 13, Step 2) targets cognitive complexity < 10 and function body < 15 LOC.
|
|
13
|
-
|
|
14
|
-
## Goals
|
|
15
|
-
|
|
16
|
-
- Extract per-entry-type formatters: `formatMessageEntry(entry)` and `formatCompactionEntry(entry)`.
|
|
17
|
-
- Reduce `buildParentContext` to a loop + filter + join orchestrator (< 15 LOC).
|
|
18
|
-
- Achieve cognitive complexity < 10 for all functions in the file.
|
|
19
|
-
- Add unit tests for the extracted formatters and the orchestrator.
|
|
20
|
-
|
|
21
|
-
## Non-Goals
|
|
22
|
-
|
|
23
|
-
- Changing the public API surface (`buildParentContext`, `extractText`) — signatures stay the same.
|
|
24
|
-
- Moving `extractText` to another module (noted as a follow-up in prior plans but out of scope).
|
|
25
|
-
- Refactoring callers (`parent-snapshot.ts`) — they are already tested via mocks.
|
|
26
|
-
|
|
27
|
-
## Background
|
|
28
|
-
|
|
29
|
-
### Current file: `src/session/context.ts`
|
|
30
|
-
|
|
31
|
-
The file exports two functions:
|
|
32
|
-
|
|
33
|
-
1. `extractText(content: unknown[]): string` — filters an array of content blocks to `TextContent` items and joins their `.text` values.
|
|
34
|
-
Used by `agent-runner.ts`, `message-formatters.ts`, and `buildParentContext` itself.
|
|
35
|
-
2. `buildParentContext(ctx: SessionContext): string` — iterates session branch entries, formatting `message` entries (user/assistant) and `compaction` entries into a text representation prefixed with a header.
|
|
36
|
-
|
|
37
|
-
The file also defines three local types (`MessageEntry`, `CompactionEntry`, `BranchEntry`) and one helper (`isTextContent`).
|
|
38
|
-
|
|
39
|
-
### Callers
|
|
40
|
-
|
|
41
|
-
- `buildParentContext` is called only from `parent-snapshot.ts` (where it is mocked in tests).
|
|
42
|
-
- `extractText` is called from `agent-runner.ts`, `message-formatters.ts`, and internally within `buildParentContext`.
|
|
43
|
-
|
|
44
|
-
### Existing tests
|
|
45
|
-
|
|
46
|
-
There are no direct unit tests for `context.ts`.
|
|
47
|
-
`parent-snapshot.test.ts` mocks `buildParentContext` entirely, so the formatting logic is currently untested.
|
|
48
|
-
|
|
49
|
-
## Design Overview
|
|
50
|
-
|
|
51
|
-
### Extracted formatters
|
|
52
|
-
|
|
53
|
-
Each formatter takes a typed entry and returns `string | undefined` (undefined when the entry should be skipped):
|
|
54
|
-
|
|
55
|
-
```typescript
|
|
56
|
-
function formatMessageEntry(entry: MessageEntry): string | undefined {
|
|
57
|
-
const msg = entry.message;
|
|
58
|
-
const text =
|
|
59
|
-
typeof msg.content === "string"
|
|
60
|
-
? msg.content
|
|
61
|
-
: extractText(msg.content);
|
|
62
|
-
if (!text.trim()) return undefined;
|
|
63
|
-
if (msg.role === "user") return `[User]: ${text.trim()}`;
|
|
64
|
-
if (msg.role === "assistant") return `[Assistant]: ${text.trim()}`;
|
|
65
|
-
return undefined; // skip toolResult and other roles
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function formatCompactionEntry(entry: CompactionEntry): string | undefined {
|
|
69
|
-
return entry.summary ? `[Summary]: ${entry.summary}` : undefined;
|
|
70
|
-
}
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
### Simplified orchestrator
|
|
74
|
-
|
|
75
|
-
```typescript
|
|
76
|
-
export function buildParentContext(ctx: SessionContext): string {
|
|
77
|
-
const entries = ctx.sessionManager.getBranch();
|
|
78
|
-
if (!entries || entries.length === 0) return "";
|
|
79
|
-
|
|
80
|
-
const parts = (entries as BranchEntry[])
|
|
81
|
-
.map(formatBranchEntry)
|
|
82
|
-
.filter((p): p is string => p !== undefined);
|
|
83
|
-
|
|
84
|
-
if (parts.length === 0) return "";
|
|
85
|
-
|
|
86
|
-
return `# Parent Conversation Context
|
|
87
|
-
The following is the conversation history from the parent session that spawned you.
|
|
88
|
-
Use this context to understand what has been discussed and decided so far.
|
|
89
|
-
|
|
90
|
-
${parts.join("\n\n")}
|
|
91
|
-
|
|
92
|
-
---
|
|
93
|
-
# Your Task (below)
|
|
94
|
-
`;
|
|
95
|
-
}
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
A thin dispatcher (`formatBranchEntry`) routes by `type`:
|
|
99
|
-
|
|
100
|
-
```typescript
|
|
101
|
-
function formatBranchEntry(entry: BranchEntry): string | undefined {
|
|
102
|
-
if (entry.type === "message") return formatMessageEntry(entry as MessageEntry);
|
|
103
|
-
if (entry.type === "compaction") return formatCompactionEntry(entry as CompactionEntry);
|
|
104
|
-
return undefined;
|
|
105
|
-
}
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
### Complexity analysis
|
|
109
|
-
|
|
110
|
-
- `formatMessageEntry`: 3 branches (string-vs-array, empty check, role) — estimated cognitive complexity ~4.
|
|
111
|
-
- `formatCompactionEntry`: 1 branch — estimated cognitive complexity ~1.
|
|
112
|
-
- `formatBranchEntry`: 2 branches — estimated cognitive complexity ~2.
|
|
113
|
-
- `buildParentContext`: 2 branches (empty entries, empty parts) — estimated cognitive complexity ~3.
|
|
114
|
-
|
|
115
|
-
All well under the < 10 target.
|
|
116
|
-
|
|
117
|
-
## Module-Level Changes
|
|
118
|
-
|
|
119
|
-
### `src/session/context.ts`
|
|
120
|
-
|
|
121
|
-
1. Add `formatMessageEntry(entry: MessageEntry): string | undefined` — private helper.
|
|
122
|
-
2. Add `formatCompactionEntry(entry: CompactionEntry): string | undefined` — private helper.
|
|
123
|
-
3. Add `formatBranchEntry(entry: BranchEntry): string | undefined` — private dispatcher.
|
|
124
|
-
4. Simplify `buildParentContext` body to use `map(formatBranchEntry).filter(...)`.
|
|
125
|
-
5. No changes to exports — `buildParentContext` and `extractText` signatures are unchanged.
|
|
126
|
-
6. No changes to local types (`MessageEntry`, `CompactionEntry`, `BranchEntry`) or `isTextContent`.
|
|
127
|
-
|
|
128
|
-
### `test/session/context.test.ts` (new)
|
|
129
|
-
|
|
130
|
-
Unit tests for:
|
|
131
|
-
|
|
132
|
-
- `extractText` — string extraction from mixed content arrays.
|
|
133
|
-
- `buildParentContext` — end-to-end formatting with user, assistant, compaction, and skipped entries.
|
|
134
|
-
|
|
135
|
-
The formatters are private, so they are tested indirectly through `buildParentContext`.
|
|
136
|
-
|
|
137
|
-
## Test Impact Analysis
|
|
138
|
-
|
|
139
|
-
1. The new `context.test.ts` enables direct testing of formatting logic that was previously untested (mocked away in `parent-snapshot.test.ts`).
|
|
140
|
-
2. No existing tests become redundant — `parent-snapshot.test.ts` tests snapshot assembly, not formatting.
|
|
141
|
-
3. No existing tests need modification — the public API is unchanged.
|
|
142
|
-
|
|
143
|
-
## TDD Order
|
|
144
|
-
|
|
145
|
-
1. **Red → Green:** Add `test/session/context.test.ts` with tests for `extractText` — empty array, text-only, mixed content types, no text content.
|
|
146
|
-
Commit: `test: add extractText unit tests (#215)`
|
|
147
|
-
|
|
148
|
-
2. **Red → Green:** Add tests for `buildParentContext` — empty branch, user messages, assistant messages, compaction entries with/without summary, mixed entry types, entries with empty text (skipped), non-message/non-compaction entries (skipped), string vs array content.
|
|
149
|
-
Commit: `test: add buildParentContext unit tests (#215)`
|
|
150
|
-
|
|
151
|
-
3. **Refactor:** Extract `formatMessageEntry`, `formatCompactionEntry`, and `formatBranchEntry` from `buildParentContext`.
|
|
152
|
-
Simplify `buildParentContext` to map/filter/join.
|
|
153
|
-
All tests from steps 1–2 must still pass.
|
|
154
|
-
Commit: `refactor: decompose buildParentContext into per-entry formatters (#215)`
|
|
155
|
-
|
|
156
|
-
## Risks and Mitigations
|
|
157
|
-
|
|
158
|
-
| Risk | Mitigation |
|
|
159
|
-
| -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
|
160
|
-
| Behavioral regression in formatting | Steps 1–2 lock in current behavior with tests before refactoring |
|
|
161
|
-
| Extracted helpers expose implementation details | Helpers are private (not exported); tested indirectly via public API |
|
|
162
|
-
| `eslint-disable` comment for `no-unnecessary-condition` on `getBranch()` check may need adjustment | Preserve the comment — runtime nullability is documented |
|
|
163
|
-
|
|
164
|
-
## Open Questions
|
|
165
|
-
|
|
166
|
-
None — the decomposition target and strategy are specified by the architecture roadmap.
|
|
@@ -1,255 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
issue: 216
|
|
3
|
-
issue_title: "Decompose startAgent in agent-manager.ts (Phase 13, Step 3)"
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Decompose `startAgent` via `RunHandle` lifecycle object
|
|
7
|
-
|
|
8
|
-
## Problem Statement
|
|
9
|
-
|
|
10
|
-
`startAgent` in `agent-manager.ts` is a ~125-line method whose complexity comes not from length alone but from **mutable closure state shared across callbacks**.
|
|
11
|
-
Two `let` variables (`unsubRecordObserver`, `detachParentSignal`) are written in one closure (`onSessionCreated` / setup block) and read in two others (`.then()` / `.catch()`).
|
|
12
|
-
The `.then()` and `.catch()` handlers duplicate finalization logic (observer unsubscription, signal detach, worktree cleanup, background counter management).
|
|
13
|
-
|
|
14
|
-
The original issue proposed extracting three methods (`handleRunCompletion`, `handleRunError`, `finalizeBackgroundRun`).
|
|
15
|
-
This plan replaces that mechanical extraction with a structural fix: introduce a **`RunHandle` lifecycle object** that owns the per-run cleanup state, eliminating the mutable closures and the duplicated finalization.
|
|
16
|
-
|
|
17
|
-
## Goals
|
|
18
|
-
|
|
19
|
-
- Eliminate mutable closure state from `startAgent` — all per-run state lives on `RunHandle`.
|
|
20
|
-
- Eliminate duplicated cleanup/finalization logic in `.then()`/`.catch()` via Tell-Don't-Ask on `RunHandle`.
|
|
21
|
-
- Teach `WorktreeState` to self-clean via `performCleanup()`, removing the ask-tell dance from callers.
|
|
22
|
-
- Reduce `startAgent` to a coordinator (~35–40 lines) with zero mutable `let` bindings.
|
|
23
|
-
- Keep all 929 lines of existing `agent-manager.test.ts` passing unchanged.
|
|
24
|
-
|
|
25
|
-
## Non-Goals
|
|
26
|
-
|
|
27
|
-
- Extracting `RunHandle` to a separate file — it stays private in `agent-manager.ts` for now.
|
|
28
|
-
- Changing the `runner.run()` options shape or the `RunResult` type.
|
|
29
|
-
- Reducing `agent-manager.test.ts` duplication (tracked in #219).
|
|
30
|
-
- Moving `pendingSteers` state to a different owner (the timing gap between `spawn()` and `startAgent()` makes this non-trivial).
|
|
31
|
-
|
|
32
|
-
## Background
|
|
33
|
-
|
|
34
|
-
### Closure tangle in `startAgent`
|
|
35
|
-
|
|
36
|
-
```text
|
|
37
|
-
unsubRecordObserver ──written in──▶ onSessionCreated callback
|
|
38
|
-
──read in────▶ .then() handler
|
|
39
|
-
──read in────▶ .catch() handler
|
|
40
|
-
|
|
41
|
-
detachParentSignal ──written in──▶ setup block
|
|
42
|
-
──read via───▶ detach closure
|
|
43
|
-
──read in────▶ .then() handler (via detach)
|
|
44
|
-
──read in────▶ .catch() handler (via detach)
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
Both variables are resource-release handles — acquired at different times, released in the same place.
|
|
48
|
-
They have no owner; they float as mutable `let` bindings shared across closures.
|
|
49
|
-
|
|
50
|
-
### Duplicated finalization
|
|
51
|
-
|
|
52
|
-
Both `.then()` and `.catch()` perform:
|
|
53
|
-
|
|
54
|
-
1. `unsubRecordObserver?.(); detach();` — release listeners
|
|
55
|
-
2. Worktree cleanup via `this.worktrees.cleanup()` + `record.worktreeState.recordCleanup()` — ask-tell dance
|
|
56
|
-
3. Background finalization: `this.runningBackground--`, `this.observer?.onAgentCompleted(record)`, `this.drainQueue()`
|
|
57
|
-
|
|
58
|
-
### Existing types
|
|
59
|
-
|
|
60
|
-
- `RunResult` is already exported from `agent-runner.ts` — `RunHandle.complete()` can accept it directly.
|
|
61
|
-
- `WorktreeManager.cleanup()` accepts `WorktreeInfo`, which `WorktreeState` satisfies structurally (has `path` and `branch`).
|
|
62
|
-
- `record.description` is available on `AgentRecord` at cleanup time, so `RunHandle` doesn't need a separate `description` parameter.
|
|
63
|
-
|
|
64
|
-
## Design Overview
|
|
65
|
-
|
|
66
|
-
### `WorktreeState.performCleanup(worktrees, description)`
|
|
67
|
-
|
|
68
|
-
Teach `WorktreeState` to orchestrate its own cleanup instead of requiring callers to do the ask-tell dance:
|
|
69
|
-
|
|
70
|
-
```typescript
|
|
71
|
-
performCleanup(worktrees: WorktreeManager, description: string): WorktreeCleanupResult {
|
|
72
|
-
const result = worktrees.cleanup(this, description);
|
|
73
|
-
this._cleanupResult = result;
|
|
74
|
-
return result;
|
|
75
|
-
}
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
This replaces the two-step pattern at both call sites:
|
|
79
|
-
|
|
80
|
-
```typescript
|
|
81
|
-
// Before (caller orchestrates):
|
|
82
|
-
const wtResult = this.worktrees.cleanup(record.worktreeState, options.description);
|
|
83
|
-
record.worktreeState.recordCleanup(wtResult);
|
|
84
|
-
|
|
85
|
-
// After (Tell-Don't-Ask):
|
|
86
|
-
const wtResult = record.worktreeState.performCleanup(this.worktrees, record.description);
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
### `RunHandle` lifecycle object
|
|
90
|
-
|
|
91
|
-
A short-lived object born when a run starts, consumed when it ends.
|
|
92
|
-
Owns the two resource-release handles and exposes `complete()`/`fail()` as the only way to finish a run.
|
|
93
|
-
|
|
94
|
-
```typescript
|
|
95
|
-
class RunHandle {
|
|
96
|
-
private unsub?: () => void;
|
|
97
|
-
private detach?: () => void;
|
|
98
|
-
private onFinished?: () => void;
|
|
99
|
-
|
|
100
|
-
constructor(
|
|
101
|
-
private readonly record: AgentRecord,
|
|
102
|
-
private readonly worktrees: WorktreeManager,
|
|
103
|
-
onFinished?: () => void,
|
|
104
|
-
) { this.onFinished = onFinished; }
|
|
105
|
-
|
|
106
|
-
wireSignal(signal: AbortSignal | undefined, onAbort: () => void): void;
|
|
107
|
-
attachObserver(unsub: () => void): void;
|
|
108
|
-
complete(result: RunResult): string;
|
|
109
|
-
fail(err: unknown): void;
|
|
110
|
-
|
|
111
|
-
private detachListeners(): void;
|
|
112
|
-
private fireOnFinished(): void; // idempotent — nulls callback after first call
|
|
113
|
-
}
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
Key design decisions:
|
|
117
|
-
|
|
118
|
-
1. **`onFinished` callback** — set once at construction, fires at most once (idempotent guard).
|
|
119
|
-
For background agents this is `() => this.finalizeBackgroundRun(record)`.
|
|
120
|
-
For foreground agents it is `undefined`.
|
|
121
|
-
This eliminates the `if (options.isBackground)` check from both `.then()` and `.catch()`.
|
|
122
|
-
|
|
123
|
-
2. **`fireOnFinished` is idempotent** — if `complete()` throws (e.g., worktree cleanup fails on the success path) and the promise chain falls through to `.catch()` → `fail()`, the callback fires exactly once.
|
|
124
|
-
`AgentRecord`'s transition guards (`if (this._status !== "stopped")`) protect against double state transitions.
|
|
125
|
-
|
|
126
|
-
3. **`complete()` returns `result.responseText`** — the branch-suffix text is stored on the record via `markCompleted(finalResult)` but the promise resolves with the original response text, matching current behavior.
|
|
127
|
-
|
|
128
|
-
4. **No `worktrees` or `description` parameters on `complete()`/`fail()`** — `RunHandle` gets `worktrees` at construction; `description` comes from `record.description`.
|
|
129
|
-
|
|
130
|
-
### `finalizeBackgroundRun(record)` on `AgentManager`
|
|
131
|
-
|
|
132
|
-
Extracts the shared background finalization:
|
|
133
|
-
|
|
134
|
-
```typescript
|
|
135
|
-
private finalizeBackgroundRun(record: AgentRecord): void {
|
|
136
|
-
this.runningBackground--;
|
|
137
|
-
try { this.observer?.onAgentCompleted(record); }
|
|
138
|
-
catch (err) { debugLog("onAgentCompleted observer", err); }
|
|
139
|
-
this.drainQueue();
|
|
140
|
-
}
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
Note: the current `.catch()` handler does not wrap `onAgentCompleted` in try/catch, but `.then()` does.
|
|
144
|
-
The extracted method always wraps it — an observer error must never prevent `drainQueue()` from running.
|
|
145
|
-
|
|
146
|
-
### Small helpers on `AgentManager`
|
|
147
|
-
|
|
148
|
-
Two additional extractions to keep `startAgent` focused:
|
|
149
|
-
|
|
150
|
-
```typescript
|
|
151
|
-
private setupWorktree(
|
|
152
|
-
id: string, record: AgentRecord, isolation: IsolationMode | undefined,
|
|
153
|
-
): string | undefined;
|
|
154
|
-
|
|
155
|
-
private flushPendingSteers(id: string, session: AgentSession): void;
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
### Resulting `startAgent` shape
|
|
159
|
-
|
|
160
|
-
After all extractions, `startAgent` becomes a coordinator with **zero mutable `let` bindings**:
|
|
161
|
-
|
|
162
|
-
```typescript
|
|
163
|
-
private startAgent(id: string, record: AgentRecord, { snapshot, type, prompt, options }: SpawnArgs) {
|
|
164
|
-
const worktreeCwd = this.setupWorktree(id, record, options.isolation);
|
|
165
|
-
|
|
166
|
-
record.markRunning(Date.now());
|
|
167
|
-
if (options.isBackground) this.runningBackground++;
|
|
168
|
-
this.observer?.onAgentStarted(record);
|
|
169
|
-
|
|
170
|
-
const handle = new RunHandle(
|
|
171
|
-
record, this.worktrees,
|
|
172
|
-
options.isBackground ? () => this.finalizeBackgroundRun(record) : undefined,
|
|
173
|
-
);
|
|
174
|
-
handle.wireSignal(options.signal, () => this.abort(id));
|
|
175
|
-
|
|
176
|
-
const runConfig = this.getRunConfig?.();
|
|
177
|
-
record.promise = this.runner.run(snapshot, type, prompt, {
|
|
178
|
-
context: { exec: this.exec, registry: this.registry, cwd: worktreeCwd, parentSession: options.parentSession },
|
|
179
|
-
model: options.model, maxTurns: options.maxTurns,
|
|
180
|
-
defaultMaxTurns: runConfig?.defaultMaxTurns, graceTurns: runConfig?.graceTurns,
|
|
181
|
-
isolated: options.isolated, thinkingLevel: options.thinkingLevel,
|
|
182
|
-
signal: record.abortController!.signal,
|
|
183
|
-
onSessionCreated: (session) => {
|
|
184
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
185
|
-
const outputFile = session.sessionManager?.getSessionFile?.() ?? undefined;
|
|
186
|
-
record.execution = { session, outputFile };
|
|
187
|
-
this.flushPendingSteers(id, session);
|
|
188
|
-
handle.attachObserver(subscribeRecordObserver(session, record, {
|
|
189
|
-
onCompact: (r, info) => this.observer?.onAgentCompacted(r, info),
|
|
190
|
-
}));
|
|
191
|
-
options.onSessionCreated?.(session, record);
|
|
192
|
-
},
|
|
193
|
-
})
|
|
194
|
-
.then((result) => handle.complete(result))
|
|
195
|
-
.catch((err: unknown) => { handle.fail(err); return ""; });
|
|
196
|
-
}
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
The `.then()` and `.catch()` are one-liners.
|
|
200
|
-
The `onSessionCreated` callback captures only `const` references (no mutable closure state).
|
|
201
|
-
The `record.promise` assignment moves inline (no intermediate `const promise`).
|
|
202
|
-
|
|
203
|
-
## Module-Level Changes
|
|
204
|
-
|
|
205
|
-
| File | Change |
|
|
206
|
-
| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
207
|
-
| `src/lifecycle/worktree-state.ts` | Add `performCleanup(worktrees, description)` method |
|
|
208
|
-
| `src/lifecycle/agent-manager.ts` | Add `RunHandle` class (private); add `finalizeBackgroundRun()`, `setupWorktree()`, `flushPendingSteers()` methods; rewrite `startAgent` to use them |
|
|
209
|
-
| `test/lifecycle/worktree-state.test.ts` | Add tests for `performCleanup` |
|
|
210
|
-
|
|
211
|
-
## Test Impact Analysis
|
|
212
|
-
|
|
213
|
-
1. **New unit tests**: `WorktreeState.performCleanup` — directly testable with a mock `WorktreeManager`.
|
|
214
|
-
`RunHandle` is tested indirectly through the existing `agent-manager.test.ts` suite (929 lines, comprehensive coverage of success/error/worktree/signal/background paths).
|
|
215
|
-
2. **Redundant tests**: None — all existing tests exercise the same public API (`spawn`, `spawnAndWait`, `abort`, `resume`).
|
|
216
|
-
3. **Tests that must stay as-is**: All of `agent-manager.test.ts` — the refactoring is behavior-preserving and these tests verify every path through `RunHandle.complete()` and `RunHandle.fail()`.
|
|
217
|
-
|
|
218
|
-
## TDD Order
|
|
219
|
-
|
|
220
|
-
1. **`WorktreeState.performCleanup`** — red: test that `performCleanup` calls the manager, records the result, and returns it.
|
|
221
|
-
Green: implement `performCleanup` on `WorktreeState`.
|
|
222
|
-
Commit: `feat: add WorktreeState.performCleanup for self-cleanup (#216)`
|
|
223
|
-
|
|
224
|
-
2. **Use `performCleanup` in `startAgent`** — refactor both cleanup sites in `.then()` and `.catch()` to use `record.worktreeState.performCleanup()`.
|
|
225
|
-
Verify: all existing agent-manager tests pass.
|
|
226
|
-
Commit: `refactor: use WorktreeState.performCleanup in startAgent (#216)`
|
|
227
|
-
|
|
228
|
-
3. **Extract `finalizeBackgroundRun`** — extract the shared background finalization block.
|
|
229
|
-
Add try/catch around `onAgentCompleted` (unifying the asymmetry between `.then()` and `.catch()`).
|
|
230
|
-
Verify: all existing agent-manager tests pass.
|
|
231
|
-
Commit: `refactor: extract finalizeBackgroundRun from startAgent (#216)`
|
|
232
|
-
|
|
233
|
-
4. **Introduce `RunHandle` and rewire `startAgent`** — add `RunHandle` class with `wireSignal`, `attachObserver`, `complete`, `fail`, `detachListeners`, `fireOnFinished`.
|
|
234
|
-
Extract `setupWorktree` and `flushPendingSteers`.
|
|
235
|
-
Rewrite `startAgent` to use `RunHandle`, eliminating all mutable `let` bindings.
|
|
236
|
-
Verify: all existing agent-manager tests pass.
|
|
237
|
-
Run `pnpm run check` to verify types.
|
|
238
|
-
Commit: `refactor: introduce RunHandle lifecycle object in startAgent (#216)`
|
|
239
|
-
|
|
240
|
-
## Risks and Mitigations
|
|
241
|
-
|
|
242
|
-
1. **`complete()` throws after `fireOnFinished`** — if worktree cleanup succeeds, state transition succeeds, but `fireOnFinished` itself throws (observer error), the `.catch()` handler calls `fail()` which calls `fireOnFinished` again.
|
|
243
|
-
Mitigation: `fireOnFinished` is idempotent (nulls callback after first call), and `finalizeBackgroundRun` wraps `onAgentCompleted` in try/catch.
|
|
244
|
-
`AgentRecord` transition guards prevent double state transitions.
|
|
245
|
-
|
|
246
|
-
2. **`complete()` throws before state transition** — e.g., `worktrees.cleanup()` throws on the success path.
|
|
247
|
-
The `.catch()` handler calls `fail()`, which marks the record as error and does best-effort worktree cleanup.
|
|
248
|
-
This matches current behavior (the success-path worktree cleanup is not wrapped in try/catch today).
|
|
249
|
-
|
|
250
|
-
3. **Subtle behavior change in error-path observer notification** — current `.catch()` does not wrap `onAgentCompleted` in try/catch; `finalizeBackgroundRun` does.
|
|
251
|
-
This is a minor hardening, not a behavior change — an observer throwing during error finalization would previously have prevented `drainQueue()` from running.
|
|
252
|
-
|
|
253
|
-
## Open Questions
|
|
254
|
-
|
|
255
|
-
- None — the design is straightforward and all decisions are driven by eliminating the identified smells.
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
issue: 217
|
|
3
|
-
issue_title: "Extract overwrite guard from UI (Phase 13, Step 4)"
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Extract overwrite guard from UI
|
|
7
|
-
|
|
8
|
-
## Problem Statement
|
|
9
|
-
|
|
10
|
-
The overwrite-guard + write + reload + notify pattern is duplicated between `AgentConfigEditor.ejectAgent` and `AgentCreationWizard.showManualWizard`.
|
|
11
|
-
Both sites check file existence, prompt for overwrite confirmation, write the file, reload the agent registry, and notify the user — identical logic with only the content and notification label differing.
|
|
12
|
-
This is the last remaining production clone group in the package.
|
|
13
|
-
|
|
14
|
-
## Goals
|
|
15
|
-
|
|
16
|
-
- Extract a shared `writeAgentFile` function into a new `src/ui/agent-file-writer.ts` module.
|
|
17
|
-
- Replace both call sites (`ejectAgent`, `showManualWizard`) with calls to the shared function.
|
|
18
|
-
- Achieve 0 production clone groups.
|
|
19
|
-
- Unit-test the extracted function in isolation.
|
|
20
|
-
|
|
21
|
-
## Non-Goals
|
|
22
|
-
|
|
23
|
-
- Extracting the partial overwrite guard in `showGenerateWizard` — that flow has different lifecycle semantics (the spawned agent does the write, and the post-write check is conditional on file existence).
|
|
24
|
-
The guard-only overlap is 5 lines, not worth a separate abstraction.
|
|
25
|
-
- Reducing test duplication in `agent-config-editor.test.ts` or `agent-creation-wizard.test.ts` — tracked in #219 (Phase 13, Step 6).
|
|
26
|
-
- Changing the `disableAgent` write path — it has no overwrite guard and different notification semantics.
|
|
27
|
-
|
|
28
|
-
## Background
|
|
29
|
-
|
|
30
|
-
### Existing modules
|
|
31
|
-
|
|
32
|
-
| Module | Role |
|
|
33
|
-
| --------------------------------- | ------------------------------------------------------------------- |
|
|
34
|
-
| `src/ui/agent-config-editor.ts` | Agent detail view with edit/delete/eject/disable/enable transitions |
|
|
35
|
-
| `src/ui/agent-creation-wizard.ts` | AI-generation and manual-form agent creation flows |
|
|
36
|
-
| `src/ui/agent-file-ops.ts` | Filesystem abstraction (`AgentFileOps` interface + production impl) |
|
|
37
|
-
| `src/ui/agent-menu.ts` | `/agents` slash command menu; defines `MenuUI` interface |
|
|
38
|
-
|
|
39
|
-
### Duplicated pattern
|
|
40
|
-
|
|
41
|
-
Both sites execute this sequence:
|
|
42
|
-
|
|
43
|
-
```typescript
|
|
44
|
-
if (this.fileOps.exists(targetPath)) {
|
|
45
|
-
const overwrite = await ui.confirm("Overwrite", `${targetPath} already exists. Overwrite?`);
|
|
46
|
-
if (!overwrite) return;
|
|
47
|
-
}
|
|
48
|
-
this.fileOps.write(targetPath, content);
|
|
49
|
-
this.registry.reload();
|
|
50
|
-
ui.notify(`${label} ${targetPath}`, "info");
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
The only differences are the `content` argument and the notification `label`.
|
|
54
|
-
|
|
55
|
-
### Dependency
|
|
56
|
-
|
|
57
|
-
Issue #214 (closure-to-class conversion) is closed — both consumer files are already class-based.
|
|
58
|
-
|
|
59
|
-
## Design Overview
|
|
60
|
-
|
|
61
|
-
### Extracted function
|
|
62
|
-
|
|
63
|
-
`writeAgentFile` is a free async function — not a class method — because both consumers are classes with different constructor signatures and no shared base.
|
|
64
|
-
The function takes narrow interface parameters following ISP: each parameter type declares only the methods the function calls.
|
|
65
|
-
|
|
66
|
-
```typescript
|
|
67
|
-
/** Minimal file operations for the overwrite-guard-and-write pattern. */
|
|
68
|
-
interface FileWriter {
|
|
69
|
-
exists(filePath: string): boolean;
|
|
70
|
-
write(filePath: string, content: string): void;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** Minimal UI for the overwrite-guard-and-write pattern. */
|
|
74
|
-
interface WriterUI {
|
|
75
|
-
confirm(title: string, message: string): Promise<boolean>;
|
|
76
|
-
notify(message: string, level: "info" | "warning" | "error"): void;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Registry that can be reloaded after file changes. */
|
|
80
|
-
interface Reloadable {
|
|
81
|
-
reload(): void;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Write an agent file with an overwrite guard.
|
|
86
|
-
*
|
|
87
|
-
* Returns true if the file was written, false if the user declined to overwrite.
|
|
88
|
-
*/
|
|
89
|
-
export async function writeAgentFile(
|
|
90
|
-
fileOps: FileWriter,
|
|
91
|
-
ui: WriterUI,
|
|
92
|
-
registry: Reloadable,
|
|
93
|
-
targetPath: string,
|
|
94
|
-
content: string,
|
|
95
|
-
label: string,
|
|
96
|
-
): Promise<boolean>;
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
### Consumer call sites
|
|
100
|
-
|
|
101
|
-
In `AgentConfigEditor.ejectAgent`:
|
|
102
|
-
|
|
103
|
-
```typescript
|
|
104
|
-
await writeAgentFile(this.fileOps, ui, this.registry, targetPath, buildEjectContent(cfg), `Ejected ${name} to`);
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
In `AgentCreationWizard.showManualWizard`:
|
|
108
|
-
|
|
109
|
-
```typescript
|
|
110
|
-
await writeAgentFile(this.fileOps, ui, this.registry, targetPath, content, "Created");
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
Both callers already hold `this.fileOps` and `this.registry` as private fields, and receive `ui` as a method parameter — no wiring changes needed.
|
|
114
|
-
|
|
115
|
-
### ISP verification
|
|
116
|
-
|
|
117
|
-
The `FileWriter` interface uses 2 of `AgentFileOps`'s 6 methods (`exists`, `write`).
|
|
118
|
-
The `WriterUI` interface uses 2 of `MenuUI`'s 6 methods (`confirm`, `notify`).
|
|
119
|
-
The `Reloadable` interface uses 1 method (`reload`).
|
|
120
|
-
All three are structurally satisfied by the existing types without adapter code.
|
|
121
|
-
|
|
122
|
-
## Module-Level Changes
|
|
123
|
-
|
|
124
|
-
1. **New `src/ui/agent-file-writer.ts`** — exports `writeAgentFile` function and the three narrow interfaces (`FileWriter`, `WriterUI`, `Reloadable`).
|
|
125
|
-
2. **`src/ui/agent-config-editor.ts`** — `ejectAgent` method: replace the inline overwrite-guard + write + reload + notify block with a call to `writeAgentFile`.
|
|
126
|
-
The `join(targetDir, ...)` and `buildEjectContent(cfg)` calls remain in the caller.
|
|
127
|
-
3. **`src/ui/agent-creation-wizard.ts`** — `showManualWizard` method: replace the inline overwrite-guard + write + reload + notify block with a call to `writeAgentFile`.
|
|
128
|
-
The `join(targetDir, ...)` and content-assembly calls remain in the caller.
|
|
129
|
-
4. **New `test/ui/agent-file-writer.test.ts`** — unit tests for `writeAgentFile`.
|
|
130
|
-
5. **`docs/architecture/architecture.md`** — add `agent-file-writer.ts` to the `ui/` layout listing and update the production-duplication section to mark the clone group as resolved.
|
|
131
|
-
|
|
132
|
-
## Test Impact Analysis
|
|
133
|
-
|
|
134
|
-
1. The new `agent-file-writer.test.ts` enables focused unit tests for the overwrite-guard + write + reload + notify sequence — previously this logic was only testable through the higher-level `ejectAgent` and `showManualWizard` flows.
|
|
135
|
-
2. Existing tests in `agent-config-editor.test.ts` (eject overwrite prompt, eject write) and `agent-creation-wizard.test.ts` (manual wizard overwrite prompt, manual wizard write) remain as integration-level tests that verify the full flow still works end-to-end.
|
|
136
|
-
They should not be removed — they test the caller's orchestration, not just the write logic.
|
|
137
|
-
3. No existing tests become redundant with this extraction.
|
|
138
|
-
|
|
139
|
-
## TDD Order
|
|
140
|
-
|
|
141
|
-
1. **Red → Green: `writeAgentFile` writes when target does not exist**
|
|
142
|
-
- New `test/ui/agent-file-writer.test.ts` with tests: writes file, reloads registry, notifies user, returns `true`.
|
|
143
|
-
- New `src/ui/agent-file-writer.ts` with the extracted function.
|
|
144
|
-
- Commit: `feat: extract writeAgentFile overwrite-guard function (#217)`
|
|
145
|
-
|
|
146
|
-
2. **Red → Green: `writeAgentFile` overwrite guard**
|
|
147
|
-
- Add tests: prompts for overwrite when file exists; writes and returns `true` when confirmed; does not write and returns `false` when declined.
|
|
148
|
-
- Implementation should already pass (the guard is part of the function body from step 1).
|
|
149
|
-
- Commit: `test: add overwrite-guard tests for writeAgentFile (#217)`
|
|
150
|
-
|
|
151
|
-
3. **Refactor: wire `ejectAgent` to use `writeAgentFile`**
|
|
152
|
-
- Replace the inline overwrite-guard block in `AgentConfigEditor.ejectAgent` with a call to `writeAgentFile`.
|
|
153
|
-
- Existing tests in `agent-config-editor.test.ts` must continue to pass.
|
|
154
|
-
- Commit: `refactor: use writeAgentFile in AgentConfigEditor.ejectAgent (#217)`
|
|
155
|
-
|
|
156
|
-
4. **Refactor: wire `showManualWizard` to use `writeAgentFile`**
|
|
157
|
-
- Replace the inline overwrite-guard block in `AgentCreationWizard.showManualWizard` with a call to `writeAgentFile`.
|
|
158
|
-
- Existing tests in `agent-creation-wizard.test.ts` must continue to pass.
|
|
159
|
-
- Commit: `refactor: use writeAgentFile in AgentCreationWizard.showManualWizard (#217)`
|
|
160
|
-
|
|
161
|
-
5. **Docs: update architecture**
|
|
162
|
-
- Add `agent-file-writer.ts` to the `ui/` layout listing in `docs/architecture/architecture.md`.
|
|
163
|
-
- Update the production-duplication section to mark the clone group as resolved.
|
|
164
|
-
- Commit: `docs: update architecture for writeAgentFile extraction (#217)`
|
|
165
|
-
|
|
166
|
-
## Risks and Mitigations
|
|
167
|
-
|
|
168
|
-
1. **Notification message format drift** — The extracted function uses `${label} ${targetPath}` for the notification.
|
|
169
|
-
Both current callers produce messages matching this pattern (`"Ejected ${name} to ${targetPath}"` and `"Created ${targetPath}"`).
|
|
170
|
-
The label parameter gives callers full control over the prefix, so no format is baked in.
|
|
171
|
-
2. **Existing test fragility** — Tests use `expect.stringContaining("already exists")` for the overwrite prompt, which is stable across the extraction.
|
|
172
|
-
No test rewrites needed.
|
|
173
|
-
|
|
174
|
-
## Open Questions
|
|
175
|
-
|
|
176
|
-
None — the issue's proposed change section is unambiguous and the dependency (#214) is resolved.
|