letmecook 0.0.15 → 0.0.17

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "letmecook",
3
- "version": "0.0.15",
3
+ "version": "0.0.17",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/rustydotwtf/letmecook.git"
package/src/agents-md.ts CHANGED
@@ -11,22 +11,19 @@ export function generateAgentsMd(session: Session): string {
11
11
  minute: "2-digit",
12
12
  });
13
13
 
14
- const hasReadOnlyRepos = session.repos.some((repo) => repo.readOnly);
15
- const hasLatestRepos = session.repos.some((repo) => repo.latest);
14
+ const hasReferenceRepos = session.repos.some((repo) => repo.reference);
16
15
  const hasSkills = session.skills && session.skills.length > 0;
17
16
 
18
17
  const repoRows = session.repos
19
18
  .map((repo) => {
20
19
  const branch = repo.branch || "default";
21
20
  const url = `https://github.com/${repo.owner}/${repo.name}`;
22
- const readOnlyStatus = repo.readOnly ? "**YES**" : "no";
23
- const latestStatus = repo.latest ? "**YES**" : "no";
24
- return `| \`${repo.dir}/\` | [${repo.owner}/${repo.name}](${url}) | ${branch} | ${readOnlyStatus} | ${latestStatus} |`;
21
+ const referenceStatus = repo.reference ? "**YES**" : "no";
22
+ return `| \`${repo.dir}/\` | [${repo.owner}/${repo.name}](${url}) | ${branch} | ${referenceStatus} |`;
25
23
  })
26
24
  .join("\n");
27
25
 
28
- const readOnlyRepos = session.repos.filter((repo) => repo.readOnly);
29
- const latestRepos = session.repos.filter((repo) => repo.latest);
26
+ const referenceRepos = session.repos.filter((repo) => repo.reference);
30
27
 
31
28
  const skillsSection = hasSkills
32
29
  ? `
@@ -40,31 +37,24 @@ These skills are available for use in this session and are automatically updated
40
37
  `
41
38
  : "";
42
39
 
43
- const readOnlyWarning = hasReadOnlyRepos
40
+ const referenceWarning = hasReferenceRepos
44
41
  ? `
45
- ## ⚠️ Read-Only Repositories
42
+ ## ⚠️ Reference Repositories
46
43
 
47
- **WARNING: The following repositories are marked as READ-ONLY:**
44
+ **WARNING: The following repositories are REFERENCES (symlinked from shared cache):**
48
45
 
49
- ${readOnlyRepos.map((repo) => `- \`${repo.dir}/\` (${repo.owner}/${repo.name})`).join("\n")}
46
+ ${referenceRepos.map((repo) => `- \`${repo.dir}/\` (${repo.owner}/${repo.name})`).join("\n")}
50
47
 
51
48
  **AI agents must NOT:**
52
49
  - Create, modify, or delete any files in these directories
53
50
  - Make commits affecting these repositories
54
51
  - Use bash commands to circumvent file permissions
55
52
 
