theslopmachine 0.6.2 → 0.7.1

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.
Files changed (77) hide show
  1. package/MANUAL.md +21 -6
  2. package/README.md +55 -7
  3. package/RELEASE.md +16 -1
  4. package/assets/agents/developer.md +41 -1
  5. package/assets/agents/slopmachine-claude.md +101 -60
  6. package/assets/agents/slopmachine.md +40 -17
  7. package/assets/claude/agents/developer.md +42 -5
  8. package/assets/skills/clarification-gate/SKILL.md +25 -5
  9. package/assets/skills/claude-worker-management/SKILL.md +290 -57
  10. package/assets/skills/developer-session-lifecycle/SKILL.md +83 -38
  11. package/assets/skills/development-guidance/SKILL.md +21 -1
  12. package/assets/skills/evaluation-triage/SKILL.md +34 -23
  13. package/assets/skills/final-evaluation-orchestration/SKILL.md +88 -50
  14. package/assets/skills/hardening-gate/SKILL.md +17 -3
  15. package/assets/skills/integrated-verification/SKILL.md +3 -3
  16. package/assets/skills/planning-gate/SKILL.md +32 -3
  17. package/assets/skills/planning-guidance/SKILL.md +72 -13
  18. package/assets/skills/retrospective-analysis/SKILL.md +2 -2
  19. package/assets/skills/scaffold-guidance/SKILL.md +129 -124
  20. package/assets/skills/submission-packaging/SKILL.md +33 -27
  21. package/assets/skills/verification-gates/SKILL.md +44 -14
  22. package/assets/slopmachine/backend-evaluation-prompt.md +1 -1
  23. package/assets/slopmachine/frontend-evaluation-prompt.md +5 -5
  24. package/assets/slopmachine/scaffold-playbooks/android-kotlin-compose.md +81 -0
  25. package/assets/slopmachine/scaffold-playbooks/android-kotlin-views.md +191 -0
  26. package/assets/slopmachine/scaffold-playbooks/android-native-java.md +203 -0
  27. package/assets/slopmachine/scaffold-playbooks/angular-default.md +181 -0
  28. package/assets/slopmachine/scaffold-playbooks/backend-baseline.md +142 -0
  29. package/assets/slopmachine/scaffold-playbooks/backend-family-matrix.md +80 -0
  30. package/assets/slopmachine/scaffold-playbooks/database-module-matrix.md +80 -0
  31. package/assets/slopmachine/scaffold-playbooks/django-default.md +166 -0
  32. package/assets/slopmachine/scaffold-playbooks/docker-baseline.md +189 -0
  33. package/assets/slopmachine/scaffold-playbooks/docker-shared-contract.md +334 -0
  34. package/assets/slopmachine/scaffold-playbooks/electron-vite-default.md +124 -0
  35. package/assets/slopmachine/scaffold-playbooks/expo-react-native-default.md +73 -0
  36. package/assets/slopmachine/scaffold-playbooks/fastapi-default.md +134 -0
  37. package/assets/slopmachine/scaffold-playbooks/frontend-baseline.md +160 -0
  38. package/assets/slopmachine/scaffold-playbooks/frontend-family-matrix.md +134 -0
  39. package/assets/slopmachine/scaffold-playbooks/generic-unknown-tech-guide.md +136 -0
  40. package/assets/slopmachine/scaffold-playbooks/go-chi-default.md +160 -0
  41. package/assets/slopmachine/scaffold-playbooks/ios-linux-portable.md +93 -0
  42. package/assets/slopmachine/scaffold-playbooks/ios-native-objective-c.md +151 -0
  43. package/assets/slopmachine/scaffold-playbooks/ios-native-swift.md +188 -0
  44. package/assets/slopmachine/scaffold-playbooks/laravel-default.md +216 -0
  45. package/assets/slopmachine/scaffold-playbooks/livewire-default.md +265 -0
  46. package/assets/slopmachine/scaffold-playbooks/overlay-module-matrix.md +130 -0
  47. package/assets/slopmachine/scaffold-playbooks/platform-family-matrix.md +79 -0
  48. package/assets/slopmachine/scaffold-playbooks/selection-matrix.md +72 -0
  49. package/assets/slopmachine/scaffold-playbooks/spring-boot-default.md +182 -0
  50. package/assets/slopmachine/scaffold-playbooks/tauri-default.md +80 -0
  51. package/assets/slopmachine/scaffold-playbooks/vue-vite-default.md +162 -0
  52. package/assets/slopmachine/scaffold-playbooks/web-default.md +96 -0
  53. package/assets/slopmachine/templates/AGENTS.md +41 -3
  54. package/assets/slopmachine/templates/CLAUDE.md +111 -0
  55. package/assets/slopmachine/test-coverage-prompt.md +561 -0
  56. package/assets/slopmachine/utils/claude_create_session.mjs +3 -2
  57. package/assets/slopmachine/utils/claude_live_channel.mjs +188 -0
  58. package/assets/slopmachine/utils/claude_live_common.mjs +411 -0
  59. package/assets/slopmachine/utils/claude_live_hook.py +47 -0
  60. package/assets/slopmachine/utils/claude_live_launch.mjs +187 -0
  61. package/assets/slopmachine/utils/claude_live_status.mjs +25 -0
  62. package/assets/slopmachine/utils/claude_live_stop.mjs +46 -0
  63. package/assets/slopmachine/utils/claude_live_turn.mjs +277 -0
  64. package/assets/slopmachine/utils/claude_resume_session.mjs +3 -2
  65. package/assets/slopmachine/utils/claude_wait_for_rate_limit_reset.mjs +23 -0
  66. package/assets/slopmachine/utils/claude_wait_for_rate_limit_reset.sh +5 -0
  67. package/assets/slopmachine/utils/claude_worker_common.mjs +361 -4
  68. package/assets/slopmachine/utils/cleanup_delivery_artifacts.py +4 -0
  69. package/assets/slopmachine/utils/export_ai_session.mjs +1 -1
  70. package/assets/slopmachine/utils/normalize_claude_session.py +153 -0
  71. package/assets/slopmachine/utils/package_claude_session.mjs +123 -0
  72. package/assets/slopmachine/utils/prepare_strict_audit_workspace.mjs +65 -0
  73. package/package.json +1 -1
  74. package/src/constants.js +42 -3
  75. package/src/init.js +173 -28
  76. package/src/install.js +156 -8
  77. package/src/send-data.js +56 -57
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env node
2
+
3
+ import path from 'node:path'
4
+
5
+ import {
6
+ DEFAULT_LAUNCH_TIMEOUT_MS,
7
+ allocatePort,
8
+ buildClaudeLaunchCommand,
9
+ buildHookSettings,
10
+ buildMcpConfig,
11
+ buildRuntimePaths,
12
+ clearRuntimeArtifacts,
13
+ emitFailure,
14
+ emitSuccess,
15
+ ensureRuntimeDirs,
16
+ makeSuffix,
17
+ makeToken,
18
+ maybeAcceptDevelopmentChannelsPrompt,
19
+ parseArgs,
20
+ readJsonIfExists,
21
+ resolveUtilsDir,
22
+ runCommand,
23
+ sanitizeName,
24
+ tmuxHasSession,
25
+ tmuxKillSession,
26
+ waitForChannelReady,
27
+ waitForHookEvent,
28
+ writeJsonIfNeeded,
29
+ writeState,
30
+ } from './claude_live_common.mjs'
31
+
32
+ const argv = parseArgs(process.argv.slice(2))
33
+
34
+ const runtimeDir = argv['runtime-dir']
35
+ const cwd = argv.cwd ? path.resolve(argv.cwd) : null
36
+ const lane = argv.lane
37
+ const agentName = argv.agent || 'developer'
38
+ const claudeCommand = argv['claude-command'] || 'claude'
39
+ const laneModel = argv.model || 'sonnet'
40
+ const laneEffort = argv.effort || null
41
+ const subagentModel = argv['subagent-model'] || 'sonnet'
42
+ const launchTimeoutMs = Number.parseInt(argv['timeout-ms'] || String(DEFAULT_LAUNCH_TIMEOUT_MS), 10)
43
+ const replace = argv.replace === '1'
44
+
45
+ if (!runtimeDir || !lane || !cwd) {
46
+ emitFailure('claude_live_launch_invalid_args', 'Missing required launch arguments', {
47
+ state_file: runtimeDir ? buildRuntimePaths(runtimeDir).stateFile : null,
48
+ })
49
+ process.exit(1)
50
+ }
51
+
52
+ const paths = buildRuntimePaths(runtimeDir)
53
+
54
+ try {
55
+ await ensureRuntimeDirs(paths)
56
+
57
+ const existingState = await readJsonIfExists(paths.stateFile)
58
+ if (existingState?.tmux_session && await tmuxHasSession(existingState.tmux_session)) {
59
+ if (!replace) {
60
+ emitSuccess(existingState.sid || null, {
61
+ state_file: paths.stateFile,
62
+ result_file: paths.resultFile,
63
+ })
64
+ process.exit(0)
65
+ }
66
+ await tmuxKillSession(existingState.tmux_session)
67
+ } else if (existingState && !replace && existingState.status && existingState.status !== 'stopped') {
68
+ emitFailure('claude_live_state_stale', 'Existing runtime state is stale. Re-run with --replace 1 to recover it.', {
69
+ sid: existingState.sid || null,
70
+ state_file: paths.stateFile,
71
+ result_file: paths.resultFile,
72
+ })
73
+ process.exit(1)
74
+ }
75
+
76
+ const suffix = makeSuffix()
77
+ const tmuxSession = argv['tmux-session'] || `sm-${sanitizeName(lane)}-${suffix}`
78
+ const channelName = argv['channel-name'] || `slopmachine-${sanitizeName(lane)}-${suffix}`
79
+ const channelPort = await allocatePort()
80
+ const channelToken = makeToken()
81
+ const utilsDir = resolveUtilsDir()
82
+
83
+ await clearRuntimeArtifacts(paths)
84
+ await writeState(runtimeDir, {
85
+ lane,
86
+ backend: 'claude-live',
87
+ status: 'starting',
88
+ cwd,
89
+ sid: null,
90
+ agent_name: agentName,
91
+ model: laneModel,
92
+ effort: laneEffort,
93
+ subagent_model: subagentModel,
94
+ tmux_session: tmuxSession,
95
+ channel_name: channelName,
96
+ channel_port: channelPort,
97
+ channel_token: channelToken,
98
+ runtime_dir: paths.runtimeDir,
99
+ settings_file: paths.settingsFile,
100
+ mcp_config_file: paths.mcpConfigFile,
101
+ hook_events_file: paths.hookEventsFile,
102
+ channel_state_file: paths.channelStateFile,
103
+ result_file: paths.resultFile,
104
+ transcript_path: null,
105
+ current_turn_id: null,
106
+ last_completed_turn_id: null,
107
+ last_hook_event: null,
108
+ last_error: null,
109
+ last_turn_number: 0,
110
+ started_at: new Date().toISOString(),
111
+ })
112
+
113
+ await Promise.all([
114
+ writeJsonIfNeeded(paths.settingsFile, buildHookSettings({
115
+ runtimeDir: paths.runtimeDir,
116
+ utilsDir,
117
+ agentName,
118
+ subagentModel,
119
+ })),
120
+ writeJsonIfNeeded(paths.mcpConfigFile, buildMcpConfig({
121
+ paths,
122
+ utilsDir,
123
+ channelName,
124
+ lane,
125
+ port: channelPort,
126
+ token: channelToken,
127
+ })),
128
+ ])
129
+
130
+ const launchCommand = buildClaudeLaunchCommand({
131
+ claudeCommand,
132
+ agentName,
133
+ displayName: lane,
134
+ settingsFile: paths.settingsFile,
135
+ mcpConfigFile: paths.mcpConfigFile,
136
+ channelName,
137
+ model: laneModel,
138
+ effort: laneEffort,
139
+ })
140
+
141
+ const launchResult = await runCommand('tmux', ['new-session', '-d', '-s', tmuxSession, '-c', cwd, launchCommand])
142
+ if (launchResult.code !== 0) {
143
+ emitFailure('claude_live_launch_failed', launchResult.stderr.trim() || 'Failed to start tmux Claude session', {
144
+ state_file: paths.stateFile,
145
+ result_file: paths.resultFile,
146
+ })
147
+ process.exit(1)
148
+ }
149
+
150
+ await maybeAcceptDevelopmentChannelsPrompt(tmuxSession, Math.min(launchTimeoutMs, 30000))
151
+
152
+ const [{ event }, channelState] = await Promise.all([
153
+ waitForHookEvent(paths, 0, new Set(['SessionStart']), launchTimeoutMs),
154
+ waitForChannelReady(paths, launchTimeoutMs),
155
+ ])
156
+
157
+ const sessionId = event.payload?.session_id || null
158
+ const transcriptPath = event.payload?.transcript_path || null
159
+ await writeState(runtimeDir, {
160
+ status: 'idle',
161
+ sid: sessionId,
162
+ transcript_path: transcriptPath,
163
+ last_hook_event: 'SessionStart',
164
+ last_error: null,
165
+ channel_status: channelState.status || 'ready',
166
+ })
167
+
168
+ emitSuccess(sessionId, {
169
+ state_file: paths.stateFile,
170
+ result_file: paths.resultFile,
171
+ })
172
+ } catch (error) {
173
+ const state = await readJsonIfExists(paths.stateFile)
174
+ if (state?.tmux_session && await tmuxHasSession(state.tmux_session)) {
175
+ await tmuxKillSession(state.tmux_session)
176
+ }
177
+ await writeState(runtimeDir, {
178
+ status: 'failed',
179
+ last_error: error instanceof Error ? error.message : String(error),
180
+ })
181
+ emitFailure('claude_live_launch_failed', error instanceof Error ? error.message : String(error), {
182
+ sid: state?.sid || null,
183
+ state_file: paths.stateFile,
184
+ result_file: paths.resultFile,
185
+ })
186
+ process.exit(1)
187
+ }
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { buildRuntimePaths, parseArgs, readState, tmuxHasSession } from './claude_live_common.mjs'
4
+
5
+ const argv = parseArgs(process.argv.slice(2))
6
+ const runtimeDir = argv['runtime-dir']
7
+
8
+ if (!runtimeDir) {
9
+ process.stdout.write(JSON.stringify({ ok: false, code: 'claude_live_status_invalid_args', msg: 'Missing --runtime-dir' }))
10
+ process.exit(1)
11
+ }
12
+
13
+ const state = await readState(runtimeDir)
14
+ if (!state) {
15
+ process.stdout.write(JSON.stringify({ ok: false, code: 'claude_live_status_missing', msg: 'Missing bridge state', state_file: buildRuntimePaths(runtimeDir).stateFile }))
16
+ process.exit(1)
17
+ }
18
+
19
+ const tmuxAlive = await tmuxHasSession(state.tmux_session)
20
+ const { channel_token: _channelToken, ...safeState } = state
21
+ process.stdout.write(JSON.stringify({
22
+ ok: true,
23
+ ...safeState,
24
+ tmux_alive: tmuxAlive,
25
+ }))
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { buildRuntimePaths, emitFailure, emitSuccess, parseArgs, readState, tmuxHasSession, tmuxKillSession, writeState } from './claude_live_common.mjs'
4
+
5
+ const argv = parseArgs(process.argv.slice(2))
6
+ const runtimeDir = argv['runtime-dir']
7
+
8
+ if (!runtimeDir) {
9
+ emitFailure('claude_live_stop_invalid_args', 'Missing --runtime-dir')
10
+ process.exit(1)
11
+ }
12
+
13
+ const paths = buildRuntimePaths(runtimeDir)
14
+ const state = await readState(runtimeDir)
15
+
16
+ if (!state) {
17
+ emitFailure('claude_live_stop_missing', 'Missing bridge state', { state_file: paths.stateFile })
18
+ process.exit(1)
19
+ }
20
+
21
+ if (state.tmux_session && await tmuxHasSession(state.tmux_session)) {
22
+ const stopResult = await tmuxKillSession(state.tmux_session)
23
+ if (stopResult.code !== 0) {
24
+ emitFailure('claude_live_stop_failed', stopResult.stderr.trim() || 'Failed to stop tmux session', {
25
+ sid: state.sid || null,
26
+ state_file: paths.stateFile,
27
+ result_file: paths.resultFile,
28
+ })
29
+ process.exit(1)
30
+ }
31
+ }
32
+
33
+ await writeState(runtimeDir, {
34
+ status: 'stopped',
35
+ current_turn_id: null,
36
+ current_turn_prompt_file: null,
37
+ current_turn_prompt_source: null,
38
+ current_turn_started_at: null,
39
+ last_error: null,
40
+ stopped_at: new Date().toISOString(),
41
+ })
42
+
43
+ emitSuccess(state.sid || null, {
44
+ state_file: paths.stateFile,
45
+ result_file: paths.resultFile,
46
+ })
@@ -0,0 +1,277 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {
4
+ DEFAULT_TURN_TIMEOUT_MS,
5
+ buildFailureResult,
6
+ buildRuntimePaths,
7
+ buildTurnId,
8
+ classifyStopFailure,
9
+ emitFailure,
10
+ emitSuccess,
11
+ postPromptToChannel,
12
+ parseArgs,
13
+ readJsonl,
14
+ readPromptInput,
15
+ readState,
16
+ tmuxHasSession,
17
+ waitForRateLimitReset,
18
+ waitForChannelReady,
19
+ waitForHookEvent,
20
+ writeState,
21
+ writeTurnArtifacts,
22
+ } from './claude_live_common.mjs'
23
+
24
+ const argv = parseArgs(process.argv.slice(2))
25
+ const runtimeDir = argv['runtime-dir']
26
+ const turnTimeoutMs = Number.parseInt(argv['timeout-ms'] || String(DEFAULT_TURN_TIMEOUT_MS), 10)
27
+ const retryOnLimit = argv['retry-on-limit'] !== '0'
28
+ const hasPromptInput = Boolean(argv['prompt-file'] || Object.hasOwn(argv, 'prompt') || argv['prompt-stdin'] === '1' || process.stdin.isTTY === false)
29
+
30
+ if (!runtimeDir || !hasPromptInput) {
31
+ emitFailure('claude_live_turn_invalid_args', 'Missing required turn arguments', {
32
+ state_file: runtimeDir ? buildRuntimePaths(runtimeDir).stateFile : null,
33
+ })
34
+ process.exit(1)
35
+ }
36
+
37
+ const paths = buildRuntimePaths(runtimeDir)
38
+ let prompt = ''
39
+
40
+ function emitTurnFailure(resultPayload) {
41
+ emitFailure(resultPayload.code, resultPayload.msg, {
42
+ sid: resultPayload.sid || null,
43
+ result_file: argv['result-file'] || paths.resultFile,
44
+ state_file: paths.stateFile,
45
+ })
46
+ }
47
+
48
+ try {
49
+ let promptSource = null
50
+ let promptSourceKind = null
51
+
52
+ const state = await readState(runtimeDir)
53
+ if (!state) {
54
+ const resultPayload = buildFailureResult('claude_live_turn_uninitialized', 'Claude live lane is not initialized')
55
+ emitTurnFailure(resultPayload)
56
+ process.exit(1)
57
+ }
58
+
59
+ if (state.status !== 'idle') {
60
+ const resultPayload = buildFailureResult('claude_live_turn_not_idle', `Claude live lane is not idle: ${state.status}`, state.sid || null)
61
+ emitTurnFailure(resultPayload)
62
+ process.exit(1)
63
+ }
64
+
65
+ if (!state.sid) {
66
+ const resultPayload = buildFailureResult('claude_live_turn_unregistered', 'Claude live lane does not have a session id yet')
67
+ emitTurnFailure(resultPayload)
68
+ process.exit(1)
69
+ }
70
+
71
+ if (!await tmuxHasSession(state.tmux_session)) {
72
+ await writeState(runtimeDir, {
73
+ status: 'failed',
74
+ last_error: 'Claude tmux session is no longer running',
75
+ })
76
+ const resultPayload = buildFailureResult('claude_session_dead', 'Claude tmux session is no longer running', state.sid)
77
+ emitTurnFailure(resultPayload)
78
+ process.exit(1)
79
+ }
80
+
81
+ ({ prompt, promptSource, promptSourceKind } = await readPromptInput(argv))
82
+ let hookStartIndex = (await readJsonl(paths.hookEventsFile)).length
83
+ const turnNumber = Number(state.last_turn_number || 0)
84
+ const turnId = buildTurnId(turnNumber)
85
+
86
+ await writeState(runtimeDir, {
87
+ status: 'running',
88
+ current_turn_id: turnId,
89
+ current_turn_prompt_file: promptSourceKind === 'file' && promptSource ? promptSource : null,
90
+ current_turn_prompt_source: promptSource,
91
+ current_turn_started_at: new Date().toISOString(),
92
+ last_error: null,
93
+ })
94
+
95
+ while (true) {
96
+ const liveState = await readState(runtimeDir) || state
97
+
98
+ await waitForChannelReady(paths, 15000)
99
+
100
+ const sendResult = await postPromptToChannel({
101
+ state: liveState,
102
+ prompt,
103
+ turnId,
104
+ })
105
+
106
+ if (sendResult.statusCode !== 200) {
107
+ const resultPayload = buildFailureResult('channel_send_failed', sendResult.body || `Channel send failed with status ${sendResult.statusCode}`, liveState.sid || state.sid)
108
+ await writeTurnArtifacts(paths, turnId, prompt, resultPayload, argv['result-file'] || null)
109
+ await writeState(runtimeDir, {
110
+ status: 'failed',
111
+ current_turn_id: null,
112
+ current_turn_prompt_file: null,
113
+ current_turn_prompt_source: null,
114
+ current_turn_started_at: null,
115
+ last_completed_turn_id: turnId,
116
+ last_turn_number: turnNumber + 1,
117
+ last_error: resultPayload.msg,
118
+ rate_limit_wait_started_at: null,
119
+ rate_limit_wait_until: null,
120
+ rate_limit_wait_ms: null,
121
+ rate_limit_wait_source: null,
122
+ rate_limit_message: null,
123
+ rate_limit_reset_at: null,
124
+ rate_limit_type: null,
125
+ rate_limit_raw_reset_at: null,
126
+ })
127
+ emitTurnFailure(resultPayload)
128
+ process.exit(1)
129
+ }
130
+
131
+ const { event, eventsLength } = await waitForHookEvent(paths, hookStartIndex, new Set(['Stop', 'StopFailure']), turnTimeoutMs)
132
+ hookStartIndex = eventsLength
133
+
134
+ if (event.label === 'Stop') {
135
+ const sessionId = event.payload?.session_id || liveState.sid || state.sid
136
+ const resultPayload = {
137
+ ok: true,
138
+ sid: sessionId,
139
+ res: event.payload?.last_assistant_message || '',
140
+ }
141
+ await writeTurnArtifacts(paths, turnId, prompt, resultPayload, argv['result-file'] || null)
142
+ await writeState(runtimeDir, {
143
+ status: 'idle',
144
+ sid: sessionId,
145
+ transcript_path: event.payload?.transcript_path || liveState.transcript_path || state.transcript_path || null,
146
+ current_turn_id: null,
147
+ current_turn_prompt_file: null,
148
+ current_turn_prompt_source: null,
149
+ current_turn_started_at: null,
150
+ last_completed_turn_id: turnId,
151
+ last_hook_event: 'Stop',
152
+ last_error: null,
153
+ last_turn_number: turnNumber + 1,
154
+ last_result_summary: resultPayload.res.slice(0, 500),
155
+ rate_limit_wait_started_at: null,
156
+ rate_limit_wait_until: null,
157
+ rate_limit_wait_ms: null,
158
+ rate_limit_wait_source: null,
159
+ rate_limit_message: null,
160
+ rate_limit_reset_at: null,
161
+ rate_limit_type: null,
162
+ rate_limit_raw_reset_at: null,
163
+ })
164
+ emitSuccess(sessionId, {
165
+ result_file: argv['result-file'] || paths.resultFile,
166
+ state_file: paths.stateFile,
167
+ })
168
+ process.exit(0)
169
+ }
170
+
171
+ const { result: resultPayload, nextStatus } = classifyStopFailure(event, liveState.sid || state.sid)
172
+ if (resultPayload.code === 'claude_usage_limit' && retryOnLimit) {
173
+ await writeState(runtimeDir, {
174
+ status: 'blocked',
175
+ sid: resultPayload.sid || liveState.sid || state.sid,
176
+ transcript_path: event.payload?.transcript_path || liveState.transcript_path || state.transcript_path || null,
177
+ last_hook_event: 'StopFailure',
178
+ last_error: resultPayload.msg,
179
+ })
180
+ await waitForRateLimitReset({
181
+ message: resultPayload.detail || resultPayload.msg,
182
+ resetAt: resultPayload.rate_limit?.resetAt || null,
183
+ rateLimitType: resultPayload.rate_limit?.rateLimitType || null,
184
+ rawResetAt: resultPayload.rate_limit?.rawResetAt ?? null,
185
+ statePath: paths.stateFile,
186
+ })
187
+
188
+ if (!await tmuxHasSession((await readState(runtimeDir))?.tmux_session || liveState.tmux_session)) {
189
+ const deadPayload = buildFailureResult('claude_session_dead', 'Claude tmux session is no longer running', resultPayload.sid || liveState.sid || state.sid)
190
+ await writeTurnArtifacts(paths, turnId, prompt, deadPayload, argv['result-file'] || null)
191
+ await writeState(runtimeDir, {
192
+ status: 'failed',
193
+ current_turn_id: null,
194
+ current_turn_prompt_file: null,
195
+ current_turn_prompt_source: null,
196
+ current_turn_started_at: null,
197
+ last_completed_turn_id: turnId,
198
+ last_turn_number: turnNumber + 1,
199
+ last_error: deadPayload.msg,
200
+ rate_limit_wait_started_at: null,
201
+ rate_limit_wait_until: null,
202
+ rate_limit_wait_ms: null,
203
+ rate_limit_wait_source: null,
204
+ rate_limit_message: null,
205
+ rate_limit_reset_at: null,
206
+ rate_limit_type: null,
207
+ rate_limit_raw_reset_at: null,
208
+ })
209
+ emitTurnFailure(deadPayload)
210
+ process.exit(1)
211
+ }
212
+
213
+ await writeState(runtimeDir, {
214
+ status: 'running',
215
+ current_turn_id: turnId,
216
+ current_turn_prompt_file: promptSourceKind === 'file' && promptSource ? promptSource : null,
217
+ current_turn_prompt_source: promptSource,
218
+ current_turn_started_at: new Date().toISOString(),
219
+ last_error: null,
220
+ })
221
+ continue
222
+ }
223
+
224
+ await writeTurnArtifacts(paths, turnId, prompt, resultPayload, argv['result-file'] || null)
225
+ await writeState(runtimeDir, {
226
+ status: nextStatus,
227
+ sid: resultPayload.sid || liveState.sid || state.sid,
228
+ transcript_path: event.payload?.transcript_path || liveState.transcript_path || state.transcript_path || null,
229
+ current_turn_id: null,
230
+ current_turn_prompt_file: null,
231
+ current_turn_prompt_source: null,
232
+ current_turn_started_at: null,
233
+ last_completed_turn_id: turnId,
234
+ last_hook_event: 'StopFailure',
235
+ last_error: resultPayload.msg,
236
+ last_turn_number: turnNumber + 1,
237
+ rate_limit_wait_started_at: null,
238
+ rate_limit_wait_until: null,
239
+ rate_limit_wait_ms: null,
240
+ rate_limit_wait_source: null,
241
+ rate_limit_message: null,
242
+ rate_limit_reset_at: null,
243
+ rate_limit_type: null,
244
+ rate_limit_raw_reset_at: null,
245
+ })
246
+ emitTurnFailure(resultPayload)
247
+ process.exit(1)
248
+ }
249
+ } catch (error) {
250
+ const state = await readState(runtimeDir)
251
+ const code = state?.tmux_session && !await tmuxHasSession(state.tmux_session) ? 'claude_session_dead' : 'claude_bridge_timeout'
252
+ const resultPayload = buildFailureResult(code, error instanceof Error ? error.message : String(error), state?.sid || null)
253
+ const turnNumber = Number(state?.last_turn_number || 0)
254
+ const turnId = buildTurnId(turnNumber)
255
+ const promptForArtifacts = prompt || await readPromptInput(argv).then((value) => value.prompt).catch(() => '')
256
+ await writeTurnArtifacts(paths, turnId, promptForArtifacts, resultPayload, argv['result-file'] || null)
257
+ await writeState(runtimeDir, {
258
+ status: 'failed',
259
+ current_turn_id: null,
260
+ current_turn_prompt_file: null,
261
+ current_turn_prompt_source: null,
262
+ current_turn_started_at: null,
263
+ last_completed_turn_id: turnId,
264
+ last_turn_number: turnNumber + 1,
265
+ last_error: resultPayload.msg,
266
+ rate_limit_wait_started_at: null,
267
+ rate_limit_wait_until: null,
268
+ rate_limit_wait_ms: null,
269
+ rate_limit_wait_source: null,
270
+ rate_limit_message: null,
271
+ rate_limit_reset_at: null,
272
+ rate_limit_type: null,
273
+ rate_limit_raw_reset_at: null,
274
+ })
275
+ emitTurnFailure(resultPayload)
276
+ process.exit(1)
277
+ }
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { parseArgs, readPrompt, buildResumeArgs, emitFailure, emitSuccess, compactClaudeResult, runClaudeWithRetry, writeJsonIfNeeded } from './claude_worker_common.mjs'
3
+ import { parseArgs, readPromptInput, buildResumeArgs, emitFailure, emitSuccess, compactClaudeResult, runClaudeWithRetry, writeJsonIfNeeded } from './claude_worker_common.mjs'
4
4
 
