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.
- package/MANUAL.md +1 -1
- package/README.md +11 -1
- package/RELEASE.md +16 -0
- package/assets/agents/developer.md +2 -1
- package/assets/agents/slopmachine-claude.md +28 -20
- package/assets/agents/slopmachine.md +22 -18
- package/assets/claude/agents/developer.md +2 -1
- package/assets/skills/beads-operations/SKILL.md +1 -1
- package/assets/skills/clarification-gate/SKILL.md +6 -4
- package/assets/skills/claude-worker-management/SKILL.md +6 -6
- package/assets/skills/developer-session-lifecycle/SKILL.md +11 -9
- package/assets/skills/development-guidance/SKILL.md +1 -0
- package/assets/skills/evaluation-triage/SKILL.md +3 -2
- package/assets/skills/final-evaluation-orchestration/SKILL.md +12 -19
- package/assets/skills/hardening-gate/SKILL.md +1 -0
- package/assets/skills/planning-guidance/SKILL.md +1 -0
- package/assets/skills/scaffold-guidance/SKILL.md +1 -0
- package/assets/skills/submission-packaging/SKILL.md +14 -11
- package/assets/skills/verification-gates/SKILL.md +5 -4
- package/assets/slopmachine/scaffold-playbooks/docker-shared-contract.md +4 -0
- package/assets/slopmachine/templates/AGENTS.md +3 -1
- package/assets/slopmachine/templates/CLAUDE.md +3 -1
- package/assets/slopmachine/utils/__pycache__/normalize_claude_session.cpython-311.pyc +0 -0
- package/assets/slopmachine/utils/claude_live_launch.mjs +2 -2
- package/assets/slopmachine/utils/normalize_claude_session.py +162 -27
- package/assets/slopmachine/utils/package_claude_session.mjs +120 -23
- package/assets/slopmachine/utils/prepare_evaluation_prompt.mjs +41 -0
- package/assets/slopmachine/workflow-init.js +1 -1
- package/package.json +1 -1
- package/src/cli.js +1 -1
- package/src/constants.js +1 -0
- package/src/init.js +117 -28
- 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
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
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
|
|
104
|
-
await
|
|
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(
|
|
110
|
-
emitSuccess(
|
|
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
|
+
}
|
package/package.json
CHANGED
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'
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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-
|
|
438
|
-
|
|
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
|
|
446
|
+
throw new Error(`Failed to package tracked Claude sessions: ${(packageResult.stderr || packageResult.stdout).trim()}`)
|
|
447
447
|
}
|
|
448
448
|
}
|
|
449
449
|
|