theslopmachine 0.6.2 → 0.7.1

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.
Files changed (77) hide show
  1. package/MANUAL.md +21 -6
  2. package/README.md +55 -7
  3. package/RELEASE.md +16 -1
  4. package/assets/agents/developer.md +41 -1
  5. package/assets/agents/slopmachine-claude.md +101 -60
  6. package/assets/agents/slopmachine.md +40 -17
  7. package/assets/claude/agents/developer.md +42 -5
  8. package/assets/skills/clarification-gate/SKILL.md +25 -5
  9. package/assets/skills/claude-worker-management/SKILL.md +290 -57
  10. package/assets/skills/developer-session-lifecycle/SKILL.md +83 -38
  11. package/assets/skills/development-guidance/SKILL.md +21 -1
  12. package/assets/skills/evaluation-triage/SKILL.md +34 -23
  13. package/assets/skills/final-evaluation-orchestration/SKILL.md +88 -50
  14. package/assets/skills/hardening-gate/SKILL.md +17 -3
  15. package/assets/skills/integrated-verification/SKILL.md +3 -3
  16. package/assets/skills/planning-gate/SKILL.md +32 -3
  17. package/assets/skills/planning-guidance/SKILL.md +72 -13
  18. package/assets/skills/retrospective-analysis/SKILL.md +2 -2
  19. package/assets/skills/scaffold-guidance/SKILL.md +129 -124
  20. package/assets/skills/submission-packaging/SKILL.md +33 -27
  21. package/assets/skills/verification-gates/SKILL.md +44 -14
  22. package/assets/slopmachine/backend-evaluation-prompt.md +1 -1
  23. package/assets/slopmachine/frontend-evaluation-prompt.md +5 -5
  24. package/assets/slopmachine/scaffold-playbooks/android-kotlin-compose.md +81 -0
  25. package/assets/slopmachine/scaffold-playbooks/android-kotlin-views.md +191 -0
  26. package/assets/slopmachine/scaffold-playbooks/android-native-java.md +203 -0
  27. package/assets/slopmachine/scaffold-playbooks/angular-default.md +181 -0
  28. package/assets/slopmachine/scaffold-playbooks/backend-baseline.md +142 -0
  29. package/assets/slopmachine/scaffold-playbooks/backend-family-matrix.md +80 -0
  30. package/assets/slopmachine/scaffold-playbooks/database-module-matrix.md +80 -0
  31. package/assets/slopmachine/scaffold-playbooks/django-default.md +166 -0
  32. package/assets/slopmachine/scaffold-playbooks/docker-baseline.md +189 -0
  33. package/assets/slopmachine/scaffold-playbooks/docker-shared-contract.md +334 -0
  34. package/assets/slopmachine/scaffold-playbooks/electron-vite-default.md +124 -0
  35. package/assets/slopmachine/scaffold-playbooks/expo-react-native-default.md +73 -0
  36. package/assets/slopmachine/scaffold-playbooks/fastapi-default.md +134 -0
  37. package/assets/slopmachine/scaffold-playbooks/frontend-baseline.md +160 -0
  38. package/assets/slopmachine/scaffold-playbooks/frontend-family-matrix.md +134 -0
  39. package/assets/slopmachine/scaffold-playbooks/generic-unknown-tech-guide.md +136 -0
  40. package/assets/slopmachine/scaffold-playbooks/go-chi-default.md +160 -0
  41. package/assets/slopmachine/scaffold-playbooks/ios-linux-portable.md +93 -0
  42. package/assets/slopmachine/scaffold-playbooks/ios-native-objective-c.md +151 -0
  43. package/assets/slopmachine/scaffold-playbooks/ios-native-swift.md +188 -0
  44. package/assets/slopmachine/scaffold-playbooks/laravel-default.md +216 -0
  45. package/assets/slopmachine/scaffold-playbooks/livewire-default.md +265 -0
  46. package/assets/slopmachine/scaffold-playbooks/overlay-module-matrix.md +130 -0
  47. package/assets/slopmachine/scaffold-playbooks/platform-family-matrix.md +79 -0
  48. package/assets/slopmachine/scaffold-playbooks/selection-matrix.md +72 -0
  49. package/assets/slopmachine/scaffold-playbooks/spring-boot-default.md +182 -0
  50. package/assets/slopmachine/scaffold-playbooks/tauri-default.md +80 -0
  51. package/assets/slopmachine/scaffold-playbooks/vue-vite-default.md +162 -0
  52. package/assets/slopmachine/scaffold-playbooks/web-default.md +96 -0
  53. package/assets/slopmachine/templates/AGENTS.md +41 -3
  54. package/assets/slopmachine/templates/CLAUDE.md +111 -0
  55. package/assets/slopmachine/test-coverage-prompt.md +561 -0
  56. package/assets/slopmachine/utils/claude_create_session.mjs +3 -2
  57. package/assets/slopmachine/utils/claude_live_channel.mjs +188 -0
  58. package/assets/slopmachine/utils/claude_live_common.mjs +411 -0
  59. package/assets/slopmachine/utils/claude_live_hook.py +47 -0
  60. package/assets/slopmachine/utils/claude_live_launch.mjs +187 -0
  61. package/assets/slopmachine/utils/claude_live_status.mjs +25 -0
  62. package/assets/slopmachine/utils/claude_live_stop.mjs +46 -0
  63. package/assets/slopmachine/utils/claude_live_turn.mjs +277 -0
  64. package/assets/slopmachine/utils/claude_resume_session.mjs +3 -2
  65. package/assets/slopmachine/utils/claude_wait_for_rate_limit_reset.mjs +23 -0
  66. package/assets/slopmachine/utils/claude_wait_for_rate_limit_reset.sh +5 -0
  67. package/assets/slopmachine/utils/claude_worker_common.mjs +361 -4
  68. package/assets/slopmachine/utils/cleanup_delivery_artifacts.py +4 -0
  69. package/assets/slopmachine/utils/export_ai_session.mjs +1 -1
  70. package/assets/slopmachine/utils/normalize_claude_session.py +153 -0
  71. package/assets/slopmachine/utils/package_claude_session.mjs +123 -0
  72. package/assets/slopmachine/utils/prepare_strict_audit_workspace.mjs +65 -0
  73. package/package.json +1 -1
  74. package/src/constants.js +42 -3
  75. package/src/init.js +173 -28
  76. package/src/install.js +156 -8
  77. package/src/send-data.js +56 -57
