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.
- package/MANUAL.md +1 -1
- package/README.md +13 -1
- package/RELEASE.md +16 -0
- package/assets/agents/developer.md +2 -1
- package/assets/agents/slopmachine-claude.md +31 -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 +43 -6
- package/assets/skills/developer-session-lifecycle/SKILL.md +13 -9
- package/assets/skills/evaluation-triage/SKILL.md +3 -2
- package/assets/skills/final-evaluation-orchestration/SKILL.md +12 -19
- package/assets/skills/submission-packaging/SKILL.md +14 -11
- package/assets/skills/verification-gates/SKILL.md +4 -4
- package/assets/slopmachine/templates/AGENTS.md +2 -1
- package/assets/slopmachine/templates/CLAUDE.md +2 -1
- package/assets/slopmachine/utils/__pycache__/normalize_claude_session.cpython-311.pyc +0 -0
- package/assets/slopmachine/utils/claude_live_common.mjs +45 -6
- package/assets/slopmachine/utils/claude_live_launch.mjs +4 -4
- 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 +21 -1
- package/package.json +1 -1
- package/src/cli.js +1 -1
- package/src/constants.js +1 -0
- package/src/init.js +118 -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
|
+
}
|
|
@@ -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
|
|
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
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)
|
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|