@@ -17,10 +17,17 @@ import { program } from 'commander';
1717import { v4 as uuidv4 } from 'uuid' ;
1818import chalk from 'chalk' ;
1919import { initializeTracing , trace } from '../core/trace/index.js' ;
20+ import { resolveRealCliBin } from './utils/real-cli-bin.js' ;
21+ import {
22+ type DeterminismWatcherHandle ,
23+ startDeterminismWatcher ,
24+ stopDeterminismWatcher ,
25+ } from './utils/determinism-watcher.js' ;
2026import {
2127 canonicalStateStore ,
2228 projectIdFromIdentifier ,
2329} from '../core/shared-state/canonical-store.js' ;
30+ import { loadProjectHandoff } from '../core/session/project-handoff.js' ;
2431import {
2532 getModelRouter ,
2633 loadModelRouterConfig ,
@@ -136,6 +143,8 @@ class ClaudeSM {
136143 private sessionId : string ;
137144 private ownsSession : boolean ;
138145 private sessionEnded : boolean ;
146+ private determinismWatcher : DeterminismWatcherHandle | null ;
147+ private skippedHandoffReason : string | null ;
139148
140149 constructor ( ) {
141150 // Load persistent defaults
@@ -166,6 +175,8 @@ class ClaudeSM {
166175 this . sessionId = process . env [ 'STACKMEMORY_SESSION' ] || uuidv4 ( ) ;
167176 this . ownsSession = ! process . env [ 'STACKMEMORY_SESSION' ] ;
168177 this . sessionEnded = false ;
178+ this . determinismWatcher = null ;
179+ this . skippedHandoffReason = null ;
169180
170181 // Ensure config directory exists
171182 if ( ! fs . existsSync ( this . claudeConfigDir ) ) {
@@ -275,19 +286,16 @@ class ClaudeSM {
275286 }
276287
277288 private resolveClaudeBin ( ) : string | null {
278- // 1) CLI-specified
279- if ( this . config . claudeBin && this . config . claudeBin . trim ( ) ) {
280- return this . config . claudeBin . trim ( ) ;
281- }
282- // 2) Env override
283- const envBin = process . env [ 'CLAUDE_BIN' ] ;
284- if ( envBin && envBin . trim ( ) ) return envBin . trim ( ) ;
285- // 3) PATH detection
286- try {
287- execSync ( 'which claude' , { stdio : 'ignore' } ) ;
288- return 'claude' ;
289- } catch { }
290- return null ;
289+ return resolveRealCliBin ( {
290+ explicitBin : this . config . claudeBin ,
291+ envBin : process . env [ 'CLAUDE_BIN' ] ,
292+ preferredPaths : [
293+ path . join ( os . homedir ( ) , '.local' , 'bin' , 'claude' ) ,
294+ '/usr/local/bin/claude' ,
295+ '/opt/homebrew/bin/claude' ,
296+ ] ,
297+ pathCommands : [ 'claude' ] ,
298+ } ) ;
291299 }
292300
293301 private gepaProcesses : ReturnType < typeof spawn > [ ] = [ ] ;
@@ -373,6 +381,30 @@ class ClaudeSM {
373381 this . gepaProcesses = [ ] ;
374382 }
375383
384+ private startDeterminismWatcher ( ) : void {
385+ this . determinismWatcher = startDeterminismWatcher ( {
386+ stackmemoryBin : this . stackmemoryPath ,
387+ cwd : process . cwd ( ) ,
388+ task : this . config . task ,
389+ instanceId : this . config . instanceId ,
390+ sessionId : this . sessionId ,
391+ tool : 'claude' ,
392+ } ) ;
393+
394+ if ( this . determinismWatcher ) {
395+ const modeLabel =
396+ this . determinismWatcher . mode === 'targeted'
397+ ? 'targeted'
398+ : 'repo-root fallback' ;
399+ console . log ( chalk . gray ( ` Determinism: ${ modeLabel } ` ) ) ;
400+ }
401+ }
402+
403+ private stopDeterminismWatcher ( ) : void {
404+ stopDeterminismWatcher ( this . determinismWatcher ) ;
405+ this . determinismWatcher = null ;
406+ }
407+
376408 private setupWorktree ( ) : string | null {
377409 if ( ! this . config . useWorktree || ! this . isGitRepo ( ) ) {
378410 return null ;
@@ -489,19 +521,25 @@ class ClaudeSM {
489521 if ( ! this . config . contextEnabled ) return null ;
490522
491523 try {
492- const handoffPath = path . join (
524+ const handoff = loadProjectHandoff (
493525 process . cwd ( ) ,
494- '.stackmemory' ,
495- 'last-handoff.md'
526+ this . isGitRepo ( ) ? this . getCurrentBranch ( ) : undefined
496527 ) ;
497- if ( fs . existsSync ( handoffPath ) ) {
498- const content = fs . readFileSync ( handoffPath , 'utf8' ) . trim ( ) ;
499- if ( content . length > 0 ) {
500- // Cap at 8000 chars to avoid excessively long system prompts
501- return content . length > 8000
502- ? content . substring ( 0 , 8000 ) + '\n\n[...truncated]'
503- : content ;
504- }
528+ if ( ! handoff ) {
529+ this . skippedHandoffReason = null ;
530+ return null ;
531+ }
532+ if ( ! handoff . compatible ) {
533+ this . skippedHandoffReason = handoff . mismatchReason || 'stale handoff' ;
534+ return null ;
535+ }
536+ this . skippedHandoffReason = null ;
537+ const content = handoff . content . trim ( ) ;
538+ if ( content . length > 0 ) {
539+ // Cap at 8000 chars to avoid excessively long system prompts
540+ return content . length > 8000
541+ ? content . substring ( 0 , 8000 ) + '\n\n[...truncated]'
542+ : content ;
505543 }
506544 } catch {
507545 // Silently continue - handoff loading is optional
@@ -759,6 +797,7 @@ class ClaudeSM {
759797 payload : Record < string , unknown > = { }
760798 ) : Promise < void > {
761799 this. stopGEPAWatcher ( ) ;
800+ this . stopDeterminismWatcher ( ) ;
762801
763802 this . saveContext (
764803 eventType === 'session_end'
@@ -1045,6 +1084,7 @@ class ClaudeSM {
10451084 }
10461085
10471086 await this . publishSessionStart ( ) ;
1087+ this . startDeterminismWatcher ( ) ;
10481088 console . log ( chalk . gray ( `🤖 Instance ID: ${ this . config . instanceId } ` ) ) ;
10491089 console . log ( chalk . gray ( `🧠 Session ID: ${ this . sessionId . slice ( 0 , 8 ) } ` ) ) ;
10501090 console . log ( chalk . gray ( `📁 Working in: ${ process . cwd ( ) } ` ) ) ;
@@ -1133,6 +1173,10 @@ class ClaudeSM {
11331173 if ( handoffContent ) {
11341174 initialInput = handoffContent ;
11351175 console . log ( chalk . gray ( ' Handoff context ready' ) ) ;
1176+ } else if ( this . skippedHandoffReason ) {
1177+ console . log (
1178+ chalk . gray ( ` Handoff skipped: ${ this . skippedHandoffReason } ` )
1179+ ) ;
11361180 }
11371181
11381182 const theoryContent = this . getTheoryContent ( ) ;
0 commit comments