5
5
  const argv = parseArgs(process.argv.slice(2))
6
6
 
7
7
  try {
8
- const prompt = await readPrompt(argv['prompt-file'])
8
+ const { prompt } = await readPromptInput(argv)
9
9
  const { parsed, failure } = await runClaudeWithRetry({
10
10
  claudeCommand: argv['claude-command'] || 'claude',
11
11
  cwd: argv.cwd,
@@ -22,6 +22,7 @@ try {
22
22
  ok: false,
23
23
  code: failure?.code || 'claude_resume_failed',
24
24
  msg: failure?.msg || 'claude_resume_failed',
25
+ detail: failure?.detail || null,
25
26
  sid: failure?.sid || null,
26
27
  }
27
28
  await writeJsonIfNeeded(argv['result-file'], resultPayload)
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { emitFailure, emitSuccess, parseArgs, waitForRateLimitReset } from './claude_worker_common.mjs'
4
+
5
+ const argv = parseArgs(process.argv.slice(2))
6
+
7
+ try {
8
+ const waitInfo = await waitForRateLimitReset({
9
+ message: argv.message || '',
10
+ statePath: argv['state-file'] || null,
11
+ dryRun: argv['dry-run'] === '1',
12
+ bufferMs: argv['buffer-ms'] ? Number.parseInt(argv['buffer-ms'], 10) : undefined,
13
+ })
14
+
15
+ emitSuccess(null, {
16
+ wait_ms: waitInfo.waitMs,
17
+ reset_at: waitInfo.resetAt.toISOString(),
18
+ source: waitInfo.source,
19
+ })
20
+ } catch (error) {
21
+ emitFailure('claude_wait_for_rate_limit_reset_failed', error instanceof Error ? error.message : String(error))
22
+ process.exitCode = 1
23
+ }
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
5
+ exec node "$SCRIPT_DIR/claude_wait_for_rate_limit_reset.mjs" "$@"