aboutsummaryrefslogtreecommitdiffstats
path: root/src/main
diff options
context:
space:
mode:
authorbndw <ben@bdw.to>2026-02-28 19:14:01 -0800
committerbndw <ben@bdw.to>2026-02-28 19:14:01 -0800
commit9a636af9090b122db2e55737fca3e78550aab9df (patch)
treef76f3b118b525907e92fb29df096567a6eeabd06 /src/main
parente2a0bc68726c1b8dca179ee1f6826b88d8dd09f5 (diff)
fix: scope artifacts to sessions
Diffstat (limited to 'src/main')
-rw-r--r--src/main/claude/index.ts51
-rw-r--r--src/main/claude/phases.ts61
2 files changed, 58 insertions, 54 deletions
diff --git a/src/main/claude/index.ts b/src/main/claude/index.ts
index b8c9c07..4dd49f2 100644
--- a/src/main/claude/index.ts
+++ b/src/main/claude/index.ts
@@ -1,31 +1,24 @@
1import { query, type SDKMessage, type Query } from "@anthropic-ai/claude-agent-sdk"; 1import { query, type SDKMessage, type Query } from "@anthropic-ai/claude-agent-sdk";
2import type { Session } from "../db/sessions"; 2import type { Session } from "../db/sessions";
3import { getPhaseConfig, getNextPhase, getArtifactFilename, getSessionArtifactDir } from "./phases"; 3import { getPhaseConfig, getNextPhase, getArtifactFilename } from "./phases";
4import type { Phase, UserPermissionMode } from "./phases"; 4import type { Phase, UserPermissionMode } from "./phases";
5import { getProject } from "../db/projects"; 5import { getProject } from "../db/projects";
6import { updateSession } from "../db/sessions"; 6import { updateSession } from "../db/sessions";
7import fs from "node:fs"; 7import fs from "node:fs";
8import path from "node:path"; 8import path from "node:path";
9import os from "node:os";
10 9
11// Track active queries by session ID 10// Track active queries by session ID
12const activeQueries = new Map<string, Query>(); 11const activeQueries = new Map<string, Query>();
13 12
14// Global storage in home directory
15const GLOBAL_CLAUDE_FLOW_DIR = path.join(os.homedir(), ".claude-flow");
16
17function ensureDir(dirPath: string): void { 13function ensureDir(dirPath: string): void {
18 if (!fs.existsSync(dirPath)) { 14 if (!fs.existsSync(dirPath)) {
19 fs.mkdirSync(dirPath, { recursive: true }); 15 fs.mkdirSync(dirPath, { recursive: true });
20 } 16 }
21} 17}
22 18
23function getProjectDir(projectId: string): string { 19// Artifacts live inside the project directory so the SDK's Write tool can reach them
24 return path.join(GLOBAL_CLAUDE_FLOW_DIR, "projects", projectId); 20function getSessionDir(projectPath: string, sessionId: string): string {
25} 21 return path.join(projectPath, ".claude-flow", "sessions", sessionId);
26
27function getSessionDir(projectId: string, sessionId: string): string {
28 return path.join(getProjectDir(projectId), "sessions", sessionId);
29} 22}
30 23
31export interface SendMessageOptions { 24export interface SendMessageOptions {
@@ -42,12 +35,13 @@ export async function sendMessage({
42 const project = getProject(session.project_id); 35 const project = getProject(session.project_id);
43 if (!project) throw new Error("Project not found"); 36 if (!project) throw new Error("Project not found");
44 37
45 // Ensure session artifact directory exists in global storage 38 // Ensure session artifact directory exists inside the project
46 const sessionDir = getSessionDir(session.project_id, session.id); 39 const sessionDir = getSessionDir(project.path, session.id);
47 ensureDir(sessionDir); 40 ensureDir(sessionDir);
48 41
49 const phaseConfig = getPhaseConfig( 42 const phaseConfig = getPhaseConfig(
50 session.phase as Phase, 43 session.phase as Phase,
44 sessionDir,
51 session.permission_mode as UserPermissionMode 45 session.permission_mode as UserPermissionMode
52 ); 46 );
53 47
@@ -112,22 +106,26 @@ export function advancePhase(session: Session): Phase | null {
112} 106}
113 107
114/** 108/**
115 * Get the artifact path for a session and phase (in global storage) 109 * Get the artifact path for a session and phase (inside the project directory)
116 */ 110 */
117export function getArtifactPath(session: Session): string { 111export function getArtifactPath(session: Session): string {
112 const project = getProject(session.project_id);
113 if (!project) throw new Error("Project not found");
118 const filename = getArtifactFilename(session.phase as Phase); 114 const filename = getArtifactFilename(session.phase as Phase);
119 return path.join(getSessionDir(session.project_id, session.id), filename); 115 return path.join(getSessionDir(project.path, session.id), filename);
120} 116}
121 117
122/** 118/**
123 * Read an artifact file for a session (from global storage) 119 * Read an artifact file for a session
124 */ 120 */
125export function readSessionArtifact( 121export function readSessionArtifact(
126 projectId: string, 122 projectId: string,
127 sessionId: string, 123 sessionId: string,
128 filename: string 124 filename: string
129): string | null { 125): string | null {
130 const filePath = path.join(getSessionDir(projectId, sessionId), filename); 126 const project = getProject(projectId);
127 if (!project) return null;
128 const filePath = path.join(getSessionDir(project.path, sessionId), filename);
131 if (fs.existsSync(filePath)) { 129 if (fs.existsSync(filePath)) {
132 return fs.readFileSync(filePath, "utf-8"); 130 return fs.readFileSync(filePath, "utf-8");
133 } 131 }
@@ -135,7 +133,7 @@ export function readSessionArtifact(
135} 133}
136 134
137/** 135/**
138 * Write an artifact file for a session (to global storage) 136 * Write an artifact file for a session
139 */ 137 */
140export function writeSessionArtifact( 138export function writeSessionArtifact(
141 projectId: string, 139 projectId: string,
@@ -143,7 +141,9 @@ export function writeSessionArtifact(
143 filename: string, 141 filename: string,
144 content: string 142 content: string
145): void { 143): void {
146 const dir = getSessionDir(projectId, sessionId); 144 const project = getProject(projectId);
145 if (!project) throw new Error("Project not found");
146 const dir = getSessionDir(project.path, sessionId);
147 ensureDir(dir); 147 ensureDir(dir);
148 fs.writeFileSync(path.join(dir, filename), content, "utf-8"); 148 fs.writeFileSync(path.join(dir, filename), content, "utf-8");
149} 149}
@@ -168,22 +168,17 @@ export function writeClaudeMd(projectPath: string, content: string): void {
168} 168}
169 169
170/** 170/**
171 * Clear session artifacts from global storage 171 * Clear session artifacts
172 */ 172 */
173export function clearSessionArtifacts(projectId: string, sessionId: string): void { 173export function clearSessionArtifacts(projectId: string, sessionId: string): void {
174 const dir = getSessionDir(projectId, sessionId); 174 const project = getProject(projectId);
175 if (!project) return;
176 const dir = getSessionDir(project.path, sessionId);
175 if (fs.existsSync(dir)) { 177 if (fs.existsSync(dir)) {
176 fs.rmSync(dir, { recursive: true, force: true }); 178 fs.rmSync(dir, { recursive: true, force: true });
177 } 179 }
178} 180}
179 181
180/**
181 * Get the initial message for a phase
182 */
183export function getPhaseInitialMessage(phase: Phase): string {
184 return getPhaseConfig(phase).initialMessage;
185}
186
187// Re-export types 182// Re-export types
188export type { SDKMessage } from "@anthropic-ai/claude-agent-sdk"; 183export type { SDKMessage } from "@anthropic-ai/claude-agent-sdk";
189export type { Phase, UserPermissionMode } from "./phases"; 184export type { Phase, UserPermissionMode } from "./phases";
diff --git a/src/main/claude/phases.ts b/src/main/claude/phases.ts
index f1df719..89e7c22 100644
--- a/src/main/claude/phases.ts
+++ b/src/main/claude/phases.ts
@@ -3,6 +3,7 @@ import type { PermissionMode } from "@anthropic-ai/claude-agent-sdk";
3export type Phase = "research" | "plan" | "implement"; 3export type Phase = "research" | "plan" | "implement";
4export type UserPermissionMode = "acceptEdits" | "bypassPermissions"; 4export type UserPermissionMode = "acceptEdits" | "bypassPermissions";
5 5
6// External interface — consumers see a resolved string
6export interface PhaseConfig { 7export interface PhaseConfig {
7 systemPrompt: string; 8 systemPrompt: string;
8 tools: string[]; 9 tools: string[];
@@ -10,27 +11,24 @@ export interface PhaseConfig {
10 initialMessage: string; 11 initialMessage: string;
11} 12}
12 13
13// Get session-specific artifact path (relative to ~/.claude-flow/) 14// Internal template — systemPrompt is a function that receives the artifact dir
14export function getSessionArtifactDir(sessionId: string): string { 15interface PhaseConfigTemplate {
15 return `sessions/${sessionId}`; 16 systemPrompt: (artifactDir: string) => string;
16} 17 tools: string[];
17 18 permissionMode: PermissionMode;
18export function getArtifactPath(phase: Phase, sessionId: string): string { 19 initialMessage: string;
19 const dir = getSessionArtifactDir(sessionId);
20 const filename = phase === "research" ? "research.md" : "plan.md";
21 return `${dir}/${filename}`;
22} 20}
23 21
24export const phaseConfigs: Record<Phase, PhaseConfig> = { 22const phaseConfigTemplates: Record<Phase, PhaseConfigTemplate> = {
25 research: { 23 research: {
26 permissionMode: "acceptEdits", 24 permissionMode: "acceptEdits",
27 tools: ["Read", "Glob", "Grep", "Bash", "Write"], 25 tools: ["Read", "Glob", "Grep", "Bash", "Write"],
28 initialMessage: 26 initialMessage:
29 "What areas of the codebase should I research? What are you trying to build?", 27 "What areas of the codebase should I research? What are you trying to build?",
30 systemPrompt: `You are in RESEARCH mode. Your ONLY job is to understand the codebase. 28 systemPrompt: (artifactDir) => `You are in RESEARCH mode. Your ONLY job is to understand the codebase.
31 29
32CRITICAL RULES: 30CRITICAL RULES:
331. You MUST write ALL findings to the session research.md — this is your PRIMARY output 311. You MUST write ALL findings to ${artifactDir}/research.md — this is your PRIMARY output
342. DO NOT just respond in chat. The document viewer shows research.md, so write there. 322. DO NOT just respond in chat. The document viewer shows research.md, so write there.
353. DO NOT suggest moving to planning or implementation 333. DO NOT suggest moving to planning or implementation
364. DO NOT ask "are you ready to implement?" or similar 344. DO NOT ask "are you ready to implement?" or similar
@@ -46,7 +44,7 @@ WORKFLOW:
461. Read CLAUDE.md (create at project root if missing) 441. Read CLAUDE.md (create at project root if missing)
472. Ask what to research (if unclear) 452. Ask what to research (if unclear)
483. Read files thoroughly using Read, Glob, Grep 463. Read files thoroughly using Read, Glob, Grep
494. Write findings to session research.md 474. Write findings to ${artifactDir}/research.md
505. Update CLAUDE.md with any new general insights worth sharing 485. Update CLAUDE.md with any new general insights worth sharing
51 49
52FORMAT for research.md: 50FORMAT for research.md:
@@ -69,7 +67,7 @@ FORMAT for research.md:
69[Things that need clarification] 67[Things that need clarification]
70\`\`\` 68\`\`\`
71 69
72Remember: Your output goes in research.md, not chat. Chat is for clarifying questions only.`, 70Remember: Your output goes in ${artifactDir}/research.md, not chat. Chat is for clarifying questions only.`,
73 }, 71 },
74 72
75 plan: { 73 plan: {
@@ -77,10 +75,10 @@ Remember: Your output goes in research.md, not chat. Chat is for clarifying ques
77 tools: ["Read", "Glob", "Grep", "Write"], 75 tools: ["Read", "Glob", "Grep", "Write"],
78 initialMessage: 76 initialMessage:
79 "I'll create a detailed implementation plan based on my research. Writing to plan.md...", 77 "I'll create a detailed implementation plan based on my research. Writing to plan.md...",
80 systemPrompt: `You are in PLANNING mode. Your ONLY job is to create an implementation plan. 78 systemPrompt: (artifactDir) => `You are in PLANNING mode. Your ONLY job is to create an implementation plan.
81 79
82CRITICAL RULES: 80CRITICAL RULES:
831. You MUST write the plan to session plan.md — this is your PRIMARY output 811. You MUST write the plan to ${artifactDir}/plan.md — this is your PRIMARY output
842. DO NOT just respond in chat. The document viewer shows plan.md, so write there. 822. DO NOT just respond in chat. The document viewer shows plan.md, so write there.
853. DO NOT implement anything — no code changes to source files 833. DO NOT implement anything — no code changes to source files
864. DO NOT ask "should I start implementing?" or similar 844. DO NOT ask "should I start implementing?" or similar
@@ -89,12 +87,12 @@ CRITICAL RULES:
89 87
90CONTEXT: 88CONTEXT:
91- Read CLAUDE.md at project root for codebase overview 89- Read CLAUDE.md at project root for codebase overview
92- Read the session research.md to understand the specific task 90- Read ${artifactDir}/research.md to understand the specific task
93 91
94WORKFLOW: 92WORKFLOW:
951. Read CLAUDE.md for codebase overview 931. Read CLAUDE.md for codebase overview
962. Read the session research.md to understand the specific task 942. Read ${artifactDir}/research.md to understand the specific task
973. Write a detailed plan to session plan.md 953. Write a detailed plan to ${artifactDir}/plan.md
984. Include specific code snippets showing proposed changes 964. Include specific code snippets showing proposed changes
995. Make the plan detailed enough that implementation is mechanical 975. Make the plan detailed enough that implementation is mechanical
100 98
@@ -132,7 +130,7 @@ FORMAT for plan.md:
132 130
133When the user adds annotations to plan.md and clicks Review, address each annotation and update the document. 131When the user adds annotations to plan.md and clicks Review, address each annotation and update the document.
134 132
135Remember: Your output goes in plan.md, not chat. Chat is for clarifying questions only.`, 133Remember: Your output goes in ${artifactDir}/plan.md, not chat. Chat is for clarifying questions only.`,
136 }, 134 },
137 135
138 implement: { 136 implement: {
@@ -140,19 +138,19 @@ Remember: Your output goes in plan.md, not chat. Chat is for clarifying question
140 tools: ["Read", "Write", "Edit", "Bash", "Glob", "Grep"], 138 tools: ["Read", "Write", "Edit", "Bash", "Glob", "Grep"],
141 initialMessage: 139 initialMessage:
142 "Starting implementation. I'll follow the plan exactly and mark tasks complete as I go.", 140 "Starting implementation. I'll follow the plan exactly and mark tasks complete as I go.",
143 systemPrompt: `You are in IMPLEMENTATION mode. Execute the approved plan. 141 systemPrompt: (artifactDir) => `You are in IMPLEMENTATION mode. Execute the approved plan.
144 142
145CRITICAL RULES: 143CRITICAL RULES:
1461. Read session plan.md and follow it exactly 1441. Read ${artifactDir}/plan.md and follow it exactly
1472. Mark tasks complete in plan.md as you finish them: - [ ] → - [x] 1452. Mark tasks complete in ${artifactDir}/plan.md as you finish them: - [ ] → - [x]
1483. DO NOT deviate from the plan without asking 1463. DO NOT deviate from the plan without asking
1494. Run tests/typecheck if available 1474. Run tests/typecheck if available
1505. Stop and ask if you encounter issues not covered by the plan 1485. Stop and ask if you encounter issues not covered by the plan
151 149
152WORKFLOW: 150WORKFLOW:
1531. Read session plan.md 1511. Read ${artifactDir}/plan.md
1542. Execute each task in order 1522. Execute each task in order
1553. Update plan.md to mark tasks complete 1533. Update ${artifactDir}/plan.md to mark tasks complete
1564. Continue until all tasks are done 1544. Continue until all tasks are done
157 155
158When complete, summarize what was done and any follow-up tasks.`, 156When complete, summarize what was done and any follow-up tasks.`,
@@ -161,15 +159,26 @@ When complete, summarize what was done and any follow-up tasks.`,
161 159
162export function getPhaseConfig( 160export function getPhaseConfig(
163 phase: Phase, 161 phase: Phase,
162 artifactDir: string,
164 userPermissionMode?: UserPermissionMode 163 userPermissionMode?: UserPermissionMode
165): PhaseConfig { 164): PhaseConfig {
166 const config = { ...phaseConfigs[phase] }; 165 const template = phaseConfigTemplates[phase];
166 const config: PhaseConfig = {
167 systemPrompt: template.systemPrompt(artifactDir),
168 tools: template.tools,
169 permissionMode: template.permissionMode,
170 initialMessage: template.initialMessage,
171 };
167 if (phase === "implement" && userPermissionMode) { 172 if (phase === "implement" && userPermissionMode) {
168 config.permissionMode = userPermissionMode; 173 config.permissionMode = userPermissionMode;
169 } 174 }
170 return config; 175 return config;
171} 176}
172 177
178export function getPhaseInitialMessage(phase: Phase): string {
179 return phaseConfigTemplates[phase].initialMessage;
180}
181
173export function getNextPhase(phase: Phase): Phase | null { 182export function getNextPhase(phase: Phase): Phase | null {
174 const transitions: Record<Phase, Phase | null> = { 183 const transitions: Record<Phase, Phase | null> = {
175 research: "plan", 184 research: "plan",