theslopmachine 0.7.3 → 0.7.6

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.
@@ -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
+ }
@@ -9,6 +9,7 @@ const target = path.resolve(process.cwd(), targetInput)
9
9
  const beadsCommand = process.env.BR_COMMAND || 'br'
10
10
 
11
11
  const ROOT_TITLE = 'SlopMachine Workflow'
12
+ const PHASE_KEYS = ['P1', 'P2', 'P3', 'P4', 'P5', 'P6', 'P7', 'P8', 'P9', 'P10']
12
13
  const PHASE_TITLES = [
13
14
  'P1 Clarification',
14
15
  'P2 Planning',
@@ -17,10 +18,11 @@ const PHASE_TITLES = [
17
18
  'P5 Integrated Verification',
18
19
  'P6 Hardening',
19
20
  'P7 Evaluation and Fix Verification',
20
- 'P8 Final Human Decision',
21
+ 'P8 Final Readiness Decision',
21
22
  'P9 Submission Packaging',
22
23
  'P10 Retrospective',
23
24
  ]
25
+ const requestedStartPhase = process.env.SLOPMACHINE_REQUESTED_START_PHASE || null
24
26
 
25
27
  function log(message) {
26
28
  console.log(`[workflow-init] ${message}`)
@@ -115,10 +117,17 @@ async function ensureLifecycleSeeded() {
115
117
 
116
118
  log('Seeding workflow root and P1-P10 phases')
117
119
  const rootId = await createIssue(ROOT_TITLE)
120
+ const requestedStartIndex = requestedStartPhase ? PHASE_KEYS.indexOf(requestedStartPhase) : -1
121
+
122
+ if (requestedStartPhase && requestedStartIndex === -1) {
123
+ die(`Unsupported requested start phase '${requestedStartPhase}'. Use one of: ${PHASE_KEYS.join(', ')}`)
124
+ }
118
125
 
119
126
  let previousPhaseId = null
127
+ const phaseIds = []
120
128
  for (const title of PHASE_TITLES) {
121
129
  const phaseId = await createIssue(title)
130
+ phaseIds.push(phaseId)
122
131
 
123
132
  const parentResult = await runBeads(['update', phaseId, '--parent', rootId], { env: { ...process.env, CI: '1' } })
124
133
  if (parentResult.code !== 0) {
@@ -136,6 +145,17 @@ async function ensureLifecycleSeeded() {
136
145
 
137
146
  previousPhaseId = phaseId
138
147
  }
148
+
149
+ if (requestedStartIndex > 0) {
150
+ log(`Closing phases before requested start phase ${requestedStartPhase}`)
151
+ for (let index = 0; index < requestedStartIndex; index += 1) {
152
+ const closeResult = await runBeads(['close', phaseIds[index]], { env: { ...process.env, CI: '1' } })
153
+ if (closeResult.code !== 0) {
154
+ console.error(`${closeResult.stdout}${closeResult.stderr}`.trim())
155
+ die(`Failed to close seeded phase '${PHASE_TITLES[index]}' before '${requestedStartPhase}'.`)
156
+ }
157
+ }
158
+ }
139
159
  }
140
160
 
141
161
  async function main() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "theslopmachine",
3
- "version": "0.7.3",
3
+ "version": "0.7.6",
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)
@@ -265,6 +315,7 @@ async function runWorkflowBootstrap(paths, targetPath, trackerScript) {
265
315
  env: {
266
316
  ...process.env,
267
317
  BR_COMMAND: trackerCommand || '',
318
+ SLOPMACHINE_REQUESTED_START_PHASE: options.requestedStartPhase || '',
268
319
  HOME: paths.home,
269
320
  },
270
321
  })
@@ -282,6 +333,52 @@ async function writeFileIfMissing(filePath, content) {
282
333
  return true
283
334
  }
284
335
 
336
+ function normalizeProjectMetadataString(value) {
337
+ return typeof value === 'string' ? value : ''
338
+ }
339
+
340
+ function buildProjectMetadata(existingMetadata) {
341
+ const metadata = existingMetadata && typeof existingMetadata === 'object' && !Array.isArray(existingMetadata)
342
+ ? existingMetadata
343
+ : {}
344
+ const projectType = normalizeProjectMetadataString(metadata.project_type).trim().toLowerCase()
345
+
346
+ return {
347
+ prompt: normalizeProjectMetadataString(metadata.prompt),
348
+ project_type: VALID_PROJECT_TYPES.has(projectType) ? projectType : '',
349
+ frontend_language: normalizeProjectMetadataString(metadata.frontend_language),
350
+ backend_language: normalizeProjectMetadataString(metadata.backend_language),
351
+ database: normalizeProjectMetadataString(metadata.database),
352
+ frontend_framework: normalizeProjectMetadataString(metadata.frontend_framework),
353
+ backend_framework: normalizeProjectMetadataString(metadata.backend_framework),
354
+ }
355
+ }
356
+
357
+ function projectMetadataMatchesSchema(existingMetadata, normalizedMetadata) {
358
+ if (!existingMetadata || typeof existingMetadata !== 'object' || Array.isArray(existingMetadata)) {
359
+ return false
360
+ }
361
+
362
+ const expectedKeys = Object.keys(normalizedMetadata)
363
+ const existingKeys = Object.keys(existingMetadata)
364
+
365
+ return existingKeys.length === expectedKeys.length
366
+ && expectedKeys.every((key) => existingMetadata[key] === normalizedMetadata[key])
367
+ }
368
+
369
+ async function ensureProjectMetadata(projectMetadataPath) {
370
+ const existed = await pathExists(projectMetadataPath)
371
+ const existingMetadata = await readJsonIfExists(projectMetadataPath)
372
+ const normalizedMetadata = buildProjectMetadata(existingMetadata)
373
+
374
+ if (projectMetadataMatchesSchema(existingMetadata, normalizedMetadata)) {
375
+ return
376
+ }
377
+
378
+ log(existed ? 'Normalizing parent metadata.json' : 'Creating parent metadata.json')
379
+ await writeJson(projectMetadataPath, normalizedMetadata)
380
+ }
381
+
285
382
  async function createInitialPhaseArtifacts(targetPath, options) {
286
383
  const questionsContent = `# Questions\n\n` +
287
384
  `This file records only prompt items that needed interpretation because they were unclear, incomplete, or materially ambiguous.\n\n` +
@@ -501,21 +598,7 @@ async function createRepoStructure(targetPath, agentsTemplate, options) {
501
598
  )
502
599
 
503
600
  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
- }
601
+ await ensureProjectMetadata(projectMetadataPath)
519
602
 
520
603
  const workflowMetadataPath = path.join(targetPath, '.ai', 'metadata.json')
521
604
  if (!(await pathExists(workflowMetadataPath))) {
@@ -617,12 +700,19 @@ async function maybeOpenOpencode(targetPath, openAfterInit) {
617
700
 
618
701
  export async function runInit(args = []) {
619
702
  const paths = buildPaths()
620
- const { openAfterInit, targetInput, adoptExisting, requestedStartPhase } = parseInitArgs(args)
703
+ const { openAfterInit, targetInput, adoptExisting, continueFromExisting, requestedStartPhase } = parseInitArgs(args)
621
704
  const { trackerScript, agentsTemplate } = await assertRequiredFiles(paths)
622
- const targetPath = await resolveTarget(targetInput)
705
+ const initialTargetPath = await resolveTarget(targetInput)
706
+ const targetPath = shouldUseParentAsWorkspaceRoot(initialTargetPath, { continueFromExisting, targetInput })
707
+ ? path.dirname(initialTargetPath)
708
+ : initialTargetPath
623
709
 
624
710
  log(`Target: ${targetPath}`)
625
711
 
712
+ if (targetPath !== initialTargetPath) {
713
+ log(`Detected existing repo/ working directory at ${initialTargetPath}; using ${targetPath} as the workspace root`)
714
+ }
715
+
626
716
  if (adoptExisting) {
627
717
  const adoptionSummary = await prepareExistingProjectForAdoption(targetPath)
628
718
  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