agent-ecosystem/.planning/phases/01-provider-plumbing/01-01-PLAN.md
matt ae4833e310 feat(planning): add project planning documents and configuration for SSH multi-context workspaces
- 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.
2026-02-12 09:37:58 +09:00

14 KiB

phase plan type wave depends_on files_modified autonomous must_haves
01-provider-plumbing 01 execute 1
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
true
truths artifacts key_links
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
path provides contains
src/main/services/discovery/ProjectScanner.ts getFileSystemProvider() getter getFileSystemProvider
path provides contains
src/main/services/parsing/SessionParser.ts Provider-aware session parsing getFileSystemProvider
path provides contains
src/main/services/discovery/SubagentResolver.ts Provider-aware subagent resolution getFileSystemProvider
path provides contains
src/main/services/analysis/SubagentDetailBuilder.ts Provider-aware subagent detail building fsProvider
from to via pattern
src/main/services/parsing/SessionParser.ts src/main/utils/jsonl.ts parseJsonlFile(filePath, provider) parseJsonlFile(.*,.*getFileSystemProvider
from to via pattern
src/main/services/discovery/SubagentResolver.ts src/main/utils/jsonl.ts parseJsonlFile(filePath, provider) parseJsonlFile(.*,.*getFileSystemProvider
from to via pattern
src/main/services/analysis/SubagentDetailBuilder.ts src/main/services/infrastructure/FileSystemProvider.ts fsProvider.exists() replaces fs.access() fsProvider.exists
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.

<execution_context> @/Users/bskim/.claude/get-shit-done/workflows/execute-plan.md @/Users/bskim/.claude/get-shit-done/templates/summary.md </execution_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

Task 1: Add provider getter to ProjectScanner and thread through SessionParser + SubagentResolver 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 **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:

    const messages = await parseJsonlFile(filePath);
    

    to:

    const messages = await parseJsonlFile(filePath, this.projectScanner.getFileSystemProvider());
    
  2. In parseSubagentFile() (line 342), change:

    const messages = await parseJsonlFile(filePath);
    

    to:

    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:

const messages = await parseJsonlFile(filePath);

to:

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:

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). 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:

grep -n 'parseJsonlFile(filePath)' src/main/services/parsing/SessionParser.ts src/main/services/discovery/SubagentResolver.ts
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. Task 2: Fix SubagentDetailBuilder to use FileSystemProvider instead of hardcoded fs/os imports src/main/services/analysis/SubagentDetailBuilder.ts src/main/services/analysis/ChunkBuilder.ts **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:

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:
    // 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:
    // 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:

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:
    const fsProvider = projectScanner.getFileSystemProvider();
    const projectsDir = projectScanner.getProjectsDir();
    
  4. Pass them to the call:
    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:

initializeSubagentHandlers(builder, cache, parser, resolver, scanner);

src/main/index.ts — no changes needed since it calls initializeIpcHandlers and reinitializeServiceHandlers which will propagate the change. 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:

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:

grep -rn 'parseJsonlFile(filePath)$' src/main/services/
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. 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:
    # 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

<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>
After completion, create `.planning/phases/01-provider-plumbing/01-01-SUMMARY.md`