56
- **Why are these read-only?**
57
- These repositories are included for reference only. The user wants to read and understand the code without risk of accidental modifications.
58
- `
59
- : "";
60
-
61
- const latestNotice = hasLatestRepos
62
- ? `
63
- ## 🔄 Latest Repositories
64
-
65
- These repositories are pinned to **Latest** and will be refreshed before resuming the session (only if clean).
66
-
67
- ${latestRepos.map((repo) => `- \`${repo.dir}/\` (${repo.owner}/${repo.name})`).join("\n")}
53
+ **Why are these references?**
54
+ - These directories are **symlinks** to a shared cache (~/.letmecook/references/)
55
+ - Any modifications would affect ALL sessions using this repository
56
+ - They are automatically refreshed to latest before each session resume
57
+ - They are included for reference only - the user wants to read and understand the code without risk of accidental modifications
68
58
  `
69
59
  : "";
70
60
 
@@ -76,11 +66,10 @@ ${session.goal ? `> ${session.goal}\n` : ""}
76
66
 
77
67
  ## Repositories
78
68
 
79
- | Directory | Repository | Branch | Read-Only | Latest |
80
- |-----------|------------|--------|-----------|--------|
69
+ | Directory | Repository | Branch | Reference |
70
+ |-----------|------------|--------|-----------|
81
71
  ${repoRows}
82
- ${readOnlyWarning}
83
- ${latestNotice}
72
+ ${referenceWarning}
84
73
  ${skillsSection}
85
74
  ## Important Notes
86
75
 
@@ -6,6 +6,8 @@ import { writeAgentsMd } from "../agents-md";
6
6
  import { recordRepoHistory } from "../repo-history";
7
7
  import { showAddReposPrompt } from "../ui/add-repos";
8
8
  import { runCommands, hideCommandRunner, type CommandTask } from "../ui/common/command-runner";
9
+ import { ensureReferenceRepo, linkReferenceRepo, ensureReferencesDir } from "../reference-repo";
10
+ import { rm } from "node:fs/promises";
9
11
 
10
12
  export interface AddReposParams {
11
13
  renderer: CliRenderer;
@@ -18,7 +20,10 @@ export interface AddReposResult {
18
20
  }
19
21
 
20
22
  function reposToCommandTasks(repos: RepoSpec[], sessionPath: string): CommandTask[] {
21
- return repos.map((repo) => {
23
+ // Only create command tasks for non-reference repos
24
+ const regularRepos = repos.filter((repo) => !repo.reference);
25
+
26
+ return regularRepos.map((repo) => {
22
27
  const url = `https://github.com/${repo.owner}/${repo.name}.git`;
23
28
  const targetDir = join(sessionPath, repo.dir);
24
29
  const args = repo.branch
@@ -44,6 +49,34 @@ function reposToCommandTasks(repos: RepoSpec[], sessionPath: string): CommandTas
44
49
  });
45
50
  }
46
51
 
