1- import { existsSync } from 'node:fs' ;
1+ import { existsSync , readdirSync } from 'node:fs' ;
22import { join } from 'node:path' ;
33
44import type { ProjectConfig } from '../../types/index.js' ;
5- import { cloneRepo , createTempDir , runCommand } from '../../utils/repo.js' ;
5+ import { cloneRepo , createTempDir , getWorkspaceDir , runCommand } from '../../utils/repo.js' ;
66import type { AgentLogger } from '../utils/logging.js' ;
77import { warmTypeScriptCache } from '../utils/setup.js' ;
88
@@ -14,9 +14,128 @@ export interface SetupRepositoryOptions {
1414 warmTsCache ?: boolean ;
1515}
1616
17+ /**
18+ * Resolve the path to the existing workspace directory for a snapshot-reuse run.
19+ *
20+ * Snapshot images bake the workspace into `/workspace/cascade-<projectId>-*`.
21+ * We locate the first matching directory rather than creating a new one so the
22+ * snapshot-resident installation artifacts are used as-is.
23+ *
24+ * Returns `null` when no baked-in workspace directory can be found.
25+ */
26+ export function findSnapshotWorkspaceDir ( projectId : string ) : string | null {
27+ const workspaceBase = getWorkspaceDir ( ) ;
28+ const prefix = `cascade-${ projectId } -` ;
29+ try {
30+ const entries = readdirSync ( workspaceBase ) ;
31+ const match = entries . find ( ( e ) => {
32+ if ( ! e . startsWith ( prefix ) ) return false ;
33+ const suffix = e . slice ( prefix . length ) ;
34+ return / ^ \d + $ / . test ( suffix ) ;
35+ } ) ;
36+ return match ? `${ workspaceBase } /${ match } ` : null ;
37+ } catch {
38+ return null ;
39+ }
40+ }
41+
42+ /**
43+ * Refresh an existing snapshot workspace via git fetch + reset + optional branch checkout.
44+ *
45+ * This is the "warm-start" path: the Docker image already contains the cloned repo,
46+ * installed dependencies, and any setup artifacts from the previous run. We only
47+ * need to bring the working tree up to date with the remote.
48+ */
49+ async function refreshSnapshotWorkspace (
50+ repoDir : string ,
51+ project : ProjectConfig ,
52+ log : AgentLogger ,
53+ agentType : string ,
54+ prBranch ?: string ,
55+ ) : Promise < void > {
56+ const branch = prBranch ?? project . baseBranch ?? 'main' ;
57+
58+ log . info ( 'Refreshing snapshot workspace' , { repoDir, branch, agentType } ) ;
59+
60+ // Fetch latest refs from origin (tolerates transient network errors)
61+ const fetchResult = await runCommand ( 'git' , [ 'fetch' , 'origin' ] , repoDir ) ;
62+ if ( fetchResult . exitCode !== 0 ) {
63+ log . warn ( 'git fetch exited with non-zero code (continuing)' , {
64+ exitCode : fetchResult . exitCode ,
65+ stderr : fetchResult . stderr . slice ( - 500 ) ,
66+ } ) ;
67+ }
68+
69+ // Reset to the remote tracking branch to discard any stale local changes
70+ const resetResult = await runCommand ( 'git' , [ 'reset' , '--hard' , `origin/${ branch } ` ] , repoDir ) ;
71+ if ( resetResult . exitCode !== 0 ) {
72+ log . warn ( 'git reset --hard exited with non-zero code (continuing)' , {
73+ exitCode : resetResult . exitCode ,
74+ stderr : resetResult . stderr . slice ( - 500 ) ,
75+ } ) ;
76+ }
77+
78+ // Checkout the target branch (no-op when already on the correct branch)
79+ const checkoutResult = await runCommand ( 'git' , [ 'checkout' , branch ] , repoDir ) ;
80+ if ( checkoutResult . exitCode !== 0 ) {
81+ log . warn ( 'git checkout exited with non-zero code (continuing)' , {
82+ exitCode : checkoutResult . exitCode ,
83+ stderr : checkoutResult . stderr . slice ( - 500 ) ,
84+ } ) ;
85+ }
86+
87+ log . info ( 'Snapshot workspace refreshed' , { repoDir, branch } ) ;
88+ }
89+
90+ /**
91+ * Warm the TypeScript compiler cache for a repo directory, logging the result.
92+ * Extracted to keep setupRepository within the cognitive-complexity limit.
93+ */
94+ async function maybeWarmTsCache (
95+ repoDir : string ,
96+ warmTsCache : boolean | undefined ,
97+ log : AgentLogger ,
98+ ) : Promise < void > {
99+ if ( ! warmTsCache ) return ;
100+ log . info ( 'Warming TypeScript cache' , { repoDir } ) ;
101+ const tscResult = await warmTypeScriptCache ( repoDir ) ;
102+ if ( tscResult ) {
103+ log . info ( 'TypeScript cache warmed' , {
104+ durationMs : tscResult . durationMs ,
105+ hadErrors : ! ! tscResult . error ,
106+ } ) ;
107+ }
108+ }
109+
17110export async function setupRepository ( options : SetupRepositoryOptions ) : Promise < string > {
18111 const { project, log, agentType, prBranch, warmTsCache } = options ;
19112
113+ // ── Snapshot-reuse path ────────────────────────────────────────────────────
114+ // When CASCADE_SNAPSHOT_REUSE=true the container image already contains the
115+ // repo, dependencies, and prior setup work. Locate the baked-in workspace
116+ // and refresh it with fetch/reset semantics instead of cloning from scratch.
117+ if ( process . env . CASCADE_SNAPSHOT_REUSE === 'true' && project . repo ) {
118+ const snapshotDir = findSnapshotWorkspaceDir ( project . id ) ;
119+ if ( snapshotDir ) {
120+ log . info ( 'Snapshot reuse detected — skipping clone' , {
121+ projectId : project . id ,
122+ agentType,
123+ snapshotDir,
124+ } ) ;
125+ await refreshSnapshotWorkspace ( snapshotDir , project , log , agentType , prBranch ) ;
126+ await maybeWarmTsCache ( snapshotDir , warmTsCache , log ) ;
127+ return snapshotDir ;
128+ }
129+
130+ // Snapshot directory not found — fall through to cold-start clone
131+ log . warn ( 'Snapshot reuse requested but no workspace directory found — falling back to clone' , {
132+ projectId : project . id ,
133+ agentType,
134+ } ) ;
135+ }
136+
137+ // ── Cold-start path (clone) ────────────────────────────────────────────────
138+
20139 // Create temp directory for all agents
21140 const repoDir = createTempDir ( project . id ) ;
22141
@@ -52,17 +171,7 @@ export async function setupRepository(options: SetupRepositoryOptions): Promise<
52171 }
53172 }
54173
55- // Warm TypeScript cache to avoid slow first-run compilation during agent execution
56- if ( warmTsCache ) {
57- log . info ( 'Warming TypeScript cache' , { repoDir } ) ;
58- const tscResult = await warmTypeScriptCache ( repoDir ) ;
59- if ( tscResult ) {
60- log . info ( 'TypeScript cache warmed' , {
61- durationMs : tscResult . durationMs ,
62- hadErrors : ! ! tscResult . error ,
63- } ) ;
64- }
65- }
174+ await maybeWarmTsCache ( repoDir , warmTsCache , log ) ;
66175
67176 return repoDir ;
68177}
0 commit comments