theslopmachine 1.0.26-beta.2 → 1.0.26-beta.3

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.
@@ -274,7 +274,7 @@ export async function captureTmuxPaneArtifact(sessionName, filePath) {
274
274
  }
275
275
 
276
276
  export async function tmuxSendEnter(sessionName) {
277
- return runCommand('tmux', ['send-keys', '-t', sessionName, 'Enter'])
277
+ return runCommand('tmux', ['send-keys', '-t', sessionName, 'C-m'])
278
278
  }
279
279
 
280
280
  export async function tmuxSendKeys(sessionName, ...keys) {
@@ -282,11 +282,37 @@ export async function tmuxSendKeys(sessionName, ...keys) {
282
282
  }
283
283
 
284
284
  export async function tmuxSendCommand(sessionName, command) {
285
- return tmuxSendKeys(sessionName, command, 'Enter')
285
+ return tmuxSendKeys(sessionName, command, 'C-m')
286
+ }
287
+
288
+ async function executableExists(filePath) {
289
+ try {
290
+ await fs.access(filePath, fsConstants.X_OK)
291
+ return true
292
+ } catch {
293
+ return false
294
+ }
295
+ }
296
+
297
+ async function resolveTmuxShell() {
298
+ const candidates = [
299
+ process.env.SHELL,
300
+ '/bin/zsh',
301
+ '/usr/bin/zsh',
302
+ '/bin/bash',
303
+ '/usr/bin/bash',
304
+ '/bin/sh',
305
+ '/usr/bin/sh',
306
+ ].filter(Boolean)
307
+
308
+ for (const candidate of candidates) {
309
+ if (await executableExists(candidate)) return candidate
310
+ }
311
+ return process.env.SHELL || '/bin/sh'
286
312
  }
287
313
 
288
314
  export async function tmuxStartShellSession(sessionName, cwd) {
289
- const shell = process.env.SHELL || '/bin/zsh'
315
+ const shell = await resolveTmuxShell()
290
316
  const args = ['new-session', '-d', '-s', sessionName, '-c', cwd, shell]
291
317
  if (path.basename(shell) === 'zsh') args.push('-f')
292
318
  if (path.basename(shell) === 'bash') args.push('--noprofile', '--norc')
@@ -294,7 +320,7 @@ export async function tmuxStartShellSession(sessionName, cwd) {
294
320
  }
295
321
 
296
322
  export async function tmuxRespawnShellPane(sessionName, cwd) {
297
- const shell = process.env.SHELL || '/bin/zsh'
323
+ const shell = await resolveTmuxShell()
298
324
  const args = ['respawn-pane', '-k', '-t', sessionName, '-c', cwd, shell]
299
325
  if (path.basename(shell) === 'zsh') args.push('-f')
300
326
  if (path.basename(shell) === 'bash') args.push('--noprofile', '--norc')
@@ -491,6 +517,10 @@ function detectClaudeStartupPrompt(pane) {
491
517
  return 'workspace-trust'
492
518
  }
493
519
 
520
+ if (pane.includes('Choose the text style that looks best with your terminal')) {
521
+ return 'theme-selection'
522
+ }
523
+
494
524
  return null
495
525
  }
496
526
 
@@ -537,6 +567,14 @@ export function detectClaudePopupOrBlockedPane(pane) {
537
567
  }
538
568
 
539
569
  const normalized = pane.toLowerCase()
570
+ if (normalized.includes('select login method') || normalized.includes('not logged in') || normalized.includes('run /login')) {
571
+ return 'login-required'
572
+ }
573
+
574
+ if (normalized.includes('cannot be used with root/sudo privileges')) {
575
+ return 'root-not-allowed'
576
+ }
577
+
540
578
  if (/press\s+(enter|return)|hit\s+(enter|return)|enter\s+to\s+continue|return\s+to\s+continue|enter\s+to\s+confirm|return\s+to\s+confirm/.test(normalized)) {
541
579
  return 'enter-confirmation'
542
580
  }
@@ -63,12 +63,25 @@ function shouldRetryFreshTmuxLaunch(error) {
63
63
  return [
64
64
  'claude_launch_no_sessionstart',
65
65
  'claude_launch_not_ready',
66
- 'claude_launch_prompt_blocked',
67
66
  'claude_launch_missing_session_id',
68
67
  'claude_orientation_submit_hook_missing',
69
68
  ].includes(code)
70
69
  }
71
70
 
71
+ function hasProvenClaudeSession(state) {
72
+ if (!state?.sid) return false
73
+ if (!['idle', 'running', 'stopped'].includes(state.status)) return false
74
+ return Boolean(
75
+ state.last_hook_event === 'SessionStart'
76
+ || state.transcript_path
77
+ || state.orientation_verified_at
78
+ )
79
+ }
80
+
81
+ function resumableSidFromState(state) {
82
+ return hasProvenClaudeSession(state) ? state.sid : null
83
+ }
84
+
72
85
  async function waitForLaunchReadyWithRecovery({ paths, runtimeDir, launchTimeoutMs }) {
73
86
  const windows = buildLaunchAttemptWindows(launchTimeoutMs)
74
87
  let lastInspection = null
@@ -380,18 +393,19 @@ try {
380
393
  }
381
394
 
382
395
  const paths = buildRuntimePaths(runtimeDir)
396
+ let provenSidBeforeLaunch = null
383
397
 
384
398
  try {
385
399
  await ensureRuntimeDirs(paths)
386
400
 
387
401
  const existingState = await readJsonIfExists(paths.stateFile)
388
- const preSessionFailedState = existingState
389
- && existingState.status === 'failed'
390
- && !existingState.sid
391
- && !existingState.transcript_path
392
- && !await tmuxHasSession(existingState.tmux_session)
402
+ provenSidBeforeLaunch = resumableSidFromState(existingState)
403
+ const existingTmuxAlive = existingState?.tmux_session ? await tmuxHasSession(existingState.tmux_session) : false
404
+ const incompleteLaunchState = existingState
405
+ && ['starting', 'failed'].includes(existingState.status)
406
+ && !hasProvenClaudeSession(existingState)
393
407
  let resumeSessionId = explicitResumeSessionId
394
- if (!preSessionFailedState && existingState?.tmux_session && await tmuxHasSession(existingState.tmux_session)) {
408
+ if (!incompleteLaunchState && existingState?.tmux_session && existingTmuxAlive) {
395
409
  if (!replace) {
396
410
  const existingInspection = await inspectLaunchState(paths, existingState.tmux_session)
397
411
  const canReuseExistingClaudeProcess = existingState.status !== 'stopped' && !resumeRequested
@@ -420,7 +434,15 @@ try {
420
434
  process.exit(0)
421
435
  }
422
436
  if (existingState.status === 'stopped' || resumeRequested) {
423
- resumeSessionId = resumeSessionId || existingState.sid || null
437
+ resumeSessionId = resumeSessionId || resumableSidFromState(existingState)
438
+ if (resumeRequested && !resumeSessionId) {
439
+ emitFailure('claude_live_resume_unproven_sid', 'Cannot resume: runtime state does not contain a proven Claude session id.', {
440
+ sid: existingState.sid || null,
441
+ state_file: paths.stateFile,
442
+ result_file: paths.resultFile,
443
+ })
444
+ process.exit(1)
445
+ }
424
446
  await writeState(runtimeDir, {
425
447
  status: 'starting',
426
448
  sid: resumeSessionId,
@@ -438,7 +460,7 @@ try {
438
460
  })
439
461
  process.exit(1)
440
462
  }
441
- resumeSessionId = resumeSessionId || existingState.sid || null
463
+ resumeSessionId = resumeSessionId || resumableSidFromState(existingState)
442
464
  await writeState(runtimeDir, {
443
465
  status: 'starting',
444
466
  sid: resumeSessionId,
@@ -458,28 +480,30 @@ try {
458
480
  })
459
481
  process.exit(1)
460
482
  }
461
- if (resumeRequested && !resumeSessionId) resumeSessionId = existingState.sid || null
483
+ if (resumeRequested && !resumeSessionId) resumeSessionId = resumableSidFromState(existingState)
462
484
  }
463
- } else if (existingState && !preSessionFailedState && !replace && existingState.status && existingState.status !== 'stopped') {
464
- if (!existingState.sid) {
465
- emitFailure('claude_live_state_stale', 'Existing runtime state is stale and has no Claude session id to resume.', {
485
+ } else if (existingState && !incompleteLaunchState && !replace && existingState.status && existingState.status !== 'stopped') {
486
+ const existingSid = resumableSidFromState(existingState)
487
+ if (!existingSid) {
488
+ emitFailure('claude_live_state_stale', 'Existing runtime state is stale and has no proven Claude session id to resume.', {
466
489
  sid: existingState.sid || null,
467
490
  state_file: paths.stateFile,
468
491
  result_file: paths.resultFile,
469
492
  })
470
493
  process.exit(1)
471
494
  }
472
- resumeSessionId = resumeSessionId || existingState.sid
495
+ resumeSessionId = resumeSessionId || existingSid
473
496
  } else if (resumeRequested && !resumeSessionId) {
474
- if (!existingState?.sid) {
475
- emitFailure('claude_live_resume_missing_sid', 'Cannot resume: no --resume-sid was provided and runtime state has no Claude session id.', {
476
- sid: null,
497
+ const existingSid = resumableSidFromState(existingState)
498
+ if (!existingSid) {
499
+ emitFailure('claude_live_resume_missing_sid', 'Cannot resume: no --resume-sid was provided and runtime state has no proven Claude session id.', {
500
+ sid: existingState?.sid || null,
477
501
  state_file: paths.stateFile,
478
502
  result_file: paths.resultFile,
479
503
  })
480
504
  process.exit(1)
481
505
  }
482
- resumeSessionId = existingState.sid
506
+ resumeSessionId = existingSid
483
507
  }
484
508
 
485
509
  const utilsDir = resolveUtilsDir()
@@ -491,20 +515,22 @@ try {
491
515
  : buildStableTmuxSessionName({ cwd })
492
516
 
493
517
  const tmuxAlreadyAlive = await tmuxHasSession(tmuxSession)
494
- if (tmuxAlreadyAlive && !replace && !existingState?.tmux_session) {
518
+ const currentState = await readJsonIfExists(paths.stateFile)
519
+ if (tmuxAlreadyAlive && !replace && !existingState?.tmux_session && currentState?.tmux_session !== tmuxSession) {
495
520
  throw Object.assign(new Error(`Project tmux host already exists: ${tmuxSession}. Reuse the active runtime state or relaunch with --replace 1.`), {
496
521
  code: 'claude_live_tmux_name_in_use',
497
522
  })
498
523
  }
499
524
 
500
- const resumingExistingLane = Boolean(resumeSessionId || existingState?.sid)
525
+ const provenExistingSid = resumableSidFromState(existingState)
526
+ const resumingExistingLane = Boolean(resumeSessionId || provenExistingSid)
501
527
  await clearRuntimeArtifacts(paths)
502
528
  await writeState(runtimeDir, {
503
529
  lane,
504
530
  backend: 'claude-live',
505
531
  status: 'starting',
506
532
  cwd,
507
- sid: resumeSessionId || existingState?.sid || null,
533
+ sid: resumeSessionId || provenExistingSid,
508
534
  resume_from_sid: resumeSessionId,
509
535
  agent_name: agentName,
510
536
  model: laneModel,
@@ -667,6 +693,8 @@ try {
667
693
  }
668
694
  await writeState(runtimeDir, {
669
695
  status: 'failed',
696
+ sid: provenSidBeforeLaunch,
697
+ resume_from_sid: null,
670
698
  last_error: error instanceof Error ? error.message : String(error),
671
699
  cleanup_warning: cleanupWarning,
672
700
  })
@@ -674,7 +702,7 @@ try {
674
702
  ? error.code
675
703
  : 'claude_live_launch_failed'
676
704
  emitFailure(failureCode, error instanceof Error ? error.message : String(error), {
677
- sid: state?.sid || null,
705
+ sid: provenSidBeforeLaunch,
678
706
  state_file: paths.stateFile,
679
707
  result_file: paths.resultFile,
680
708
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "theslopmachine",
3
- "version": "1.0.26-beta.2",
3
+ "version": "1.0.26-beta.3",
4
4
  "description": "SlopMachine installer and project bootstrap CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",