- Introduced new planning files including PROJECT.md, REQUIREMENTS.md, ROADMAP.md, and STATE.md to outline the vision and requirements for SSH multi-context workspaces. - Added ARCHITECTURE.md and CONCERNS.md to detail the codebase structure and address technical debt, known bugs, and security considerations. - Created CONVENTIONS.md to establish coding standards and practices for the project. - Updated .gitignore to exclude demo files and added configuration for planning tools. This commit lays the groundwork for enhancing SSH functionality and user experience in managing multiple workspaces.
329 lines
14 KiB
Markdown
329 lines
14 KiB
Markdown
---
|
|
phase: 01-provider-plumbing
|
|
plan: 01
|
|
type: execute
|
|
wave: 1
|
|
depends_on: []
|
|
files_modified:
|
|
- src/main/services/discovery/ProjectScanner.ts
|
|
- src/main/services/parsing/SessionParser.ts
|
|
- src/main/services/discovery/SubagentResolver.ts
|
|
- src/main/services/analysis/SubagentDetailBuilder.ts
|
|
- src/main/services/analysis/ChunkBuilder.ts
|
|
- test/main/services/parsing/SessionParser.test.ts
|
|
autonomous: true
|
|
|
|
must_haves:
|
|
truths:
|
|
- "SessionParser.parseSessionFile() passes FileSystemProvider to parseJsonlFile()"
|
|
- "SubagentResolver.parseSubagentFile() passes FileSystemProvider to parseJsonlFile()"
|
|
- "SubagentDetailBuilder uses FileSystemProvider.exists() instead of fs.access()"
|
|
- "SubagentDetailBuilder constructs paths using ProjectScanner.getSubagentsPath() instead of os.homedir()"
|
|
- "No service in the parsing stack imports fs/promises directly for session data reads"
|
|
artifacts:
|
|
- path: "src/main/services/discovery/ProjectScanner.ts"
|
|
provides: "getFileSystemProvider() getter"
|
|
contains: "getFileSystemProvider"
|
|
- path: "src/main/services/parsing/SessionParser.ts"
|
|
provides: "Provider-aware session parsing"
|
|
contains: "getFileSystemProvider"
|
|
- path: "src/main/services/discovery/SubagentResolver.ts"
|
|
provides: "Provider-aware subagent resolution"
|
|
contains: "getFileSystemProvider"
|
|
- path: "src/main/services/analysis/SubagentDetailBuilder.ts"
|
|
provides: "Provider-aware subagent detail building"
|
|
contains: "fsProvider"
|
|
key_links:
|
|
- from: "src/main/services/parsing/SessionParser.ts"
|
|
to: "src/main/utils/jsonl.ts"
|
|
via: "parseJsonlFile(filePath, provider)"
|
|
pattern: "parseJsonlFile\\(.*,.*getFileSystemProvider"
|
|
- from: "src/main/services/discovery/SubagentResolver.ts"
|
|
to: "src/main/utils/jsonl.ts"
|
|
via: "parseJsonlFile(filePath, provider)"
|
|
pattern: "parseJsonlFile\\(.*,.*getFileSystemProvider"
|
|
- from: "src/main/services/analysis/SubagentDetailBuilder.ts"
|
|
to: "src/main/services/infrastructure/FileSystemProvider.ts"
|
|
via: "fsProvider.exists() replaces fs.access()"
|
|
pattern: "fsProvider\\.exists"
|
|
---
|
|
|
|
<objective>
|
|
Thread FileSystemProvider through the entire session parsing stack so SSH sessions load correctly.
|
|
|
|
Purpose: Currently, SessionParser, SubagentResolver, and SubagentDetailBuilder all call parseJsonlFile() without passing a FileSystemProvider, causing silent fallback to LocalFileSystemProvider. In SSH mode, this means sessions show "No conversation history" because the local filesystem has no matching files. SubagentDetailBuilder additionally hardcodes os.homedir() for path construction. This plan fixes all three services to use the provider from ProjectScanner.
|
|
|
|
Output: All three services use FileSystemProvider consistently. SSH sessions display full conversation history and subagent drill-down works over SFTP.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@/Users/bskim/.claude/get-shit-done/workflows/execute-plan.md
|
|
@/Users/bskim/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/ROADMAP.md
|
|
@.planning/STATE.md
|
|
@.planning/phases/01-provider-plumbing/01-RESEARCH.md
|
|
|
|
@src/main/services/infrastructure/FileSystemProvider.ts
|
|
@src/main/services/discovery/ProjectScanner.ts
|
|
@src/main/services/parsing/SessionParser.ts
|
|
@src/main/services/discovery/SubagentResolver.ts
|
|
@src/main/services/analysis/SubagentDetailBuilder.ts
|
|
@src/main/services/analysis/ChunkBuilder.ts
|
|
@src/main/utils/jsonl.ts
|
|
@src/main/ipc/subagents.ts
|
|
@src/main/index.ts
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Add provider getter to ProjectScanner and thread through SessionParser + SubagentResolver</name>
|
|
<files>
|
|
src/main/services/discovery/ProjectScanner.ts
|
|
src/main/services/parsing/SessionParser.ts
|
|
src/main/services/discovery/SubagentResolver.ts
|
|
test/main/services/parsing/SessionParser.test.ts
|
|
</files>
|
|
<action>
|
|
**ProjectScanner (1 change):**
|
|
Add a public getter method `getFileSystemProvider()` that returns `this.fsProvider`. Place it in the "Utility Methods" section near `getProjectsDir()` and `getTodosDir()`:
|
|
```typescript
|
|
getFileSystemProvider(): FileSystemProvider {
|
|
return this.fsProvider;
|
|
}
|
|
```
|
|
This requires adding `FileSystemProvider` to the type imports (it's currently only imported as a type for the constructor parameter — verify the import is accessible for the return type annotation).
|
|
|
|
**SessionParser (2 changes):**
|
|
1. In `parseSessionFile()` (line 77), change:
|
|
```typescript
|
|
const messages = await parseJsonlFile(filePath);
|
|
```
|
|
to:
|
|
```typescript
|
|
const messages = await parseJsonlFile(filePath, this.projectScanner.getFileSystemProvider());
|
|
```
|
|
|
|
2. In `parseSubagentFile()` (line 342), change:
|
|
```typescript
|
|
const messages = await parseJsonlFile(filePath);
|
|
```
|
|
to:
|
|
```typescript
|
|
const messages = await parseJsonlFile(filePath, this.projectScanner.getFileSystemProvider());
|
|
```
|
|
|
|
No constructor changes needed — SessionParser already receives ProjectScanner in its constructor.
|
|
|
|
**SubagentResolver (1 change):**
|
|
In the private `parseSubagentFile()` method (line 88), change:
|
|
```typescript
|
|
const messages = await parseJsonlFile(filePath);
|
|
```
|
|
to:
|
|
```typescript
|
|
const messages = await parseJsonlFile(filePath, this.projectScanner.getFileSystemProvider());
|
|
```
|
|
|
|
No constructor changes needed — SubagentResolver already receives ProjectScanner in its constructor.
|
|
|
|
**Test updates (SessionParser.test.ts):**
|
|
The existing `mockProjectScanner` object (lines 24-33) must include the new `getFileSystemProvider` method. Add:
|
|
```typescript
|
|
getFileSystemProvider: vi.fn().mockReturnValue(new LocalFileSystemProvider()),
|
|
```
|
|
Import `LocalFileSystemProvider` from `@main/services/infrastructure/LocalFileSystemProvider` (or use a minimal mock object with type 'local' if import resolution is an issue in tests — check the existing test's module resolution).
|
|
</action>
|
|
<verify>
|
|
Run `pnpm typecheck` — no type errors in the modified files.
|
|
Run `pnpm test test/main/services/parsing/SessionParser.test.ts` — all existing tests pass.
|
|
Grep for bare `parseJsonlFile(filePath)` calls (without second argument) in SessionParser.ts and SubagentResolver.ts — should find zero matches:
|
|
```bash
|
|
grep -n 'parseJsonlFile(filePath)' src/main/services/parsing/SessionParser.ts src/main/services/discovery/SubagentResolver.ts
|
|
```
|
|
</verify>
|
|
<done>
|
|
SessionParser.parseSessionFile() and parseSubagentFile() both pass FileSystemProvider to parseJsonlFile(). SubagentResolver.parseSubagentFile() passes FileSystemProvider to parseJsonlFile(). ProjectScanner exposes getFileSystemProvider() getter. All existing SessionParser tests pass with updated mock.
|
|
</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Fix SubagentDetailBuilder to use FileSystemProvider instead of hardcoded fs/os imports</name>
|
|
<files>
|
|
src/main/services/analysis/SubagentDetailBuilder.ts
|
|
src/main/services/analysis/ChunkBuilder.ts
|
|
</files>
|
|
<action>
|
|
**SubagentDetailBuilder (major refactor of buildSubagentDetail function):**
|
|
|
|
The current function (lines 39-135) has three problems:
|
|
1. Imports `fs/promises` dynamically (line 48) — bypasses provider abstraction
|
|
2. Uses `os.homedir()` to construct paths (line 53) — always resolves to local home directory
|
|
3. Uses `fs.access()` for existence check (line 58) — bypasses provider abstraction
|
|
|
|
Refactor the function signature to accept `fsProvider` and `projectsDir` parameters:
|
|
```typescript
|
|
export async function buildSubagentDetail(
|
|
projectId: string,
|
|
_sessionId: string,
|
|
subagentId: string,
|
|
sessionParser: SessionParser,
|
|
subagentResolver: SubagentResolver,
|
|
buildChunksFn: (messages: ParsedMessage[], subagents: Process[]) => EnhancedChunk[],
|
|
fsProvider: FileSystemProvider,
|
|
projectsDir: string
|
|
): Promise<SubagentDetail | null>
|
|
```
|
|
|
|
Replace the function body's path construction and existence check (lines 47-62):
|
|
- Remove the dynamic `fs`, `path`, `os` imports at lines 48-50.
|
|
- Add a static `import * as path from 'path'` at the top of the file (alongside existing imports).
|
|
- Add `import type { FileSystemProvider } from '../infrastructure/FileSystemProvider'` at the top.
|
|
- Replace path construction:
|
|
```typescript
|
|
// OLD (lines 53-54):
|
|
const claudeDir = path.join(os.homedir(), '.claude', 'projects');
|
|
const subagentPath = path.join(claudeDir, projectId, 'subagents', `agent-${subagentId}.jsonl`);
|
|
|
|
// NEW:
|
|
const subagentPath = path.join(projectsDir, projectId, 'subagents', `agent-${subagentId}.jsonl`);
|
|
```
|
|
- Replace existence check:
|
|
```typescript
|
|
// OLD (lines 57-62):
|
|
try {
|
|
await fs.access(subagentPath);
|
|
} catch {
|
|
logger.warn(`Subagent file not found: ${subagentPath}`);
|
|
return null;
|
|
}
|
|
|
|
// NEW:
|
|
if (!(await fsProvider.exists(subagentPath))) {
|
|
logger.warn(`Subagent file not found: ${subagentPath}`);
|
|
return null;
|
|
}
|
|
```
|
|
|
|
The rest of the function body (lines 64-134) can remain unchanged — it delegates to `sessionParser.parseSessionFile()` and `subagentResolver.resolveSubagents()` which now use the provider from Task 1.
|
|
|
|
**ChunkBuilder (update the delegation call):**
|
|
|
|
In `ChunkBuilder.buildSubagentDetail()` (lines 426-442), update the call to pass the new parameters. The ChunkBuilder needs access to `fsProvider` and `projectsDir`. Two options — use the simpler one: pass them as parameters from the IPC layer.
|
|
|
|
Update `ChunkBuilder.buildSubagentDetail()` signature and implementation:
|
|
```typescript
|
|
async buildSubagentDetail(
|
|
projectId: string,
|
|
sessionId: string,
|
|
subagentId: string,
|
|
sessionParser: SessionParser,
|
|
subagentResolver: SubagentResolver,
|
|
fsProvider: FileSystemProvider,
|
|
projectsDir: string
|
|
): Promise<SubagentDetail | null> {
|
|
return buildSubagentDetailFn(
|
|
projectId,
|
|
sessionId,
|
|
subagentId,
|
|
sessionParser,
|
|
subagentResolver,
|
|
(messages, subagents) => this.buildChunks(messages, subagents),
|
|
fsProvider,
|
|
projectsDir
|
|
);
|
|
}
|
|
```
|
|
|
|
Add `import type { FileSystemProvider } from '../infrastructure/FileSystemProvider'` to ChunkBuilder.ts imports if not already present.
|
|
|
|
**IPC subagents.ts (update the call site):**
|
|
|
|
In `handleGetSubagentDetail()` (line 101), the call to `chunkBuilder.buildSubagentDetail()` needs `fsProvider` and `projectsDir`. The subagent handler module has access to `sessionParser` which has `projectScanner`. However, the handler doesn't have direct access to projectScanner.
|
|
|
|
Add `projectScanner` to the subagent handler's service dependencies:
|
|
1. Add `let projectScanner: ProjectScanner;` to the module-level service variables
|
|
2. Update `initializeSubagentHandlers` to accept and store `projectScanner`
|
|
3. In the handler, get provider and projectsDir from projectScanner:
|
|
```typescript
|
|
const fsProvider = projectScanner.getFileSystemProvider();
|
|
const projectsDir = projectScanner.getProjectsDir();
|
|
```
|
|
4. Pass them to the call:
|
|
```typescript
|
|
const builtDetail = await chunkBuilder.buildSubagentDetail(
|
|
safeProjectId,
|
|
safeSessionId,
|
|
safeSubagentId,
|
|
sessionParser,
|
|
subagentResolver,
|
|
fsProvider,
|
|
projectsDir
|
|
);
|
|
```
|
|
|
|
**IPC handlers.ts (update initialization calls):**
|
|
|
|
Update `initializeSubagentHandlers` calls in both `initializeIpcHandlers` and `reinitializeServiceHandlers` to pass `scanner`:
|
|
```typescript
|
|
initializeSubagentHandlers(builder, cache, parser, resolver, scanner);
|
|
```
|
|
|
|
**src/main/index.ts** — no changes needed since it calls `initializeIpcHandlers` and `reinitializeServiceHandlers` which will propagate the change.
|
|
</action>
|
|
<verify>
|
|
Run `pnpm typecheck` — no type errors across the entire project.
|
|
Run `pnpm test` — all tests pass (especially ChunkBuilder tests).
|
|
Grep for `fs/promises` or `os.homedir` in SubagentDetailBuilder.ts — should find zero matches:
|
|
```bash
|
|
grep -n "fs/promises\|os\.homedir" src/main/services/analysis/SubagentDetailBuilder.ts
|
|
```
|
|
Grep for bare `parseJsonlFile(filePath)` (without second arg) across all services — should find zero matches:
|
|
```bash
|
|
grep -rn 'parseJsonlFile(filePath)$' src/main/services/
|
|
```
|
|
</verify>
|
|
<done>
|
|
SubagentDetailBuilder uses fsProvider.exists() instead of fs.access(), constructs paths using projectsDir parameter instead of os.homedir(), and no longer imports fs/promises or os. ChunkBuilder passes fsProvider and projectsDir through to SubagentDetailBuilder. IPC subagent handler obtains provider from ProjectScanner and passes it through the call chain. Full session parsing and subagent drill-down chain uses FileSystemProvider consistently.
|
|
</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
After both tasks are complete, verify the full provider chain:
|
|
|
|
1. **Type safety:** `pnpm typecheck` passes with zero errors
|
|
2. **Test suite:** `pnpm test` passes — all existing tests remain green
|
|
3. **No local filesystem leaks in services:**
|
|
```bash
|
|
# Should find ZERO matches in these three files:
|
|
grep -n "fs/promises\|fs\.access\|os\.homedir\|parseJsonlFile(filePath)" \
|
|
src/main/services/parsing/SessionParser.ts \
|
|
src/main/services/discovery/SubagentResolver.ts \
|
|
src/main/services/analysis/SubagentDetailBuilder.ts
|
|
```
|
|
4. **Provider flows through entire chain:** Trace the call path:
|
|
- `index.ts` creates `ProjectScanner(projectsDir, undefined, provider)` in SSH mode
|
|
- `SessionParser.parseSessionFile()` calls `parseJsonlFile(filePath, this.projectScanner.getFileSystemProvider())`
|
|
- `SubagentResolver.parseSubagentFile()` calls `parseJsonlFile(filePath, this.projectScanner.getFileSystemProvider())`
|
|
- `SubagentDetailBuilder.buildSubagentDetail()` receives `fsProvider` and `projectsDir`, uses `fsProvider.exists()` for file check
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- All parseJsonlFile() calls in SessionParser and SubagentResolver pass the FileSystemProvider from ProjectScanner
|
|
- SubagentDetailBuilder does not import fs/promises, os, or use os.homedir()
|
|
- SubagentDetailBuilder uses fsProvider.exists() for file existence checks
|
|
- SubagentDetailBuilder uses projectsDir parameter for path construction (not hardcoded ~/.claude/projects)
|
|
- pnpm typecheck passes
|
|
- pnpm test passes
|
|
- The provider chain is complete: SSH provider set in index.ts flows all the way to parseJsonlFile() in every code path
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/01-provider-plumbing/01-01-SUMMARY.md`
|
|
</output>
|