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.
- package/MANUAL.md +21 -6
- package/README.md +55 -7
- package/RELEASE.md +16 -1
- package/assets/agents/developer.md +41 -1
- package/assets/agents/slopmachine-claude.md +101 -60
- package/assets/agents/slopmachine.md +40 -17
- package/assets/claude/agents/developer.md +42 -5
- package/assets/skills/clarification-gate/SKILL.md +25 -5
- package/assets/skills/claude-worker-management/SKILL.md +290 -57
- package/assets/skills/developer-session-lifecycle/SKILL.md +83 -38
- package/assets/skills/development-guidance/SKILL.md +21 -1
- package/assets/skills/evaluation-triage/SKILL.md +34 -23
- package/assets/skills/final-evaluation-orchestration/SKILL.md +88 -50
- package/assets/skills/hardening-gate/SKILL.md +17 -3
- package/assets/skills/integrated-verification/SKILL.md +3 -3
- package/assets/skills/planning-gate/SKILL.md +32 -3
- package/assets/skills/planning-guidance/SKILL.md +72 -13
- package/assets/skills/retrospective-analysis/SKILL.md +2 -2
- package/assets/skills/scaffold-guidance/SKILL.md +129 -124
- package/assets/skills/submission-packaging/SKILL.md +33 -27
- package/assets/skills/verification-gates/SKILL.md +44 -14
- package/assets/slopmachine/backend-evaluation-prompt.md +1 -1
- package/assets/slopmachine/frontend-evaluation-prompt.md +5 -5
- package/assets/slopmachine/scaffold-playbooks/android-kotlin-compose.md +81 -0
- package/assets/slopmachine/scaffold-playbooks/android-kotlin-views.md +191 -0
- package/assets/slopmachine/scaffold-playbooks/android-native-java.md +203 -0
- package/assets/slopmachine/scaffold-playbooks/angular-default.md +181 -0
- package/assets/slopmachine/scaffold-playbooks/backend-baseline.md +142 -0
- package/assets/slopmachine/scaffold-playbooks/backend-family-matrix.md +80 -0
- package/assets/slopmachine/scaffold-playbooks/database-module-matrix.md +80 -0
- package/assets/slopmachine/scaffold-playbooks/django-default.md +166 -0
- package/assets/slopmachine/scaffold-playbooks/docker-baseline.md +189 -0
- package/assets/slopmachine/scaffold-playbooks/docker-shared-contract.md +334 -0
- package/assets/slopmachine/scaffold-playbooks/electron-vite-default.md +124 -0
- package/assets/slopmachine/scaffold-playbooks/expo-react-native-default.md +73 -0
- package/assets/slopmachine/scaffold-playbooks/fastapi-default.md +134 -0
- package/assets/slopmachine/scaffold-playbooks/frontend-baseline.md +160 -0
- package/assets/slopmachine/scaffold-playbooks/frontend-family-matrix.md +134 -0
- package/assets/slopmachine/scaffold-playbooks/generic-unknown-tech-guide.md +136 -0
- package/assets/slopmachine/scaffold-playbooks/go-chi-default.md +160 -0
- package/assets/slopmachine/scaffold-playbooks/ios-linux-portable.md +93 -0
- package/assets/slopmachine/scaffold-playbooks/ios-native-objective-c.md +151 -0
- package/assets/slopmachine/scaffold-playbooks/ios-native-swift.md +188 -0
- package/assets/slopmachine/scaffold-playbooks/laravel-default.md +216 -0
- package/assets/slopmachine/scaffold-playbooks/livewire-default.md +265 -0
- package/assets/slopmachine/scaffold-playbooks/overlay-module-matrix.md +130 -0
- package/assets/slopmachine/scaffold-playbooks/platform-family-matrix.md +79 -0
- package/assets/slopmachine/scaffold-playbooks/selection-matrix.md +72 -0
- package/assets/slopmachine/scaffold-playbooks/spring-boot-default.md +182 -0
- package/assets/slopmachine/scaffold-playbooks/tauri-default.md +80 -0
- package/assets/slopmachine/scaffold-playbooks/vue-vite-default.md +162 -0
- package/assets/slopmachine/scaffold-playbooks/web-default.md +96 -0
- package/assets/slopmachine/templates/AGENTS.md +41 -3
- package/assets/slopmachine/templates/CLAUDE.md +111 -0
- package/assets/slopmachine/test-coverage-prompt.md +561 -0
- package/assets/slopmachine/utils/claude_create_session.mjs +3 -2
- package/assets/slopmachine/utils/claude_live_channel.mjs +188 -0
- package/assets/slopmachine/utils/claude_live_common.mjs +411 -0
- package/assets/slopmachine/utils/claude_live_hook.py +47 -0
- package/assets/slopmachine/utils/claude_live_launch.mjs +187 -0
- package/assets/slopmachine/utils/claude_live_status.mjs +25 -0
- package/assets/slopmachine/utils/claude_live_stop.mjs +46 -0
- package/assets/slopmachine/utils/claude_live_turn.mjs +277 -0
- package/assets/slopmachine/utils/claude_resume_session.mjs +3 -2
- package/assets/slopmachine/utils/claude_wait_for_rate_limit_reset.mjs +23 -0
- package/assets/slopmachine/utils/claude_wait_for_rate_limit_reset.sh +5 -0
- package/assets/slopmachine/utils/claude_worker_common.mjs +361 -4
- package/assets/slopmachine/utils/cleanup_delivery_artifacts.py +4 -0
- package/assets/slopmachine/utils/export_ai_session.mjs +1 -1
- package/assets/slopmachine/utils/normalize_claude_session.py +153 -0
- package/assets/slopmachine/utils/package_claude_session.mjs +123 -0
- package/assets/slopmachine/utils/prepare_strict_audit_workspace.mjs +65 -0
- package/package.json +1 -1
- package/src/constants.js +42 -3
- package/src/init.js +173 -28
- package/src/install.js +156 -8
- 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,
|
|
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
|
|
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
|
+
}
|