Remove internal-only bundled skills and mock helpers (#376)
* Remove internal-only bundled skills and mock rate-limit behavior This takes the next planned Phase C-lite slice by deleting bundled skills that only ever registered for internal users and replacing the internal mock rate-limit helper with a stable no-op external stub. The external build keeps the same behavior while removing a concentrated block of USER_TYPE-gated dead code. Constraint: Limit this PR to isolated internal-only helpers and avoid bridge, oauth, or rebrand behavior Rejected: Broad USER_TYPE cleanup across mixed runtime surfaces | too risky for the next medium-sized PR Confidence: high Scope-risk: moderate Reversibility: clean Directive: The next cleanup pass should continue with similarly isolated USER_TYPE helpers before touching main.tsx or protocol-heavy code Tested: bun run build Tested: bun run smoke Tested: bun run verify:privacy Tested: bun run test:provider Tested: bun run test:provider-recommendation Not-tested: Full repo typecheck (upstream baseline remains noisy) * Align internal-only helper removal with remaining user guidance This follow-up fixes the mock billing stub to be a true no-op and removes stale user-facing references to /verify and /skillify from the same PR. It also leaves a clearer paper trail for review: the deleted verify skill was explicitly ant-gated before removal, and the remaining mock helper callers still resolve to safe no-op returns in the external build. Constraint: Keep the PR focused on consistency fixes and reviewer-requested evidence, not new cleanup scope Rejected: Leave stale guidance for a later PR | would make this branch internally inconsistent after skill removal Confidence: high Scope-risk: narrow Reversibility: clean Directive: When deleting gated features, always sweep user guidance and coordinator prompts in the same pass Tested: bun run build Tested: bun run smoke Tested: bun run verify:privacy Tested: bun run test:provider Tested: bun run test:provider-recommendation Not-tested: Full repo typecheck (upstream baseline remains noisy; changed-file scan still shows only pre-existing tipRegistry errors outside edited lines) * Clarify generic workflow wording after skill removal This removes the last generic verification-skill wording that could still be read as pointing at a deleted bundled command. The guidance now talks about project workflows rather than a specific bundled verify skill. Constraint: Keep the follow-up limited to reviewer-facing wording cleanup on the same PR Rejected: Leave generic wording as-is | still too easy to misread after the explicit /verify references were removed Confidence: high Scope-risk: narrow Reversibility: clean Directive: When removing bundled commands, scrub both explicit and generic references in the same branch Tested: bun run build Tested: bun run smoke Not-tested: Additional checks unchanged by wording-only follow-up --------- Co-authored-by: anandh8x <test@example.com>
This commit is contained in:
@@ -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/<name>/\`) 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/<project-name>-instructions.md\`) and each worktree gets a one-line CLAUDE.local.md stub that imports it: \`@~/.claude/<project-name>-instructions.md\`. Never put this import in the project CLAUDE.md — that would check a personal reference into the team-shared file.
|
- Only if Phase 2 found multiple git worktrees: ask whether their worktrees are nested inside the main repo (e.g., \`.claude/worktrees/<name>/\`) 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/<project-name>-instructions.md\`) and each worktree gets a one-line CLAUDE.local.md stub that imports it: \`@~/.claude/<project-name>-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")
|
- 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.
|
- **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.
|
- **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:
|
- **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 <file>\` via PostToolUse
|
• **Format-on-edit hook** (automatic) — \`ruff format <file>\` 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"
|
• **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.
|
- 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:
|
**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")
|
- 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?")
|
- 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:
|
**Then suggest additional skills** beyond the queue when you find:
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ export function getCoordinatorUserContext(
|
|||||||
export function getCoordinatorSystemPrompt(): string {
|
export function getCoordinatorSystemPrompt(): string {
|
||||||
const workerCapabilities = isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)
|
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 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.
|
return `You are Claude Code, an AI assistant that orchestrates software engineering tasks across multiple workers.
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
// Mock rate limits for testing [internal-only]
|
// Mock rate limits for testing [internal-only]
|
||||||
// This allows testing various rate limit scenarios without hitting actual limits
|
// The external build keeps this module as a stable no-op surface so imports
|
||||||
//
|
// remain valid without exposing internal-only rate-limit simulation behavior.
|
||||||
// ⚠️ 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.
|
|
||||||
|
|
||||||
import type { SubscriptionType } from '../services/oauth/types.js'
|
|
||||||
import { setMockBillingAccessOverride } from '../utils/billing.js'
|
import { setMockBillingAccessOverride } from '../utils/billing.js'
|
||||||
import type { OverageDisabledReason } from './claudeAiLimits.js'
|
import type { OverageDisabledReason } from './claudeAiLimits.js'
|
||||||
|
|
||||||
|
type SubscriptionType = string
|
||||||
|
|
||||||
type MockHeaders = {
|
type MockHeaders = {
|
||||||
'anthropic-ratelimit-unified-status'?:
|
'anthropic-ratelimit-unified-status'?:
|
||||||
| 'allowed'
|
| 'allowed'
|
||||||
@@ -29,7 +27,6 @@ type MockHeaders = {
|
|||||||
'anthropic-ratelimit-unified-fallback'?: 'available'
|
'anthropic-ratelimit-unified-fallback'?: 'available'
|
||||||
'anthropic-ratelimit-unified-fallback-percentage'?: string
|
'anthropic-ratelimit-unified-fallback-percentage'?: string
|
||||||
'retry-after'?: string
|
'retry-after'?: string
|
||||||
// Early warning utilization headers
|
|
||||||
'anthropic-ratelimit-unified-5h-utilization'?: string
|
'anthropic-ratelimit-unified-5h-utilization'?: string
|
||||||
'anthropic-ratelimit-unified-5h-reset'?: string
|
'anthropic-ratelimit-unified-5h-reset'?: string
|
||||||
'anthropic-ratelimit-unified-5h-surpassed-threshold'?: string
|
'anthropic-ratelimit-unified-5h-surpassed-threshold'?: string
|
||||||
@@ -79,679 +76,53 @@ export type MockScenario =
|
|||||||
| 'extra-usage-required'
|
| 'extra-usage-required'
|
||||||
| 'clear'
|
| '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(
|
export function setMockHeader(
|
||||||
key: MockHeaderKey,
|
_key: MockHeaderKey,
|
||||||
value: string | undefined,
|
_value: string | undefined,
|
||||||
): void {
|
): void {}
|
||||||
if (process.env.USER_TYPE !== 'ant') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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<Record<keyof MockHeaders, string>> = 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(
|
export function addExceededLimit(
|
||||||
type: 'five_hour' | 'seven_day' | 'seven_day_opus' | 'seven_day_sonnet',
|
_type: 'five_hour' | 'seven_day' | 'seven_day_opus' | 'seven_day_sonnet',
|
||||||
hoursFromNow: number,
|
_hoursFromNow: number,
|
||||||
): void {
|
): void {}
|
||||||
if (process.env.USER_TYPE !== 'ant') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
export function setMockEarlyWarning(
|
||||||
claimAbbrev: '5h' | '7d' | 'overage',
|
_claimAbbrev: '5h' | '7d' | 'overage',
|
||||||
utilization: number,
|
_utilization: number,
|
||||||
hoursFromNow?: number,
|
_hoursFromNow?: number,
|
||||||
): void {
|
): void {}
|
||||||
if (process.env.USER_TYPE !== 'ant') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mockEnabled = true
|
export function clearMockEarlyWarning(): void {}
|
||||||
|
|
||||||
// Clear ALL early warning headers first (5h is checked before 7d, so we need
|
export function setMockRateLimitScenario(_scenario: MockScenario): void {}
|
||||||
// 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 getMockHeaderless429Message(): string | null {
|
export function getMockHeaderless429Message(): string | null {
|
||||||
if (process.env.USER_TYPE !== 'ant') {
|
return null
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMockHeaders(): MockHeaders | null {
|
export function getMockHeaders(): MockHeaders | null {
|
||||||
if (
|
return null
|
||||||
!mockEnabled ||
|
|
||||||
process.env.USER_TYPE !== 'ant' ||
|
|
||||||
Object.keys(mockHeaders).length === 0
|
|
||||||
) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return mockHeaders
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMockStatus(): string {
|
export function getMockStatus(): string {
|
||||||
if (
|
return 'No mock headers active (using real limits)'
|
||||||
!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')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearMockHeaders(): void {
|
export function clearMockHeaders(): void {
|
||||||
mockHeaders = {}
|
|
||||||
exceededLimits = []
|
|
||||||
mockSubscriptionType = null
|
|
||||||
mockFastModeRateLimitDurationMs = null
|
|
||||||
mockFastModeRateLimitExpiresAt = null
|
|
||||||
mockHeaderless429Message = null
|
|
||||||
setMockBillingAccessOverride(null)
|
setMockBillingAccessOverride(null)
|
||||||
mockEnabled = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyMockHeaders(
|
export function applyMockHeaders(
|
||||||
headers: globalThis.Headers,
|
headers: globalThis.Headers,
|
||||||
): globalThis.Headers {
|
): globalThis.Headers {
|
||||||
const mock = getMockHeaders()
|
return headers
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we should process rate limits even without subscription
|
|
||||||
// This is for Ant employees testing with mocks
|
|
||||||
export function shouldProcessMockLimits(): boolean {
|
export function shouldProcessMockLimits(): boolean {
|
||||||
if (process.env.USER_TYPE !== 'ant') {
|
return false
|
||||||
return false
|
|
||||||
}
|
|
||||||
return mockEnabled || Boolean(process.env.CLAUDE_MOCK_HEADERLESS_429)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCurrentMockScenario(): MockScenario | null {
|
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
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -802,81 +173,28 @@ export function getScenarioDescription(scenario: MockScenario): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mock subscription type management
|
|
||||||
export function setMockSubscriptionType(
|
export function setMockSubscriptionType(
|
||||||
subscriptionType: SubscriptionType | null,
|
_subscriptionType: SubscriptionType | null,
|
||||||
): void {
|
): void {}
|
||||||
if (process.env.USER_TYPE !== 'ant') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
mockEnabled = true
|
|
||||||
mockSubscriptionType = subscriptionType
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getMockSubscriptionType(): SubscriptionType | null {
|
export function getMockSubscriptionType(): SubscriptionType | null {
|
||||||
if (!mockEnabled || process.env.USER_TYPE !== 'ant') {
|
return null
|
||||||
return null
|
|
||||||
}
|
|
||||||
// Return the explicitly set subscription type, or default to 'max'
|
|
||||||
return mockSubscriptionType || DEFAULT_MOCK_SUBSCRIPTION
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export a function that checks if we should use mock subscription
|
|
||||||
export function shouldUseMockSubscription(): boolean {
|
export function shouldUseMockSubscription(): boolean {
|
||||||
return (
|
return false
|
||||||
mockEnabled &&
|
|
||||||
mockSubscriptionType !== null &&
|
|
||||||
process.env.USER_TYPE === 'ant'
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mock billing access (admin vs non-admin)
|
export function setMockBillingAccess(_hasAccess: boolean | null): void {
|
||||||
export function setMockBillingAccess(hasAccess: boolean | null): void {
|
// External build: internal mock billing access overrides are disabled.
|
||||||
if (process.env.USER_TYPE !== 'ant') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
mockEnabled = true
|
|
||||||
setMockBillingAccessOverride(hasAccess)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mock fast mode rate limit handling
|
|
||||||
export function isMockFastModeRateLimitScenario(): boolean {
|
export function isMockFastModeRateLimitScenario(): boolean {
|
||||||
return mockFastModeRateLimitDurationMs !== null
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
export function checkMockFastModeRateLimit(
|
export function checkMockFastModeRateLimit(
|
||||||
isFastModeActive?: boolean,
|
_isFastModeActive?: boolean,
|
||||||
): MockHeaders | null {
|
): MockHeaders | null {
|
||||||
if (mockFastModeRateLimitDurationMs === null) {
|
return 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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -645,7 +645,7 @@ const internalOnlyTips: Tip[] =
|
|||||||
{
|
{
|
||||||
id: 'skillify',
|
id: 'skillify',
|
||||||
content: async () =>
|
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,
|
cooldownSessions: 15,
|
||||||
isRelevant: async () => true,
|
isRelevant: async () => true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,12 +4,8 @@ import { registerBatchSkill } from './batch.js'
|
|||||||
import { registerClaudeInChromeSkill } from './claudeInChrome.js'
|
import { registerClaudeInChromeSkill } from './claudeInChrome.js'
|
||||||
import { registerDebugSkill } from './debug.js'
|
import { registerDebugSkill } from './debug.js'
|
||||||
import { registerKeybindingsSkill } from './keybindings.js'
|
import { registerKeybindingsSkill } from './keybindings.js'
|
||||||
import { registerLoremIpsumSkill } from './loremIpsum.js'
|
|
||||||
import { registerRememberSkill } from './remember.js'
|
|
||||||
import { registerSimplifySkill } from './simplify.js'
|
import { registerSimplifySkill } from './simplify.js'
|
||||||
import { registerSkillifySkill } from './skillify.js'
|
|
||||||
import { registerUpdateConfigSkill } from './updateConfig.js'
|
import { registerUpdateConfigSkill } from './updateConfig.js'
|
||||||
import { registerVerifySkill } from './verify.js'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize all bundled skills.
|
* Initialize all bundled skills.
|
||||||
@@ -23,11 +19,7 @@ import { registerVerifySkill } from './verify.js'
|
|||||||
export function initBundledSkills(): void {
|
export function initBundledSkills(): void {
|
||||||
registerUpdateConfigSkill()
|
registerUpdateConfigSkill()
|
||||||
registerKeybindingsSkill()
|
registerKeybindingsSkill()
|
||||||
registerVerifySkill()
|
|
||||||
registerDebugSkill()
|
registerDebugSkill()
|
||||||
registerLoremIpsumSkill()
|
|
||||||
registerSkillifySkill()
|
|
||||||
registerRememberSkill()
|
|
||||||
registerSimplifySkill()
|
registerSimplifySkill()
|
||||||
registerBatchSkill()
|
registerBatchSkill()
|
||||||
if (feature('KAIROS') || feature('KAIROS_DREAM')) {
|
if (feature('KAIROS') || feature('KAIROS_DREAM')) {
|
||||||
|
|||||||
@@ -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,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -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 }]
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -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<typeof m, { type: 'user' }> => m.type === 'user')
|
|
||||||
.map(m => {
|
|
||||||
const content = m.message.content
|
|
||||||
if (typeof content === 'string') return content
|
|
||||||
return content
|
|
||||||
.filter(
|
|
||||||
(b): b is Extract<typeof b, { type: 'text' }> => 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:
|
|
||||||
<session_memory>
|
|
||||||
{{sessionMemory}}
|
|
||||||
</session_memory>
|
|
||||||
|
|
||||||
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:
|
|
||||||
<user_messages>
|
|
||||||
{{userMessages}}
|
|
||||||
</user_messages>
|
|
||||||
|
|
||||||
## 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/<name>/SKILL.md\`) — for workflows specific to this project
|
|
||||||
- **Personal** (\`~/.claude/skills/<name>/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 }]
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import { parseFrontmatter } from '../../utils/frontmatterParser.js'
|
|
||||||
import { registerBundledSkill } from '../bundledSkills.js'
|
|
||||||
|
|
||||||
function loadVerifyContent(): { skillMd: string; skillFiles: Record<string, string> } {
|
|
||||||
try {
|
|
||||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
|
||||||
const { SKILL_FILES, SKILL_MD } = require('./verifyContent.js') as {
|
|
||||||
SKILL_FILES: Record<string, string>
|
|
||||||
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') }]
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user