52
+ async function setupReferenceRepos(
53
+ repos: RepoSpec[],
54
+ sessionPath: string,
55
+ ): Promise<{ successful: RepoSpec[]; failed: Array<{ repo: RepoSpec; error: string }> }> {
56
+ const referenceRepos = repos.filter((repo) => repo.reference);
57
+ const successful: RepoSpec[] = [];
58
+ const failed: Array<{ repo: RepoSpec; error: string }> = [];
59
+
60
+ if (referenceRepos.length === 0) {
61
+ return { successful, failed };
62
+ }
63
+
64
+ await ensureReferencesDir();
65
+
66
+ for (const repo of referenceRepos) {
67
+ try {
68
+ await ensureReferenceRepo(repo);
69
+ await linkReferenceRepo(repo, sessionPath);
70
+ successful.push(repo);
71
+ } catch (error) {
72
+ const errorMsg = error instanceof Error ? error.message : String(error);
73
+ failed.push({ repo, error: errorMsg });
74
+ }
75
+ }
76
+
77
+ return { successful, failed };
78
+ }
79
+
47
80
  export async function addReposFlow(params: AddReposParams): Promise<AddReposResult> {
48
81
  const { renderer, session } = params;
49
82
 
@@ -54,38 +87,115 @@ export async function addReposFlow(params: AddReposParams): Promise<AddReposResu
54
87
  const newRepos = addResult.repos.filter((r) => !existingSpecs.has(r.spec));
55
88
 
56
89
  if (newRepos.length > 0) {
90
+ // Setup reference repos first (symlinks from cache)
91
+ const { successful: successfulRefs, failed: failedRefs } = await setupReferenceRepos(
92
+ newRepos,
93
+ session.path,
94
+ );
95
+
96
+ // Report reference repo failures
97
+ if (failedRefs.length > 0) {
98
+ console.error(`\n⚠️ ${failedRefs.length} reference repo(s) failed:`);
99
+ failedRefs.forEach(({ repo, error }) => {
100
+ console.error(` ✗ ${repo.owner}/${repo.name}: ${error}`);
101
+ });
102
+ }
103
+
104
+ // Clone regular repos
57
105
  const tasks = reposToCommandTasks(newRepos, session.path);
106
+ let results: Awaited<ReturnType<typeof runCommands>> = [];
58
107
 
59
- const results = await runCommands(renderer, {
60
- title: "Adding repositories",
61
- tasks,
62
- showOutput: true,
63
- outputLines: 5,
64
- });
108
+ if (tasks.length > 0) {
109
+ results = await runCommands(renderer, {
110
+ title: "Adding repositories",
111
+ tasks,
112
+ showOutput: true,
113
+ outputLines: 5,
114
+ allowAbort: true,
115
+ allowSkip: tasks.length > 1,
116
+ allowBackground: true,
117
+ sessionName: session.name,
118
+ });
65
119
 
66
- // Check for errors
67
- const errors = results.filter((r) => !r.success);
68
- if (errors.length > 0) {
69
- console.error(`\n⚠️ ${errors.length} repository(ies) failed to clone:`);
70
- errors.forEach((err) => {
71
- console.error(` ✗ ${err.task.label}`);
72
- if (err.error) {
73
- console.error(` ${err.error}`);
120
+ // Handle aborted operation - return without changes
121
+ const wasAborted = results.some((r) => r.outcome === "aborted");
122
+ if (wasAborted) {
123
+ hideCommandRunner(renderer);
124
+ // Clean up any partially cloned repos (non-reference only)
125
+ const regularRepos = newRepos.filter((r) => !r.reference);
126
+ for (const repo of regularRepos) {
127
+ const repoPath = join(session.path, repo.dir);
128
+ try {
129
+ await rm(repoPath, { recursive: true, force: true });
130
+ } catch {
131
+ // Ignore cleanup errors
132
+ }
74
133
  }
75
- });
134
+ // Also clean up reference symlinks
135
+ for (const repo of successfulRefs) {
136
+ const repoPath = join(session.path, repo.dir);
137
+ try {
138
+ await rm(repoPath, { recursive: true, force: true });
139
+ } catch {
140
+ // Ignore cleanup errors
141
+ }
142
+ }
143
+ return { session, cancelled: true };
144
+ }
145
+
146
+ // Clean up skipped repo directories
147
+ const skippedResults = results.filter((r) => r.outcome === "skipped");
148
+ for (const skipped of skippedResults) {
149
+ const repoSpec = skipped.task.label.replace("Cloning ", "");
150
+ const repo = newRepos.find((r) => `${r.owner}/${r.name}` === repoSpec);
151
+ if (repo) {
152
+ const repoPath = join(session.path, repo.dir);
153
+ try {
154
+ await rm(repoPath, { recursive: true, force: true });
155
+ } catch {
156
+ // Ignore cleanup errors
157
+ }
158
+ }
159
+ }
160
+
161
+ // Check for errors (not skipped/aborted)
162
+ const errors = results.filter((r) => r.outcome === "error");
163
+ if (errors.length > 0) {
164
+ console.error(`\n⚠️ ${errors.length} repository(ies) failed to clone:`);
165
+ errors.forEach((err) => {
166
+ console.error(` ✗ ${err.task.label}`);
167
+ if (err.error) {
168
+ console.error(` ${err.error}`);
169
+ }
170
+ });
171
+ }
76
172
  }
77
173
 
78
174
  await new Promise((resolve) => setTimeout(resolve, 700));
79
175
  hideCommandRunner(renderer);
80
176
 
81
- const allRepos = [...session.repos, ...newRepos];
82
- const updatedSession = await updateSessionRepos(session.name, allRepos);
83
- const nextSession = updatedSession ?? session;
177
+ // Filter to only successfully cloned repos
178
+ const successfulCloned = newRepos.filter((repo) => {
179
+ if (repo.reference) return false; // Reference repos handled separately
180
+ const result = results.find((r) => r.task.label === `Cloning ${repo.owner}/${repo.name}`);
181
+ return result?.outcome === "completed";
182
+ });
183
+
184
+ // Combine successful reference and cloned repos
185
+ const successfulRepos = [...successfulRefs, ...successfulCloned];
84
186
 
85
- await recordRepoHistory(newRepos);
86
- await writeAgentsMd(nextSession);
187
+ if (successfulRepos.length > 0) {
188
+ const allRepos = [...session.repos, ...successfulRepos];
189
+ const updatedSession = await updateSessionRepos(session.name, allRepos);
190
+ const nextSession = updatedSession ?? session;
191
+
192
+ await recordRepoHistory(successfulRepos);
193
+ await writeAgentsMd(nextSession);
194
+
195
+ return { session: nextSession, cancelled: false };
196
+ }
87
197
 
88
- return { session: nextSession, cancelled: false };
198
+ return { session, cancelled: false };
89
199
  }
90
200
  }
91
201
 
@@ -6,6 +6,7 @@ import { recordRepoHistory } from "../repo-history";
6
6
  import { removeSkillFromSession } from "../skills";
7
7
  import { createRenderer, destroyRenderer } from "../ui/renderer";
8
8
  import { runCommands, hideCommandRunner, type CommandTask } from "../ui/common/command-runner";
9
+ import { rm } from "node:fs/promises";
9
10
 
10
11
  export interface EditSessionParams {
11
12
  session: Session;
@@ -74,10 +75,51 @@ export async function editSession(params: EditSessionParams): Promise<Session |
74
75
  tasks,
75
76
  showOutput: true,
76
77
  outputLines: 5,
78
+ allowAbort: true,
79
+ allowSkip: tasks.length > 1,
80
+ allowBackground: true,
77
81
  });
78
82
 
79
- // Check for errors
80
- const errors = results.filter((r) => !r.success);
83
+ // Handle aborted operation
84
+ const wasAborted = results.some((r) => r.outcome === "aborted");
85
+ if (wasAborted) {
86
+ hideCommandRunner(renderer);
87
+ // Clean up any partially cloned repos
88
+ for (const repo of newRepos) {
89
+ const repoPath = join(currentSession.path, repo.dir);
90
+ try {
91
+ await rm(repoPath, { recursive: true, force: true });
92
+ } catch {
93
+ // Ignore cleanup errors
94
+ }
95
+ }
96
+ destroyRenderer();
97
+ return session; // Return unchanged session
98
+ }
99
+
100
+ // Clean up skipped repo directories
101
+ const skippedResults = results.filter((r) => r.outcome === "skipped");
102
+ for (const skipped of skippedResults) {
103
+ const repoSpec = skipped.task.label.replace("Cloning ", "");
104
+ const repo = newRepos.find((r) => `${r.owner}/${r.name}` === repoSpec);
105
+ if (repo) {
106
+ const repoPath = join(currentSession.path, repo.dir);
107
+ try {
108
+ await rm(repoPath, { recursive: true, force: true });
109
+ } catch {
110
+ // Ignore cleanup errors
111
+ }
112
+ }
113
+ }
114
+
115
+ // Filter to only successfully cloned repos
116
+ const successfulRepos = newRepos.filter((repo) => {
117
+ const result = results.find((r) => r.task.label === `Cloning ${repo.owner}/${repo.name}`);
118
+ return result?.outcome === "completed";
119
+ });
120
+
121
+ // Check for errors (not skipped/aborted)
122
+ const errors = results.filter((r) => r.outcome === "error");
81
123
  if (errors.length > 0) {
82
124
  console.error(`\n⚠️ ${errors.length} repository(ies) failed to clone:`);
83
125
  errors.forEach((err) => {
@@ -91,13 +133,23 @@ export async function editSession(params: EditSessionParams): Promise<Session |
91
133
  await new Promise((resolve) => setTimeout(resolve, 700));
92
134
  hideCommandRunner(renderer);
93
135
 
94
- await recordRepoHistory(newRepos);
95
- await writeAgentsMd(currentSession);
96
- }
136
+ if (successfulRepos.length > 0) {
137
+ await recordRepoHistory(successfulRepos);
138
+ await writeAgentsMd(currentSession);
139
+ }
97
140
 
98
- const updatedSession = await updateSessionRepos(session.name, updates.repos);
99
- if (updatedSession) {
100
- currentSession = updatedSession;
141
+ // Update session with existing + successful repos only
142
+ const allRepos = [...session.repos, ...successfulRepos];
143
+ const updatedSession = await updateSessionRepos(session.name, allRepos);
144
+ if (updatedSession) {
145
+ currentSession = updatedSession;
146
+ }
147
+ } else {
148
+ // No new repos, just update with the provided list
149
+ const updatedSession = await updateSessionRepos(session.name, updates.repos);
150
+ if (updatedSession) {
151
+ currentSession = updatedSession;
152
+ }
101
153
  }
102
154
  }
103
155
 
@@ -113,10 +165,27 @@ export async function editSession(params: EditSessionParams): Promise<Session |
113
165
  tasks,
114
166
  showOutput: true,
115
167
  outputLines: 5,
168
+ allowAbort: true,
169
+ allowSkip: tasks.length > 1,
170
+ allowBackground: true,
171
+ });
172
+
173
+ // Handle aborted operation
174
+ const wasAborted = results.some((r) => r.outcome === "aborted");
175
+ if (wasAborted) {
176
+ hideCommandRunner(renderer);
177
+ destroyRenderer();
178
+ return currentSession; // Return current session state
179
+ }
180
+
181
+ // Filter to only successfully installed skills
182
+ const successfulSkills = newSkills.filter((skill) => {
183
+ const result = results.find((r) => r.task.label === `Installing ${skill}`);
184
+ return result?.outcome === "completed";
116
185
  });
