@gotgenes/pi-subagents 7.5.0 → 7.5.1

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 CHANGED
@@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [7.5.1](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v7.5.0...pi-subagents-v7.5.1) (2026-05-26)
9
+
10
+
11
+ ### Documentation
12
+
13
+ * plan decompose buildParentContext ([#215](https://github.com/gotgenes/pi-packages/issues/215)) ([9103609](https://github.com/gotgenes/pi-packages/commit/910360991b50c320927c1457bfef6b7cb5624b7b))
14
+ * **retro:** add planning stage notes for issue [#215](https://github.com/gotgenes/pi-packages/issues/215) ([5c534d5](https://github.com/gotgenes/pi-packages/commit/5c534d5efb640ef1d72d6ccf7bf2e15ac2acf755))
15
+ * **retro:** add TDD stage notes for issue [#215](https://github.com/gotgenes/pi-packages/issues/215) ([79064d0](https://github.com/gotgenes/pi-packages/commit/79064d072c36c2f92013dbfba58ce1de1ab01bce))
16
+
8
17
  ## [7.5.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v7.4.0...pi-subagents-v7.5.0) (2026-05-26)
9
18
 
10
19
 
@@ -0,0 +1,166 @@
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.
@@ -0,0 +1,35 @@
1
+ ---
2
+ issue: 215
3
+ issue_title: "Decompose buildParentContext (cognitive 30) (Phase 13, Step 2)"
4
+ ---
5
+
6
+ # Retro: #215 — Decompose buildParentContext
7
+
8
+ ## Stage: Planning (2026-05-25T12:00:00Z)
9
+
10
+ ### Session summary
11
+
12
+ Produced a 3-step TDD plan to decompose `buildParentContext` in `src/session/context.ts`.
13
+ Steps 1–2 add tests locking current behavior for `extractText` and `buildParentContext`; step 3 extracts three private helpers (`formatMessageEntry`, `formatCompactionEntry`, `formatBranchEntry`) and simplifies the orchestrator to map/filter/join.
14
+
15
+ ### Observations
16
+
17
+ - No existing unit tests cover `context.ts` — `parent-snapshot.test.ts` mocks `buildParentContext` entirely, so the formatting logic is currently untested.
18
+ - The decomposition is straightforward with no design ambiguity; the architecture roadmap specifies the exact extraction targets.
19
+ - All extracted helpers remain private (not exported), keeping the public API surface unchanged.
20
+ - The `eslint-disable` comment on the `getBranch()` nullability check must be preserved through the refactoring step.
21
+
22
+ ## Stage: Implementation — TDD (2026-05-25T22:36:00Z)
23
+
24
+ ### Session summary
25
+
26
+ Completed all 3 TDD steps: 2 test-only commits locking `extractText` (5 tests) and `buildParentContext` (14 tests) behavior, then a refactor commit extracting `formatMessageEntry`, `formatCompactionEntry`, and `formatBranchEntry`.
27
+ Test count increased from 939 to 958 (+19).
28
+ All checks green: full suite, `pnpm run check`, `pnpm run lint`, `pnpm fallow dead-code`.
29
+
30
+ ### Observations
31
+
32
+ - Because `extractText` and `buildParentContext` already existed, both test steps passed immediately (no red phase) — this is correct for behavior-locking tests before a refactor.
33
+ - The `makeCtx` helper in the test file creates a minimal `SessionContext` satisfying only `sessionManager.getBranch()`; the extra required fields (`cwd`, `model`, `modelRegistry`, `getSystemPrompt`) are satisfied with stubs.
34
+ - The `eslint-disable` comment on the `getBranch()` nullability check was preserved unchanged through the refactor.
35
+ - No deviations from the plan.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-subagents",
3
- "version": "7.5.0",
3
+ "version": "7.5.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/service.ts"
@@ -30,6 +30,28 @@ export function extractText(content: unknown[]): string {
30
30
  .join("\n");
31
31
  }
32
32
 
33
+ /** Format a message entry (user/assistant); returns undefined for roles to skip. */
34
+ function formatMessageEntry(entry: MessageEntry): string | undefined {
35
+ const msg = entry.message;
36
+ const text = typeof msg.content === "string" ? msg.content : extractText(msg.content);
37
+ if (!text.trim()) return undefined;
38
+ if (msg.role === "user") return `[User]: ${text.trim()}`;
39
+ if (msg.role === "assistant") return `[Assistant]: ${text.trim()}`;
40
+ return undefined; // skip toolResult and other roles
41
+ }
42
+
43
+ /** Format a compaction entry; returns undefined when no summary is present. */
44
+ function formatCompactionEntry(entry: CompactionEntry): string | undefined {
45
+ return entry.summary ? `[Summary]: ${entry.summary}` : undefined;
46
+ }
47
+
48
+ /** Dispatch a branch entry to the appropriate formatter. */
49
+ function formatBranchEntry(entry: BranchEntry): string | undefined {
50
+ if (entry.type === "message") return formatMessageEntry(entry as MessageEntry);
51
+ if (entry.type === "compaction") return formatCompactionEntry(entry as CompactionEntry);
52
+ return undefined;
53
+ }
54
+
33
55
  /**
34
56
  * Build a text representation of the parent conversation context.
35
57
  * Used when inherit_context is true to give the subagent visibility
@@ -40,30 +62,9 @@ export function buildParentContext(ctx: SessionContext): string {
40
62
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- getBranch() may return undefined at runtime despite its type
41
63
  if (!entries || entries.length === 0) return "";
42
64
 
43
- const parts: string[] = [];
44
-
45
- for (const rawEntry of entries as BranchEntry[]) {
46
- if (rawEntry.type === "message") {
47
- const entry = rawEntry as MessageEntry;
48
- const msg = entry.message;
49
- if (msg.role === "user") {
50
- const text = typeof msg.content === "string"
51
- ? msg.content
52
- : extractText(msg.content);
53
- if (text.trim()) parts.push(`[User]: ${text.trim()}`);
54
- } else if (msg.role === "assistant") {
55
- const text = typeof msg.content === "string" ? msg.content : extractText(msg.content);
56
- if (text.trim()) parts.push(`[Assistant]: ${text.trim()}`);
57
- }
58
- // Skip toolResult messages — too verbose for context
59
- } else if (rawEntry.type === "compaction") {
60
- // Include compaction summaries — they're already condensed
61
- const entry = rawEntry as CompactionEntry;
62
- if (entry.summary) {
63
- parts.push(`[Summary]: ${entry.summary}`);
64
- }
65
- }
66
- }
65
+ const parts = (entries as BranchEntry[])
66
+ .map(formatBranchEntry)
67
+ .filter((p): p is string => p !== undefined);
67
68
 
68
69
  if (parts.length === 0) return "";
69
70