theslopmachine 0.7.0 → 0.7.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/README.md +1 -1
- package/RELEASE.md +2 -2
- package/assets/agents/developer.md +13 -13
- package/assets/agents/slopmachine-claude.md +7 -5
- package/assets/agents/slopmachine.md +6 -5
- package/assets/claude/agents/developer.md +6 -6
- package/assets/skills/clarification-gate/SKILL.md +9 -18
- package/assets/skills/claude-worker-management/SKILL.md +34 -22
- package/assets/skills/developer-session-lifecycle/SKILL.md +2 -1
- package/assets/skills/development-guidance/SKILL.md +3 -0
- package/assets/skills/evaluation-triage/SKILL.md +6 -4
- package/assets/skills/final-evaluation-orchestration/SKILL.md +16 -13
- package/assets/skills/hardening-gate/SKILL.md +3 -0
- package/assets/skills/integrated-verification/SKILL.md +2 -0
- package/assets/skills/planning-guidance/SKILL.md +1 -0
- package/assets/skills/submission-packaging/SKILL.md +6 -4
- package/assets/skills/verification-gates/SKILL.md +7 -2
- package/assets/slopmachine/test-coverage-prompt.md +561 -0
- package/assets/slopmachine/utils/claude_create_session.mjs +2 -2
- package/assets/slopmachine/utils/claude_live_common.mjs +8 -3
- package/assets/slopmachine/utils/claude_live_launch.mjs +9 -3
- package/assets/slopmachine/utils/claude_live_stop.mjs +1 -0
- package/assets/slopmachine/utils/claude_live_turn.mjs +37 -10
- package/assets/slopmachine/utils/claude_resume_session.mjs +2 -2
- package/assets/slopmachine/utils/claude_worker_common.mjs +140 -3
- package/assets/slopmachine/utils/package_claude_session.mjs +35 -8
- package/package.json +1 -1
- package/src/constants.js +2 -2
- package/src/init.js +7 -1
- package/src/install.js +94 -21
|
@@ -8,7 +8,7 @@ import crypto from 'node:crypto'
|
|
|
8
8
|
import { fileURLToPath } from 'node:url'
|
|
9
9
|
import { spawn } from 'node:child_process'
|
|
10
10
|
|
|
11
|
-
import { emitFailure, emitSuccess, parseArgs, readJsonFile, readPrompt, sleep, waitForRateLimitReset, writeFileIfNeeded, writeJsonIfNeeded } from './claude_worker_common.mjs'
|
|
11
|
+
import { emitFailure, emitSuccess, extractRateLimitMetadata, parseArgs, readJsonFile, readPrompt, sleep, waitForRateLimitReset, writeFileIfNeeded, writeJsonIfNeeded } from './claude_worker_common.mjs'
|
|
12
12
|
|
|
13
13
|
export { emitFailure, emitSuccess, parseArgs, readPrompt, sleep, waitForRateLimitReset, writeJsonIfNeeded }
|
|
14
14
|
|
|
@@ -279,7 +279,7 @@ export function buildMcpConfig({ paths, utilsDir, channelName, lane, port, token
|
|
|
279
279
|
}
|
|
280
280
|
}
|
|
281
281
|
|
|
282
|
-
export function buildClaudeLaunchCommand({ claudeCommand, agentName, displayName, settingsFile, mcpConfigFile, channelName, model }) {
|
|
282
|
+
export function buildClaudeLaunchCommand({ claudeCommand, agentName, displayName, settingsFile, mcpConfigFile, channelName, model, effort = null }) {
|
|
283
283
|
const parts = [
|
|
284
284
|
shellQuote(claudeCommand),
|
|
285
285
|
'--agent',
|
|
@@ -299,6 +299,10 @@ export function buildClaudeLaunchCommand({ claudeCommand, agentName, displayName
|
|
|
299
299
|
parts.push('--model', shellQuote(model))
|
|
300
300
|
}
|
|
301
301
|
|
|
302
|
+
if (effort) {
|
|
303
|
+
parts.push('--effort', shellQuote(effort))
|
|
304
|
+
}
|
|
305
|
+
|
|
302
306
|
return parts.join(' ')
|
|
303
307
|
}
|
|
304
308
|
|
|
@@ -382,10 +386,11 @@ export function classifyStopFailure(event, fallbackSid = null) {
|
|
|
382
386
|
const payload = event?.payload || null
|
|
383
387
|
const sid = payload?.session_id || fallbackSid || null
|
|
384
388
|
const message = extractFailureMessage(payload) || 'claude_stop_failure'
|
|
389
|
+
const rateLimit = extractRateLimitMetadata(payload)
|
|
385
390
|
|
|
386
391
|
if (/hit your limit|usage limit|capacity|overloaded/i.test(message)) {
|
|
387
392
|
return {
|
|
388
|
-
result: { ok: false, code: 'claude_usage_limit', msg: 'usage_limit', detail: message, sid },
|
|
393
|
+
result: { ok: false, code: 'claude_usage_limit', msg: 'usage_limit', detail: message, rate_limit: rateLimit, sid },
|
|
389
394
|
nextStatus: 'blocked',
|
|
390
395
|
}
|
|
391
396
|
}
|
|
@@ -36,6 +36,9 @@ const cwd = argv.cwd ? path.resolve(argv.cwd) : null
|
|
|
36
36
|
const lane = argv.lane
|
|
37
37
|
const agentName = argv.agent || 'developer'
|
|
38
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'
|
|
39
42
|
const launchTimeoutMs = Number.parseInt(argv['timeout-ms'] || String(DEFAULT_LAUNCH_TIMEOUT_MS), 10)
|
|
40
43
|
const replace = argv.replace === '1'
|
|
41
44
|
|
|
@@ -85,7 +88,9 @@ try {
|
|
|
85
88
|
cwd,
|
|
86
89
|
sid: null,
|
|
87
90
|
agent_name: agentName,
|
|
88
|
-
model:
|
|
91
|
+
model: laneModel,
|
|
92
|
+
effort: laneEffort,
|
|
93
|
+
subagent_model: subagentModel,
|
|
89
94
|
tmux_session: tmuxSession,
|
|
90
95
|
channel_name: channelName,
|
|
91
96
|
channel_port: channelPort,
|
|
@@ -110,7 +115,7 @@ try {
|
|
|
110
115
|
runtimeDir: paths.runtimeDir,
|
|
111
116
|
utilsDir,
|
|
112
117
|
agentName,
|
|
113
|
-
subagentModel
|
|
118
|
+
subagentModel,
|
|
114
119
|
})),
|
|
115
120
|
writeJsonIfNeeded(paths.mcpConfigFile, buildMcpConfig({
|
|
116
121
|
paths,
|
|
@@ -129,7 +134,8 @@ try {
|
|
|
129
134
|
settingsFile: paths.settingsFile,
|
|
130
135
|
mcpConfigFile: paths.mcpConfigFile,
|
|
131
136
|
channelName,
|
|
132
|
-
model:
|
|
137
|
+
model: laneModel,
|
|
138
|
+
effort: laneEffort,
|
|
133
139
|
})
|
|
134
140
|
|
|
135
141
|
const launchResult = await runCommand('tmux', ['new-session', '-d', '-s', tmuxSession, '-c', cwd, launchCommand])
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
|
|
5
3
|
import {
|
|
6
4
|
DEFAULT_TURN_TIMEOUT_MS,
|
|
7
5
|
buildFailureResult,
|
|
@@ -13,7 +11,7 @@ import {
|
|
|
13
11
|
postPromptToChannel,
|
|
14
12
|
parseArgs,
|
|
15
13
|
readJsonl,
|
|
16
|
-
|
|
14
|
+
readPromptInput,
|
|
17
15
|
readState,
|
|
18
16
|
tmuxHasSession,
|
|
19
17
|
waitForRateLimitReset,
|
|
@@ -25,11 +23,11 @@ import {
|
|
|
25
23
|
|
|
26
24
|
const argv = parseArgs(process.argv.slice(2))
|
|
27
25
|
const runtimeDir = argv['runtime-dir']
|
|
28
|
-
const promptFile = argv['prompt-file']
|
|
29
26
|
const turnTimeoutMs = Number.parseInt(argv['timeout-ms'] || String(DEFAULT_TURN_TIMEOUT_MS), 10)
|
|
30
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)
|
|
31
29
|
|
|
32
|
-
if (!runtimeDir || !
|
|
30
|
+
if (!runtimeDir || !hasPromptInput) {
|
|
33
31
|
emitFailure('claude_live_turn_invalid_args', 'Missing required turn arguments', {
|
|
34
32
|
state_file: runtimeDir ? buildRuntimePaths(runtimeDir).stateFile : null,
|
|
35
33
|
})
|
|
@@ -37,6 +35,7 @@ if (!runtimeDir || !promptFile) {
|
|
|
37
35
|
}
|
|
38
36
|
|
|
39
37
|
const paths = buildRuntimePaths(runtimeDir)
|
|
38
|
+
let prompt = ''
|
|
40
39
|
|
|
41
40
|
function emitTurnFailure(resultPayload) {
|
|
42
41
|
emitFailure(resultPayload.code, resultPayload.msg, {
|
|
@@ -47,6 +46,9 @@ function emitTurnFailure(resultPayload) {
|
|
|
47
46
|
}
|
|
48
47
|
|
|
49
48
|
try {
|
|
49
|
+
let promptSource = null
|
|
50
|
+
let promptSourceKind = null
|
|
51
|
+
|
|
50
52
|
const state = await readState(runtimeDir)
|
|
51
53
|
if (!state) {
|
|
52
54
|
const resultPayload = buildFailureResult('claude_live_turn_uninitialized', 'Claude live lane is not initialized')
|
|
@@ -76,7 +78,7 @@ try {
|
|
|
76
78
|
process.exit(1)
|
|
77
79
|
}
|
|
78
80
|
|
|
79
|
-
|
|
81
|
+
({ prompt, promptSource, promptSourceKind } = await readPromptInput(argv))
|
|
80
82
|
let hookStartIndex = (await readJsonl(paths.hookEventsFile)).length
|
|
81
83
|
const turnNumber = Number(state.last_turn_number || 0)
|
|
82
84
|
const turnId = buildTurnId(turnNumber)
|
|
@@ -84,7 +86,8 @@ try {
|
|
|
84
86
|
await writeState(runtimeDir, {
|
|
85
87
|
status: 'running',
|
|
86
88
|
current_turn_id: turnId,
|
|
87
|
-
current_turn_prompt_file:
|
|
89
|
+
current_turn_prompt_file: promptSourceKind === 'file' && promptSource ? promptSource : null,
|
|
90
|
+
current_turn_prompt_source: promptSource,
|
|
88
91
|
current_turn_started_at: new Date().toISOString(),
|
|
89
92
|
last_error: null,
|
|
90
93
|
})
|
|
@@ -107,6 +110,7 @@ try {
|
|
|
107
110
|
status: 'failed',
|
|
108
111
|
current_turn_id: null,
|
|
109
112
|
current_turn_prompt_file: null,
|
|
113
|
+
current_turn_prompt_source: null,
|
|
110
114
|
current_turn_started_at: null,
|
|
111
115
|
last_completed_turn_id: turnId,
|
|
112
116
|
last_turn_number: turnNumber + 1,
|
|
@@ -116,6 +120,9 @@ try {
|
|
|
116
120
|
rate_limit_wait_ms: null,
|
|
117
121
|
rate_limit_wait_source: null,
|
|
118
122
|
rate_limit_message: null,
|
|
123
|
+
rate_limit_reset_at: null,
|
|
124
|
+
rate_limit_type: null,
|
|
125
|
+
rate_limit_raw_reset_at: null,
|
|
119
126
|
})
|
|
120
127
|
emitTurnFailure(resultPayload)
|
|
121
128
|
process.exit(1)
|
|
@@ -138,6 +145,7 @@ try {
|
|
|
138
145
|
transcript_path: event.payload?.transcript_path || liveState.transcript_path || state.transcript_path || null,
|
|
139
146
|
current_turn_id: null,
|
|
140
147
|
current_turn_prompt_file: null,
|
|
148
|
+
current_turn_prompt_source: null,
|
|
141
149
|
current_turn_started_at: null,
|
|
142
150
|
last_completed_turn_id: turnId,
|
|
143
151
|
last_hook_event: 'Stop',
|
|
@@ -149,6 +157,9 @@ try {
|
|
|
149
157
|
rate_limit_wait_ms: null,
|
|
150
158
|
rate_limit_wait_source: null,
|
|
151
159
|
rate_limit_message: null,
|
|
160
|
+
rate_limit_reset_at: null,
|
|
161
|
+
rate_limit_type: null,
|
|
162
|
+
rate_limit_raw_reset_at: null,
|
|
152
163
|
})
|
|
153
164
|
emitSuccess(sessionId, {
|
|
154
165
|
result_file: argv['result-file'] || paths.resultFile,
|
|
@@ -168,6 +179,9 @@ try {
|
|
|
168
179
|
})
|
|
169
180
|
await waitForRateLimitReset({
|
|
170
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,
|
|
171
185
|
statePath: paths.stateFile,
|
|
172
186
|
})
|
|
173
187
|
|
|
@@ -178,6 +192,7 @@ try {
|
|
|
178
192
|
status: 'failed',
|
|
179
193
|
current_turn_id: null,
|
|
180
194
|
current_turn_prompt_file: null,
|
|
195
|
+
current_turn_prompt_source: null,
|
|
181
196
|
current_turn_started_at: null,
|
|
182
197
|
last_completed_turn_id: turnId,
|
|
183
198
|
last_turn_number: turnNumber + 1,
|
|
@@ -187,6 +202,9 @@ try {
|
|
|
187
202
|
rate_limit_wait_ms: null,
|
|
188
203
|
rate_limit_wait_source: null,
|
|
189
204
|
rate_limit_message: null,
|
|
205
|
+
rate_limit_reset_at: null,
|
|
206
|
+
rate_limit_type: null,
|
|
207
|
+
rate_limit_raw_reset_at: null,
|
|
190
208
|
})
|
|
191
209
|
emitTurnFailure(deadPayload)
|
|
192
210
|
process.exit(1)
|
|
@@ -195,7 +213,8 @@ try {
|
|
|
195
213
|
await writeState(runtimeDir, {
|
|
196
214
|
status: 'running',
|
|
197
215
|
current_turn_id: turnId,
|
|
198
|
-
current_turn_prompt_file:
|
|
216
|
+
current_turn_prompt_file: promptSourceKind === 'file' && promptSource ? promptSource : null,
|
|
217
|
+
current_turn_prompt_source: promptSource,
|
|
199
218
|
current_turn_started_at: new Date().toISOString(),
|
|
200
219
|
last_error: null,
|
|
201
220
|
})
|
|
@@ -209,6 +228,7 @@ try {
|
|
|
209
228
|
transcript_path: event.payload?.transcript_path || liveState.transcript_path || state.transcript_path || null,
|
|
210
229
|
current_turn_id: null,
|
|
211
230
|
current_turn_prompt_file: null,
|
|
231
|
+
current_turn_prompt_source: null,
|
|
212
232
|
current_turn_started_at: null,
|
|
213
233
|
last_completed_turn_id: turnId,
|
|
214
234
|
last_hook_event: 'StopFailure',
|
|
@@ -219,6 +239,9 @@ try {
|
|
|
219
239
|
rate_limit_wait_ms: null,
|
|
220
240
|
rate_limit_wait_source: null,
|
|
221
241
|
rate_limit_message: null,
|
|
242
|
+
rate_limit_reset_at: null,
|
|
243
|
+
rate_limit_type: null,
|
|
244
|
+
rate_limit_raw_reset_at: null,
|
|
222
245
|
})
|
|
223
246
|
emitTurnFailure(resultPayload)
|
|
224
247
|
process.exit(1)
|
|
@@ -229,12 +252,13 @@ try {
|
|
|
229
252
|
const resultPayload = buildFailureResult(code, error instanceof Error ? error.message : String(error), state?.sid || null)
|
|
230
253
|
const turnNumber = Number(state?.last_turn_number || 0)
|
|
231
254
|
const turnId = buildTurnId(turnNumber)
|
|
232
|
-
const
|
|
233
|
-
await writeTurnArtifacts(paths, turnId,
|
|
255
|
+
const promptForArtifacts = prompt || await readPromptInput(argv).then((value) => value.prompt).catch(() => '')
|
|
256
|
+
await writeTurnArtifacts(paths, turnId, promptForArtifacts, resultPayload, argv['result-file'] || null)
|
|
234
257
|
await writeState(runtimeDir, {
|
|
235
258
|
status: 'failed',
|
|
236
259
|
current_turn_id: null,
|
|
237
260
|
current_turn_prompt_file: null,
|
|
261
|
+
current_turn_prompt_source: null,
|
|
238
262
|
current_turn_started_at: null,
|
|
239
263
|
last_completed_turn_id: turnId,
|
|
240
264
|
last_turn_number: turnNumber + 1,
|
|
@@ -244,6 +268,9 @@ try {
|
|
|
244
268
|
rate_limit_wait_ms: null,
|
|
245
269
|
rate_limit_wait_source: null,
|
|
246
270
|
rate_limit_message: null,
|
|
271
|
+
rate_limit_reset_at: null,
|
|
272
|
+
rate_limit_type: null,
|
|
273
|
+
rate_limit_raw_reset_at: null,
|
|
247
274
|
})
|
|
248
275
|
emitTurnFailure(resultPayload)
|
|
249
276
|
process.exit(1)
|
|
@@ -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,
|
|
@@ -16,6 +16,33 @@ function utilsDir() {
|
|
|
16
16
|
return path.dirname(fileURLToPath(import.meta.url))
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
function normalizeResetAtCandidate(value) {
|
|
20
|
+
if (value === null || value === undefined || value === '') {
|
|
21
|
+
return null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
25
|
+
const ms = value > 1_000_000_000_000 ? value : value * 1000
|
|
26
|
+
return new Date(ms)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (typeof value === 'string') {
|
|
30
|
+
const trimmed = value.trim()
|
|
31
|
+
if (!trimmed) {
|
|
32
|
+
return null
|
|
33
|
+
}
|
|
34
|
+
if (/^\d+$/.test(trimmed)) {
|
|
35
|
+
return normalizeResetAtCandidate(Number(trimmed))
|
|
36
|
+
}
|
|
37
|
+
const parsed = Date.parse(trimmed)
|
|
38
|
+
if (!Number.isNaN(parsed)) {
|
|
39
|
+
return new Date(parsed)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
|
|
19
46
|
export function parseArgs(argv) {
|
|
20
47
|
const result = {}
|
|
21
48
|
for (let index = 0; index < argv.length; index += 1) {
|
|
@@ -66,6 +93,50 @@ export async function readPrompt(promptFile) {
|
|
|
66
93
|
return content.trim()
|
|
67
94
|
}
|
|
68
95
|
|
|
96
|
+
async function readPromptFromStdin() {
|
|
97
|
+
const chunks = []
|
|
98
|
+
for await (const chunk of process.stdin) {
|
|
99
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
|
|
100
|
+
}
|
|
101
|
+
return Buffer.concat(chunks).toString('utf8').trim()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function readPromptInput(argv) {
|
|
105
|
+
const hasPromptFile = typeof argv['prompt-file'] === 'string' && argv['prompt-file'].trim().length > 0
|
|
106
|
+
const hasPromptInline = typeof argv.prompt === 'string'
|
|
107
|
+
const hasPromptStdin = argv['prompt-stdin'] === '1' || process.stdin.isTTY === false
|
|
108
|
+
|
|
109
|
+
if (hasPromptStdin) {
|
|
110
|
+
return {
|
|
111
|
+
prompt: await readPromptFromStdin(),
|
|
112
|
+
promptSource: 'stdin',
|
|
113
|
+
promptSourceKind: 'stdin',
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (hasPromptFile && hasPromptInline) {
|
|
118
|
+
throw new Error('Prompt input is ambiguous: provide only one of legacy fallback inputs --prompt-file or --prompt when stdin is not used')
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (hasPromptFile) {
|
|
122
|
+
return {
|
|
123
|
+
prompt: await readPrompt(argv['prompt-file']),
|
|
124
|
+
promptSource: path.resolve(argv['prompt-file']),
|
|
125
|
+
promptSourceKind: 'file',
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (hasPromptInline) {
|
|
130
|
+
return {
|
|
131
|
+
prompt: String(argv.prompt).trim(),
|
|
132
|
+
promptSource: 'inline',
|
|
133
|
+
promptSourceKind: 'inline',
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
throw new Error('Missing prompt input: pipe prompt text through stdin (preferred) or use legacy fallback --prompt-file <file> / --prompt <text>')
|
|
138
|
+
}
|
|
139
|
+
|
|
69
140
|
export async function resolveClaudeSessionPath(sessionId, cwd) {
|
|
70
141
|
const projectsRoot = path.join(os.homedir(), '.claude', 'projects')
|
|
71
142
|
const directKey = cwd.replaceAll(path.sep, '-')
|
|
@@ -278,6 +349,47 @@ export function classifyClaudeFailure(parsed, fallbackMessage = '') {
|
|
|
278
349
|
}
|
|
279
350
|
}
|
|
280
351
|
|
|
352
|
+
export function extractRateLimitMetadata(payload = null) {
|
|
353
|
+
if (!payload || typeof payload !== 'object') {
|
|
354
|
+
return null
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const info = payload.rate_limit_info && typeof payload.rate_limit_info === 'object'
|
|
358
|
+
? payload.rate_limit_info
|
|
359
|
+
: payload.rateLimitInfo && typeof payload.rateLimitInfo === 'object'
|
|
360
|
+
? payload.rateLimitInfo
|
|
361
|
+
: payload.rateLimit && typeof payload.rateLimit === 'object'
|
|
362
|
+
? payload.rateLimit
|
|
363
|
+
: null
|
|
364
|
+
|
|
365
|
+
const rawResetAt =
|
|
366
|
+
info?.resetsAt
|
|
367
|
+
?? info?.resetAt
|
|
368
|
+
?? payload.resetsAt
|
|
369
|
+
?? payload.resetAt
|
|
370
|
+
?? payload.rate_limit_reset_at
|
|
371
|
+
?? payload.limit_reset_at
|
|
372
|
+
?? null
|
|
373
|
+
|
|
374
|
+
const normalizedResetAt = normalizeResetAtCandidate(rawResetAt)
|
|
375
|
+
const rateLimitType =
|
|
376
|
+
info?.rateLimitType
|
|
377
|
+
?? info?.type
|
|
378
|
+
?? payload.rateLimitType
|
|
379
|
+
?? payload.rate_limit_type
|
|
380
|
+
?? null
|
|
381
|
+
|
|
382
|
+
if (!normalizedResetAt && !rateLimitType && rawResetAt === null) {
|
|
383
|
+
return null
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
resetAt: normalizedResetAt ? normalizedResetAt.toISOString() : null,
|
|
388
|
+
rawResetAt,
|
|
389
|
+
rateLimitType,
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
281
393
|
function normalizeOffset(token) {
|
|
282
394
|
if (!token) return null
|
|
283
395
|
const normalized = token.trim().toUpperCase()
|
|
@@ -399,7 +511,19 @@ function parseAbsoluteCandidate(candidate, now) {
|
|
|
399
511
|
return parseTimeOnlyCandidate(cleaned, now)
|
|
400
512
|
}
|
|
401
513
|
|
|
402
|
-
export function calculateRateLimitReset({ message, now = new Date(), bufferMs = DEFAULT_RATE_LIMIT_BUFFER_MS } = {}) {
|
|
514
|
+
export function calculateRateLimitReset({ message, resetAt = null, rateLimitType = null, rawResetAt = null, now = new Date(), bufferMs = DEFAULT_RATE_LIMIT_BUFFER_MS } = {}) {
|
|
515
|
+
const explicitResetAt = normalizeResetAtCandidate(resetAt)
|
|
516
|
+
if (explicitResetAt) {
|
|
517
|
+
return {
|
|
518
|
+
waitMs: Math.max(explicitResetAt.getTime() - now.getTime() + bufferMs, 0),
|
|
519
|
+
resetAt: explicitResetAt,
|
|
520
|
+
source: 'structured_reset_at',
|
|
521
|
+
rawMessage: String(message || '').trim(),
|
|
522
|
+
rateLimitType: rateLimitType || null,
|
|
523
|
+
rawResetAt,
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
403
527
|
const rawMessage = String(message || '').trim()
|
|
404
528
|
if (!rawMessage) {
|
|
405
529
|
const fallbackMs = msUntilNextQuotaReset(now)
|
|
@@ -408,6 +532,8 @@ export function calculateRateLimitReset({ message, now = new Date(), bufferMs =
|
|
|
408
532
|
resetAt: new Date(now.getTime() + fallbackMs),
|
|
409
533
|
source: 'fallback_default_quota_window',
|
|
410
534
|
rawMessage,
|
|
535
|
+
rateLimitType: rateLimitType || null,
|
|
536
|
+
rawResetAt,
|
|
411
537
|
}
|
|
412
538
|
}
|
|
413
539
|
|
|
@@ -418,6 +544,8 @@ export function calculateRateLimitReset({ message, now = new Date(), bufferMs =
|
|
|
418
544
|
resetAt: relative,
|
|
419
545
|
source: 'relative_duration',
|
|
420
546
|
rawMessage,
|
|
547
|
+
rateLimitType: rateLimitType || null,
|
|
548
|
+
rawResetAt,
|
|
421
549
|
}
|
|
422
550
|
}
|
|
423
551
|
|
|
@@ -430,6 +558,8 @@ export function calculateRateLimitReset({ message, now = new Date(), bufferMs =
|
|
|
430
558
|
resetAt: parsed,
|
|
431
559
|
source: 'anchored_phrase',
|
|
432
560
|
rawMessage,
|
|
561
|
+
rateLimitType: rateLimitType || null,
|
|
562
|
+
rawResetAt,
|
|
433
563
|
}
|
|
434
564
|
}
|
|
435
565
|
}
|
|
@@ -443,6 +573,8 @@ export function calculateRateLimitReset({ message, now = new Date(), bufferMs =
|
|
|
443
573
|
resetAt: parsed,
|
|
444
574
|
source: 'iso_datetime',
|
|
445
575
|
rawMessage,
|
|
576
|
+
rateLimitType: rateLimitType || null,
|
|
577
|
+
rawResetAt,
|
|
446
578
|
}
|
|
447
579
|
}
|
|
448
580
|
}
|
|
@@ -453,11 +585,13 @@ export function calculateRateLimitReset({ message, now = new Date(), bufferMs =
|
|
|
453
585
|
resetAt: new Date(now.getTime() + fallbackMs),
|
|
454
586
|
source: 'fallback_default_quota_window',
|
|
455
587
|
rawMessage,
|
|
588
|
+
rateLimitType: rateLimitType || null,
|
|
589
|
+
rawResetAt,
|
|
456
590
|
}
|
|
457
591
|
}
|
|
458
592
|
|
|
459
|
-
export async function waitForRateLimitReset({ message, statePath = null, dryRun = false, bufferMs = DEFAULT_RATE_LIMIT_BUFFER_MS } = {}) {
|
|
460
|
-
const waitInfo = calculateRateLimitReset({ message, bufferMs })
|
|
593
|
+
export async function waitForRateLimitReset({ message, resetAt = null, rateLimitType = null, rawResetAt = null, statePath = null, dryRun = false, bufferMs = DEFAULT_RATE_LIMIT_BUFFER_MS } = {}) {
|
|
594
|
+
const waitInfo = calculateRateLimitReset({ message, resetAt, rateLimitType, rawResetAt, bufferMs })
|
|
461
595
|
if (statePath) {
|
|
462
596
|
const existing = (await readJsonIfExists(statePath)) || {}
|
|
463
597
|
await writeJsonIfNeeded(statePath, {
|
|
@@ -468,6 +602,9 @@ export async function waitForRateLimitReset({ message, statePath = null, dryRun
|
|
|
468
602
|
rate_limit_wait_ms: waitInfo.waitMs,
|
|
469
603
|
rate_limit_wait_source: waitInfo.source,
|
|
470
604
|
rate_limit_message: waitInfo.rawMessage || null,
|
|
605
|
+
rate_limit_reset_at: waitInfo.resetAt.toISOString(),
|
|
606
|
+
rate_limit_type: waitInfo.rateLimitType || null,
|
|
607
|
+
rate_limit_raw_reset_at: waitInfo.rawResetAt ?? null,
|
|
471
608
|
})
|
|
472
609
|
}
|
|
473
610
|
if (!dryRun && waitInfo.waitMs > 0) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import fs from 'node:fs/promises'
|
|
4
|
+
import os from 'node:os'
|
|
4
5
|
import path from 'node:path'
|
|
5
6
|
import { spawn } from 'node:child_process'
|
|
6
7
|
|
|
@@ -73,6 +74,23 @@ async function createZipArchive(sourceDir, outputPath) {
|
|
|
73
74
|
}
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
async function normalizeClaudeJsonlFiles(projectDir) {
|
|
78
|
+
const normalizerScript = path.join(path.dirname(new URL(import.meta.url).pathname), 'normalize_claude_session.py')
|
|
79
|
+
const entries = await fs.readdir(projectDir, { withFileTypes: true }).catch(() => [])
|
|
80
|
+
const jsonlFiles = entries
|
|
81
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.jsonl'))
|
|
82
|
+
.map((entry) => path.join(projectDir, entry.name))
|
|
83
|
+
|
|
84
|
+
for (const filePath of jsonlFiles) {
|
|
85
|
+
const tempOutputPath = `${filePath}.normalized`
|
|
86
|
+
const result = await run('python3', [normalizerScript, filePath, '--output', tempOutputPath], projectDir)
|
|
87
|
+
if (result.code !== 0) {
|
|
88
|
+
throw new Error(`Failed to normalize Claude session file ${path.basename(filePath)}: ${(result.stderr || result.stdout).trim()}`)
|
|
89
|
+
}
|
|
90
|
+
await fs.rename(tempOutputPath, filePath)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
76
94
|
try {
|
|
77
95
|
const sessionId = argv['session-id']
|
|
78
96
|
const transcriptPath = await resolveClaudeSessionPath(sessionId, argv.cwd)
|
|
@@ -80,16 +98,25 @@ try {
|
|
|
80
98
|
throw new Error(`Claude transcript not found for session ${sessionId}`)
|
|
81
99
|
}
|
|
82
100
|
|
|
83
|
-
const
|
|
101
|
+
const sourceProjectDir = path.dirname(transcriptPath)
|
|
102
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'slopmachine-claude-project-'))
|
|
103
|
+
const projectDir = path.join(tempRoot, path.basename(sourceProjectDir))
|
|
104
|
+
await fs.cp(sourceProjectDir, projectDir, { recursive: true })
|
|
105
|
+
await normalizeClaudeJsonlFiles(projectDir)
|
|
84
106
|
const included = (await fs.readdir(projectDir).catch(() => [])).sort((left, right) => left.localeCompare(right))
|
|
85
107
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
108
|
+
try {
|
|
109
|
+
await createZipArchive(projectDir, argv.output)
|
|
110
|
+
emitSuccess(sessionId, {
|
|
111
|
+
output: argv.output,
|
|
112
|
+
project_dir: sourceProjectDir,
|
|
113
|
+
label: argv.label || null,
|
|
114
|
+
included,
|
|
115
|
+
normalized: true,
|
|
116
|
+
})
|
|
117
|
+
} finally {
|
|
118
|
+
await fs.rm(tempRoot, { recursive: true, force: true }).catch(() => {})
|
|
119
|
+
}
|
|
93
120
|
} catch (error) {
|
|
94
121
|
emitFailure('package_claude_session_failed', error instanceof Error ? error.message : String(error))
|
|
95
122
|
process.exitCode = 1
|
package/package.json
CHANGED
package/src/constants.js
CHANGED
|
@@ -62,6 +62,7 @@ export const CLAUDE_REQUIRED_AGENT_FILES = ["developer.md"];
|
|
|
62
62
|
export const REQUIRED_SLOPMACHINE_FILES = [
|
|
63
63
|
"backend-evaluation-prompt.md",
|
|
64
64
|
"frontend-evaluation-prompt.md",
|
|
65
|
+
"test-coverage-prompt.md",
|
|
65
66
|
"workflow-init.js",
|
|
66
67
|
"templates/AGENTS.md",
|
|
67
68
|
"templates/CLAUDE.md",
|
|
@@ -81,6 +82,7 @@ export const REQUIRED_SLOPMACHINE_FILES = [
|
|
|
81
82
|
"utils/prepare_strict_audit_workspace.mjs",
|
|
82
83
|
"utils/claude_wait_for_rate_limit_reset.mjs",
|
|
83
84
|
"utils/claude_wait_for_rate_limit_reset.sh",
|
|
85
|
+
"utils/normalize_claude_session.py",
|
|
84
86
|
"scaffold-playbooks/docker-shared-contract.md",
|
|
85
87
|
"scaffold-playbooks/generic-unknown-tech-guide.md",
|
|
86
88
|
"scaffold-playbooks/frontend-family-matrix.md",
|
|
@@ -113,8 +115,6 @@ export const REQUIRED_SLOPMACHINE_FILES = [
|
|
|
113
115
|
"utils/cleanup_delivery_artifacts.py",
|
|
114
116
|
];
|
|
115
117
|
|
|
116
|
-
export const REQUIRED_SLOPMACHINE_ROOT_FILES = ["test-coverage-prompt.md"];
|
|
117
|
-
|
|
118
118
|
export const MCP_ENTRIES = {
|
|
119
119
|
context7: {
|
|
120
120
|
enabled: true,
|
package/src/init.js
CHANGED
|
@@ -288,7 +288,13 @@ async function createInitialPhaseArtifacts(targetPath, options) {
|
|
|
288
288
|
`## Bootstrap Status\n\n` +
|
|
289
289
|
`- Workspace initialized by slopmachine.\n` +
|
|
290
290
|
`${options.adoptExisting ? '- Existing project adoption mode is active.\n' : ''}` +
|
|
291
|
-
`${options.requestedStartPhase ? `- Requested start phase: ${options.requestedStartPhase}.\n` : ''}`
|
|
291
|
+
`${options.requestedStartPhase ? `- Requested start phase: ${options.requestedStartPhase}.\n` : ''}` +
|
|
292
|
+
`\n## Entry Template\n\n` +
|
|
293
|
+
`Copy this exact structure for each clarification item:\n\n` +
|
|
294
|
+
`### 1. Clarification Defaults for Planning\n` +
|
|
295
|
+
`- Question: Can the drafted clarification defaults be used for planning?\n` +
|
|
296
|
+
`- My Understanding: The prompt was large enough that planning needed explicit confirmation that the clarification package was acceptable. We needed to lock this in rather than carrying uncertainty forward into the planning phase.\n` +
|
|
297
|
+
`- Solution: Yes. Proceed with the drafted defaults, allowing planning to start from the approved clarification brief instead of an uncertain baseline.\n`
|
|
292
298
|
|
|
293
299
|
const prePlanningBriefContent = `# Pre-Planning Brief\n\n` +
|
|
294
300
|
`Capture the planning-critical project shape here before real planning begins.\n\n` +
|