theslopmachine 0.7.2 → 0.7.5

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 (33) hide show
  1. package/MANUAL.md +1 -1
  2. package/README.md +11 -1
  3. package/RELEASE.md +16 -0
  4. package/assets/agents/developer.md +2 -1
  5. package/assets/agents/slopmachine-claude.md +28 -20
  6. package/assets/agents/slopmachine.md +22 -18
  7. package/assets/claude/agents/developer.md +2 -1
  8. package/assets/skills/beads-operations/SKILL.md +1 -1
  9. package/assets/skills/clarification-gate/SKILL.md +6 -4
  10. package/assets/skills/claude-worker-management/SKILL.md +6 -6
  11. package/assets/skills/developer-session-lifecycle/SKILL.md +11 -9
  12. package/assets/skills/development-guidance/SKILL.md +1 -0
  13. package/assets/skills/evaluation-triage/SKILL.md +3 -2
  14. package/assets/skills/final-evaluation-orchestration/SKILL.md +12 -19
  15. package/assets/skills/hardening-gate/SKILL.md +1 -0
  16. package/assets/skills/planning-guidance/SKILL.md +1 -0
  17. package/assets/skills/scaffold-guidance/SKILL.md +1 -0
  18. package/assets/skills/submission-packaging/SKILL.md +14 -11
  19. package/assets/skills/verification-gates/SKILL.md +5 -4
  20. package/assets/slopmachine/scaffold-playbooks/docker-shared-contract.md +4 -0
  21. package/assets/slopmachine/templates/AGENTS.md +3 -1
  22. package/assets/slopmachine/templates/CLAUDE.md +3 -1
  23. package/assets/slopmachine/utils/__pycache__/normalize_claude_session.cpython-311.pyc +0 -0
  24. package/assets/slopmachine/utils/claude_live_launch.mjs +2 -2
  25. package/assets/slopmachine/utils/normalize_claude_session.py +162 -27
  26. package/assets/slopmachine/utils/package_claude_session.mjs +120 -23
  27. package/assets/slopmachine/utils/prepare_evaluation_prompt.mjs +41 -0
  28. package/assets/slopmachine/workflow-init.js +1 -1
  29. package/package.json +1 -1
  30. package/src/cli.js +1 -1
  31. package/src/constants.js +1 -0
  32. package/src/init.js +117 -28
  33. package/src/send-data.js +4 -4
@@ -9,6 +9,22 @@ import { parseArgs, emitFailure, emitSuccess, resolveClaudeSessionPath } from '.
9
9
 
10
10
  const argv = parseArgs(process.argv.slice(2))
11
11
 
