@gotgenes/pi-subagents 6.19.1 → 7.1.0
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 +41 -0
- package/docs/architecture/architecture.md +193 -95
- package/docs/architecture/history/phase-10-structural-decomposition.md +141 -0
- package/docs/plans/0185-remove-persistent-agent-memory.md +161 -0
- package/docs/plans/0192-define-session-context-interface.md +107 -0
- package/docs/retro/0185-remove-persistent-agent-memory.md +73 -0
- package/docs/retro/0188-replace-any-casts-with-sdk-types.md +29 -0
- package/docs/retro/0192-define-session-context-interface.md +35 -0
- package/package.json +1 -1
- package/src/config/agent-types.ts +0 -20
- package/src/config/custom-agents.ts +1 -11
- package/src/index.ts +1 -3
- package/src/session/prompts.ts +1 -6
- package/src/session/safe-fs.ts +45 -0
- package/src/session/session-config.ts +3 -49
- package/src/session/skill-loader.ts +1 -1
- package/src/types.ts +21 -5
- package/src/ui/agent-config-editor.ts +0 -1
- package/src/ui/agent-creation-wizard.ts +0 -1
- package/src/session/memory.ts +0 -168
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 185
|
|
3
|
+
issue_title: "pi-subagents: Remove persistent agent memory feature"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Remove persistent agent memory feature
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
The `memory.ts` module and all supporting code for persistent agent memory (`MEMORY.md`, `agent-memory/` directories) should be removed from `pi-subagents`.
|
|
11
|
+
The memory feature invents its own filesystem layout, file format, and security model — all outside the stated scope of `pi-subagents`, which is agent spawning, execution, and result retrieval.
|
|
12
|
+
This follows the same scope-reduction rationale as the scheduling subsystem removal (issue #52).
|
|
13
|
+
|
|
14
|
+
## Goals
|
|
15
|
+
|
|
16
|
+
- Remove the `memory.ts` module and all memory-related code from the package.
|
|
17
|
+
- Remove `MemoryScope` type and `memory` field from `AgentConfig`.
|
|
18
|
+
- Remove memory block injection from session assembly.
|
|
19
|
+
- Remove memory tool augmentation from agent-types.
|
|
20
|
+
- Remove memory parsing from custom agent loading.
|
|
21
|
+
- Remove memory display from UI components.
|
|
22
|
+
- Extract `isSymlink`, `isUnsafeName`, and `safeReadFile` to a shared utility module — `skill-loader.ts` depends on them independently of memory.
|
|
23
|
+
- Update architecture documentation to reflect the removal.
|
|
24
|
+
|
|
25
|
+
## Non-Goals
|
|
26
|
+
|
|
27
|
+
- Replacing memory with an alternative persistence mechanism — that is out of scope.
|
|
28
|
+
- Changing the `skill-loader.ts` logic beyond updating the import source for the extracted utilities.
|
|
29
|
+
|
|
30
|
+
## Background
|
|
31
|
+
|
|
32
|
+
The memory system spans six source modules and two UI modules:
|
|
33
|
+
|
|
34
|
+
| File | Memory surface |
|
|
35
|
+
| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
36
|
+
| `src/session/memory.ts` | Core module: `buildMemoryBlock`, `buildReadOnlyMemoryBlock`, `resolveMemoryDir`, `ensureMemoryDir`, `readMemoryIndex`, plus shared utilities `isSymlink`, `isUnsafeName`, `safeReadFile` |
|
|
37
|
+
| `src/types.ts` | `MemoryScope` type, `memory?: MemoryScope` field on `AgentConfig` |
|
|
38
|
+
| `src/session/session-config.ts` | `AssemblerIO.buildMemoryBlock` / `buildReadOnlyMemoryBlock` fields, memory logic block (~25 lines), `MemoryScope` import |
|
|
39
|
+
| `src/config/agent-types.ts` | `getMemoryToolNames()`, `getReadOnlyMemoryToolNames()`, `MEMORY_TOOL_NAMES`, `READONLY_MEMORY_TOOL_NAMES` |
|
|
40
|
+
| `src/config/custom-agents.ts` | `parseMemory()` function, `memory: parseMemory(fm.memory)` assignment |
|
|
41
|
+
| `src/session/prompts.ts` | `memoryBlock` field on `PromptExtras`, injection into system prompt |
|
|
42
|
+
| `src/ui/agent-config-editor.ts` | `if (cfg.memory) fmFields.push(...)` |
|
|
43
|
+
| `src/ui/agent-creation-wizard.ts` | `memory:` line in frontmatter help text |
|
|
44
|
+
| `src/index.ts` | Imports `buildMemoryBlock`, `buildReadOnlyMemoryBlock`; wires them into `assemblerIO` |
|
|
45
|
+
|
|
46
|
+
The three utility functions (`isSymlink`, `isUnsafeName`, `safeReadFile`) are imported by `skill-loader.ts` for filesystem safety — they are not memory-specific and must survive the removal.
|
|
47
|
+
|
|
48
|
+
AGENTS.md constraint: the architecture doc in `docs/architecture/architecture.md` references `memory.ts` in the module listing and domain model diagram — both need updating.
|
|
49
|
+
|
|
50
|
+
## Design Overview
|
|
51
|
+
|
|
52
|
+
This is a pure removal with one extraction.
|
|
53
|
+
The only design decision is where to place the extracted utilities.
|
|
54
|
+
|
|
55
|
+
The three functions (`isSymlink`, `isUnsafeName`, `safeReadFile`) are filesystem safety primitives used by `skill-loader.ts`.
|
|
56
|
+
They belong in a new `src/session/safe-fs.ts` module in the session domain, co-located with their sole remaining consumer.
|
|
57
|
+
|
|
58
|
+
The removal works consumers-first, declaration-last:
|
|
59
|
+
|
|
60
|
+
1. Extract shared utilities to `safe-fs.ts` (additive, no behavior change).
|
|
61
|
+
2. Remove all memory consumers — session-config, prompts, agent-types, custom-agents, UI, index wiring.
|
|
62
|
+
3. Remove the declarations — `MemoryScope`, `AgentConfig.memory`, `memory.ts`.
|
|
63
|
+
4. Update architecture docs.
|
|
64
|
+
|
|
65
|
+
## Module-Level Changes
|
|
66
|
+
|
|
67
|
+
### New files
|
|
68
|
+
|
|
69
|
+
- `src/session/safe-fs.ts` — extracted `isSymlink`, `isUnsafeName`, `safeReadFile`.
|
|
70
|
+
- `test/session/safe-fs.test.ts` — tests moved from `memory.test.ts` for these three functions.
|
|
71
|
+
|
|
72
|
+
### Modified files
|
|
73
|
+
|
|
74
|
+
- `src/session/skill-loader.ts` — change import from `#src/session/memory` to `#src/session/safe-fs`.
|
|
75
|
+
- `src/session/session-config.ts` — remove `buildMemoryBlock` / `buildReadOnlyMemoryBlock` from `AssemblerIO`; remove `MemoryScope` import; remove entire memory logic block (~lines 215–242).
|
|
76
|
+
- `src/session/prompts.ts` — remove `memoryBlock` from `PromptExtras` interface; remove `extras?.memoryBlock` injection.
|
|
77
|
+
- `src/config/agent-types.ts` — remove `MEMORY_TOOL_NAMES`, `READONLY_MEMORY_TOOL_NAMES`, `getMemoryToolNames()`, `getReadOnlyMemoryToolNames()`.
|
|
78
|
+
- `src/config/custom-agents.ts` — remove `MemoryScope` import; remove `memory: parseMemory(fm.memory)` assignment; remove `parseMemory()` function.
|
|
79
|
+
- `src/types.ts` — remove `MemoryScope` type; remove `memory?: MemoryScope` from `AgentConfig`; remove associated doc comment.
|
|
80
|
+
- `src/ui/agent-config-editor.ts` — remove `if (cfg.memory)` line.
|
|
81
|
+
- `src/ui/agent-creation-wizard.ts` — remove `memory:` line from frontmatter help text.
|
|
82
|
+
- `src/index.ts` — remove `buildMemoryBlock` / `buildReadOnlyMemoryBlock` import; remove those fields from `assemblerIO` object.
|
|
83
|
+
- `docs/architecture/architecture.md` — remove `memory.ts` from module listing; remove Memory node from domain model Mermaid diagram; update session domain description.
|
|
84
|
+
|
|
85
|
+
### Deleted files
|
|
86
|
+
|
|
87
|
+
- `src/session/memory.ts` — entire module.
|
|
88
|
+
- `test/session/memory.test.ts` — entire test file (tests for `isSymlink`, `isUnsafeName`, `safeReadFile` are moved to `safe-fs.test.ts` first; remaining memory-specific tests are deleted).
|
|
89
|
+
|
|
90
|
+
### Test files modified
|
|
91
|
+
|
|
92
|
+
- `test/session/session-config.test.ts` — remove `mockBuildMemoryBlock` / `mockBuildReadOnlyMemoryBlock` mocks from `AssemblerIO` construction; remove "assembleSessionConfig — memory block selection" describe block (~lines 354–427).
|
|
93
|
+
- `test/session/prompts.test.ts` — remove "injects memory block in replace mode", "injects memory block in append mode", and "injects both memory and skills" test cases.
|
|
94
|
+
- `test/config/agent-types.test.ts` — remove `getMemoryToolNames` / `getReadOnlyMemoryToolNames` imports and test suite (~lines 26–51).
|
|
95
|
+
- `test/config/custom-agents.test.ts` — remove memory scope parsing tests (~lines 361–403).
|
|
96
|
+
|
|
97
|
+
## Test Impact Analysis
|
|
98
|
+
|
|
99
|
+
1. The extraction of `isSymlink`, `isUnsafeName`, `safeReadFile` to `safe-fs.ts` enables their tests to exist independently of the memory module — currently they are co-located with memory-specific tests in `memory.test.ts`.
|
|
100
|
+
2. The memory-specific tests in `memory.test.ts` (`resolveMemoryDir`, `ensureMemoryDir`, `readMemoryIndex`, `buildMemoryBlock`, `buildReadOnlyMemoryBlock`) become redundant and are deleted — the code they test is being removed.
|
|
101
|
+
3. The memory block selection tests in `session-config.test.ts` (~70 lines) test the memory branching logic in `assembleSessionConfig` — they are deleted because that logic is removed.
|
|
102
|
+
4. The memory injection tests in `prompts.test.ts` test `memoryBlock` injection — deleted because the field and injection code are removed.
|
|
103
|
+
5. The memory parsing tests in `custom-agents.test.ts` test `parseMemory` — deleted because the function is removed.
|
|
104
|
+
6. The memory tool name helper tests in `agent-types.test.ts` test `getMemoryToolNames` / `getReadOnlyMemoryToolNames` — deleted because the functions are removed.
|
|
105
|
+
7. All other tests remain as-is — they do not depend on memory functionality.
|
|
106
|
+
|
|
107
|
+
## TDD Order
|
|
108
|
+
|
|
109
|
+
1. **Extract utilities to `safe-fs.ts`.**
|
|
110
|
+
Create `src/session/safe-fs.ts` with `isSymlink`, `isUnsafeName`, `safeReadFile`.
|
|
111
|
+
Create `test/session/safe-fs.test.ts` with tests moved from `memory.test.ts` for these three functions.
|
|
112
|
+
Update `src/session/skill-loader.ts` import to point to `#src/session/safe-fs`.
|
|
113
|
+
Update `src/session/memory.ts` import to point to `#src/session/safe-fs` (temporary — keeps memory working until removal).
|
|
114
|
+
Verify: `pnpm vitest run` and `pnpm run check`.
|
|
115
|
+
Commit: `refactor: extract safe-fs utilities from memory module`
|
|
116
|
+
|
|
117
|
+
2. **Remove memory from session assembly and config layers.**
|
|
118
|
+
Remove `buildMemoryBlock` / `buildReadOnlyMemoryBlock` from `AssemblerIO` in `session-config.ts`.
|
|
119
|
+
Remove `MemoryScope` import and all memory logic from `session-config.ts`.
|
|
120
|
+
Remove `memoryBlock` from `PromptExtras` in `prompts.ts` and its injection logic.
|
|
121
|
+
Remove `getMemoryToolNames`, `getReadOnlyMemoryToolNames`, `MEMORY_TOOL_NAMES`, `READONLY_MEMORY_TOOL_NAMES` from `agent-types.ts`.
|
|
122
|
+
Remove `getMemoryToolNames` / `getReadOnlyMemoryToolNames` import from `session-config.ts`.
|
|
123
|
+
Remove `buildMemoryBlock` / `buildReadOnlyMemoryBlock` import and `assemblerIO` fields from `index.ts`.
|
|
124
|
+
Update `test/session/session-config.test.ts`: remove memory mocks from IO construction and memory block selection test suite.
|
|
125
|
+
Update `test/session/prompts.test.ts`: remove memory injection tests.
|
|
126
|
+
Update `test/config/agent-types.test.ts`: remove memory tool name helper tests.
|
|
127
|
+
Verify: `pnpm vitest run` and `pnpm run check`.
|
|
128
|
+
Commit: `feat!: remove memory from session assembly and config layers`
|
|
129
|
+
|
|
130
|
+
3. **Remove memory from types, custom-agents, and UI.**
|
|
131
|
+
Remove `MemoryScope` type and `memory` field from `AgentConfig` in `types.ts`.
|
|
132
|
+
Remove `parseMemory()` function, `MemoryScope` import, and `memory:` assignment from `custom-agents.ts`.
|
|
133
|
+
Remove `if (cfg.memory)` line from `agent-config-editor.ts`.
|
|
134
|
+
Remove `memory:` help text line from `agent-creation-wizard.ts`.
|
|
135
|
+
Update `test/config/custom-agents.test.ts`: remove memory parsing tests.
|
|
136
|
+
Verify: `pnpm vitest run` and `pnpm run check`.
|
|
137
|
+
Commit: `feat!: remove MemoryScope type and memory config field`
|
|
138
|
+
|
|
139
|
+
4. **Delete `memory.ts` and its test file.**
|
|
140
|
+
Delete `src/session/memory.ts`.
|
|
141
|
+
Delete `test/session/memory.test.ts`.
|
|
142
|
+
Verify: `pnpm vitest run` and `pnpm run check`.
|
|
143
|
+
Commit: `feat!: delete memory module`
|
|
144
|
+
|
|
145
|
+
5. **Update architecture documentation.**
|
|
146
|
+
Remove Memory node from the domain model Mermaid diagram.
|
|
147
|
+
Remove `memory.ts` from the module listing.
|
|
148
|
+
Update session domain description.
|
|
149
|
+
Commit: `docs: update architecture after memory removal`
|
|
150
|
+
|
|
151
|
+
## Risks and Mitigations
|
|
152
|
+
|
|
153
|
+
| Risk | Mitigation |
|
|
154
|
+
| -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
155
|
+
| Users with `memory:` in custom agent frontmatter — the field silently becomes a no-op. | `parseMemory` removal means unknown frontmatter keys are ignored by the markdown parser. No error, no crash — just no memory. This is acceptable for a feature removal. |
|
|
156
|
+
| `skill-loader.ts` breaks if the utility extraction has a typo or missing re-export. | Step 1 runs full test suite before proceeding. The skill-loader tests exercise `isUnsafeName` and `safeReadFile` indirectly. |
|
|
157
|
+
| Architecture doc Mermaid diagram breaks after node removal. | Verify the diagram renders correctly after editing — remove both the node and all edges referencing it. |
|
|
158
|
+
|
|
159
|
+
## Open Questions
|
|
160
|
+
|
|
161
|
+
None — the issue scope is unambiguous and all affected files have been traced.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 192
|
|
3
|
+
issue_title: "Define SessionContext narrow interface"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Define `SessionContext` narrow interface
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
`SubagentRuntime.currentCtx` is typed `{ pi: unknown; ctx: unknown }`.
|
|
11
|
+
Every consumer must cast through `as any` to read fields from the SDK context.
|
|
12
|
+
This forces context queries (`buildSnapshot`, `getModelInfo`, `getSessionInfo`) to live as closures in `index.ts` with repeated `as any` casts, rather than as typed methods on the state holder.
|
|
13
|
+
|
|
14
|
+
The SDK exports `ExtensionContext` — the `unknown` typing is a historical choice, not a constraint.
|
|
15
|
+
|
|
16
|
+
## Goals
|
|
17
|
+
|
|
18
|
+
- Define a narrow `SessionContext` interface in `src/types.ts` capturing the 5 fields `SubagentRuntime` actually reads.
|
|
19
|
+
- Pure additive — no consumers change in this step.
|
|
20
|
+
- Provide the typed foundation for Layer 1 (#193) and subsequent closure-to-class conversion issues.
|
|
21
|
+
|
|
22
|
+
## Non-Goals
|
|
23
|
+
|
|
24
|
+
- Changing `SubagentRuntime.currentCtx` type (that's #193).
|
|
25
|
+
- Converting closure factories to classes (#195, #196).
|
|
26
|
+
- Removing any `as any` casts from `index.ts` (that's #193).
|
|
27
|
+
|
|
28
|
+
## Background
|
|
29
|
+
|
|
30
|
+
Phase 11, Layer 0 in `docs/architecture/architecture.md`.
|
|
31
|
+
This is the first step in a 5-issue sequence (issues #192–#196) that converts closure factories to classes, eliminating 44 adapter closures in `index.ts`.
|
|
32
|
+
|
|
33
|
+
The SDK's `ExtensionContext` interface (in `@earendil-works/pi-coding-agent`) is broad — it exposes `ui`, `abort()`, `shutdown()`, `compact()`, etc.
|
|
34
|
+
ISP (Interface Segregation Principle) from `code-design` mandates a narrow interface capturing only what `SubagentRuntime` needs.
|
|
35
|
+
|
|
36
|
+
The 5 fields consumed by runtime (traced from `index.ts` lines 214–223 and `lifecycle/parent-snapshot.ts`):
|
|
37
|
+
|
|
38
|
+
1. `cwd` — working directory for agent sessions.
|
|
39
|
+
2. `model` — parent model instance for fallback resolution.
|
|
40
|
+
3. `modelRegistry` — resolving config model strings.
|
|
41
|
+
4. `getSystemPrompt()` — system prompt for append-mode agents.
|
|
42
|
+
5. `sessionManager.getSessionFile()` / `.getSessionId()` / `.getBranch()` — session identification and context inheritance.
|
|
43
|
+
|
|
44
|
+
The local `ModelRegistry` interface (in `src/session/model-resolver.ts`) already exists as a narrow ISP interface.
|
|
45
|
+
`SessionContext` will reference it rather than redeclaring model-registry methods inline.
|
|
46
|
+
|
|
47
|
+
## Design Overview
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import type { ModelRegistry } from "#src/session/model-resolver";
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Narrow interface capturing the 5 ExtensionContext fields SubagentRuntime needs.
|
|
54
|
+
* Avoids coupling runtime to the full SDK ExtensionContext surface.
|
|
55
|
+
*/
|
|
56
|
+
export interface SessionContext {
|
|
57
|
+
readonly cwd: string;
|
|
58
|
+
readonly model: unknown;
|
|
59
|
+
readonly modelRegistry: ModelRegistry | undefined;
|
|
60
|
+
getSystemPrompt(): string;
|
|
61
|
+
readonly sessionManager: {
|
|
62
|
+
getSessionFile(): string | undefined;
|
|
63
|
+
getSessionId(): string;
|
|
64
|
+
getBranch(): unknown[];
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Design decisions:
|
|
70
|
+
|
|
71
|
+
1. `model` stays `unknown` — the runtime only passes it through to `resolveModel`; narrowing it gains nothing and would couple to `@earendil-works/pi-ai`'s `Model<Api>` generic.
|
|
72
|
+
2. `modelRegistry` is `ModelRegistry | undefined` — the SDK type says `ModelRegistry` (non-optional), but `SubagentRuntime.currentCtx` can be undefined, and the architecture doc specifies this signature.
|
|
73
|
+
The `| undefined` reflects reality at the cast boundary (pre-bind, the registry may not exist).
|
|
74
|
+
3. `sessionManager` uses an inline structural type rather than importing `ReadonlySessionManager` — we only need 3 of its 13 methods; a separate named type would be over-engineering for a nested structural slice.
|
|
75
|
+
4. `getBranch()` returns `unknown[]` — the runtime passes entries through to `buildParentContext()` which already type-narrows internally.
|
|
76
|
+
|
|
77
|
+
## Module-Level Changes
|
|
78
|
+
|
|
79
|
+
| File | Change |
|
|
80
|
+
| -------------- | -------------------------------------------------------------------------------------------------------------- |
|
|
81
|
+
| `src/types.ts` | Add `SessionContext` interface export. Add `import type { ModelRegistry }` from `#src/session/model-resolver`. |
|
|
82
|
+
|
|
83
|
+
No other files change — this is pure additive.
|
|
84
|
+
|
|
85
|
+
## Test Impact Analysis
|
|
86
|
+
|
|
87
|
+
1. No new unit tests are needed — `SessionContext` is a pure type definition with no runtime behavior.
|
|
88
|
+
2. No existing tests become redundant.
|
|
89
|
+
3. A compile-time check (`pnpm run check`) verifies the interface is well-formed and the import resolves.
|
|
90
|
+
|
|
91
|
+
## TDD Order
|
|
92
|
+
|
|
93
|
+
1. **Add `SessionContext` interface to `src/types.ts`** — add the interface with its import.
|
|
94
|
+
Verify with `pnpm run check` (type-check passes).
|
|
95
|
+
Commit: `feat(pi-subagents): define SessionContext narrow interface (#192)`
|
|
96
|
+
|
|
97
|
+
## Risks and Mitigations
|
|
98
|
+
|
|
99
|
+
| Risk | Mitigation |
|
|
100
|
+
| ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
101
|
+
| Interface shape doesn't match real `ExtensionContext` at runtime | Traced all 5 fields against SDK `.d.ts` declarations; shapes align exactly. |
|
|
102
|
+
| Circular import from `types.ts` → `session/model-resolver.ts` | `model-resolver.ts` does not import from `types.ts`; no cycle. |
|
|
103
|
+
| Future SDK changes break the narrow interface | The cast boundary (Layer 1, #193) will be the single enforcement point — structural typing ensures compile-time detection. |
|
|
104
|
+
|
|
105
|
+
## Open Questions
|
|
106
|
+
|
|
107
|
+
None — the issue's "Proposed change" section fully specifies the interface shape.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 185
|
|
3
|
+
issue_title: "pi-subagents: Remove persistent agent memory feature"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #185 — pi-subagents: Remove persistent agent memory feature
|
|
7
|
+
|
|
8
|
+
## Stage: Planning (2026-05-24T20:46:56Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Traced all memory-related code across 9 source files, 5 test files, and the architecture doc.
|
|
13
|
+
Produced a 5-step TDD plan: extract shared utilities (`isSymlink`, `isUnsafeName`, `safeReadFile`) to `safe-fs.ts`, then remove memory consumers (session assembly, config, UI), then delete the module, then update docs.
|
|
14
|
+
|
|
15
|
+
### Observations
|
|
16
|
+
|
|
17
|
+
- The three utility functions in `memory.ts` are the only complication — `skill-loader.ts` imports them independently of memory.
|
|
18
|
+
Extracting to `src/session/safe-fs.ts` keeps them co-located with their sole remaining consumer.
|
|
19
|
+
- The removal is consumers-first, declaration-last: session-config and prompts lose their memory logic before `MemoryScope` is removed from `types.ts`, avoiding intermediate type errors.
|
|
20
|
+
- No ambiguous design choices — the issue scope section is precise about what to remove and what to extract.
|
|
21
|
+
- Memory field in custom agent frontmatter will silently become a no-op (ignored by the YAML parser) — no user-facing error, just loss of the feature.
|
|
22
|
+
- The `AssemblerIO` interface shrinks from 4 fields to 2 after removal, which is a welcome simplification.
|
|
23
|
+
|
|
24
|
+
## Stage: Implementation — TDD (2026-05-24T22:41:23Z)
|
|
25
|
+
|
|
26
|
+
### Session summary
|
|
27
|
+
|
|
28
|
+
All 5 TDD steps completed across 5 commits.
|
|
29
|
+
Test count went from 901 (54 files) to 848 (53 files) — a net reduction of 53 tests and 1 file, reflecting the deletion of the `memory.test.ts` file with its memory-specific tests and the removal of memory-related tests from `session-config.test.ts`, `prompts.test.ts`, `agent-types.test.ts`, and `custom-agents.test.ts`.
|
|
30
|
+
New file `safe-fs.test.ts` was created with 13 tests for the extracted utilities.
|
|
31
|
+
|
|
32
|
+
### Observations
|
|
33
|
+
|
|
34
|
+
- Step 1 had a subtle bug: after re-exporting `isUnsafeName` from `safe-fs` in `memory.ts`, the function was not imported into the `memory.ts` module scope itself, so `resolveMemoryDir` got a `ReferenceError` at runtime.
|
|
35
|
+
Fix was trivial: add `isUnsafeName` to the import alongside `isSymlink` and `safeReadFile`.
|
|
36
|
+
- Step 3 introduced a type error in `memory.ts` (still alive at that point): `MemoryScope` was imported from `#src/types` which no longer exported it.
|
|
37
|
+
Fix: inline the literal union `"user" | "project" | "local"` directly in `memory.ts` as a local type, so it compiles cleanly until deletion in step 4.
|
|
38
|
+
- The `SKILL.md` for `package-pi-subagents` also listed `memory.ts` in the session domain table — updated alongside `architecture.md` in the docs commit.
|
|
39
|
+
- No deviations from the plan other than the two minor bugs above (both self-corrected within the same TDD step).
|
|
40
|
+
|
|
41
|
+
## Stage: Final Retrospective (2026-05-24T22:47:55Z)
|
|
42
|
+
|
|
43
|
+
### Session summary
|
|
44
|
+
|
|
45
|
+
Shipped issue #185 as `pi-subagents-v7.0.0`.
|
|
46
|
+
CI passed, issue closed, release-please PR #190 merged.
|
|
47
|
+
Three sessions total: planning, TDD (5 steps / 5 commits), shipping.
|
|
48
|
+
|
|
49
|
+
### Observations
|
|
50
|
+
|
|
51
|
+
#### What went well
|
|
52
|
+
|
|
53
|
+
- The issue's "Scope" section was precise enough that the planning session required no `ask_user` and the Explore agent's trace matched the final commit diff exactly.
|
|
54
|
+
- Consumers-first, declaration-last ordering kept each commit independently compilable (after the two self-corrected fixes).
|
|
55
|
+
- The `SKILL.md` domain table update was caught naturally during the docs step even though the plan didn't list it.
|
|
56
|
+
|
|
57
|
+
#### What caused friction (agent side)
|
|
58
|
+
|
|
59
|
+
- `missing-context` — In TDD step 1, `memory.ts` was updated to re-export `isUnsafeName` from `safe-fs`, but the function was not imported into `memory.ts`'s own scope.
|
|
60
|
+
`resolveMemoryDir` threw a `ReferenceError` at runtime.
|
|
61
|
+
Impact: one extra test-run cycle (~5 seconds) and a trivial one-line fix; no rework commit.
|
|
62
|
+
- `missing-context` — In TDD step 3, removing `MemoryScope` from `types.ts` broke `memory.ts` (scheduled for deletion in step 4).
|
|
63
|
+
The plan said "consumers-first" but didn't account for the doomed module itself being a consumer of the type.
|
|
64
|
+
Impact: one extra `pnpm run check` cycle and a local type inline; no rework commit.
|
|
65
|
+
Both share the same root cause: incremental deletion plans must account for doomed files' own imports at each intermediate step.
|
|
66
|
+
|
|
67
|
+
#### What caused friction (user side)
|
|
68
|
+
|
|
69
|
+
- None observed — all three sessions ran without user corrections or redirections.
|
|
70
|
+
|
|
71
|
+
### Changes made
|
|
72
|
+
|
|
73
|
+
1. Added a TDD planning rule to `.pi/skills/testing/SKILL.md` about accounting for doomed modules' own imports during multi-step deletion.
|
|
@@ -38,3 +38,32 @@ Test count: 902 → 901 (one test removed).
|
|
|
38
38
|
- `ui-observer.ts` had one analogous fix: `event.assistantMessageEvent?.type` → `.type` (the field is required on `MessageUpdateEvent`).
|
|
39
39
|
- No test file changes were needed for `ui-observer.ts` — its existing tests all emit conforming events.
|
|
40
40
|
- The plan's contravariance reasoning about mock session compatibility was correct: `pnpm run check` passed without updating `MockSession.subscribe`.
|
|
41
|
+
|
|
42
|
+
## Stage: Final Retrospective (2026-05-24T20:41:42Z)
|
|
43
|
+
|
|
44
|
+
### Session summary
|
|
45
|
+
|
|
46
|
+
Clean two-step refactoring shipped as `pi-subagents-v6.19.1`.
|
|
47
|
+
Both TDD steps completed in a single session with two minor deviations from the plan, both caught by pre-commit hooks.
|
|
48
|
+
Test count: 902 → 901 (one non-conforming test removed).
|
|
49
|
+
|
|
50
|
+
### Observations
|
|
51
|
+
|
|
52
|
+
#### What went well
|
|
53
|
+
|
|
54
|
+
- The plan's contravariance analysis of `MockSession.subscribe` vs `SubscribableSession` was correct — zero test infrastructure changes needed.
|
|
55
|
+
- Pre-commit hooks (ESLint) caught both deviations from the plan at commit time, before they could reach CI.
|
|
56
|
+
- The user providing the Pi source path (`~/development/pi/pi`) unblocked verification of `AssistantMessage.usage` field requirements without guesswork.
|
|
57
|
+
|
|
58
|
+
#### What caused friction (agent side)
|
|
59
|
+
|
|
60
|
+
- `missing-context` — The plan's Risk 3 said "keep `?? ""` for safety" on `TextContent.text`, but `@typescript-eslint/no-unnecessary-condition` rejected it at commit time because `TextContent.text: string` is non-optional.
|
|
61
|
+
Impact: one failed commit attempt, immediate fix (changed `.map((c) => c.text ?? "")` to `.map((c) => c.text)`).
|
|
62
|
+
- `missing-context` — The plan's step 2 didn't anticipate the five additional lint errors surfaced by properly typing the `record-observer` callback: unnecessary optional chain on `event.message?.role`, unnecessary `if (u)` guard, and three unnecessary `?? 0` guards on usage fields.
|
|
63
|
+
Impact: added ~5 minutes of investigation to verify the SDK types were truly non-optional before removing guards; required removing one test case (`"ignores message_end without usage"`).
|
|
64
|
+
Both instances stem from the same root cause: the plan checked that the SDK types *existed* but didn't trace the *field optionality* of types downstream of `AgentSessionEvent` (specifically `AgentMessage.usage` and `Usage.{input,output,cacheWrite}`).
|
|
65
|
+
|
|
66
|
+
#### What caused friction (user side)
|
|
67
|
+
|
|
68
|
+
- The Pi source path (`~/development/pi/pi`) was provided reactively after I'd already spent several tool calls trying to locate `AgentMessage` type definitions in `node_modules`.
|
|
69
|
+
Sharing this path earlier (or noting it in the package skill) would have saved ~4 `grep`/`find` calls.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 192
|
|
3
|
+
issue_title: "Define SessionContext narrow interface"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #192 — Define SessionContext narrow interface
|
|
7
|
+
|
|
8
|
+
## Stage: Planning (2026-05-24T16:00:00Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Planned the pure-additive `SessionContext` interface for `src/types.ts`.
|
|
13
|
+
Traced all 5 consumed fields against the SDK's `ExtensionContext` type declarations to confirm shape alignment.
|
|
14
|
+
Single TDD step: add the interface and verify with `pnpm run check`.
|
|
15
|
+
|
|
16
|
+
### Observations
|
|
17
|
+
|
|
18
|
+
- The interface is trivial in scope — one new export with no consumers changing.
|
|
19
|
+
This is intentionally the smallest possible first step to unblock Layer 1 (#193).
|
|
20
|
+
- `ModelRegistry` already exists as a local narrow interface in `src/session/model-resolver.ts`; `SessionContext` imports it rather than redeclaring.
|
|
21
|
+
- `sessionManager` uses an inline structural type (3 methods) rather than importing the SDK's `ReadonlySessionManager` (13 methods) — ISP applies here.
|
|
22
|
+
- No design ambiguity required `ask_user`; the issue's proposed change section was fully specified.
|
|
23
|
+
|
|
24
|
+
## Stage: Implementation — TDD (2026-05-24T19:55:00Z)
|
|
25
|
+
|
|
26
|
+
### Session summary
|
|
27
|
+
|
|
28
|
+
Added the `SessionContext` interface to `src/types.ts` with an `import type { ModelRegistry }` from `#src/session/model-resolver`.
|
|
29
|
+
Single compile-time step — no runtime tests needed for a pure type definition.
|
|
30
|
+
Baseline: 53 test files, 848 tests; final: unchanged.
|
|
31
|
+
|
|
32
|
+
### Observations
|
|
33
|
+
|
|
34
|
+
- Pre-existing lint failure in `docs/architecture/architecture.md` (5 unused MD053 link references for issues #164, #165, #170, #171, #172) was fixed as part of the baseline verification and included in the feat commit.
|
|
35
|
+
- The interface landed exactly as planned — no deviations from the plan's Design Overview.
|
package/package.json
CHANGED
|
@@ -133,23 +133,3 @@ export class AgentTypeRegistry implements AgentConfigLookup {
|
|
|
133
133
|
|
|
134
134
|
/** All known built-in tool names. */
|
|
135
135
|
export const BUILTIN_TOOL_NAMES: string[] = ["read", "bash", "edit", "write", "grep", "find", "ls"];
|
|
136
|
-
|
|
137
|
-
/** Tool names required for memory management. */
|
|
138
|
-
const MEMORY_TOOL_NAMES = ["read", "write", "edit"];
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Get memory tool names (read/write/edit) not already in the provided set.
|
|
142
|
-
*/
|
|
143
|
-
export function getMemoryToolNames(existingToolNames: Set<string>): string[] {
|
|
144
|
-
return MEMORY_TOOL_NAMES.filter(n => !existingToolNames.has(n));
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/** Tool names needed for read-only memory access. */
|
|
148
|
-
const READONLY_MEMORY_TOOL_NAMES = ["read"];
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Get read-only memory tool names not already in the provided set.
|
|
152
|
-
*/
|
|
153
|
-
export function getReadOnlyMemoryToolNames(existingToolNames: Set<string>): string[] {
|
|
154
|
-
return READONLY_MEMORY_TOOL_NAMES.filter(n => !existingToolNames.has(n));
|
|
155
|
-
}
|
|
@@ -7,7 +7,7 @@ import { basename, join } from "node:path";
|
|
|
7
7
|
import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import { BUILTIN_TOOL_NAMES } from "#src/config/agent-types";
|
|
9
9
|
import { debugLog } from "#src/debug";
|
|
10
|
-
import type { AgentConfig,
|
|
10
|
+
import type { AgentConfig, ThinkingLevel } from "#src/types";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Scan for custom agent .md files from multiple locations.
|
|
@@ -69,7 +69,6 @@ function loadFromDir(dir: string, agents: Map<string, AgentConfig>, source: "pro
|
|
|
69
69
|
inheritContext: fm.inherit_context != null ? fm.inherit_context === true : undefined,
|
|
70
70
|
runInBackground: fm.run_in_background != null ? fm.run_in_background === true : undefined,
|
|
71
71
|
isolated: fm.isolated != null ? fm.isolated === true : undefined,
|
|
72
|
-
memory: parseMemory(fm.memory),
|
|
73
72
|
isolation: fm.isolation === "worktree" ? "worktree" : undefined,
|
|
74
73
|
enabled: fm.enabled !== false, // default true; explicitly false disables
|
|
75
74
|
source,
|
|
@@ -119,15 +118,6 @@ function csvListOptional(val: unknown): string[] | undefined {
|
|
|
119
118
|
return parseCsvField(val);
|
|
120
119
|
}
|
|
121
120
|
|
|
122
|
-
/**
|
|
123
|
-
* Parse a memory scope field.
|
|
124
|
-
* omitted → undefined; "user"/"project"/"local" → MemoryScope.
|
|
125
|
-
*/
|
|
126
|
-
function parseMemory(val: unknown): MemoryScope | undefined {
|
|
127
|
-
if (val === "user" || val === "project" || val === "local") return val;
|
|
128
|
-
return undefined;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
121
|
/**
|
|
132
122
|
* Parse an inherit field (extensions, skills).
|
|
133
123
|
* omitted/true → true (inherit all); false/"none"/empty → false; csv → listed names.
|
package/src/index.ts
CHANGED
|
@@ -34,7 +34,7 @@ import { createSubagentRuntime } from "#src/runtime";
|
|
|
34
34
|
import { publishSubagentsService, unpublishSubagentsService } from "#src/service/service";
|
|
35
35
|
import { createSubagentsService } from "#src/service/service-adapter";
|
|
36
36
|
import { detectEnv } from "#src/session/env";
|
|
37
|
-
|
|
37
|
+
|
|
38
38
|
import { type ModelRegistry, resolveModel } from "#src/session/model-resolver";
|
|
39
39
|
import { buildAgentPrompt } from "#src/session/prompts";
|
|
40
40
|
import { deriveSubagentSessionDir } from "#src/session/session-dir";
|
|
@@ -146,8 +146,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
146
146
|
createSession: (opts) => createAgentSession(opts as any),
|
|
147
147
|
assemblerIO: {
|
|
148
148
|
preloadSkills,
|
|
149
|
-
buildMemoryBlock,
|
|
150
|
-
buildReadOnlyMemoryBlock,
|
|
151
149
|
buildAgentPrompt,
|
|
152
150
|
},
|
|
153
151
|
};
|
package/src/session/prompts.ts
CHANGED
|
@@ -5,10 +5,8 @@
|
|
|
5
5
|
import type { EnvInfo } from "#src/session/env";
|
|
6
6
|
import type { AgentPromptConfig } from "#src/types";
|
|
7
7
|
|
|
8
|
-
/** Extra sections to inject into the system prompt (
|
|
8
|
+
/** Extra sections to inject into the system prompt (skills, etc.). */
|
|
9
9
|
export interface PromptExtras {
|
|
10
|
-
/** Persistent memory content to inject (first 200 lines of MEMORY.md + instructions). */
|
|
11
|
-
memoryBlock?: string;
|
|
12
10
|
/** Preloaded skill contents to inject. */
|
|
13
11
|
skillBlocks?: { name: string; content: string }[];
|
|
14
12
|
}
|
|
@@ -45,9 +43,6 @@ Platform: ${env.platform}`;
|
|
|
45
43
|
|
|
46
44
|
// Build optional extras suffix
|
|
47
45
|
const extraSections: string[] = [];
|
|
48
|
-
if (extras?.memoryBlock) {
|
|
49
|
-
extraSections.push(extras.memoryBlock);
|
|
50
|
-
}
|
|
51
46
|
if (extras?.skillBlocks?.length) {
|
|
52
47
|
for (const skill of extras.skillBlocks) {
|
|
53
48
|
extraSections.push(
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* safe-fs.ts — Filesystem safety utilities for reading untrusted paths.
|
|
3
|
+
*
|
|
4
|
+
* Used by skill-loader.ts to reject symlinks and path-traversal names
|
|
5
|
+
* before reading skill files from disk.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, lstatSync, readFileSync } from "node:fs";
|
|
9
|
+
import { debugLog } from "#src/debug";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Returns true if a name contains characters not allowed in agent/skill names.
|
|
13
|
+
* Uses a whitelist: only alphanumeric, hyphens, underscores, and dots (no leading dot).
|
|
14
|
+
*/
|
|
15
|
+
export function isUnsafeName(name: string): boolean {
|
|
16
|
+
if (!name || name.length > 128) return true;
|
|
17
|
+
return !/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(name);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Returns true if the given path is a symlink (defense against symlink attacks).
|
|
22
|
+
*/
|
|
23
|
+
export function isSymlink(filePath: string): boolean {
|
|
24
|
+
try {
|
|
25
|
+
return lstatSync(filePath).isSymbolicLink();
|
|
26
|
+
} catch (err) {
|
|
27
|
+
debugLog("lstatSync", err);
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Safely read a file, rejecting symlinks.
|
|
34
|
+
* Returns undefined if the file doesn't exist, is a symlink, or can't be read.
|
|
35
|
+
*/
|
|
36
|
+
export function safeReadFile(filePath: string): string | undefined {
|
|
37
|
+
if (!existsSync(filePath)) return undefined;
|
|
38
|
+
if (isSymlink(filePath)) return undefined;
|
|
39
|
+
try {
|
|
40
|
+
return readFileSync(filePath, "utf-8");
|
|
41
|
+
} catch (err) {
|
|
42
|
+
debugLog("readFileSync", err);
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -10,20 +10,11 @@
|
|
|
10
10
|
* before invoking this function, keeping the assembler synchronous.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {
|
|
14
|
-
type AgentConfigLookup,
|
|
15
|
-
getMemoryToolNames,
|
|
16
|
-
getReadOnlyMemoryToolNames,
|
|
17
|
-
} from "#src/config/agent-types";
|
|
13
|
+
import type { AgentConfigLookup } from "#src/config/agent-types";
|
|
18
14
|
import type { EnvInfo } from "#src/session/env";
|
|
19
15
|
import type { PromptExtras } from "#src/session/prompts";
|
|
20
16
|
import type { PreloadedSkill } from "#src/session/skill-loader";
|
|
21
|
-
import type {
|
|
22
|
-
AgentPromptConfig,
|
|
23
|
-
MemoryScope,
|
|
24
|
-
SubagentType,
|
|
25
|
-
ThinkingLevel,
|
|
26
|
-
} from "#src/types";
|
|
17
|
+
import type { AgentPromptConfig, SubagentType, ThinkingLevel } from "#src/types";
|
|
27
18
|
|
|
28
19
|
// ── Public interfaces ────────────────────────────────────────────────────────
|
|
29
20
|
|
|
@@ -52,12 +43,6 @@ export interface ToolFilterConfig {
|
|
|
52
43
|
*/
|
|
53
44
|
export interface AssemblerIO {
|
|
54
45
|
preloadSkills: (skills: string[], cwd: string) => PreloadedSkill[];
|
|
55
|
-
buildMemoryBlock: (name: string, scope: MemoryScope, cwd: string) => string;
|
|
56
|
-
buildReadOnlyMemoryBlock: (
|
|
57
|
-
name: string,
|
|
58
|
-
scope: MemoryScope,
|
|
59
|
-
cwd: string,
|
|
60
|
-
) => string;
|
|
61
46
|
buildAgentPrompt: (
|
|
62
47
|
config: AgentPromptConfig,
|
|
63
48
|
cwd: string,
|
|
@@ -210,38 +195,7 @@ export function assembleSessionConfig(
|
|
|
210
195
|
}
|
|
211
196
|
}
|
|
212
197
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
// Persistent memory: detect write capability and branch accordingly.
|
|
216
|
-
// Account for disallowedTools — a tool in the base set but on the denylist
|
|
217
|
-
// is not truly available.
|
|
218
|
-
if (agentConfig.memory) {
|
|
219
|
-
const existingNames = new Set(toolNames);
|
|
220
|
-
const denied = agentConfig.disallowedTools
|
|
221
|
-
? new Set(agentConfig.disallowedTools)
|
|
222
|
-
: undefined;
|
|
223
|
-
const effectivelyHas = (name: string) =>
|
|
224
|
-
existingNames.has(name) && !denied?.has(name);
|
|
225
|
-
const hasWriteTools = effectivelyHas("write") || effectivelyHas("edit");
|
|
226
|
-
|
|
227
|
-
if (hasWriteTools) {
|
|
228
|
-
const extraNames = getMemoryToolNames(existingNames);
|
|
229
|
-
if (extraNames.length > 0) toolNames = [...toolNames, ...extraNames];
|
|
230
|
-
extras.memoryBlock = io.buildMemoryBlock(
|
|
231
|
-
agentConfig.name,
|
|
232
|
-
agentConfig.memory,
|
|
233
|
-
effectiveCwd,
|
|
234
|
-
);
|
|
235
|
-
} else {
|
|
236
|
-
const extraNames = getReadOnlyMemoryToolNames(existingNames);
|
|
237
|
-
if (extraNames.length > 0) toolNames = [...toolNames, ...extraNames];
|
|
238
|
-
extras.memoryBlock = io.buildReadOnlyMemoryBlock(
|
|
239
|
-
agentConfig.name,
|
|
240
|
-
agentConfig.memory,
|
|
241
|
-
effectiveCwd,
|
|
242
|
-
);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
198
|
+
const toolNames = registry.getToolNamesForType(type);
|
|
245
199
|
|
|
246
200
|
// Build system prompt from the resolved agent config
|
|
247
201
|
const systemPrompt = io.buildAgentPrompt(
|
|
@@ -24,7 +24,7 @@ import { homedir } from "node:os";
|
|
|
24
24
|
import { join } from "node:path";
|
|
25
25
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
26
26
|
import { debugLog } from "#src/debug";
|
|
27
|
-
import { isSymlink, isUnsafeName, safeReadFile } from "#src/session/
|
|
27
|
+
import { isSymlink, isUnsafeName, safeReadFile } from "#src/session/safe-fs";
|
|
28
28
|
|
|
29
29
|
export interface PreloadedSkill {
|
|
30
30
|
name: string;
|