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.
Files changed (30) hide show
  1. package/README.md +1 -1
  2. package/RELEASE.md +2 -2
  3. package/assets/agents/developer.md +13 -13
  4. package/assets/agents/slopmachine-claude.md +7 -5
  5. package/assets/agents/slopmachine.md +6 -5
  6. package/assets/claude/agents/developer.md +6 -6
  7. package/assets/skills/clarification-gate/SKILL.md +9 -18
  8. package/assets/skills/claude-worker-management/SKILL.md +34 -22
  9. package/assets/skills/developer-session-lifecycle/SKILL.md +2 -1
  10. package/assets/skills/development-guidance/SKILL.md +3 -0
  11. package/assets/skills/evaluation-triage/SKILL.md +6 -4
  12. package/assets/skills/final-evaluation-orchestration/SKILL.md +16 -13
  13. package/assets/skills/hardening-gate/SKILL.md +3 -0
  14. package/assets/skills/integrated-verification/SKILL.md +2 -0
  15. package/assets/skills/planning-guidance/SKILL.md +1 -0
  16. package/assets/skills/submission-packaging/SKILL.md +6 -4
  17. package/assets/skills/verification-gates/SKILL.md +7 -2
  18. package/assets/slopmachine/test-coverage-prompt.md +561 -0
  19. package/assets/slopmachine/utils/claude_create_session.mjs +2 -2
  20. package/assets/slopmachine/utils/claude_live_common.mjs +8 -3
  21. package/assets/slopmachine/utils/claude_live_launch.mjs +9 -3
  22. package/assets/slopmachine/utils/claude_live_stop.mjs +1 -0
  23. package/assets/slopmachine/utils/claude_live_turn.mjs +37 -10
  24. package/assets/slopmachine/utils/claude_resume_session.mjs +2 -2
  25. package/assets/slopmachine/utils/claude_worker_common.mjs +140 -3
  26. package/assets/slopmachine/utils/package_claude_session.mjs +35 -8
  27. package/package.json +1 -1
  28. package/src/constants.js +2 -2
  29. package/src/init.js +7 -1
  30. 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: argv.model || null,
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: argv.model || 'sonnet',
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: argv.model || null,
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])
@@ -34,6 +34,7 @@ await writeState(runtimeDir, {
34
34
  status: 'stopped',
35
35
  current_turn_id: null,
36
36
  current_turn_prompt_file: null,
37
+ current_turn_prompt_source: null,
37
38
  current_turn_started_at: null,
38
39
  last_error: null,
39
40
  stopped_at: new Date().toISOString(),
@@ -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
- readPrompt,
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 || !promptFile) {
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
- const prompt = await readPrompt(promptFile)
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: path.resolve(promptFile),
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: path.resolve(promptFile),
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 prompt = await readPrompt(promptFile).catch(() => '')
233
- await writeTurnArtifacts(paths, turnId, prompt, resultPayload, argv['result-file'] || null)
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, 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,
@@ -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 projectDir = path.dirname(transcriptPath)
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
- await createZipArchive(projectDir, argv.output)
87
- emitSuccess(sessionId, {
88
- output: argv.output,
89
- project_dir: projectDir,
90
- label: argv.label || null,
91
- included,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "theslopmachine",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "SlopMachine installer and project bootstrap CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
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` +