12
+ function parseSessionIds(value) {
13
+ return String(value || '')
14
+ .split(',')
15
+ .map((entry) => entry.trim())
16
+ .filter(Boolean)
17
+ }
18
+
19
+ function collectRequestedSessionIds(argv) {
20
+ const requested = [
21
+ ...parseSessionIds(argv['session-ids']),
22
+ ...parseSessionIds(argv['session-id']),
23
+ ]
24
+
25
+ return [...new Set(requested)]
26
+ }
27
+
12
28
  async function pathExists(targetPath) {
13
29
  try {
14
30
  await fs.access(targetPath)
@@ -76,42 +92,123 @@ async function createZipArchive(sourceDir, outputPath) {
76
92
 
77
93
  async function normalizeClaudeJsonlFiles(projectDir) {
78
94
  const normalizerScript = path.join(path.dirname(new URL(import.meta.url).pathname), 'normalize_claude_session.py')
79
- const entries = await fs.readdir(projectDir, { withFileTypes: true }).catch(() => [])
80
- const jsonlFiles = entries
81
- .filter((entry) => entry.isFile() && entry.name.endsWith('.jsonl'))
82
- .map((entry) => path.join(projectDir, entry.name))
83
-
84
- for (const filePath of jsonlFiles) {
85
- const tempOutputPath = `${filePath}.normalized`
86
- const result = await run('python3', [normalizerScript, filePath, '--output', tempOutputPath], projectDir)
87
- if (result.code !== 0) {
88
- throw new Error(`Failed to normalize Claude session file ${path.basename(filePath)}: ${(result.stderr || result.stdout).trim()}`)
95
+ const normalizedDir = path.join(tempRootForNormalize(projectDir), path.basename(projectDir))
96
+ await fs.rm(normalizedDir, { recursive: true, force: true }).catch(() => {})
97
+ const result = await run('python3', [normalizerScript, projectDir, '--output', normalizedDir, '--recursive'], projectDir)
98
+ if (result.code !== 0) {
99
+ throw new Error(`Failed to recursively normalize Claude session files: ${(result.stderr || result.stdout).trim()}`)
100
+ }
101
+ await fs.rm(projectDir, { recursive: true, force: true })
102
+ await fs.rename(normalizedDir, projectDir)
103
+ }
104
+
105
+ function tempRootForNormalize(projectDir) {
106
+ return path.join(path.dirname(projectDir), '.normalized-work')
107
+ }
108
+
109
+ async function resolveTrackedSessionArtifacts(sessionIds, cwd) {
110
+ const resolved = []
111
+
112
+ for (const sessionId of sessionIds) {
113
+ const transcriptPath = await resolveClaudeSessionPath(sessionId, cwd)
114
+ if (!(await pathExists(transcriptPath))) {
115
+ throw new Error(`Claude transcript not found for tracked session ${sessionId}`)
89
116
  }
90
- await fs.rename(tempOutputPath, filePath)
117
+
118
+ const sourceProjectDir = path.dirname(transcriptPath)
119
+ const sourceSessionDir = path.join(sourceProjectDir, sessionId)
120
+
121
+ resolved.push({
122
+ sessionId,
123
+ transcriptPath,
124
+ sourceProjectDir,
125
+ sourceSessionDir,
126
+ hasSessionDir: await pathExists(sourceSessionDir),
127
+ })
128
+ }
129
+
130
+ return resolved
131
+ }
132
+
133
+ function groupResolvedSessionsByProjectDir(resolvedSessions) {
134
+ const groups = new Map()
135
+
136
+ for (const session of resolvedSessions) {
137
+ if (!groups.has(session.sourceProjectDir)) {
138
+ groups.set(session.sourceProjectDir, [])
139
+ }
140
+ groups.get(session.sourceProjectDir).push(session)
141
+ }
142
+
143
+ return [...groups.entries()].map(([sourceProjectDir, sessions]) => ({ sourceProjectDir, sessions }))
144
+ }
145
+
146
+ async function stageTrackedSessionArtifacts(projectGroups, tempRoot) {
147
+ const multiProject = projectGroups.length > 1
148
+ const packageRoot = path.join(
149
+ tempRoot,
150
+ multiProject ? 'claude-projects' : path.basename(projectGroups[0].sourceProjectDir),
151
+ )
152
+ const included = []
153
+ const projects = []
154
+
155
+ await fs.mkdir(packageRoot, { recursive: true })
156
+
157
+ for (const group of projectGroups) {
158
+ const groupRoot = multiProject
159
+ ? path.join(packageRoot, path.basename(group.sourceProjectDir))
160
+ : packageRoot
161
+
162
+ await fs.mkdir(groupRoot, { recursive: true })
163
+
164
+ for (const session of group.sessions) {
165
+ const transcriptTargetPath = path.join(groupRoot, `${session.sessionId}.jsonl`)
166
+ await fs.copyFile(session.transcriptPath, transcriptTargetPath)
167
+ included.push(path.relative(packageRoot, transcriptTargetPath))
168
+
169
+ if (session.hasSessionDir) {
170
+ const sessionDirTargetPath = path.join(groupRoot, session.sessionId)
171
+ await fs.cp(session.sourceSessionDir, sessionDirTargetPath, { recursive: true })
172
+ included.push(path.relative(packageRoot, sessionDirTargetPath))
173
+ }
174
+ }
175
+
176
+ projects.push({
177
+ source_project_dir: group.sourceProjectDir,
178
+ packaged_dir: path.relative(packageRoot, groupRoot) || '.',
179
+ session_ids: group.sessions.map((session) => session.sessionId),
180
+ })
181
+ }
182
+
183
+ return {
184
+ packageRoot,
185
+ included: included.sort((left, right) => left.localeCompare(right)),
186
+ projects,
91
187
  }
92
188
  }
93
189
 
94
190
  try {
95
- const sessionId = argv['session-id']
96
- const transcriptPath = await resolveClaudeSessionPath(sessionId, argv.cwd)
97
- if (!(await pathExists(transcriptPath))) {
98
- throw new Error(`Claude transcript not found for session ${sessionId}`)
191
+ const sessionIds = collectRequestedSessionIds(argv)
192
+ if (sessionIds.length === 0) {
193
+ throw new Error('Missing --session-ids <id1,id2,...> or --session-id <id>')
99
194
  }
100
195
 
101
- const sourceProjectDir = path.dirname(transcriptPath)
196
+ const resolvedSessions = await resolveTrackedSessionArtifacts(sessionIds, argv.cwd)
197
+ const projectGroups = groupResolvedSessionsByProjectDir(resolvedSessions)
102
198
  const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'slopmachine-claude-project-'))
103
- const projectDir = path.join(tempRoot, path.basename(sourceProjectDir))
104
- await fs.cp(sourceProjectDir, projectDir, { recursive: true })
105
- await normalizeClaudeJsonlFiles(projectDir)
106
- const included = (await fs.readdir(projectDir).catch(() => [])).sort((left, right) => left.localeCompare(right))
199
+ const { packageRoot, included, projects } = await stageTrackedSessionArtifacts(projectGroups, tempRoot)
200
+ await normalizeClaudeJsonlFiles(packageRoot)
107
201
 
108
202
  try {
109
- await createZipArchive(projectDir, argv.output)
110
- emitSuccess(sessionId, {
203
+ await createZipArchive(packageRoot, argv.output)
204
+ emitSuccess(sessionIds[0], {
111
205
  output: argv.output,
112
- project_dir: sourceProjectDir,
206
+ project_dir: projectGroups.length === 1 ? projectGroups[0].sourceProjectDir : null,
207
+ project_dirs: projectGroups.map((group) => group.sourceProjectDir),
113
208
  label: argv.label || null,
209
+ session_ids: sessionIds,
114
210
  included,
211
+ projects,
115
212
  normalized: true,
116
213
  })
117
214
  } finally {
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs/promises'
4
+ import path from 'node:path'
5
+
6
+ import { parseArgs } from './claude_worker_common.mjs'
7
+
8
+ const argv = parseArgs(process.argv.slice(2))
9
+
10
+ function fail(message) {
11
+ process.stderr.write(`${message}\n`)
12
+ process.exit(1)
13
+ }
14
+
15
+ try {
16
+ const promptFile = argv['prompt-file'] ? path.resolve(argv['prompt-file']) : null
17
+ const projectPromptFile = argv['project-prompt-file'] ? path.resolve(argv['project-prompt-file']) : null
18
+
19
+ if (!promptFile) {
20
+ fail('Missing --prompt-file')
21
+ }
22
+
23
+ let promptText = await fs.readFile(promptFile, 'utf8')
24
+
25
+ if (promptText.includes('{prompt}')) {
26
+ if (!projectPromptFile) {
27
+ fail('Missing --project-prompt-file for a prompt template that requires {prompt}.')
28
+ }
29
+ const projectPrompt = await fs.readFile(projectPromptFile, 'utf8')
30
+ promptText = promptText.replaceAll('{prompt}', projectPrompt)
31
+ }
32
+
33
+ const unresolvedPlaceholders = [...promptText.matchAll(/\{[a-z_]+\}/g)].map((match) => match[0])
34
+ if (unresolvedPlaceholders.length > 0) {
35
+ fail(`Unsupported unresolved prompt placeholders remain: ${[...new Set(unresolvedPlaceholders)].join(', ')}`)
36
+ }
37
+
38
+ process.stdout.write(promptText)
39
+ } catch (error) {
40
+ fail(error instanceof Error ? error.message : String(error))
41
+ }
@@ -17,7 +17,7 @@ const PHASE_TITLES = [
17
17
  'P5 Integrated Verification',
18
18
  'P6 Hardening',
19
19
  'P7 Evaluation and Fix Verification',
20
- 'P8 Final Human Decision',
20
+ 'P8 Final Readiness Decision',
21
21
  'P9 Submission Packaging',
22
22
  'P10 Retrospective',
23
23
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "theslopmachine",
3
- "version": "0.7.2",
3
+ "version": "0.7.5",
4
4
  "description": "SlopMachine installer and project bootstrap CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/cli.js CHANGED
@@ -10,7 +10,7 @@ function printHelp() {
10
10
  Commands:
11
11
  setup Configure theslopmachine in the local user environment
12
12
  upgrade Install theslopmachine@latest globally and rerun setup
13
- init Bootstrap a workspace (--adopt wraps an existing project, -o opens OpenCode in repo/)
13
+ init Bootstrap a workspace (--adopt wraps an existing project, --continue-from PX is a smoother existing-project alias and auto-wraps when run inside repo/, -o opens OpenCode in repo/)
14
14
  set-token Store the upload token in machine config
15
15
  send-data Upload workflow artifacts to Supabase
16
16
  help Show this help text`)
package/src/constants.js CHANGED
@@ -79,6 +79,7 @@ export const REQUIRED_SLOPMACHINE_FILES = [
79
79
  "utils/claude_export_session.mjs",
80
80
  "utils/export_ai_session.mjs",
81
81
  "utils/package_claude_session.mjs",
82
+ "utils/prepare_evaluation_prompt.mjs",
82
83
  "utils/prepare_strict_audit_workspace.mjs",
83
84
  "utils/claude_wait_for_rate_limit_reset.mjs",
84
85
  "utils/claude_wait_for_rate_limit_reset.sh",
package/src/init.js CHANGED
@@ -3,7 +3,19 @@ import { randomUUID } from 'node:crypto'
3
3
  import path from 'node:path'
4
4
 
5
5
  import { buildPaths } from './constants.js'
6
- import { ensureDir, log, pathExists, resolveCommand, runCommand, warn } from './utils.js'
6
+ import { ensureDir, log, pathExists, readJsonIfExists, resolveCommand, runCommand, warn, writeJson } from './utils.js'
7
+
8
+ const ROOT_GITIGNORE_ENTRIES = [
9
+ '/*',
10
+ '!/.gitignore',
11
+ '!/repo/',
12
+ '!/repo/**',
13
+ '!/docs/',
14
+ '!/docs/**',
15
+ '!/.tmp/',
16
+ '!/.tmp/**',
17
+ '!/metadata.json',
18
+ ]
7
19
 
8
20
  const GITIGNORE_ENTRIES = [
9
21
  '.DS_Store',
@@ -25,7 +37,7 @@ const GITIGNORE_ENTRIES = [
25
37
  ]
26
38
 
27
39
  const ALLOWED_EXISTING_ENTRIES = new Set(['.DS_Store', '.git'])
28
- const INITIAL_COMMIT_PATHS = ['.gitignore', '.tmp', 'docs', 'metadata.json', 'repo', 'sessions']
40
+ const INITIAL_COMMIT_PATHS = ['.gitignore', '.tmp', 'docs', 'metadata.json', 'repo']
29
41
  const ADOPTION_ROOT_KEEP = new Set([
30
42
  '.DS_Store',
31
43
  '.git',
@@ -39,6 +51,7 @@ const ADOPTION_ROOT_KEEP = new Set([
39
51
  'repo',
40
52
  ])
41
53
  const VALID_START_PHASES = new Set(['P1', 'P2', 'P3', 'P4', 'P5', 'P6', 'P7', 'P8', 'P9', 'P10'])
54
+ const VALID_PROJECT_TYPES = new Set(['fullstack', 'backend', 'android', 'ios', 'desktop', 'web'])
42
55
  const PHASE_LABELS = {
43
56
  P1: 'P1 Clarification',
44
57
  P2: 'P2 Planning',
@@ -47,7 +60,7 @@ const PHASE_LABELS = {
47
60
  P5: 'P5 Integrated Verification',
48
61
  P6: 'P6 Hardening',
49
62
  P7: 'P7 Evaluation and Fix Verification',
50
- P8: 'P8 Final Human Decision',
63
+ P8: 'P8 Final Readiness Decision',
51
64
  P9: 'P9 Submission Packaging',
52
65
  P10: 'P10 Retrospective',
53
66
  }
@@ -84,9 +97,20 @@ async function resolveBrCommand(paths) {
84
97
  function parseInitArgs(args) {
85
98
  let openAfterInit = false
86
99
  let adoptExisting = false
100
+ let continueFromExisting = false
87
101
  let targetInput = '.'
88
102
  let requestedStartPhase = null
89
103
 
104
+ function setRequestedStartPhase(optionName, phase) {
105
+ if (!VALID_START_PHASES.has(phase)) {
106
+ throw new Error(`Unsupported start phase '${phase}'. Use one of: ${Array.from(VALID_START_PHASES).join(', ')}`)
107
+ }
108
+ if (requestedStartPhase && requestedStartPhase !== phase) {
109
+ throw new Error(`Conflicting start phases: '${requestedStartPhase}' and '${phase}'. Use only one of --phase or --continue-from.`)
110
+ }
111
+ requestedStartPhase = phase
112
+ }
113
+
90
114
  for (let index = 0; index < args.length; index += 1) {
91
115
  const arg = args[index]
92
116
 
@@ -105,20 +129,33 @@ function parseInitArgs(args) {
105
129
  if (!nextArg) {
106
130
  throw new Error('Missing value for --phase')
107
131
  }
108
- if (!VALID_START_PHASES.has(nextArg)) {
109
- throw new Error(`Unsupported start phase '${nextArg}'. Use one of: ${Array.from(VALID_START_PHASES).join(', ')}`)
110
- }
111
- requestedStartPhase = nextArg
132
+ setRequestedStartPhase('--phase', nextArg)
112
133
  index += 1
113
134
  continue
114
135
  }
115
136
 
116
137
  if (arg.startsWith('--phase=')) {
117
138
  const phase = arg.slice('--phase='.length)
118
- if (!VALID_START_PHASES.has(phase)) {
119
- throw new Error(`Unsupported start phase '${phase}'. Use one of: ${Array.from(VALID_START_PHASES).join(', ')}`)
139
+ setRequestedStartPhase('--phase', phase)
140
+ continue
141
+ }
142
+
143
+ if (arg === '--continue-from') {
144
+ const nextArg = args[index + 1]
145
+ if (!nextArg) {
146
+ throw new Error('Missing value for --continue-from')
120
147
  }
121
- requestedStartPhase = phase
148
+ adoptExisting = true
149
+ continueFromExisting = true
150
+ setRequestedStartPhase('--continue-from', nextArg)
151
+ index += 1
152
+ continue
153
+ }
154
+
155
+ if (arg.startsWith('--continue-from=')) {
156
+ adoptExisting = true
157
+ continueFromExisting = true
158
+ setRequestedStartPhase('--continue-from', arg.slice('--continue-from='.length))
122
159
  continue
123
160
  }
124
161
 
@@ -129,7 +166,13 @@ function parseInitArgs(args) {
129
166
  targetInput = arg
130
167
  }
131
168
 
132
- return { openAfterInit, targetInput, adoptExisting, requestedStartPhase }
169
+ return { openAfterInit, targetInput, adoptExisting, continueFromExisting, requestedStartPhase }
170
+ }
171
+
172
+ function shouldUseParentAsWorkspaceRoot(targetPath, options) {
173
+ return options.continueFromExisting
174
+ && options.targetInput === '.'
175
+ && path.basename(targetPath) === 'repo'
133
176
  }
134
177
 
135
178
  async function assertRequiredFiles(paths) {
@@ -238,6 +281,13 @@ async function ensureGitignore(targetPath) {
238
281
  const existingLines = new Set(existingContent.split(/\r?\n/))
239
282
 
240
283
  const linesToAppend = []
284
+ for (const entry of ROOT_GITIGNORE_ENTRIES) {
285
+ if (!existingLines.has(entry)) {
286
+ linesToAppend.push(entry)
287
+ log(`Added .gitignore entry: ${entry}`)
288
+ }
289
+ }
290
+
241
291
  for (const entry of GITIGNORE_ENTRIES) {
242
292
  if (!existingLines.has(entry)) {
243
293
  linesToAppend.push(entry)
@@ -282,6 +332,52 @@ async function writeFileIfMissing(filePath, content) {
282
332
  return true
283
333
  }
284
334
 
335
+ function normalizeProjectMetadataString(value) {
336
+ return typeof value === 'string' ? value : ''
337
+ }
338
+
339
+ function buildProjectMetadata(existingMetadata) {
340
+ const metadata = existingMetadata && typeof existingMetadata === 'object' && !Array.isArray(existingMetadata)
341
+ ? existingMetadata
342
+ : {}
343
+ const projectType = normalizeProjectMetadataString(metadata.project_type).trim().toLowerCase()
344
+
345
+ return {
346
+ prompt: normalizeProjectMetadataString(metadata.prompt),
347
+ project_type: VALID_PROJECT_TYPES.has(projectType) ? projectType : '',
348
+ frontend_language: normalizeProjectMetadataString(metadata.frontend_language),
349
+ backend_language: normalizeProjectMetadataString(metadata.backend_language),
350
+ database: normalizeProjectMetadataString(metadata.database),
351
+ frontend_framework: normalizeProjectMetadataString(metadata.frontend_framework),
352
+ backend_framework: normalizeProjectMetadataString(metadata.backend_framework),
353
+ }
354
+ }
355
+
356
+ function projectMetadataMatchesSchema(existingMetadata, normalizedMetadata) {
357
+ if (!existingMetadata || typeof existingMetadata !== 'object' || Array.isArray(existingMetadata)) {
358
+ return false
359
+ }
360
+
361
+ const expectedKeys = Object.keys(normalizedMetadata)
362
+ const existingKeys = Object.keys(existingMetadata)
363
+
364
+ return existingKeys.length === expectedKeys.length
365
+ && expectedKeys.every((key) => existingMetadata[key] === normalizedMetadata[key])
366
+ }
367
+
368
+ async function ensureProjectMetadata(projectMetadataPath) {
369
+ const existed = await pathExists(projectMetadataPath)
370
+ const existingMetadata = await readJsonIfExists(projectMetadataPath)
371
+ const normalizedMetadata = buildProjectMetadata(existingMetadata)
372
+
373
+ if (projectMetadataMatchesSchema(existingMetadata, normalizedMetadata)) {
374
+ return
375
+ }
376
+
377
+ log(existed ? 'Normalizing parent metadata.json' : 'Creating parent metadata.json')
378
+ await writeJson(projectMetadataPath, normalizedMetadata)
379
+ }
380
+
285
381
  async function createInitialPhaseArtifacts(targetPath, options) {
286
382
  const questionsContent = `# Questions\n\n` +
287
383
  `This file records only prompt items that needed interpretation because they were unclear, incomplete, or materially ambiguous.\n\n` +
@@ -501,21 +597,7 @@ async function createRepoStructure(targetPath, agentsTemplate, options) {
501
597
  )
502
598
 
503
599
  const projectMetadataPath = path.join(targetPath, 'metadata.json')
504
- if (!(await pathExists(projectMetadataPath))) {
505
- log('Creating parent metadata.json')
506
- await fs.writeFile(projectMetadataPath, `${JSON.stringify({
507
- prompt: null,
508
- project_type: null,
509
- frontend_language: null,
510
- backend_language: null,
511
- database: null,
512
- session_id: null,
513
- frontend_framework: null,
514
- backend_framework: null,
515
- bootstrap_mode: options.adoptExisting ? 'adopt' : 'new',
516
- requested_start_phase: options.requestedStartPhase,
517
- }, null, 2)}\n`, 'utf8')
518
- }
600
+ await ensureProjectMetadata(projectMetadataPath)
519
601
 
520
602
  const workflowMetadataPath = path.join(targetPath, '.ai', 'metadata.json')
521
603
  if (!(await pathExists(workflowMetadataPath))) {
@@ -617,12 +699,19 @@ async function maybeOpenOpencode(targetPath, openAfterInit) {
617
699
 
618
700
  export async function runInit(args = []) {
619
701
  const paths = buildPaths()
620
- const { openAfterInit, targetInput, adoptExisting, requestedStartPhase } = parseInitArgs(args)
702
+ const { openAfterInit, targetInput, adoptExisting, continueFromExisting, requestedStartPhase } = parseInitArgs(args)
621
703
  const { trackerScript, agentsTemplate } = await assertRequiredFiles(paths)
622
- const targetPath = await resolveTarget(targetInput)
704
+ const initialTargetPath = await resolveTarget(targetInput)
705
+ const targetPath = shouldUseParentAsWorkspaceRoot(initialTargetPath, { continueFromExisting, targetInput })
706
+ ? path.dirname(initialTargetPath)
707
+ : initialTargetPath
623
708
 
624
709
  log(`Target: ${targetPath}`)
625
710
 
711
+ if (targetPath !== initialTargetPath) {
712
+ log(`Detected existing repo/ working directory at ${initialTargetPath}; using ${targetPath} as the workspace root`)
713
+ }
714
+
626
715
  if (adoptExisting) {
627
716
  const adoptionSummary = await prepareExistingProjectForAdoption(targetPath)
628
717
  if (adoptionSummary.movedEntries.length > 0) {
package/src/send-data.js CHANGED
@@ -428,14 +428,14 @@ async function exportClaudeProjectArtifacts(claudeSessions, workspaceRoot, stagi
428
428
  const utilsDir = path.join(buildPaths().slopmachineDir, 'utils')
429
429
  const packageClaudeSessionScript = path.join(utilsDir, 'package_claude_session.mjs')
430
430
  const outputPath = path.join(stagingDir, 'claude-sessions.zip')
431
- const anchorSession = claudeSessions[0]
431
+ const trackedSessionIds = [...new Set(claudeSessions.map((session) => session.sessionId).filter(Boolean))]
432
432
 
433
433
  const packageResult = await runCommand(process.execPath, [
434
434
  packageClaudeSessionScript,
435
435
  '--cwd',
436
436
  workspaceRoot,
437
- '--session-id',
438
- anchorSession.sessionId,
437
+ '--session-ids',
438
+ trackedSessionIds.join(','),
439
439
  '--label',
440
440
  'claude-sessions',
441
441
  '--output',
@@ -443,7 +443,7 @@ async function exportClaudeProjectArtifacts(claudeSessions, workspaceRoot, stagi
443
443
  ])
444
444
 
445
445
  if (packageResult.code !== 0) {
446
- throw new Error(`Failed to package Claude project sessions: ${(packageResult.stderr || packageResult.stdout).trim()}`)
446
+ throw new Error(`Failed to package tracked Claude sessions: ${(packageResult.stderr || packageResult.stdout).trim()}`)
447
447
  }
448
448
  }
449
449