theslopmachine 0.6.1 → 0.6.2
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/assets/agents/slopmachine-claude.md +3 -3
- package/assets/skills/claude-worker-management/SKILL.md +6 -4
- package/assets/slopmachine/utils/claude_create_session.mjs +17 -5
- package/assets/slopmachine/utils/claude_resume_session.mjs +17 -5
- package/assets/slopmachine/utils/claude_worker_common.mjs +33 -11
- package/package.json +1 -1
|
@@ -390,10 +390,10 @@ Timeout rule:
|
|
|
390
390
|
- when you call the Claude create or resume wrappers through the OpenCode Bash tool, use a long-running timeout of at least `3600000` ms (1 hour)
|
|
391
391
|
- do not use ordinary short Bash timeouts for Claude worker turns
|
|
392
392
|
|
|
393
|
-
Use wrapper
|
|
393
|
+
Use wrapper files as the owner-facing contract:
|
|
394
394
|
|
|
395
|
-
-
|
|
396
|
-
-
|
|
395
|
+
- read the wrapper `result-file` after process completion and use that as the semantic Claude response contract
|
|
396
|
+
- treat wrapper terminal stdout as only a tiny pointer or status channel
|
|
397
397
|
- for long-running or flaky calls, inspect the wrapper `state-file` and `result-file` rather than treating Bash process lifetime alone as the source of truth
|
|
398
398
|
|
|
399
399
|
Do not paste raw Claude JSON payloads into owner prompts, Beads comments, or metadata fields.
|
|
@@ -20,8 +20,9 @@ Use this skill whenever `slopmachine-claude` needs to create, resume, or message
|
|
|
20
20
|
- do not read Claude transcript files as the normal communication channel
|
|
21
21
|
- communicate with the Claude worker through the packaged wrapper scripts in `~/slopmachine/utils/`
|
|
22
22
|
- treat raw Claude stdout and stderr as trace artifacts written to files, not as owner-session context
|
|
23
|
-
-
|
|
24
|
-
-
|
|
23
|
+
- treat the wrapper `result-file` as the semantic source of truth in normal owner flow
|
|
24
|
+
- treat terminal stdout from the wrapper as only a tiny pointer or status channel
|
|
25
|
+
- always capture the session id and normalized result from the `result-file`
|
|
25
26
|
- always re-pass `--agent developer` on every call, even when resuming an existing session
|
|
26
27
|
- always constrain Claude to a single-session developer lane by limiting tools to `Read Write Edit Bash Glob Grep`
|
|
27
28
|
- do not allow Claude internal agent fan-out in the normal developer path
|
|
@@ -68,9 +69,9 @@ node ~/slopmachine/utils/claude_resume_session.mjs --cwd "$PWD" --session-id <se
|
|
|
68
69
|
|
|
69
70
|
## Result capture rule
|
|
70
71
|
|
|
71
|
-
The wrapper scripts should
|
|
72
|
+
The wrapper scripts should pipe the raw Claude JSON output to file, parse it after process exit, and persist a normalized `result-file` plus a live `state-file`.
|
|
72
73
|
|
|
73
|
-
Use
|
|
74
|
+
Use the `result-file` fields only:
|
|
74
75
|
|
|
75
76
|
- `sid`
|
|
76
77
|
- `res`
|
|
@@ -84,6 +85,7 @@ Treat `res` as the worker's answer.
|
|
|
84
85
|
Do not feed raw Claude JSON into the owner session.
|
|
85
86
|
Do not rely on transcript scraping for normal turn-to-turn orchestration.
|
|
86
87
|
Do not rely on Bash stdout alone when the wrapper state or result files provide a clearer source of truth.
|
|
88
|
+
Read `result-file` after process completion before deciding the next owner turn.
|
|
87
89
|
|
|
88
90
|
## Developer-slot continuity
|
|
89
91
|
|
|
@@ -18,25 +18,37 @@ try {
|
|
|
18
18
|
})
|
|
19
19
|
|
|
20
20
|
if (failure || !parsed || parsed.is_error === true) {
|
|
21
|
-
|
|
21
|
+
const resultPayload = {
|
|
22
22
|
ok: false,
|
|
23
23
|
code: failure?.code || 'claude_create_failed',
|
|
24
24
|
msg: failure?.msg || 'claude_create_failed',
|
|
25
25
|
sid: failure?.sid || null,
|
|
26
|
+
}
|
|
27
|
+
await writeJsonIfNeeded(argv['result-file'], resultPayload)
|
|
28
|
+
emitFailure(failure?.code || 'claude_create_failed', failure?.msg || 'claude_create_failed', {
|
|
29
|
+
sid: failure?.sid || null,
|
|
30
|
+
result_file: argv['result-file'] || null,
|
|
31
|
+
state_file: argv['state-file'] || null,
|
|
26
32
|
})
|
|
27
|
-
emitFailure(failure?.code || 'claude_create_failed', failure?.msg || 'claude_create_failed', failure?.sid ? { sid: failure.sid } : {})
|
|
28
33
|
process.exit(1)
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
const compact = compactClaudeResult(parsed)
|
|
32
37
|
await writeJsonIfNeeded(argv['result-file'], { ok: true, sid: compact.sid, res: compact.res })
|
|
33
|
-
emitSuccess(compact.sid,
|
|
38
|
+
emitSuccess(compact.sid, {
|
|
39
|
+
result_file: argv['result-file'] || null,
|
|
40
|
+
state_file: argv['state-file'] || null,
|
|
41
|
+
})
|
|
34
42
|
} catch (error) {
|
|
35
|
-
|
|
43
|
+
const resultPayload = {
|
|
36
44
|
ok: false,
|
|
37
45
|
code: 'claude_create_exception',
|
|
38
46
|
msg: error instanceof Error ? error.message : String(error),
|
|
47
|
+
}
|
|
48
|
+
await writeJsonIfNeeded(argv['result-file'], resultPayload)
|
|
49
|
+
emitFailure('claude_create_exception', error instanceof Error ? error.message : String(error), {
|
|
50
|
+
result_file: argv['result-file'] || null,
|
|
51
|
+
state_file: argv['state-file'] || null,
|
|
39
52
|
})
|
|
40
|
-
emitFailure('claude_create_exception', error instanceof Error ? error.message : String(error))
|
|
41
53
|
process.exit(1)
|
|
42
54
|
}
|
|
@@ -18,25 +18,37 @@ try {
|
|
|
18
18
|
})
|
|
19
19
|
|
|
20
20
|
if (failure || !parsed || parsed.is_error === true) {
|
|
21
|
-
|
|
21
|
+
const resultPayload = {
|
|
22
22
|
ok: false,
|
|
23
23
|
code: failure?.code || 'claude_resume_failed',
|
|
24
24
|
msg: failure?.msg || 'claude_resume_failed',
|
|
25
25
|
sid: failure?.sid || null,
|
|
26
|
+
}
|
|
27
|
+
await writeJsonIfNeeded(argv['result-file'], resultPayload)
|
|
28
|
+
emitFailure(failure?.code || 'claude_resume_failed', failure?.msg || 'claude_resume_failed', {
|
|
29
|
+
sid: failure?.sid || null,
|
|
30
|
+
result_file: argv['result-file'] || null,
|
|
31
|
+
state_file: argv['state-file'] || null,
|
|
26
32
|
})
|
|
27
|
-
emitFailure(failure?.code || 'claude_resume_failed', failure?.msg || 'claude_resume_failed', failure?.sid ? { sid: failure.sid } : {})
|
|
28
33
|
process.exit(1)
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
const compact = compactClaudeResult(parsed)
|
|
32
37
|
await writeJsonIfNeeded(argv['result-file'], { ok: true, sid: compact.sid, res: compact.res })
|
|
33
|
-
emitSuccess(compact.sid,
|
|
38
|
+
emitSuccess(compact.sid, {
|
|
39
|
+
result_file: argv['result-file'] || null,
|
|
40
|
+
state_file: argv['state-file'] || null,
|
|
41
|
+
})
|
|
34
42
|
} catch (error) {
|
|
35
|
-
|
|
43
|
+
const resultPayload = {
|
|
36
44
|
ok: false,
|
|
37
45
|
code: 'claude_resume_exception',
|
|
38
46
|
msg: error instanceof Error ? error.message : String(error),
|
|
47
|
+
}
|
|
48
|
+
await writeJsonIfNeeded(argv['result-file'], resultPayload)
|
|
49
|
+
emitFailure('claude_resume_exception', error instanceof Error ? error.message : String(error), {
|
|
50
|
+
result_file: argv['result-file'] || null,
|
|
51
|
+
state_file: argv['state-file'] || null,
|
|
39
52
|
})
|
|
40
|
-
emitFailure('claude_resume_exception', error instanceof Error ? error.message : String(error))
|
|
41
53
|
process.exit(1)
|
|
42
54
|
}
|
|
@@ -39,6 +39,11 @@ export async function writeJsonIfNeeded(filePath, value) {
|
|
|
39
39
|
await writeFileIfNeeded(filePath, `${JSON.stringify(value, null, 2)}\n`)
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
export async function readJsonFile(filePath) {
|
|
43
|
+
const content = await fs.readFile(filePath, 'utf8')
|
|
44
|
+
return JSON.parse(content)
|
|
45
|
+
}
|
|
46
|
+
|
|
42
47
|
export async function readPrompt(promptFile) {
|
|
43
48
|
const content = await fs.readFile(promptFile, 'utf8')
|
|
44
49
|
return content.trim()
|
|
@@ -140,27 +145,27 @@ export async function runClaude({ claudeCommand, args, cwd, rawOutputPath, rawEr
|
|
|
140
145
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
141
146
|
})
|
|
142
147
|
|
|
143
|
-
let
|
|
144
|
-
let
|
|
148
|
+
let stdoutBytes = 0
|
|
149
|
+
let stderrBytes = 0
|
|
145
150
|
|
|
146
151
|
void stateWriter.update({ status: 'running', pid: child.pid ?? null })
|
|
147
152
|
|
|
148
153
|
child.stdout.on('data', (chunk) => {
|
|
149
154
|
const text = chunk.toString()
|
|
150
|
-
stdout += text
|
|
151
155
|
stdoutWriter?.write(text)
|
|
156
|
+
stdoutBytes += Buffer.byteLength(text, 'utf8')
|
|
152
157
|
void stateWriter.update({
|
|
153
|
-
stdout_bytes:
|
|
158
|
+
stdout_bytes: stdoutBytes,
|
|
154
159
|
last_stdout_at: new Date().toISOString(),
|
|
155
160
|
})
|
|
156
161
|
})
|
|
157
162
|
|
|
158
163
|
child.stderr.on('data', (chunk) => {
|
|
159
164
|
const text = chunk.toString()
|
|
160
|
-
stderr += text
|
|
161
165
|
stderrWriter?.write(text)
|
|
166
|
+
stderrBytes += Buffer.byteLength(text, 'utf8')
|
|
162
167
|
void stateWriter.update({
|
|
163
|
-
stderr_bytes:
|
|
168
|
+
stderr_bytes: stderrBytes,
|
|
164
169
|
last_stderr_at: new Date().toISOString(),
|
|
165
170
|
})
|
|
166
171
|
})
|
|
@@ -174,7 +179,7 @@ export async function runClaude({ claudeCommand, args, cwd, rawOutputPath, rawEr
|
|
|
174
179
|
finished_at: new Date().toISOString(),
|
|
175
180
|
exit_code: code ?? 1,
|
|
176
181
|
})
|
|
177
|
-
resolve({ code: code ?? 1
|
|
182
|
+
resolve({ code: code ?? 1 })
|
|
178
183
|
})
|
|
179
184
|
})
|
|
180
185
|
|
|
@@ -186,8 +191,8 @@ export function sleep(ms) {
|
|
|
186
191
|
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
187
192
|
}
|
|
188
193
|
|
|
189
|
-
export function emitSuccess(sessionId,
|
|
190
|
-
process.stdout.write(JSON.stringify({ ok: true, sid: sessionId,
|
|
194
|
+
export function emitSuccess(sessionId, extra = {}) {
|
|
195
|
+
process.stdout.write(JSON.stringify({ ok: true, sid: sessionId, ...extra }))
|
|
191
196
|
}
|
|
192
197
|
|
|
193
198
|
export function emitFailure(code, message, extra = {}) {
|
|
@@ -217,6 +222,13 @@ export function compactClaudeResult(parsed) {
|
|
|
217
222
|
}
|
|
218
223
|
}
|
|
219
224
|
|
|
225
|
+
export async function parseClaudeResultFile(rawOutputPath) {
|
|
226
|
+
if (!rawOutputPath) {
|
|
227
|
+
throw new Error('rawOutputPath is required to parse Claude result files')
|
|
228
|
+
}
|
|
229
|
+
return readJsonFile(rawOutputPath)
|
|
230
|
+
}
|
|
231
|
+
|
|
220
232
|
export function classifyClaudeFailure(parsed, fallbackMessage = '') {
|
|
221
233
|
const rawMessage = String(parsed?.result || fallbackMessage || '').trim()
|
|
222
234
|
const sessionId = parsed?.session_id || null
|
|
@@ -286,14 +298,24 @@ export async function runClaudeWithRetry({ claudeCommand, args, cwd, rawOutputPa
|
|
|
286
298
|
const result = await runClaude({ claudeCommand, args, cwd, rawOutputPath, rawErrorPath, statePath, attempt })
|
|
287
299
|
let parsed = null
|
|
288
300
|
try {
|
|
289
|
-
parsed =
|
|
301
|
+
parsed = await parseClaudeResultFile(rawOutputPath)
|
|
290
302
|
} catch {}
|
|
291
303
|
|
|
292
304
|
if (parsed && parsed.is_error !== true && result.code === 0) {
|
|
293
305
|
return { result, parsed, attempts: attempt }
|
|
294
306
|
}
|
|
295
307
|
|
|
296
|
-
|
|
308
|
+
let stderrText = ''
|
|
309
|
+
try {
|
|
310
|
+
stderrText = rawErrorPath ? await fs.readFile(rawErrorPath, 'utf8') : ''
|
|
311
|
+
} catch {}
|
|
312
|
+
|
|
313
|
+
let stdoutText = ''
|
|
314
|
+
try {
|
|
315
|
+
stdoutText = rawOutputPath ? await fs.readFile(rawOutputPath, 'utf8') : ''
|
|
316
|
+
} catch {}
|
|
317
|
+
|
|
318
|
+
const failure = classifyClaudeFailure(parsed, (stderrText || stdoutText).trim())
|
|
297
319
|
const canRetry = attempt < maxAttempts && (failure.retryable || (!parsed && result.code !== 0))
|
|
298
320
|
if (!canRetry) {
|
|
299
321
|
return { result, parsed, failure, attempts: attempt }
|