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.
@@ -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 outputs as the owner-facing contract:
393
+ Use wrapper files as the owner-facing contract:
394
394
 
395
- - success: compact parsed fields such as `sid` and `res`
396
- - failure: compact parsed fields such as `code` and `msg`
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
- - consume only the compact parsed wrapper output in normal owner flow
24
- - always capture the compact `sid` and `res` fields from wrapper output
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 reduce the raw Claude result to a tiny machine-parseable object and also persist state/result files for monitoring.
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 these fields only:
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
- await writeJsonIfNeeded(argv['result-file'], {
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, compact.res)
38
+ emitSuccess(compact.sid, {
39
+ result_file: argv['result-file'] || null,
40
+ state_file: argv['state-file'] || null,
41
+ })
34
42
  } catch (error) {
35
- await writeJsonIfNeeded(argv['result-file'], {
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
- await writeJsonIfNeeded(argv['result-file'], {
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, compact.res)
38
+ emitSuccess(compact.sid, {
39
+ result_file: argv['result-file'] || null,
40
+ state_file: argv['state-file'] || null,
41
+ })
34
42
  } catch (error) {
35
- await writeJsonIfNeeded(argv['result-file'], {
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 stdout = ''
144
- let stderr = ''
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: Buffer.byteLength(stdout, 'utf8'),
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: Buffer.byteLength(stderr, 'utf8'),
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, stdout, stderr })
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, result, extra = {}) {
190
- process.stdout.write(JSON.stringify({ ok: true, sid: sessionId, res: result, ...extra }))
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 = parseClaudeJson(result.stdout)
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
- const failure = classifyClaudeFailure(parsed, (result.stderr || result.stdout).trim())
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 }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "theslopmachine",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "SlopMachine installer and project bootstrap CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",