diff --git a/src/commands/init.ts b/src/commands/init.ts index 93ac40ef..721bb19d 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -70,7 +70,7 @@ If the user chose personal CLAUDE.local.md or both: ask about them, not the code - Only if Phase 2 found multiple git worktrees: ask whether their worktrees are nested inside the main repo (e.g., \`.claude/worktrees//\`) or siblings/external (e.g., \`../myrepo-feature/\`). If nested, the upward file walk finds the main repo's CLAUDE.local.md automatically — no special handling needed. If sibling/external, the personal content should live in a home-directory file (e.g., \`~/.claude/-instructions.md\`) and each worktree gets a one-line CLAUDE.local.md stub that imports it: \`@~/.claude/-instructions.md\`. Never put this import in the project CLAUDE.md — that would check a personal reference into the team-shared file. - Any communication preferences? (e.g., "be terse", "always explain tradeoffs", "don't summarize at the end") -**Synthesize a proposal from Phase 2 findings** — e.g., format-on-edit if a formatter exists, a \`/verify\` skill if tests exist, a CLAUDE.md note for anything from the gap-fill answers that's a guideline rather than a workflow. For each, pick the artifact type that fits, **constrained by the Phase 1 skills+hooks choice**: +**Synthesize a proposal from Phase 2 findings** — e.g., format-on-edit if a formatter exists, a project verification workflow if tests exist, a CLAUDE.md note for anything from the gap-fill answers that's a guideline rather than a workflow. For each, pick the artifact type that fits, **constrained by the Phase 1 skills+hooks choice**: - **Hook** (stricter) — deterministic shell command on a tool event; Claude can't skip it. Fits mechanical, fast, per-edit steps: formatting, linting, running a quick test on the changed file. - **Skill** (on-demand) — you or Claude invoke \`/skill-name\` when you want it. Fits workflows that don't belong on every edit: deep verification, session reports, deploys. @@ -85,7 +85,7 @@ If the user chose personal CLAUDE.local.md or both: ask about them, not the code - **Keep previews compact — the preview box truncates with no scrolling.** One line per item, no blank lines between items, no header. Example preview content: • **Format-on-edit hook** (automatic) — \`ruff format \` via PostToolUse - • **/verify skill** (on-demand) — \`make lint && make typecheck && make test\` + • **Verification workflow** (on-demand) — \`make lint && make typecheck && make test\` • **CLAUDE.md note** (guideline) — "run lint/typecheck/test before marking done" - Option labels stay short ("Looks good", "Drop the hook", "Drop the skill") — the tool auto-adds an "Other" free-text option, so don't add your own catch-all. @@ -157,7 +157,7 @@ Skills add capabilities Claude can use on demand without bloating every session. **First, consume \`skill\` entries from the Phase 3 preference queue.** Each queued skill preference becomes a SKILL.md tailored to what the user described. For each: - Name it from the preference (e.g., "verify-deep", "session-report", "deploy-sandbox") -- Write the body using the user's own words from the interview plus whatever Phase 2 found (test commands, report format, deploy target). If the preference maps to an existing bundled skill (e.g., \`/verify\`), write a project skill that adds the user's specific constraints on top — tell the user the bundled one still exists and theirs is additive. +- Write the body using the user's own words from the interview plus whatever Phase 2 found (test commands, report format, deploy target). If the preference maps to an existing project workflow, write a project skill that captures the user's specific constraints on top. - Ask a quick follow-up if the preference is underspecified (e.g., "which test command should verify-deep run?") **Then suggest additional skills** beyond the queue when you find: diff --git a/src/coordinator/coordinatorMode.ts b/src/coordinator/coordinatorMode.ts index fc5dc4eb..d0d14298 100644 --- a/src/coordinator/coordinatorMode.ts +++ b/src/coordinator/coordinatorMode.ts @@ -111,7 +111,7 @@ export function getCoordinatorUserContext( export function getCoordinatorSystemPrompt(): string { const workerCapabilities = isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE) ? 'Workers have access to Bash, Read, and Edit tools, plus MCP tools from configured MCP servers.' - : 'Workers have access to standard tools, MCP tools from configured MCP servers, and project skills via the Skill tool. Delegate skill invocations (e.g. /commit, /verify) to workers.' + : 'Workers have access to standard tools, MCP tools from configured MCP servers, and project skills via the Skill tool. Delegate skill invocations (e.g. /commit or project workflow skills) to workers.' return `You are Claude Code, an AI assistant that orchestrates software engineering tasks across multiple workers. diff --git a/src/services/mockRateLimits.ts b/src/services/mockRateLimits.ts index 519af680..81fb6c9b 100644 --- a/src/services/mockRateLimits.ts +++ b/src/services/mockRateLimits.ts @@ -1,14 +1,12 @@ // Mock rate limits for testing [internal-only] -// This allows testing various rate limit scenarios without hitting actual limits -// -// ⚠️ WARNING: This is for internal testing/demo purposes only! -// The mock headers may not exactly match the API specification or real-world behavior. -// Always validate against actual API responses before relying on this for production features. +// The external build keeps this module as a stable no-op surface so imports +// remain valid without exposing internal-only rate-limit simulation behavior. -import type { SubscriptionType } from '../services/oauth/types.js' import { setMockBillingAccessOverride } from '../utils/billing.js' import type { OverageDisabledReason } from './claudeAiLimits.js' +type SubscriptionType = string + type MockHeaders = { 'anthropic-ratelimit-unified-status'?: | 'allowed' @@ -29,7 +27,6 @@ type MockHeaders = { 'anthropic-ratelimit-unified-fallback'?: 'available' 'anthropic-ratelimit-unified-fallback-percentage'?: string 'retry-after'?: string - // Early warning utilization headers 'anthropic-ratelimit-unified-5h-utilization'?: string 'anthropic-ratelimit-unified-5h-reset'?: string 'anthropic-ratelimit-unified-5h-surpassed-threshold'?: string @@ -79,679 +76,53 @@ export type MockScenario = | 'extra-usage-required' | 'clear' -let mockHeaders: MockHeaders = {} -let mockEnabled = false -let mockHeaderless429Message: string | null = null -let mockSubscriptionType: SubscriptionType | null = null -let mockFastModeRateLimitDurationMs: number | null = null -let mockFastModeRateLimitExpiresAt: number | null = null -// Default subscription type for mock testing -const DEFAULT_MOCK_SUBSCRIPTION: SubscriptionType = 'max' - -// Track individual exceeded limits with their reset times -type ExceededLimit = { - type: 'five_hour' | 'seven_day' | 'seven_day_opus' | 'seven_day_sonnet' - resetsAt: number // Unix timestamp -} - -let exceededLimits: ExceededLimit[] = [] - -// New approach: Toggle individual headers export function setMockHeader( - key: MockHeaderKey, - value: string | undefined, -): void { - if (process.env.USER_TYPE !== 'ant') { - return - } + _key: MockHeaderKey, + _value: string | undefined, +): void {} - mockEnabled = true - - // Special case for retry-after which doesn't have the prefix - const fullKey = ( - key === 'retry-after' ? 'retry-after' : `anthropic-ratelimit-unified-${key}` - ) as keyof MockHeaders - - if (value === undefined || value === 'clear') { - delete mockHeaders[fullKey] - if (key === 'claim') { - exceededLimits = [] - } - // Update retry-after if status changed - if (key === 'status' || key === 'overage-status') { - updateRetryAfter() - } - return - } else { - // Handle special cases for reset times - if (key === 'reset' || key === 'overage-reset') { - // If user provides a number, treat it as hours from now - const hours = Number(value) - if (!isNaN(hours)) { - value = String(Math.floor(Date.now() / 1000) + hours * 3600) - } - } - - // Handle claims - add to exceeded limits - if (key === 'claim') { - const validClaims = [ - 'five_hour', - 'seven_day', - 'seven_day_opus', - 'seven_day_sonnet', - ] - if (validClaims.includes(value)) { - // Determine reset time based on claim type - let resetsAt: number - if (value === 'five_hour') { - resetsAt = Math.floor(Date.now() / 1000) + 5 * 3600 - } else if ( - value === 'seven_day' || - value === 'seven_day_opus' || - value === 'seven_day_sonnet' - ) { - resetsAt = Math.floor(Date.now() / 1000) + 7 * 24 * 3600 - } else { - resetsAt = Math.floor(Date.now() / 1000) + 3600 - } - - // Add to exceeded limits (remove if already exists) - exceededLimits = exceededLimits.filter(l => l.type !== value) - exceededLimits.push({ type: value as ExceededLimit['type'], resetsAt }) - - // Set the representative claim (furthest reset time) - updateRepresentativeClaim() - return - } - } - // Widen to a string-valued record so dynamic key assignment is allowed. - // MockHeaders values are string-literal unions; assigning a raw user-input - // string requires widening, but this is mock/test code so it's acceptable. - const headers: Partial> = mockHeaders - headers[fullKey] = value - - // Update retry-after if status changed - if (key === 'status' || key === 'overage-status') { - updateRetryAfter() - } - } - - // If all headers are cleared, disable mocking - if (Object.keys(mockHeaders).length === 0) { - mockEnabled = false - } -} - -// Helper to update retry-after based on current state -function updateRetryAfter(): void { - const status = mockHeaders['anthropic-ratelimit-unified-status'] - const overageStatus = - mockHeaders['anthropic-ratelimit-unified-overage-status'] - const reset = mockHeaders['anthropic-ratelimit-unified-reset'] - - if ( - status === 'rejected' && - (!overageStatus || overageStatus === 'rejected') && - reset - ) { - // Calculate seconds until reset - const resetTimestamp = Number(reset) - const secondsUntilReset = Math.max( - 0, - resetTimestamp - Math.floor(Date.now() / 1000), - ) - mockHeaders['retry-after'] = String(secondsUntilReset) - } else { - delete mockHeaders['retry-after'] - } -} - -// Update the representative claim based on exceeded limits -function updateRepresentativeClaim(): void { - if (exceededLimits.length === 0) { - delete mockHeaders['anthropic-ratelimit-unified-representative-claim'] - delete mockHeaders['anthropic-ratelimit-unified-reset'] - delete mockHeaders['retry-after'] - return - } - - // Find the limit with the furthest reset time - const furthest = exceededLimits.reduce((prev, curr) => - curr.resetsAt > prev.resetsAt ? curr : prev, - ) - - // Set the representative claim (appears for both warning and rejected) - mockHeaders['anthropic-ratelimit-unified-representative-claim'] = - furthest.type - mockHeaders['anthropic-ratelimit-unified-reset'] = String(furthest.resetsAt) - - // Add retry-after if rejected and no overage available - if (mockHeaders['anthropic-ratelimit-unified-status'] === 'rejected') { - const overageStatus = - mockHeaders['anthropic-ratelimit-unified-overage-status'] - if (!overageStatus || overageStatus === 'rejected') { - // Calculate seconds until reset - const secondsUntilReset = Math.max( - 0, - furthest.resetsAt - Math.floor(Date.now() / 1000), - ) - mockHeaders['retry-after'] = String(secondsUntilReset) - } else { - // Overage is available, no retry-after - delete mockHeaders['retry-after'] - } - } else { - delete mockHeaders['retry-after'] - } -} - -// Add function to add exceeded limit with custom reset time export function addExceededLimit( - type: 'five_hour' | 'seven_day' | 'seven_day_opus' | 'seven_day_sonnet', - hoursFromNow: number, -): void { - if (process.env.USER_TYPE !== 'ant') { - return - } + _type: 'five_hour' | 'seven_day' | 'seven_day_opus' | 'seven_day_sonnet', + _hoursFromNow: number, +): void {} - mockEnabled = true - const resetsAt = Math.floor(Date.now() / 1000) + hoursFromNow * 3600 - - // Remove existing limit of same type - exceededLimits = exceededLimits.filter(l => l.type !== type) - exceededLimits.push({ type, resetsAt }) - - // Update status to rejected if we have exceeded limits - if (exceededLimits.length > 0) { - mockHeaders['anthropic-ratelimit-unified-status'] = 'rejected' - } - - updateRepresentativeClaim() -} - -// Set mock early warning utilization for time-relative thresholds -// claimAbbrev: '5h' or '7d' -// utilization: 0-1 (e.g., 0.92 for 92% used) -// hoursFromNow: hours until reset (default: 4 for 5h, 120 for 7d) export function setMockEarlyWarning( - claimAbbrev: '5h' | '7d' | 'overage', - utilization: number, - hoursFromNow?: number, -): void { - if (process.env.USER_TYPE !== 'ant') { - return - } + _claimAbbrev: '5h' | '7d' | 'overage', + _utilization: number, + _hoursFromNow?: number, +): void {} - mockEnabled = true +export function clearMockEarlyWarning(): void {} - // Clear ALL early warning headers first (5h is checked before 7d, so we need - // to clear 5h headers when testing 7d to avoid 5h taking priority) - clearMockEarlyWarning() - - // Default hours based on claim type (early in window to trigger warning) - const defaultHours = claimAbbrev === '5h' ? 4 : 5 * 24 - const hours = hoursFromNow ?? defaultHours - const resetsAt = Math.floor(Date.now() / 1000) + hours * 3600 - - mockHeaders[`anthropic-ratelimit-unified-${claimAbbrev}-utilization`] = - String(utilization) - mockHeaders[`anthropic-ratelimit-unified-${claimAbbrev}-reset`] = - String(resetsAt) - // Set the surpassed-threshold header to trigger early warning - mockHeaders[ - `anthropic-ratelimit-unified-${claimAbbrev}-surpassed-threshold` - ] = String(utilization) - - // Set status to allowed so early warning logic can upgrade it - if (!mockHeaders['anthropic-ratelimit-unified-status']) { - mockHeaders['anthropic-ratelimit-unified-status'] = 'allowed' - } -} - -// Clear mock early warning headers -export function clearMockEarlyWarning(): void { - delete mockHeaders['anthropic-ratelimit-unified-5h-utilization'] - delete mockHeaders['anthropic-ratelimit-unified-5h-reset'] - delete mockHeaders['anthropic-ratelimit-unified-5h-surpassed-threshold'] - delete mockHeaders['anthropic-ratelimit-unified-7d-utilization'] - delete mockHeaders['anthropic-ratelimit-unified-7d-reset'] - delete mockHeaders['anthropic-ratelimit-unified-7d-surpassed-threshold'] -} - -export function setMockRateLimitScenario(scenario: MockScenario): void { - if (process.env.USER_TYPE !== 'ant') { - return - } - - if (scenario === 'clear') { - mockHeaders = {} - mockHeaderless429Message = null - mockEnabled = false - return - } - - mockEnabled = true - - // Set reset times for demos - const fiveHoursFromNow = Math.floor(Date.now() / 1000) + 5 * 3600 - const sevenDaysFromNow = Math.floor(Date.now() / 1000) + 7 * 24 * 3600 - - // Clear existing headers - mockHeaders = {} - mockHeaderless429Message = null - - // Only clear exceeded limits for scenarios that explicitly set them - // Overage scenarios should preserve existing exceeded limits - const preserveExceededLimits = [ - 'overage-active', - 'overage-warning', - 'overage-exhausted', - ].includes(scenario) - if (!preserveExceededLimits) { - exceededLimits = [] - } - - switch (scenario) { - case 'normal': - mockHeaders = { - 'anthropic-ratelimit-unified-status': 'allowed', - 'anthropic-ratelimit-unified-reset': String(fiveHoursFromNow), - } - break - - case 'session-limit-reached': - exceededLimits = [{ type: 'five_hour', resetsAt: fiveHoursFromNow }] - updateRepresentativeClaim() - mockHeaders['anthropic-ratelimit-unified-status'] = 'rejected' - break - - case 'approaching-weekly-limit': - mockHeaders = { - 'anthropic-ratelimit-unified-status': 'allowed_warning', - 'anthropic-ratelimit-unified-reset': String(sevenDaysFromNow), - 'anthropic-ratelimit-unified-representative-claim': 'seven_day', - } - break - - case 'weekly-limit-reached': - exceededLimits = [{ type: 'seven_day', resetsAt: sevenDaysFromNow }] - updateRepresentativeClaim() - mockHeaders['anthropic-ratelimit-unified-status'] = 'rejected' - break - - case 'overage-active': { - // If no limits have been exceeded yet, default to 5-hour - if (exceededLimits.length === 0) { - exceededLimits = [{ type: 'five_hour', resetsAt: fiveHoursFromNow }] - } - updateRepresentativeClaim() - mockHeaders['anthropic-ratelimit-unified-status'] = 'rejected' - mockHeaders['anthropic-ratelimit-unified-overage-status'] = 'allowed' - // Set overage reset time (monthly) - const endOfMonthActive = new Date() - endOfMonthActive.setMonth(endOfMonthActive.getMonth() + 1, 1) - endOfMonthActive.setHours(0, 0, 0, 0) - mockHeaders['anthropic-ratelimit-unified-overage-reset'] = String( - Math.floor(endOfMonthActive.getTime() / 1000), - ) - break - } - - case 'overage-warning': { - // If no limits have been exceeded yet, default to 5-hour - if (exceededLimits.length === 0) { - exceededLimits = [{ type: 'five_hour', resetsAt: fiveHoursFromNow }] - } - updateRepresentativeClaim() - mockHeaders['anthropic-ratelimit-unified-status'] = 'rejected' - mockHeaders['anthropic-ratelimit-unified-overage-status'] = - 'allowed_warning' - // Overage typically resets monthly, but for demo let's say end of month - const endOfMonth = new Date() - endOfMonth.setMonth(endOfMonth.getMonth() + 1, 1) - endOfMonth.setHours(0, 0, 0, 0) - mockHeaders['anthropic-ratelimit-unified-overage-reset'] = String( - Math.floor(endOfMonth.getTime() / 1000), - ) - break - } - - case 'overage-exhausted': { - // If no limits have been exceeded yet, default to 5-hour - if (exceededLimits.length === 0) { - exceededLimits = [{ type: 'five_hour', resetsAt: fiveHoursFromNow }] - } - updateRepresentativeClaim() - mockHeaders['anthropic-ratelimit-unified-status'] = 'rejected' - mockHeaders['anthropic-ratelimit-unified-overage-status'] = 'rejected' - // Both subscription and overage are exhausted - // Subscription resets based on the exceeded limit, overage resets monthly - const endOfMonthExhausted = new Date() - endOfMonthExhausted.setMonth(endOfMonthExhausted.getMonth() + 1, 1) - endOfMonthExhausted.setHours(0, 0, 0, 0) - mockHeaders['anthropic-ratelimit-unified-overage-reset'] = String( - Math.floor(endOfMonthExhausted.getTime() / 1000), - ) - break - } - - case 'out-of-credits': { - // Out of credits - subscription limit hit, overage rejected due to insufficient credits - // (wallet is empty) - if (exceededLimits.length === 0) { - exceededLimits = [{ type: 'five_hour', resetsAt: fiveHoursFromNow }] - } - updateRepresentativeClaim() - mockHeaders['anthropic-ratelimit-unified-status'] = 'rejected' - mockHeaders['anthropic-ratelimit-unified-overage-status'] = 'rejected' - mockHeaders['anthropic-ratelimit-unified-overage-disabled-reason'] = - 'out_of_credits' - const endOfMonth = new Date() - endOfMonth.setMonth(endOfMonth.getMonth() + 1, 1) - endOfMonth.setHours(0, 0, 0, 0) - mockHeaders['anthropic-ratelimit-unified-overage-reset'] = String( - Math.floor(endOfMonth.getTime() / 1000), - ) - break - } - - case 'org-zero-credit-limit': { - // Org service has zero credit limit - admin set org-level spend cap to $0 - // Non-admin Team/Enterprise users should not see "Request extra usage" option - if (exceededLimits.length === 0) { - exceededLimits = [{ type: 'five_hour', resetsAt: fiveHoursFromNow }] - } - updateRepresentativeClaim() - mockHeaders['anthropic-ratelimit-unified-status'] = 'rejected' - mockHeaders['anthropic-ratelimit-unified-overage-status'] = 'rejected' - mockHeaders['anthropic-ratelimit-unified-overage-disabled-reason'] = - 'org_service_zero_credit_limit' - const endOfMonthZero = new Date() - endOfMonthZero.setMonth(endOfMonthZero.getMonth() + 1, 1) - endOfMonthZero.setHours(0, 0, 0, 0) - mockHeaders['anthropic-ratelimit-unified-overage-reset'] = String( - Math.floor(endOfMonthZero.getTime() / 1000), - ) - break - } - - case 'org-spend-cap-hit': { - // Org spend cap hit for the month - org overages temporarily disabled - // Non-admin Team/Enterprise users should not see "Request extra usage" option - if (exceededLimits.length === 0) { - exceededLimits = [{ type: 'five_hour', resetsAt: fiveHoursFromNow }] - } - updateRepresentativeClaim() - mockHeaders['anthropic-ratelimit-unified-status'] = 'rejected' - mockHeaders['anthropic-ratelimit-unified-overage-status'] = 'rejected' - mockHeaders['anthropic-ratelimit-unified-overage-disabled-reason'] = - 'org_level_disabled_until' - const endOfMonthHit = new Date() - endOfMonthHit.setMonth(endOfMonthHit.getMonth() + 1, 1) - endOfMonthHit.setHours(0, 0, 0, 0) - mockHeaders['anthropic-ratelimit-unified-overage-reset'] = String( - Math.floor(endOfMonthHit.getTime() / 1000), - ) - break - } - - case 'member-zero-credit-limit': { - // Member has zero credit limit - admin set this user's individual limit to $0 - // Non-admin Team/Enterprise users SHOULD see "Request extra usage" (admin can allocate more) - if (exceededLimits.length === 0) { - exceededLimits = [{ type: 'five_hour', resetsAt: fiveHoursFromNow }] - } - updateRepresentativeClaim() - mockHeaders['anthropic-ratelimit-unified-status'] = 'rejected' - mockHeaders['anthropic-ratelimit-unified-overage-status'] = 'rejected' - mockHeaders['anthropic-ratelimit-unified-overage-disabled-reason'] = - 'member_zero_credit_limit' - const endOfMonthMember = new Date() - endOfMonthMember.setMonth(endOfMonthMember.getMonth() + 1, 1) - endOfMonthMember.setHours(0, 0, 0, 0) - mockHeaders['anthropic-ratelimit-unified-overage-reset'] = String( - Math.floor(endOfMonthMember.getTime() / 1000), - ) - break - } - - case 'seat-tier-zero-credit-limit': { - // Seat tier has zero credit limit - admin set this seat tier's limit to $0 - // Non-admin Team/Enterprise users SHOULD see "Request extra usage" (admin can allocate more) - if (exceededLimits.length === 0) { - exceededLimits = [{ type: 'five_hour', resetsAt: fiveHoursFromNow }] - } - updateRepresentativeClaim() - mockHeaders['anthropic-ratelimit-unified-status'] = 'rejected' - mockHeaders['anthropic-ratelimit-unified-overage-status'] = 'rejected' - mockHeaders['anthropic-ratelimit-unified-overage-disabled-reason'] = - 'seat_tier_zero_credit_limit' - const endOfMonthSeatTier = new Date() - endOfMonthSeatTier.setMonth(endOfMonthSeatTier.getMonth() + 1, 1) - endOfMonthSeatTier.setHours(0, 0, 0, 0) - mockHeaders['anthropic-ratelimit-unified-overage-reset'] = String( - Math.floor(endOfMonthSeatTier.getTime() / 1000), - ) - break - } - - case 'opus-limit': { - exceededLimits = [{ type: 'seven_day_opus', resetsAt: sevenDaysFromNow }] - updateRepresentativeClaim() - // Always send 429 rejected status - the error handler will decide whether - // to show an error or return NO_RESPONSE_REQUESTED based on fallback eligibility - mockHeaders['anthropic-ratelimit-unified-status'] = 'rejected' - break - } - - case 'opus-warning': { - mockHeaders = { - 'anthropic-ratelimit-unified-status': 'allowed_warning', - 'anthropic-ratelimit-unified-reset': String(sevenDaysFromNow), - 'anthropic-ratelimit-unified-representative-claim': 'seven_day_opus', - } - break - } - - case 'sonnet-limit': { - exceededLimits = [ - { type: 'seven_day_sonnet', resetsAt: sevenDaysFromNow }, - ] - updateRepresentativeClaim() - mockHeaders['anthropic-ratelimit-unified-status'] = 'rejected' - break - } - - case 'sonnet-warning': { - mockHeaders = { - 'anthropic-ratelimit-unified-status': 'allowed_warning', - 'anthropic-ratelimit-unified-reset': String(sevenDaysFromNow), - 'anthropic-ratelimit-unified-representative-claim': 'seven_day_sonnet', - } - break - } - - case 'fast-mode-limit': { - updateRepresentativeClaim() - mockHeaders['anthropic-ratelimit-unified-status'] = 'rejected' - // Duration in ms (> 20s threshold to trigger cooldown) - mockFastModeRateLimitDurationMs = 10 * 60 * 1000 - break - } - - case 'fast-mode-short-limit': { - updateRepresentativeClaim() - mockHeaders['anthropic-ratelimit-unified-status'] = 'rejected' - // Duration in ms (< 20s threshold, won't trigger cooldown) - mockFastModeRateLimitDurationMs = 10 * 1000 - break - } - - case 'extra-usage-required': { - // Headerless 429 — exercises the entitlement-rejection path in errors.ts - mockHeaderless429Message = - 'Extra usage is required for long context requests.' - break - } - - default: - break - } -} +export function setMockRateLimitScenario(_scenario: MockScenario): void {} export function getMockHeaderless429Message(): string | null { - if (process.env.USER_TYPE !== 'ant') { - return null - } - // Env var path for -p / SDK testing where slash commands aren't available - if (process.env.CLAUDE_MOCK_HEADERLESS_429) { - return process.env.CLAUDE_MOCK_HEADERLESS_429 - } - if (!mockEnabled) { - return null - } - return mockHeaderless429Message + return null } export function getMockHeaders(): MockHeaders | null { - if ( - !mockEnabled || - process.env.USER_TYPE !== 'ant' || - Object.keys(mockHeaders).length === 0 - ) { - return null - } - return mockHeaders + return null } export function getMockStatus(): string { - if ( - !mockEnabled || - (Object.keys(mockHeaders).length === 0 && !mockSubscriptionType) - ) { - return 'No mock headers active (using real limits)' - } - - const lines: string[] = [] - lines.push('Active mock headers:') - - // Show subscription type - either explicitly set or default - const effectiveSubscription = - mockSubscriptionType || DEFAULT_MOCK_SUBSCRIPTION - if (mockSubscriptionType) { - lines.push(` Subscription Type: ${mockSubscriptionType} (explicitly set)`) - } else { - lines.push(` Subscription Type: ${effectiveSubscription} (default)`) - } - - Object.entries(mockHeaders).forEach(([key, value]) => { - if (value !== undefined) { - // Format the header name nicely - const formattedKey = key - .replace('anthropic-ratelimit-unified-', '') - .replace(/-/g, ' ') - .replace(/\b\w/g, c => c.toUpperCase()) - - // Format timestamps as human-readable - if (key.includes('reset') && value) { - const timestamp = Number(value) - const date = new Date(timestamp * 1000) - lines.push(` ${formattedKey}: ${value} (${date.toLocaleString()})`) - } else { - lines.push(` ${formattedKey}: ${value}`) - } - } - }) - - // Show exceeded limits if any - if (exceededLimits.length > 0) { - lines.push('\nExceeded limits (contributing to representative claim):') - exceededLimits.forEach(limit => { - const date = new Date(limit.resetsAt * 1000) - lines.push(` ${limit.type}: resets at ${date.toLocaleString()}`) - }) - } - - return lines.join('\n') + return 'No mock headers active (using real limits)' } export function clearMockHeaders(): void { - mockHeaders = {} - exceededLimits = [] - mockSubscriptionType = null - mockFastModeRateLimitDurationMs = null - mockFastModeRateLimitExpiresAt = null - mockHeaderless429Message = null setMockBillingAccessOverride(null) - mockEnabled = false } export function applyMockHeaders( headers: globalThis.Headers, ): globalThis.Headers { - const mock = getMockHeaders() - if (!mock) { - return headers - } - - // Create a new Headers object with original headers - // eslint-disable-next-line eslint-plugin-n/no-unsupported-features/node-builtins - const newHeaders = new globalThis.Headers(headers) - - // Apply mock headers (overwriting originals) - Object.entries(mock).forEach(([key, value]) => { - if (value !== undefined) { - newHeaders.set(key, value) - } - }) - - return newHeaders + return headers } -// Check if we should process rate limits even without subscription -// This is for Ant employees testing with mocks export function shouldProcessMockLimits(): boolean { - if (process.env.USER_TYPE !== 'ant') { - return false - } - return mockEnabled || Boolean(process.env.CLAUDE_MOCK_HEADERLESS_429) + return false } export function getCurrentMockScenario(): MockScenario | null { - if (!mockEnabled) { - return null - } - - // Reverse lookup the scenario from current headers - if (!mockHeaders) return null - - const status = mockHeaders['anthropic-ratelimit-unified-status'] - const overage = mockHeaders['anthropic-ratelimit-unified-overage-status'] - const claim = mockHeaders['anthropic-ratelimit-unified-representative-claim'] - - if (claim === 'seven_day_opus') { - return status === 'rejected' ? 'opus-limit' : 'opus-warning' - } - - if (claim === 'seven_day_sonnet') { - return status === 'rejected' ? 'sonnet-limit' : 'sonnet-warning' - } - - if (overage === 'rejected') return 'overage-exhausted' - if (overage === 'allowed_warning') return 'overage-warning' - if (overage === 'allowed') return 'overage-active' - - if (status === 'rejected') { - if (claim === 'five_hour') return 'session-limit-reached' - if (claim === 'seven_day') return 'weekly-limit-reached' - } - - if (status === 'allowed_warning') { - if (claim === 'seven_day') return 'approaching-weekly-limit' - } - - if (status === 'allowed') return 'normal' - return null } @@ -802,81 +173,28 @@ export function getScenarioDescription(scenario: MockScenario): string { } } -// Mock subscription type management export function setMockSubscriptionType( - subscriptionType: SubscriptionType | null, -): void { - if (process.env.USER_TYPE !== 'ant') { - return - } - mockEnabled = true - mockSubscriptionType = subscriptionType -} + _subscriptionType: SubscriptionType | null, +): void {} export function getMockSubscriptionType(): SubscriptionType | null { - if (!mockEnabled || process.env.USER_TYPE !== 'ant') { - return null - } - // Return the explicitly set subscription type, or default to 'max' - return mockSubscriptionType || DEFAULT_MOCK_SUBSCRIPTION + return null } -// Export a function that checks if we should use mock subscription export function shouldUseMockSubscription(): boolean { - return ( - mockEnabled && - mockSubscriptionType !== null && - process.env.USER_TYPE === 'ant' - ) + return false } -// Mock billing access (admin vs non-admin) -export function setMockBillingAccess(hasAccess: boolean | null): void { - if (process.env.USER_TYPE !== 'ant') { - return - } - mockEnabled = true - setMockBillingAccessOverride(hasAccess) +export function setMockBillingAccess(_hasAccess: boolean | null): void { + // External build: internal mock billing access overrides are disabled. } -// Mock fast mode rate limit handling export function isMockFastModeRateLimitScenario(): boolean { - return mockFastModeRateLimitDurationMs !== null + return false } export function checkMockFastModeRateLimit( - isFastModeActive?: boolean, + _isFastModeActive?: boolean, ): MockHeaders | null { - if (mockFastModeRateLimitDurationMs === null) { - return null - } - - // Only throw when fast mode is active - if (!isFastModeActive) { - return null - } - - // Check if the rate limit has expired - if ( - mockFastModeRateLimitExpiresAt !== null && - Date.now() >= mockFastModeRateLimitExpiresAt - ) { - clearMockHeaders() - return null - } - - // Set expiry on first error (not when scenario is configured) - if (mockFastModeRateLimitExpiresAt === null) { - mockFastModeRateLimitExpiresAt = - Date.now() + mockFastModeRateLimitDurationMs - } - - // Compute dynamic retry-after based on remaining time - const remainingMs = mockFastModeRateLimitExpiresAt - Date.now() - const headersToSend = { ...mockHeaders } - headersToSend['retry-after'] = String( - Math.max(1, Math.ceil(remainingMs / 1000)), - ) - - return headersToSend + return null } diff --git a/src/services/tips/tipRegistry.ts b/src/services/tips/tipRegistry.ts index d1377b2d..be63aa44 100644 --- a/src/services/tips/tipRegistry.ts +++ b/src/services/tips/tipRegistry.ts @@ -645,7 +645,7 @@ const internalOnlyTips: Tip[] = { id: 'skillify', content: async () => - '[internal] Use /skillify at the end of a workflow to turn it into a reusable skill', + '[internal] Turn repeatable workflows into reusable project skills when they keep recurring', cooldownSessions: 15, isRelevant: async () => true, }, diff --git a/src/skills/bundled/index.ts b/src/skills/bundled/index.ts index 53db272c..3a18982b 100644 --- a/src/skills/bundled/index.ts +++ b/src/skills/bundled/index.ts @@ -4,12 +4,8 @@ import { registerBatchSkill } from './batch.js' import { registerClaudeInChromeSkill } from './claudeInChrome.js' import { registerDebugSkill } from './debug.js' import { registerKeybindingsSkill } from './keybindings.js' -import { registerLoremIpsumSkill } from './loremIpsum.js' -import { registerRememberSkill } from './remember.js' import { registerSimplifySkill } from './simplify.js' -import { registerSkillifySkill } from './skillify.js' import { registerUpdateConfigSkill } from './updateConfig.js' -import { registerVerifySkill } from './verify.js' /** * Initialize all bundled skills. @@ -23,11 +19,7 @@ import { registerVerifySkill } from './verify.js' export function initBundledSkills(): void { registerUpdateConfigSkill() registerKeybindingsSkill() - registerVerifySkill() registerDebugSkill() - registerLoremIpsumSkill() - registerSkillifySkill() - registerRememberSkill() registerSimplifySkill() registerBatchSkill() if (feature('KAIROS') || feature('KAIROS_DREAM')) { diff --git a/src/skills/bundled/loremIpsum.ts b/src/skills/bundled/loremIpsum.ts deleted file mode 100644 index 053306c6..00000000 --- a/src/skills/bundled/loremIpsum.ts +++ /dev/null @@ -1,282 +0,0 @@ -import { registerBundledSkill } from '../bundledSkills.js' - -// Verified 1-token words (tested via API token counting) -// All common English words confirmed to tokenize as single tokens -const ONE_TOKEN_WORDS = [ - // Articles & pronouns - 'the', - 'a', - 'an', - 'I', - 'you', - 'he', - 'she', - 'it', - 'we', - 'they', - 'me', - 'him', - 'her', - 'us', - 'them', - 'my', - 'your', - 'his', - 'its', - 'our', - 'this', - 'that', - 'what', - 'who', - // Common verbs - 'is', - 'are', - 'was', - 'were', - 'be', - 'been', - 'have', - 'has', - 'had', - 'do', - 'does', - 'did', - 'will', - 'would', - 'can', - 'could', - 'may', - 'might', - 'must', - 'shall', - 'should', - 'make', - 'made', - 'get', - 'got', - 'go', - 'went', - 'come', - 'came', - 'see', - 'saw', - 'know', - 'take', - 'think', - 'look', - 'want', - 'use', - 'find', - 'give', - 'tell', - 'work', - 'call', - 'try', - 'ask', - 'need', - 'feel', - 'seem', - 'leave', - 'put', - // Common nouns & adjectives - 'time', - 'year', - 'day', - 'way', - 'man', - 'thing', - 'life', - 'hand', - 'part', - 'place', - 'case', - 'point', - 'fact', - 'good', - 'new', - 'first', - 'last', - 'long', - 'great', - 'little', - 'own', - 'other', - 'old', - 'right', - 'big', - 'high', - 'small', - 'large', - 'next', - 'early', - 'young', - 'few', - 'public', - 'bad', - 'same', - 'able', - // Prepositions & conjunctions - 'in', - 'on', - 'at', - 'to', - 'for', - 'of', - 'with', - 'from', - 'by', - 'about', - 'like', - 'through', - 'over', - 'before', - 'between', - 'under', - 'since', - 'without', - 'and', - 'or', - 'but', - 'if', - 'than', - 'because', - 'as', - 'until', - 'while', - 'so', - 'though', - 'both', - 'each', - 'when', - 'where', - 'why', - 'how', - // Common adverbs - 'not', - 'now', - 'just', - 'more', - 'also', - 'here', - 'there', - 'then', - 'only', - 'very', - 'well', - 'back', - 'still', - 'even', - 'much', - 'too', - 'such', - 'never', - 'again', - 'most', - 'once', - 'off', - 'away', - 'down', - 'out', - 'up', - // Tech/common words - 'test', - 'code', - 'data', - 'file', - 'line', - 'text', - 'word', - 'number', - 'system', - 'program', - 'set', - 'run', - 'value', - 'name', - 'type', - 'state', - 'end', - 'start', -] - -function generateLoremIpsum(targetTokens: number): string { - let tokens = 0 - let result = '' - - while (tokens < targetTokens) { - // Sentence: 10-20 words - const sentenceLength = 10 + Math.floor(Math.random() * 11) - let wordsInSentence = 0 - - for (let i = 0; i < sentenceLength && tokens < targetTokens; i++) { - const word = - ONE_TOKEN_WORDS[Math.floor(Math.random() * ONE_TOKEN_WORDS.length)] - result += word - tokens++ - wordsInSentence++ - - if (i === sentenceLength - 1 || tokens >= targetTokens) { - result += '. ' - } else { - result += ' ' - } - } - - // Paragraph break every 5-8 sentences (roughly 20% chance per sentence) - if (wordsInSentence > 0 && Math.random() < 0.2 && tokens < targetTokens) { - result += '\n\n' - } - } - - return result.trim() -} - -export function registerLoremIpsumSkill(): void { - if (process.env.USER_TYPE !== 'ant') { - return - } - - registerBundledSkill({ - name: 'lorem-ipsum', - description: - 'Generate filler text for long context testing. Specify token count as argument (e.g., /lorem-ipsum 50000). Outputs approximately the requested number of tokens. Ant-only.', - argumentHint: '[token_count]', - userInvocable: true, - async getPromptForCommand(args) { - const parsed = parseInt(args) - - if (args && (isNaN(parsed) || parsed <= 0)) { - return [ - { - type: 'text', - text: 'Invalid token count. Please provide a positive number (e.g., /lorem-ipsum 10000).', - }, - ] - } - - const targetTokens = parsed || 10000 - - // Cap at 500k tokens for safety - const cappedTokens = Math.min(targetTokens, 500_000) - - if (cappedTokens < targetTokens) { - return [ - { - type: 'text', - text: `Requested ${targetTokens} tokens, but capped at 500,000 for safety.\n\n${generateLoremIpsum(cappedTokens)}`, - }, - ] - } - - const loremText = generateLoremIpsum(cappedTokens) - - // Just dump the lorem ipsum text into the conversation - return [ - { - type: 'text', - text: loremText, - }, - ] - }, - }) -} diff --git a/src/skills/bundled/remember.ts b/src/skills/bundled/remember.ts deleted file mode 100644 index 40bd1a93..00000000 --- a/src/skills/bundled/remember.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { isAutoMemoryEnabled } from '../../memdir/paths.js' -import { registerBundledSkill } from '../bundledSkills.js' - -export function registerRememberSkill(): void { - if (process.env.USER_TYPE !== 'ant') { - return - } - - const SKILL_PROMPT = `# Memory Review - -## Goal -Review the user's memory landscape and produce a clear report of proposed changes, grouped by action type. Do NOT apply changes — present proposals for user approval. - -## Steps - -### 1. Gather all memory layers -Read CLAUDE.md and CLAUDE.local.md from the project root (if they exist). Your auto-memory content is already in your system prompt — review it there. Note which team memory sections exist, if any. - -**Success criteria**: You have the contents of all memory layers and can compare them. - -### 2. Classify each auto-memory entry -For each substantive entry in auto-memory, determine the best destination: - -| Destination | What belongs there | Examples | -|---|---|---| -| **CLAUDE.md** | Project conventions and instructions for Claude that all contributors should follow | "use bun not npm", "API routes use kebab-case", "test command is bun test", "prefer functional style" | -| **CLAUDE.local.md** | Personal instructions for Claude specific to this user, not applicable to other contributors | "I prefer concise responses", "always explain trade-offs", "don't auto-commit", "run tests before committing" | -| **Team memory** | Org-wide knowledge that applies across repositories (only if team memory is configured) | "deploy PRs go through #deploy-queue", "staging is at staging.internal", "platform team owns infra" | -| **Stay in auto-memory** | Working notes, temporary context, or entries that don't clearly fit elsewhere | Session-specific observations, uncertain patterns | - -**Important distinctions:** -- CLAUDE.md and CLAUDE.local.md contain instructions for Claude, not user preferences for external tools (editor theme, IDE keybindings, etc. don't belong in either) -- Workflow practices (PR conventions, merge strategies, branch naming) are ambiguous — ask the user whether they're personal or team-wide -- When unsure, ask rather than guess - -**Success criteria**: Each entry has a proposed destination or is flagged as ambiguous. - -### 3. Identify cleanup opportunities -Scan across all layers for: -- **Duplicates**: Auto-memory entries already captured in CLAUDE.md or CLAUDE.local.md → propose removing from auto-memory -- **Outdated**: CLAUDE.md or CLAUDE.local.md entries contradicted by newer auto-memory entries → propose updating the older layer -- **Conflicts**: Contradictions between any two layers → propose resolution, noting which is more recent - -**Success criteria**: All cross-layer issues identified. - -### 4. Present the report -Output a structured report grouped by action type: -1. **Promotions** — entries to move, with destination and rationale -2. **Cleanup** — duplicates, outdated entries, conflicts to resolve -3. **Ambiguous** — entries where you need the user's input on destination -4. **No action needed** — brief note on entries that should stay put - -If auto-memory is empty, say so and offer to review CLAUDE.md for cleanup. - -**Success criteria**: User can review and approve/reject each proposal individually. - -## Rules -- Present ALL proposals before making any changes -- Do NOT modify files without explicit user approval -- Do NOT create new files unless the target doesn't exist yet -- Ask about ambiguous entries — don't guess -` - - registerBundledSkill({ - name: 'remember', - description: - 'Review auto-memory entries and propose promotions to CLAUDE.md, CLAUDE.local.md, or shared memory. Also detects outdated, conflicting, and duplicate entries across memory layers.', - whenToUse: - 'Use when the user wants to review, organize, or promote their auto-memory entries. Also useful for cleaning up outdated or conflicting entries across CLAUDE.md, CLAUDE.local.md, and auto-memory.', - userInvocable: true, - isEnabled: () => isAutoMemoryEnabled(), - async getPromptForCommand(args) { - let prompt = SKILL_PROMPT - - if (args) { - prompt += `\n## Additional context from user\n\n${args}` - } - - return [{ type: 'text', text: prompt }] - }, - }) -} diff --git a/src/skills/bundled/skillify.ts b/src/skills/bundled/skillify.ts deleted file mode 100644 index daf95297..00000000 --- a/src/skills/bundled/skillify.ts +++ /dev/null @@ -1,197 +0,0 @@ -import { getSessionMemoryContent } from '../../services/SessionMemory/sessionMemoryUtils.js' -import type { Message } from '../../types/message.js' -import { getMessagesAfterCompactBoundary } from '../../utils/messages.js' -import { registerBundledSkill } from '../bundledSkills.js' - -function extractUserMessages(messages: Message[]): string[] { - return messages - .filter((m): m is Extract => m.type === 'user') - .map(m => { - const content = m.message.content - if (typeof content === 'string') return content - return content - .filter( - (b): b is Extract => b.type === 'text', - ) - .map(b => b.text) - .join('\n') - }) - .filter(text => text.trim().length > 0) -} - -const SKILLIFY_PROMPT = `# Skillify {{userDescriptionBlock}} - -You are capturing this session's repeatable process as a reusable skill. - -## Your Session Context - -Here is the session memory summary: - -{{sessionMemory}} - - -Here are the user's messages during this session. Pay attention to how they steered the process, to help capture their detailed preferences in the skill: - -{{userMessages}} - - -## Your Task - -### Step 1: Analyze the Session - -Before asking any questions, analyze the session to identify: -- What repeatable process was performed -- What the inputs/parameters were -- The distinct steps (in order) -- The success artifacts/criteria (e.g. not just "writing code," but "an open PR with CI fully passing") for each step -- Where the user corrected or steered you -- What tools and permissions were needed -- What agents were used -- What the goals and success artifacts were - -### Step 2: Interview the User - -You will use the AskUserQuestion to understand what the user wants to automate. Important notes: -- Use AskUserQuestion for ALL questions! Never ask questions via plain text. -- For each round, iterate as much as needed until the user is happy. -- The user always has a freeform "Other" option to type edits or feedback -- do NOT add your own "Needs tweaking" or "I'll provide edits" option. Just offer the substantive choices. - -**Round 1: High level confirmation** -- Suggest a name and description for the skill based on your analysis. Ask the user to confirm or rename. -- Suggest high-level goal(s) and specific success criteria for the skill. - -**Round 2: More details** -- Present the high-level steps you identified as a numbered list. Tell the user you will dig into the detail in the next round. -- If you think the skill will require arguments, suggest arguments based on what you observed. Make sure you understand what someone would need to provide. -- If it's not clear, ask if this skill should run inline (in the current conversation) or forked (as a sub-agent with its own context). Forked is better for self-contained tasks that don't need mid-process user input; inline is better when the user wants to steer mid-process. -- Ask where the skill should be saved. Suggest a default based on context (repo-specific workflows → repo, cross-repo personal workflows → user). Options: - - **This repo** (\`.claude/skills//SKILL.md\`) — for workflows specific to this project - - **Personal** (\`~/.claude/skills//SKILL.md\`) — follows you across all repos - -**Round 3: Breaking down each step** -For each major step, if it's not glaringly obvious, ask: -- What does this step produce that later steps need? (data, artifacts, IDs) -- What proves that this step succeeded, and that we can move on? -- Should the user be asked to confirm before proceeding? (especially for irreversible actions like merging, sending messages, or destructive operations) -- Are any steps independent and could run in parallel? (e.g., posting to Slack and monitoring CI at the same time) -- How should the skill be executed? (e.g. always use a Task agent to conduct code review, or invoke an agent team for a set of concurrent steps) -- What are the hard constraints or hard preferences? Things that must or must not happen? - -You may do multiple rounds of AskUserQuestion here, one round per step, especially if there are more than 3 steps or many clarification questions. Iterate as much as needed. - -IMPORTANT: Pay special attention to places where the user corrected you during the session, to help inform your design. - -**Round 4: Final questions** -- Confirm when this skill should be invoked, and suggest/confirm trigger phrases too. (e.g. For a cherrypick workflow you could say: Use when the user wants to cherry-pick a PR to a release branch. Examples: 'cherry-pick to release', 'CP this PR', 'hotfix.') -- You can also ask for any other gotchas or things to watch out for, if it's still unclear. - -Stop interviewing once you have enough information. IMPORTANT: Don't over-ask for simple processes! - -### Step 3: Write the SKILL.md - -Create the skill directory and file at the location the user chose in Round 2. - -Use this format: - -\`\`\`markdown ---- -name: {{skill-name}} -description: {{one-line description}} -allowed-tools: - {{list of tool permission patterns observed during session}} -when_to_use: {{detailed description of when Claude should automatically invoke this skill, including trigger phrases and example user messages}} -argument-hint: "{{hint showing argument placeholders}}" -arguments: - {{list of argument names}} -context: {{inline or fork -- omit for inline}} ---- - -# {{Skill Title}} -Description of skill - -## Inputs -- \`$arg_name\`: Description of this input - -## Goal -Clearly stated goal for this workflow. Best if you have clearly defined artifacts or criteria for completion. - -## Steps - -### 1. Step Name -What to do in this step. Be specific and actionable. Include commands when appropriate. - -**Success criteria**: ALWAYS include this! This shows that the step is done and we can move on. Can be a list. - -IMPORTANT: see the next section below for the per-step annotations you can optionally include for each step. - -... -\`\`\` - -**Per-step annotations**: -- **Success criteria** is REQUIRED on every step. This helps the model understand what the user expects from their workflow, and when it should have the confidence to move on. -- **Execution**: \`Direct\` (default), \`Task agent\` (straightforward subagents), \`Teammate\` (agent with true parallelism and inter-agent communication), or \`[human]\` (user does it). Only needs specifying if not Direct. -- **Artifacts**: Data this step produces that later steps need (e.g., PR number, commit SHA). Only include if later steps depend on it. -- **Human checkpoint**: When to pause and ask the user before proceeding. Include for irreversible actions (merging, sending messages), error judgment (merge conflicts), or output review. -- **Rules**: Hard rules for the workflow. User corrections during the reference session can be especially useful here. - -**Step structure tips:** -- Steps that can run concurrently use sub-numbers: 3a, 3b -- Steps requiring the user to act get \`[human]\` in the title -- Keep simple skills simple -- a 2-step skill doesn't need annotations on every step - -**Frontmatter rules:** -- \`allowed-tools\`: Minimum permissions needed (use patterns like \`Bash(gh:*)\` not \`Bash\`) -- \`context\`: Only set \`context: fork\` for self-contained skills that don't need mid-process user input. -- \`when_to_use\` is CRITICAL -- tells the model when to auto-invoke. Start with "Use when..." and include trigger phrases. Example: "Use when the user wants to cherry-pick a PR to a release branch. Examples: 'cherry-pick to release', 'CP this PR', 'hotfix'." -- \`arguments\` and \`argument-hint\`: Only include if the skill takes parameters. Use \`$name\` in the body for substitution. - -### Step 4: Confirm and Save - -Before writing the file, output the complete SKILL.md content as a yaml code block in your response so the user can review it with proper syntax highlighting. Then ask for confirmation using AskUserQuestion with a simple question like "Does this SKILL.md look good to save?" — do NOT use the body field, keep the question concise. - -After writing, tell the user: -- Where the skill was saved -- How to invoke it: \`/{{skill-name}} [arguments]\` -- That they can edit the SKILL.md directly to refine it -` - -export function registerSkillifySkill(): void { - if (process.env.USER_TYPE !== 'ant') { - return - } - - registerBundledSkill({ - name: 'skillify', - description: - "Capture this session's repeatable process into a skill. Call at end of the process you want to capture with an optional description.", - allowedTools: [ - 'Read', - 'Write', - 'Edit', - 'Glob', - 'Grep', - 'AskUserQuestion', - 'Bash(mkdir:*)', - ], - userInvocable: true, - disableModelInvocation: true, - argumentHint: '[description of the process you want to capture]', - async getPromptForCommand(args, context) { - const sessionMemory = - (await getSessionMemoryContent()) ?? 'No session memory available.' - const userMessages = extractUserMessages( - getMessagesAfterCompactBoundary(context.messages), - ) - - const userDescriptionBlock = args - ? `The user described this process as: "${args}"` - : '' - - const prompt = SKILLIFY_PROMPT.replace('{{sessionMemory}}', sessionMemory) - .replace('{{userMessages}}', userMessages.join('\n\n---\n\n')) - .replace('{{userDescriptionBlock}}', userDescriptionBlock) - - return [{ type: 'text', text: prompt }] - }, - }) -} diff --git a/src/skills/bundled/verify.ts b/src/skills/bundled/verify.ts deleted file mode 100644 index f95dd198..00000000 --- a/src/skills/bundled/verify.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { parseFrontmatter } from '../../utils/frontmatterParser.js' -import { registerBundledSkill } from '../bundledSkills.js' - -function loadVerifyContent(): { skillMd: string; skillFiles: Record } { - try { - /* eslint-disable @typescript-eslint/no-require-imports */ - const { SKILL_FILES, SKILL_MD } = require('./verifyContent.js') as { - SKILL_FILES: Record - SKILL_MD: string - } - /* eslint-enable @typescript-eslint/no-require-imports */ - return { skillMd: SKILL_MD, skillFiles: SKILL_FILES } - } catch { - return { - skillMd: - '# Verify\n\nVerify a code change does what it should by running the app.', - skillFiles: {}, - } - } -} - -export function registerVerifySkill(): void { - if (process.env.USER_TYPE !== 'ant') { - return - } - - const { skillMd, skillFiles } = loadVerifyContent() - const { frontmatter, content: skillBody } = parseFrontmatter(skillMd) - - const description = - typeof frontmatter.description === 'string' - ? frontmatter.description - : 'Verify a code change does what it should by running the app.' - - registerBundledSkill({ - name: 'verify', - description, - userInvocable: true, - files: skillFiles, - async getPromptForCommand(args) { - const parts: string[] = [skillBody.trimStart()] - if (args) { - parts.push(`## User Request\n\n${args}`) - } - return [{ type: 'text', text: parts.join('\n\n') }] - }, - }) -}