117
186
 
118
- // Check for errors
119
- const errors = results.filter((r) => !r.success);
187
+ // Check for errors (not skipped/aborted)
188
+ const errors = results.filter((r) => r.outcome === "error");
120
189
  if (errors.length > 0) {
121
190
  console.error(`\n⚠️ ${errors.length} skill(s) failed to install:`);
122
191
  errors.forEach((err) => {
@@ -130,12 +199,14 @@ export async function editSession(params: EditSessionParams): Promise<Session |
130
199
  await new Promise((resolve) => setTimeout(resolve, 700));
131
200
  hideCommandRunner(renderer);
132
201
 
133
- const allSkills = [...(session.skills || []), ...newSkills];
134
- const updatedSession = await updateSessionSkills(currentSession.name, allSkills);
202
+ if (successfulSkills.length > 0) {
203
+ const allSkills = [...(currentSession.skills || []), ...successfulSkills];
204
+ const updatedSession = await updateSessionSkills(currentSession.name, allSkills);
135
205
 
136
- if (updatedSession) {
137
- currentSession = updatedSession;
138
- await writeAgentsMd(currentSession);
206
+ if (updatedSession) {
207
+ currentSession = updatedSession;
208
+ await writeAgentsMd(currentSession);
209
+ }
139
210
  }
140
211
  }
141
212
  }
@@ -8,6 +8,8 @@ import { recordRepoHistory } from "../repo-history";
8
8
  import { showAgentProposal } from "../ui/agent-proposal";
9
9
  import { showConflictPrompt } from "../ui/conflict";
10
10
  import { runCommands, hideCommandRunner, type CommandTask } from "../ui/common/command-runner";
11
+ import { ensureReferenceRepo, linkReferenceRepo, ensureReferencesDir } from "../reference-repo";
12
+ import { rm } from "node:fs/promises";
11
13
 
12
14
  export interface NewSessionParams {
13
15
  repos: RepoSpec[];
@@ -23,7 +25,11 @@ export interface NewSessionResult {
23
25
  }
24
26
 
25
27
  function reposToCommandTasks(repos: RepoSpec[], sessionPath: string): CommandTask[] {
26
- return repos.map((repo) => {
28
+ // Only create command tasks for non-reference repos
29
+ // Reference repos are handled separately with ensureReferenceRepo + symlink
30
+ const regularRepos = repos.filter((repo) => !repo.reference);
31
+
32
+ return regularRepos.map((repo) => {
27
33
  const url = `https://github.com/${repo.owner}/${repo.name}.git`;
28
34
  const targetDir = join(sessionPath, repo.dir);
29
35
  const args = repo.branch
@@ -49,6 +55,41 @@ function reposToCommandTasks(repos: RepoSpec[], sessionPath: string): CommandTas
49
55
  });
50
56
  }
51
57
 
58
+ async function setupReferenceRepos(
59
+ repos: RepoSpec[],
60
+ sessionPath: string,
61
+ onProgress?: (repo: RepoSpec, status: "caching" | "linking" | "done" | "error") => void,
62
+ ): Promise<{ successful: RepoSpec[]; failed: Array<{ repo: RepoSpec; error: string }> }> {
63
+ const referenceRepos = repos.filter((repo) => repo.reference);
64
+ const successful: RepoSpec[] = [];
65
+ const failed: Array<{ repo: RepoSpec; error: string }> = [];
66
+
67
+ if (referenceRepos.length === 0) {
68
+ return { successful, failed };
69
+ }
70
+
71
+ await ensureReferencesDir();
72
+
73
+ for (const repo of referenceRepos) {
74
+ try {
75
+ onProgress?.(repo, "caching");
76
+ await ensureReferenceRepo(repo);
77
+
78
+ onProgress?.(repo, "linking");
79
+ await linkReferenceRepo(repo, sessionPath);
80
+
81
+ onProgress?.(repo, "done");
82
+ successful.push(repo);
83
+ } catch (error) {
84
+ const errorMsg = error instanceof Error ? error.message : String(error);
85
+ onProgress?.(repo, "error");
86
+ failed.push({ repo, error: errorMsg });
87
+ }
88
+ }
89
+
90
+ return { successful, failed };
91
+ }
92
+
52
93
  function skillsToCommandTasks(skills: string[], sessionPath: string): CommandTask[] {
53
94
  return skills.map((skill) => ({
54
95
  label: `Installing ${skill}`,
@@ -105,32 +146,94 @@ export async function createNewSession(
105
146
  skills?.length ? skills : undefined,
106
147
  );
107
148
 
108
- // Build all tasks (repos + skills)
149
+ // Setup reference repos first (symlinks from cache)
150
+ const { successful: successfulRefs, failed: failedRefs } = await setupReferenceRepos(
151
+ repos,
152
+ session.path,
153
+ );
154
+
155
+ // Report reference repo failures
156
+ if (failedRefs.length > 0) {
157
+ console.error(`\n⚠️ ${failedRefs.length} reference repo(s) failed:`);
158
+ failedRefs.forEach(({ repo, error }) => {
159
+ console.error(` ✗ ${repo.owner}/${repo.name}: ${error}`);
160
+ });
161
+ }
162
+
163
+ // Build tasks for regular repos + skills
109
164
  const tasks: CommandTask[] = [
110
165
  ...reposToCommandTasks(repos, session.path),
111
166
  ...(skills && skills.length > 0 ? skillsToCommandTasks(skills, session.path) : []),
112
167
  ];
113
168
 
114
- const results = await runCommands(renderer, {
115
- title: "Setting up session",
116
- tasks,
117
- showOutput: true,
118
- outputLines: 5,
119
- });
169
+ let results: Awaited<ReturnType<typeof runCommands>> = [];
120
170
 
121
- // Check for errors
122
- const errors = results.filter((r) => !r.success);
123
- if (errors.length > 0) {
124
- console.error(`\n⚠️ ${errors.length} task(s) failed:`);
125
- errors.forEach((err) => {
126
- console.error(` ✗ ${err.task.label}`);
127
- if (err.error) {
128
- console.error(` ${err.error}`);
129
- }
171
+ if (tasks.length > 0) {
172
+ results = await runCommands(renderer, {
173
+ title: "Setting up session",
174
+ tasks,
175
+ showOutput: true,
176
+ outputLines: 5,
177
+ allowAbort: true,
178
+ allowSkip: tasks.length > 1,
179
+ allowBackground: true,
180
+ sessionName,
130
181
  });
182
+
183
+ // Handle aborted operation - clean up and return null
184
+ const wasAborted = results.some((r) => r.outcome === "aborted");
185
+ if (wasAborted) {
186
+ hideCommandRunner(renderer);
187
+ // Clean up the partial session
188
+ await deleteSession(session.name);
189
+ return null;
190
+ }
191
+
192
+ // Clean up skipped repo directories
193
+ const skippedResults = results.filter((r) => r.outcome === "skipped");
194
+ for (const skipped of skippedResults) {
195
+ const repoName = skipped.task.label.replace("Cloning ", "").split("/")[1];
196
+ if (repoName) {
197
+ const repoPath = join(session.path, repoName);
198
+ try {
199
+ await rm(repoPath, { recursive: true, force: true });
200
+ } catch {
201
+ // Ignore cleanup errors
202
+ }
203
+ }
204
+ }
205
+
206
+ // Check for errors (not skipped/aborted)
207
+ const errors = results.filter((r) => r.outcome === "error");
208
+ if (errors.length > 0) {
209
+ console.error(`\n⚠️ ${errors.length} task(s) failed:`);
210
+ errors.forEach((err) => {
211
+ console.error(` ✗ ${err.task.label}`);
212
+ if (err.error) {
213
+ console.error(` ${err.error}`);
214
+ }
215
+ });
216
+ }
131
217
  }
132
218
 
133
- await recordRepoHistory(repos);
219
+ // Filter out skipped repos from the session
220
+ const successfulRepoResults = results.filter(
221
+ (r) => r.outcome === "completed" && r.task.label.startsWith("Cloning "),
222
+ );
223
+ const successfulClonedSpecs = successfulRepoResults
224
+ .map((r) => {
225
+ const repoSpec = r.task.label.replace("Cloning ", "");
226
+ return repos.find((repo) => `${repo.owner}/${repo.name}` === repoSpec);
227
+ })
228
+ .filter((r): r is RepoSpec => r !== undefined);
229
+
230
+ // Combine successful reference repos and cloned repos
231
+ const successfulRepoSpecs = [...successfulRefs, ...successfulClonedSpecs];
232
+
233
+ // Record only successful repos
234
+ if (successfulRepoSpecs.length > 0) {
235
+ await recordRepoHistory(successfulRepoSpecs);
236
+ }
134
237
 
135
238
  await new Promise((resolve) => setTimeout(resolve, 700));
136
239
  hideCommandRunner(renderer);
@@ -148,30 +251,52 @@ export async function createNewSession(
148
251
  skills?.length ? skills : undefined,
149
252
  );
150
253
 
151
- console.log(`\nCloning ${repos.length} repository(ies)...`);
254
+ // Setup reference repos first (symlinks from cache)
255
+ const referenceRepos = repos.filter((r) => r.reference);
256
+ if (referenceRepos.length > 0) {
257
+ console.log(`\nSetting up ${referenceRepos.length} reference repo(s)...`);
258
+ const { successful: successfulRefs, failed: failedRefs } = await setupReferenceRepos(
259
+ repos,
260
+ session.path,
261
+ );
262
+ successfulRefs.forEach((repo) => {
263
+ console.log(` ✓ ${repo.owner}/${repo.name} (linked)`);
264
+ });
265
+ failedRefs.forEach(({ repo, error }) => {
266
+ console.log(` ✗ ${repo.owner}/${repo.name}: ${error}`);
267
+ });
268
+ }
269
+
270
+ // Clone regular repos
271
+ const regularRepos = repos.filter((r) => !r.reference);
272
+ if (regularRepos.length > 0) {
273
+ console.log(`\nCloning ${regularRepos.length} repository(ies)...`);
274
+ }
152
275
 
153
276
  const tasks: CommandTask[] = [
154
277
  ...reposToCommandTasks(repos, session.path),
155
278
  ...(skills && skills.length > 0 ? skillsToCommandTasks(skills, session.path) : []),
156
279
  ];
157
280
 
158
- const results = await runCommands(renderer, {
159
- title: "Setting up session",
160
- tasks,
161
- showOutput: false, // CLI mode doesn't need visual output
162
- });
281
+ if (tasks.length > 0) {
282
+ const results = await runCommands(renderer, {
283
+ title: "Setting up session",
284
+ tasks,
285
+ showOutput: false, // CLI mode doesn't need visual output
286
+ });
163
287
 
164
- // Print results
165
- results.forEach((result) => {
166
- if (result.success) {
167
- console.log(` ✓ ${result.task.label}`);
168
- } else {
169
- console.log(` ✗ ${result.task.label}`);
170
- if (result.error) {
171
- console.log(` ${result.error}`);
288
+ // Print results
289
+ results.forEach((result) => {
290
+ if (result.success) {
291
+ console.log(` ✓ ${result.task.label}`);
292
+ } else {
293
+ console.log(` ✗ ${result.task.label}`);
294
+ if (result.error) {
295
+ console.log(` ${result.error}`);
296
+ }
172
297
  }
173
- }
174
- });
298
+ });
299
+ }
175
300
 
176
301
  await writeAgentsMd(session);
177
302
  await createClaudeMdSymlink(session.path);