package/src/install.js CHANGED
@@ -62,6 +62,41 @@ function getUnixBeadsBinDirs() {
62
62
  ]
63
63
  }
64
64
 
65
+ function getUnixCommonBinDirs() {
66
+ const home = getHomeDir()
67
+ return [
68
+ path.join(home, '.local', 'bin'),
69
+ path.join(home, '.cargo', 'bin'),
70
+ '/opt/homebrew/bin',
71
+ '/usr/local/bin',
72
+ ]
73
+ }
74
+
75
+ function getWindowsCommonBinDirs() {
76
+ if (process.platform !== 'win32') {
77
+ return []
78
+ }
79
+
80
+ const home = getHomeDir()
81
+ return [
82
+ process.env.LOCALAPPDATA ? path.join(process.env.LOCALAPPDATA, 'Programs') : null,
83
+ path.join(home, '.local', 'bin'),
84
+ path.join(home, '.cargo', 'bin'),
85
+ ].filter(Boolean)
86
+ }
87
+
88
+ function getCommonCommandCandidates(command) {
89
+ if (process.platform === 'win32') {
90
+ return getWindowsCommonBinDirs().flatMap((dir) => [
91
+ path.join(dir, `${command}.exe`),
92
+ path.join(dir, `${command}.cmd`),
93
+ path.join(dir, command),
94
+ ])
95
+ }
96
+
97
+ return getUnixCommonBinDirs().map((dir) => path.join(dir, command))
98
+ }
99
+
65
100
  function getWindowsBeadsBinDirs() {
66
101
  if (process.platform !== 'win32') {
67
102
  return []
@@ -308,14 +343,57 @@ async function installBeadsOnUnix() {
308
343
  return result
309
344
  }
310
345
 
346
+ function getVersionArgs(command) {
347
+ if (command === 'tmux') {
348
+ return ['-V']
349
+ }
350
+
351
+ if (command === 'zip') {
352
+ return ['-v']
353
+ }
354
+
355
+ return ['--version']
356
+ }
357
+
311
358
  async function getCommandVersion(command, args = ['--version']) {
312
- const exists = await commandExists(command)
313
- if (!exists) return null
314
- const result = await runCommand(command, args)
359
+ const resolved = await resolveCommand(command, {
360
+ additionalCandidates: getCommonCommandCandidates(command),
361
+ preferCandidates: true,
362
+ })
363
+ if (!resolved) return null
364
+ const versionArgs = args.length === 1 && args[0] === '--version' ? getVersionArgs(command) : args
365
+ const result = await runCommand(resolved, versionArgs)
315
366
  if (result.code !== 0) return null
316
367
  return (result.stdout || result.stderr).trim()
317
368
  }
318
369
 
370
+ function extractSemver(value) {
371
+ const match = String(value || '').match(/\d+\.\d+\.\d+/)
372
+ return match ? match[0] : null
373
+ }
374
+
375
+ function compareSemver(left, right) {
376
+ const leftParts = left.split('.').map((part) => Number.parseInt(part, 10))
377
+ const rightParts = right.split('.').map((part) => Number.parseInt(part, 10))
378
+ const length = Math.max(leftParts.length, rightParts.length)
379
+ for (let index = 0; index < length; index += 1) {
380
+ const a = leftParts[index] || 0
381
+ const b = rightParts[index] || 0
382
+ if (a > b) return 1
383
+ if (a < b) return -1
384
+ }
385
+ return 0
386
+ }
387
+
388
+ function isVersionAtLeast(currentVersion, minimumVersion) {
389
+ const current = extractSemver(currentVersion)
390
+ const minimum = extractSemver(minimumVersion)
391
+ if (!current || !minimum) {
392
+ return false
393
+ }
394
+ return compareSemver(current, minimum) >= 0
395
+ }
396
+
319
397
  async function getPythonVersion() {
320
398
  const python3Version = await getCommandVersion('python3')
321
399
  if (python3Version) {
@@ -376,6 +454,30 @@ async function tryInstallCoreDependency(name) {
376
454
  winget: ['winget', ['install', '--id', 'Python.Python.3.11', '-e']],
377
455
  choco: ['choco', ['install', 'python', '--version=3.11.7', '-y']],
378
456
  },
457
+ bash: {
458
+ brew: ['brew', ['install', 'bash']],
459
+ apt: ['sudo', ['apt-get', 'install', '-y', 'bash']],
460
+ dnf: ['sudo', ['dnf', 'install', '-y', 'bash']],
461
+ },
462
+ curl: {
463
+ brew: ['brew', ['install', 'curl']],
464
+ apt: ['sudo', ['apt-get', 'install', '-y', 'curl']],
465
+ dnf: ['sudo', ['dnf', 'install', '-y', 'curl']],
466
+ },
467
+ tar: {
468
+ apt: ['sudo', ['apt-get', 'install', '-y', 'tar']],
469
+ dnf: ['sudo', ['dnf', 'install', '-y', 'tar']],
470
+ },
471
+ tmux: {
472
+ brew: ['brew', ['install', 'tmux']],
473
+ apt: ['sudo', ['apt-get', 'install', '-y', 'tmux']],
474
+ dnf: ['sudo', ['dnf', 'install', '-y', 'tmux']],
475
+ },
476
+ zip: {
477
+ brew: ['brew', ['install', 'zip']],
478
+ apt: ['sudo', ['apt-get', 'install', '-y', 'zip']],
479
+ dnf: ['sudo', ['dnf', 'install', '-y', 'zip']],
480
+ },
379
481
  }
380
482
 
381
483
  const platformKey = managers.brew
@@ -405,19 +507,27 @@ async function tryInstallCoreDependency(name) {
405
507
  async function ensureLatestOpencode(requiredVersion) {
406
508
  const npmVersion = await getGlobalNpmPackageVersion('opencode-ai')
407
509
  const commandPath = await resolveCommand('opencode')
510
+ const commandVersion = await getCommandVersion('opencode')
408
511
 
409
512
  if (npmVersion) {
410
513
  log(`opencode detected via global npm package: opencode-ai@${npmVersion}`)
411
- if (requiredVersion && !npmVersion.includes(requiredVersion)) {
412
- warn(`opencode version differs from tested reference ${requiredVersion}`)
514
+ if (!requiredVersion || isVersionAtLeast(npmVersion, requiredVersion)) {
515
+ log(`Using existing opencode-ai@${npmVersion}`)
516
+ return
413
517
  }
414
- log('Refreshing opencode-ai to latest via npm')
518
+ warn(`opencode version ${npmVersion} is below minimum supported version ${requiredVersion}`)
519
+ log('Updating opencode-ai via npm to reach the minimum supported version')
415
520
  } else if (commandPath) {
416
521
  log(`opencode command detected at ${commandPath}`)
417
- log('Installing opencode-ai@latest globally via npm so theslopmachine can manage the current opencode install')
522
+ if (!requiredVersion || (commandVersion && isVersionAtLeast(commandVersion, requiredVersion))) {
523
+ log(`Using existing opencode command${commandVersion ? `: ${commandVersion}` : ''}`)
524
+ return
525
+ }
526
+ warn(`opencode command version${commandVersion ? ` ${commandVersion}` : ''} is below minimum supported version ${requiredVersion}`)
527
+ log('Installing opencode-ai globally via npm so theslopmachine can manage a supported opencode install')
418
528
  } else {
419
529
  warn('opencode is not installed or not available in PATH')
420
- log('Installing opencode-ai@latest globally via npm')
530
+ log('Installing opencode-ai via npm')
421
531
  }
422
532
 
423
533
  const result = await tryInstallCoreDependency('opencode')
@@ -587,6 +697,31 @@ async function checkInitShellSupport() {
587
697
  }
588
698
  }
589
699
 
700
+ async function checkClaudeCli() {
701
+ const claudeVersion = await getCommandVersion('claude')
702
+ if (!claudeVersion) {
703
+ warn('Claude CLI is required for the `slopmachine-claude` backend and was not found in PATH. Install Claude Code before using the Claude-backed workflow.')
704
+ return
705
+ }
706
+
707
+ log(`claude detected: ${claudeVersion}`)
708
+ }
709
+
710
+ async function checkArchiveSupport() {
711
+ if (process.platform === 'win32') {
712
+ const shell = await resolveCommand('pwsh') || await resolveCommand('powershell')
713
+ if (!shell) {
714
+ warn('PowerShell is required on Windows for archive packaging paths such as send-data and Claude session packaging.')
715
+ return
716
+ }
717
+
718
+ log(`archive shell detected: ${shell}`)
719
+ return
720
+ }
721
+
722
+ await ensureDependency({ name: 'zip', checkCommand: 'zip', requiredVersion: null, installable: true })
723
+ }
724
+
590
725
  async function findLegacyInstallItems(paths) {
591
726
  const legacyItems = []
592
727
  const scanTargets = [
@@ -880,11 +1015,24 @@ export async function runInstall() {
880
1015
  log(`Configuring theslopmachine in ${paths.home}`)
881
1016
 
882
1017
  section('Dependencies')
1018
+ await ensureDependency({ name: 'npm', checkCommand: 'npm', requiredVersion: null, installable: false })
883
1019
  await ensureDependency({ name: 'git', checkCommand: 'git', installable: true })
884
1020
  await ensureDependency({ name: 'python3', checkCommand: 'python3', installable: true })
1021
+ if (process.platform !== 'win32') {
1022
+ await ensureDependency({ name: 'bash', checkCommand: 'bash', installable: true })
1023
+ }
1024
+ if (process.platform !== 'win32') {
1025
+ await ensureDependency({ name: 'curl', checkCommand: 'curl', installable: true })
1026
+ }
1027
+ if (process.platform === 'linux') {
1028
+ await ensureDependency({ name: 'tar', checkCommand: 'tar', installable: true })
1029
+ }
885
1030
  await ensureDependency({ name: 'opencode', checkCommand: 'opencode', requiredVersion: OPCODE_VERSION, installable: true })
886
1031
  await ensureDependency({ name: 'beads_rust (br)', checkCommand: 'br', requiredVersion: null, installable: true })
887
1032
  await checkDocker()
1033
+ await ensureDependency({ name: 'tmux', checkCommand: 'tmux', requiredVersion: null, installable: process.platform !== 'win32' })
1034
+ await checkClaudeCli()
1035
+ await checkArchiveSupport()
888
1036
  await checkInitShellSupport()
889
1037
 
890
1038
  section('Legacy Cleanup')
package/src/send-data.js CHANGED
@@ -23,7 +23,7 @@ const RETROSPECTIVE_FILENAMES = (runId) => [
23
23
  `improvement-actions-${runId}.md`,
24
24
  ]
25
25
 
26
- const SELF_TEST_REPORTS_DIR = 'self_test_reports'
26
+ const EVALUATION_REPORTS_DIR = '.tmp'
27
27
 
28
28
  function sha256Hex(buffer) {
29
29
  return createHash('sha256').update(buffer).digest('hex')
@@ -368,7 +368,7 @@ async function buildManifest(stagingDir, projectId, runId, label, workspaceRoot)
368
368
  project_id: projectId,
369
369
  run_id: runId,
370
370
  label,
371
- bundle_categories: ['sessions', 'self-test', 'retrospective', 'beads', 'metadata'],
371
+ bundle_categories: ['sessions', 'evaluation', 'retrospective', 'beads', 'metadata'],
372
372
  files,
373
373
  file_count: files.length,
374
374
  total_bytes: totalBytes,
@@ -386,67 +386,33 @@ async function buildManifest(stagingDir, projectId, runId, label, workspaceRoot)
386
386
  }
387
387
 
388
388
  async function exportDeveloperSessionArtifacts(session, workspaceRoot, stagingDir) {
389
+ if (session.backend === 'claude' || session.backend === 'claude-live') {
390
+ throw new Error(`Claude-backed sessions should be exported through the shared project archive, not per-session packaging (${session.label})`)
391
+ }
392
+
389
393
  const utilsDir = path.join(buildPaths().slopmachineDir, 'utils')
390
- const exportScript = path.join(utilsDir, 'export_ai_session.mjs')
391
394
  const stripScript = path.join(utilsDir, 'strip_session_parent.py')
392
- const convertScript = path.join(utilsDir, 'convert_exported_ai_session.mjs')
393
- const converterPythonScript = path.join(utilsDir, 'convert_ai_session.py')
394
395
 
395
- const exportNumber = session.order + 1
396
- const rootSessionFile = path.join(stagingDir, `session-${exportNumber}.json`)
397
- const convertedSessionFile = path.join(stagingDir, 'sessions', `${session.label}.json`)
396
+ const cleanedSessionFile = path.join(stagingDir, 'sessions', `${session.label}.json`)
398
397
  const rawSessionFile = path.join(stagingDir, `.session-export-${session.label}.raw`)
399
398
 
400
- await ensureDir(path.dirname(convertedSessionFile))
401
-
402
- if (session.backend === 'claude') {
403
- const exportResult = await runCommand(process.execPath, [
404
- exportScript,
405
- '--backend',
406
- 'claude',
407
- '--cwd',
408
- workspaceRoot,
409
- '--session-id',
410
- session.sessionId,
411
- '--output',
412
- rootSessionFile,
413
- ])
414
-
415
- if (exportResult.code !== 0) {
416
- throw new Error(`Failed to export session ${session.label}: ${(exportResult.stderr || exportResult.stdout).trim()}`)
417
- }
418
- } else {
419
- const opencodeCommand = await resolveCommand('opencode')
420
- if (!opencodeCommand) {
421
- throw new Error('Unable to find `opencode` for developer session export')
422
- }
423
-
424
- await exportOpenCodeSession(opencodeCommand, session.sessionId, rawSessionFile, workspaceRoot)
425
-
426
- const stripResult = await runCommand('python3', [
427
- stripScript,
428
- rawSessionFile,
429
- '--output',
430
- rootSessionFile,
431
- ])
432
-
433
- if (stripResult.code !== 0) {
434
- throw new Error(`Failed to clean exported session ${session.label}: ${(stripResult.stderr || stripResult.stdout).trim()}`)
435
- }
399
+ const opencodeCommand = await resolveCommand('opencode')
400
+ if (!opencodeCommand) {
401
+ throw new Error('Unable to find `opencode` for developer session export')
436
402
  }
437
403
 
438
- const convertResult = await runCommand(process.execPath, [
439
- convertScript,
440
- '--converter-script',
441
- converterPythonScript,
442
- '--input',
443
- rootSessionFile,
404
+ await ensureDir(path.dirname(cleanedSessionFile))
405
+ await exportOpenCodeSession(opencodeCommand, session.sessionId, rawSessionFile, workspaceRoot)
406
+
407
+ const stripResult = await runCommand('python3', [
408
+ stripScript,
409
+ rawSessionFile,
444
410
  '--output',
445
- convertedSessionFile,
411
+ cleanedSessionFile,
446
412
  ])
447
413
 
448
- if (convertResult.code !== 0) {
449
- throw new Error(`Failed to convert exported session ${session.label}: ${(convertResult.stderr || convertResult.stdout).trim()}`)
414
+ if (stripResult.code !== 0) {
415
+ throw new Error(`Failed to clean exported session ${session.label}: ${(stripResult.stderr || stripResult.stdout).trim()}`)
450
416
  }
451
417
 
452
418
  if (await pathExists(rawSessionFile)) {
@@ -454,6 +420,33 @@ async function exportDeveloperSessionArtifacts(session, workspaceRoot, stagingDi
454
420
  }
455
421
  }
456
422
 
423
+ async function exportClaudeProjectArtifacts(claudeSessions, workspaceRoot, stagingDir) {
424
+ if (claudeSessions.length === 0) {
425
+ return
426
+ }
427
+
428
+ const utilsDir = path.join(buildPaths().slopmachineDir, 'utils')
429
+ const packageClaudeSessionScript = path.join(utilsDir, 'package_claude_session.mjs')
430
+ const outputPath = path.join(stagingDir, 'claude-sessions.zip')
431
+ const anchorSession = claudeSessions[0]
432
+
433
+ const packageResult = await runCommand(process.execPath, [
434
+ packageClaudeSessionScript,
435
+ '--cwd',
436
+ workspaceRoot,
437
+ '--session-id',
438
+ anchorSession.sessionId,
439
+ '--label',
440
+ 'claude-sessions',
441
+ '--output',
442
+ outputPath,
443
+ ])
444
+
445
+ if (packageResult.code !== 0) {
446
+ throw new Error(`Failed to package Claude project sessions: ${(packageResult.stderr || packageResult.stdout).trim()}`)
447
+ }
448
+ }
449
+
457
450
  async function createZipArchive(stagingDir, outputPath) {
458
451
  await ensureDir(path.dirname(outputPath))
459
452
 
@@ -550,15 +543,21 @@ async function stageSendDataBundle({ workspaceRoot, repoPath, ownerSessionId, de
550
543
 
551
544
  await exportOpenCodeSession(opencodeCommand, ownerSessionId, path.join(stagingDir, 'owner-session.json'), workspaceRoot)
552
545
 
553
- for (const session of developerSessions.sort((left, right) => left.order - right.order)) {
546
+ const sortedSessions = developerSessions.sort((left, right) => left.order - right.order)
547
+ const claudeSessions = sortedSessions.filter((session) => session.backend === 'claude' || session.backend === 'claude-live')
548
+ const nonClaudeSessions = sortedSessions.filter((session) => session.backend !== 'claude' && session.backend !== 'claude-live')
549
+
550
+ for (const session of nonClaudeSessions) {
554
551
  await exportDeveloperSessionArtifacts(session, workspaceRoot, stagingDir)
555
552
  }
556
553
 
554
+ await exportClaudeProjectArtifacts(claudeSessions, workspaceRoot, stagingDir)
555
+
557
556
  await copyOptionalPath(
558
- path.join(workspaceRoot, SELF_TEST_REPORTS_DIR),
559
- path.join(stagingDir, SELF_TEST_REPORTS_DIR),
557
+ path.join(workspaceRoot, EVALUATION_REPORTS_DIR),
558
+ path.join(stagingDir, EVALUATION_REPORTS_DIR),
560
559
  warnings,
561
- SELF_TEST_REPORTS_DIR,
560
+ EVALUATION_REPORTS_DIR,
562
561
  )
563
562
 
564
563
  const retrospectiveDir = path.join(buildPaths().slopmachineDir, 'retrospectives')