Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 40 additions & 4 deletions src/agent/clis/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
lastTextEmit.set(key, now)
const prefix = section ? `\x1B[90m[${section}]\x1B[0m ` : ''
// Count bullet items in accumulated text for meaningful progress
const items = text ? (text.match(/^- (?:BREAKING|DEPRECATED|NEW|CHANGED|REMOVED|Use |Do |Set |Add |Avoid |Always |Never |Prefer |Check |Ensure )/gm)?.length ?? 0) : 0

Check warning on line 83 in src/agent/clis/index.ts

View workflow job for this annotation

GitHub Actions / test

Move this regular expression to module scope to avoid re-compilation on every call
emit(items > 0 ? `${prefix}Writing... \x1B[90m(${items} items)\x1B[0m` : `${prefix}Writing...`)
return
}
Expand All @@ -88,14 +88,14 @@
return

// Handle status messages like [starting...], [retrying...], [cached]
if (/^\[(?:starting|retrying|cached)/.test(chunk)) {

Check warning on line 91 in src/agent/clis/index.ts

View workflow job for this annotation

GitHub Actions / test

Move this regular expression to module scope to avoid re-compilation on every call
const prefix = section ? `\x1B[90m[${section}]\x1B[0m ` : ''
emit(`${prefix}${chunk.slice(1, -1)}`)
return
}

// Parse individual tool names and hints from "[Read: path]" or "[Read, Glob: path1, path2]"
const match = chunk.match(/^\[([^:[\]]+)(?::\s(.+))?\]$/)

Check warning on line 98 in src/agent/clis/index.ts

View workflow job for this annotation

GitHub Actions / test

Move this regular expression to module scope to avoid re-compilation on every call
if (!match)
return

Expand All @@ -109,7 +109,7 @@
const prefix = section ? `\x1B[90m[${section}]\x1B[0m ` : ''

if ((rawName === 'Bash' || rawName === 'run_shell_command') && hint) {
const searchMatch = hint.match(/skilld search\s+"([^"]+)"/)

Check warning on line 112 in src/agent/clis/index.ts

View workflow job for this annotation

GitHub Actions / test

Move this regular expression to module scope to avoid re-compilation on every call
if (searchMatch) {
emit(`${prefix}Searching \x1B[36m"${searchMatch[1]}"\x1B[0m`)
}
Expand Down Expand Up @@ -290,7 +290,7 @@

/** Strip absolute paths from prompt so the hash is project-independent */
function normalizePromptForHash(prompt: string): string {
return prompt.replace(/\/[^\s`]*\.(?:claude|codex|gemini)\/skills\/[^\s/`]+/g, '<SKILL_DIR>')

Check warning on line 293 in src/agent/clis/index.ts

View workflow job for this annotation

GitHub Actions / test

Move this regular expression to module scope to avoid re-compilation on every call
}

function hashPrompt(prompt: string, model: OptimizeModel, section: SkillSection): string {
Expand Down Expand Up @@ -333,7 +333,7 @@
const skilldDir = join(skillDir, '.skilld')
const outputPath = join(skilldDir, outputFile)
const logsDir = join(skilldDir, 'logs')
const logName = section.toUpperCase().replace(/-/g, '_')

Check warning on line 336 in src/agent/clis/index.ts

View workflow job for this annotation

GitHub Actions / test

Move this regular expression to module scope to avoid re-compilation on every call

// Remove stale output so we don't read a leftover from a previous run
if (existsSync(outputPath))
Expand Down Expand Up @@ -533,7 +533,7 @@

// Always write stderr on failure; write all logs in debug mode
const logsDir = join(skilldDir, 'logs')
const logName = section.toUpperCase().replace(/-/g, '_')

Check warning on line 536 in src/agent/clis/index.ts

View workflow job for this annotation

GitHub Actions / test

Move this regular expression to module scope to avoid re-compilation on every call
if (debug || (stderr && (!raw || code !== 0))) {
mkdirSync(logsDir, { recursive: true })
if (stderr)
Expand Down Expand Up @@ -715,10 +715,21 @@
}
}

// Retry failed sections once (sequential to avoid rate limits)
for (const { section, prompt } of retryQueue) {
onProgress?.({ chunk: `[${section}: retrying...]`, type: 'reasoning', text: '', reasoning: '', section })
await delay(STAGGER_MS)
// Retry failed sections (sequential, with rate-limit aware backoff)
for (const { index, section, prompt } of retryQueue) {
const prevError = getRetryError(spawnResults[index]!)
const rateLimitDelay = parseRateLimitDelay(prevError)

if (rateLimitDelay != null) {
const waitSec = Math.max(rateLimitDelay, 5)
onProgress?.({ chunk: `[${section}] Rate limited, waiting ${waitSec}s...`, type: 'reasoning', text: '', reasoning: '', section })
await delay(waitSec * 1000)
}
else {
onProgress?.({ chunk: `[${section}: retrying...]`, type: 'reasoning', text: '', reasoning: '', section })
await delay(STAGGER_MS)
}

const result = await optimizeSection({
section,
prompt,
Expand Down Expand Up @@ -792,6 +803,31 @@

// ── Helpers ──────────────────────────────────────────────────────────

/** Check if an error string indicates a rate limit (429) */
function isRateLimitError(error: string | undefined): boolean {
if (!error)
return false
return /\b429\b/.test(error)

Check warning on line 810 in src/agent/clis/index.ts

View workflow job for this annotation

GitHub Actions / test

Move this regular expression to module scope to avoid re-compilation on every call
|| /rate.?limit/i.test(error)

Check warning on line 811 in src/agent/clis/index.ts

View workflow job for this annotation

GitHub Actions / test

Move this regular expression to module scope to avoid re-compilation on every call
|| /exhausted.*capacity/i.test(error)
|| /quota.*reset/i.test(error)
}

/** Parse delay hint from rate limit error (e.g. "reset after 5s" β†’ 5). Returns undefined if not a rate limit. */
function parseRateLimitDelay(error: string | undefined): number | undefined {
if (!error || !isRateLimitError(error))
return undefined
const match = error.match(/reset\s+after\s+(\d+)s/i)
return match ? Number(match[1]) : 10 // default 10s if no hint
}

/** Extract error string from a PromiseSettledResult */
function getRetryError(result: PromiseSettledResult<SectionResult>): string | undefined {
if (result.status === 'rejected')
return String(result.reason)
return result.value.error
}

/** Shorten absolute paths for display: /home/user/project/.claude/skills/vue/SKILL.md β†’ .claude/.../SKILL.md */
function shortenPath(p: string): string {
const refIdx = p.indexOf('.skilld/')
Expand Down
5 changes: 4 additions & 1 deletion src/commands/sync-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1452,7 +1452,10 @@ export async function enhanceSkillWithLLM(opts: EnhanceOptions): Promise<void> {
writeFileSync(join(skillDir, 'SKILL.md'), skillMd)
}
else {
llmLog.error(`Enhancement failed${error ? `: ${error}` : ''}`)
if (error && /\b429\b|rate.?limit|exhausted.*capacity|quota.*reset/i.test(error))
llmLog.error(`Rate limited by LLM provider. Try again shortly or use a different model via \`skilld config\``)
else
llmLog.error(`Enhancement failed${error ? `: ${error}` : ''}`)
}
}

Expand Down
Loading