feat: 添加工具类增强与状态管理改进

- 新增 workflowRuns、remoteTriggerAudit、pipeStatus 等工具
- 增强 permissionSetup: auto mode 和 bypass permissions 始终可用
- 新增多组测试覆盖 (modifiers, teamDiscovery, deepLink 等)
- 修复 parseInt 缺少 radix 参数
- 移除多余 biome-ignore 注释

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
unraid
2026-04-22 22:38:10 +08:00
parent 94c4b37eed
commit fb41513b32
54 changed files with 1037 additions and 102 deletions

View File

@@ -381,7 +381,7 @@ export function useMultiSelectState<T>({
// Handle numeric keys (1-9) for direct selection
if (!hideIndexes && /^[0-9]+$/.test(normalizedInput)) {
const index = parseInt(normalizedInput) - 1
const index = parseInt(normalizedInput, 10) - 1
if (index >= 0 && index < options.length) {
const value = options[index]!.value
const newValues = selectedValues.includes(value)

View File

@@ -255,7 +255,7 @@ export const useSelectInput = <T>({
disableSelection !== 'numeric' &&
/^[0-9]+$/.test(normalizedInput)
) {
const index = parseInt(normalizedInput) - 1
const index = parseInt(normalizedInput, 10) - 1
if (index >= 0 && index < state.options.length) {
const selectedOption = state.options[index]!
if (selectedOption.disabled === true) {

View File

@@ -62,7 +62,6 @@ export function isNavigableMessage(msg: NavigableMessage): boolean {
return !stripSystemReminders(b.text!).startsWith('<')
}
case 'system':
// biome-ignore lint/nursery/useExhaustiveSwitchCases: blocklist — fallthrough return-true is the design
switch (msg.subtype) {
case 'api_metrics':
case 'stop_hook_summary':

View File

@@ -288,7 +288,6 @@ export function useNotifications(): {
// Imperative read (not useAppState) — a subscription in a mount-only
// effect would be vestigial and make every caller re-render on queue changes.
// eslint-disable-next-line react-hooks/exhaustive-deps
// biome-ignore lint/correctness/useExhaustiveDependencies: mount-only effect, store is a stable context ref
useEffect(() => {
if (store.getState().notifications.queue.length > 0) {
processQueue()

View File

@@ -0,0 +1,96 @@
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
let nativePrewarmCalls = 0
let nativeReturnValue = false
let nativeShouldThrow = false
const nativeIsModifierPressed = mock((modifier: string) => {
if (nativeShouldThrow) {
throw new Error('native modifier failure')
}
return nativeReturnValue
})
mock.module('modifiers-napi', () => ({
prewarm: async () => {
nativePrewarmCalls++
},
isModifierPressed: nativeIsModifierPressed,
}))
const originalPlatform = process.platform
async function loadModule() {
return import(`../modifiers.ts?case=${Math.random()}`)
}
beforeEach(() => {
nativePrewarmCalls = 0
nativeReturnValue = false
nativeShouldThrow = false
nativeIsModifierPressed.mockClear()
Object.defineProperty(process, 'platform', {
value: originalPlatform,
configurable: true,
})
})
afterEach(() => {
Object.defineProperty(process, 'platform', {
value: originalPlatform,
configurable: true,
})
})
describe('src/utils/modifiers', () => {
test('does not touch the native module on non-darwin', async () => {
Object.defineProperty(process, 'platform', {
value: 'win32',
configurable: true,
})
const mod = await loadModule()
mod.prewarmModifiers()
expect(nativePrewarmCalls).toBe(0)
expect(mod.isModifierPressed('shift')).toBe(false)
expect(nativeIsModifierPressed).not.toHaveBeenCalled()
})
test('caches native prewarm after the first darwin call', async () => {
Object.defineProperty(process, 'platform', {
value: 'darwin',
configurable: true,
})
const mod = await loadModule()
mod.prewarmModifiers()
mod.prewarmModifiers()
// prewarm is fire-and-forget async — flush microtasks
await new Promise(resolve => setTimeout(resolve, 0))
expect(nativePrewarmCalls).toBe(1)
})
test('forwards modifier checks to the native module on darwin', async () => {
Object.defineProperty(process, 'platform', {
value: 'darwin',
configurable: true,
})
nativeReturnValue = true
const mod = await loadModule()
expect(mod.isModifierPressed('shift')).toBe(true)
expect(nativeIsModifierPressed).toHaveBeenCalledWith('shift')
})
test('returns false when native modifier checks throw on darwin', async () => {
Object.defineProperty(process, 'platform', {
value: 'darwin',
configurable: true,
})
nativeShouldThrow = true
const mod = await loadModule()
expect(mod.isModifierPressed('shift')).toBe(false)
})
})

View File

@@ -0,0 +1,69 @@
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
import { rm } from 'fs/promises'
import { tmpdir } from 'os'
import { join } from 'path'
import { writeRegistry } from '../pipeRegistry'
import { formatPipeRegistryStatus } from '../pipeStatus'
let tempDir: string
let previousConfigDir: string | undefined
beforeEach(() => {
previousConfigDir = process.env.CLAUDE_CONFIG_DIR
tempDir = join(
tmpdir(),
`pipe-status-${Date.now()}-${Math.random().toString(16).slice(2)}`,
)
process.env.CLAUDE_CONFIG_DIR = tempDir
})
afterEach(async () => {
if (previousConfigDir === undefined) {
delete process.env.CLAUDE_CONFIG_DIR
} else {
process.env.CLAUDE_CONFIG_DIR = previousConfigDir
}
await rm(tempDir, { recursive: true, force: true })
})
describe('pipe status', () => {
test('formats registry main and sub pipe communication state', async () => {
await writeRegistry({
version: 1,
mainMachineId: 'machine-main-123456',
main: {
id: 'main-id',
pid: 123,
machineId: 'machine-main-123456',
startedAt: 1,
ip: '127.0.0.1',
mac: '00:11:22:33:44:55',
hostname: 'main-host',
pipeName: 'main-pipe',
tcpPort: 43123,
},
subs: [
{
id: 'sub-id',
pid: 456,
machineId: 'machine-sub-123456',
startedAt: 2,
ip: '127.0.0.2',
mac: '66:77:88:99:aa:bb',
hostname: 'sub-host',
pipeName: 'sub-pipe',
tcpPort: 43124,
subIndex: 1,
boundToMain: 'main-pipe',
},
],
})
const formatted = await formatPipeRegistryStatus()
expect(formatted).toContain('Pipe registry: 1 main, 1 sub(s)')
expect(formatted).toContain('[main] main-pipe')
expect(formatted).toContain('[sub-1] sub-pipe')
expect(formatted).toContain('bound=main-pipe')
})
})

View File

@@ -0,0 +1,37 @@
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
import { formatRemoteControlLocalStatus } from '../remoteControlStatus'
let previousBaseUrl: string | undefined
let previousToken: string | undefined
beforeEach(() => {
previousBaseUrl = process.env.CLAUDE_BRIDGE_BASE_URL
previousToken = process.env.CLAUDE_BRIDGE_OAUTH_TOKEN
})
afterEach(() => {
if (previousBaseUrl === undefined) {
delete process.env.CLAUDE_BRIDGE_BASE_URL
} else {
process.env.CLAUDE_BRIDGE_BASE_URL = previousBaseUrl
}
if (previousToken === undefined) {
delete process.env.CLAUDE_BRIDGE_OAUTH_TOKEN
} else {
process.env.CLAUDE_BRIDGE_OAUTH_TOKEN = previousToken
}
})
describe('remote control status', () => {
test('formats self-hosted bridge local config without remote calls', () => {
process.env.CLAUDE_BRIDGE_BASE_URL = 'http://127.0.0.1:8787'
process.env.CLAUDE_BRIDGE_OAUTH_TOKEN = 'token'
const status = formatRemoteControlLocalStatus()
expect(status).toContain('Remote Control: self-hosted')
expect(status).toContain('base_url=http://127.0.0.1:8787')
expect(status).toContain('token=present')
expect(status).toContain('entitlement=checked at remote-control startup')
})
})

View File

@@ -0,0 +1,43 @@
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
import { rm } from 'fs/promises'
import { tmpdir } from 'os'
import { join } from 'path'
import {
appendRemoteTriggerAuditRecord,
formatRemoteTriggerAuditStatus,
listRemoteTriggerAuditRecords,
} from '../remoteTriggerAudit'
let tempDir = ''
beforeEach(() => {
tempDir = join(
tmpdir(),
`remote-trigger-audit-${Date.now()}-${Math.random().toString(16).slice(2)}`,
)
})
afterEach(async () => {
await rm(tempDir, { recursive: true, force: true })
})
describe('remote trigger audit', () => {
test('records and formats local remote trigger audit events', async () => {
await appendRemoteTriggerAuditRecord(
{ action: 'run', triggerId: 'abc', ok: true, status: 200, createdAt: 1 },
tempDir,
)
await appendRemoteTriggerAuditRecord(
{ action: 'create', ok: false, error: 'bad request', createdAt: 2 },
tempDir,
)
const records = await listRemoteTriggerAuditRecords(tempDir)
expect(records).toHaveLength(2)
expect(records[0].action).toBe('create')
expect(formatRemoteTriggerAuditStatus(records)).toContain(
'RemoteTrigger audit records: 2',
)
expect(formatRemoteTriggerAuditStatus(records)).toContain('Failures: 1')
})
})

View File

@@ -0,0 +1,68 @@
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
import { mkdirSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { getTeammateStatuses } from '../teamDiscovery'
let tempHome: string
let previousConfigDir: string | undefined
beforeEach(() => {
previousConfigDir = process.env.CLAUDE_CONFIG_DIR
tempHome = join(
tmpdir(),
`team-discovery-${Date.now()}-${Math.random().toString(16).slice(2)}`,
)
process.env.CLAUDE_CONFIG_DIR = tempHome
})
afterEach(() => {
if (previousConfigDir === undefined) {
delete process.env.CLAUDE_CONFIG_DIR
} else {
process.env.CLAUDE_CONFIG_DIR = previousConfigDir
}
rmSync(tempHome, { recursive: true, force: true })
})
function writeTeamConfig(teamName: string, config: unknown): void {
const teamDir = join(tempHome, 'teams', teamName)
mkdirSync(teamDir, { recursive: true })
writeFileSync(join(teamDir, 'config.json'), JSON.stringify(config, null, 2))
}
describe('getTeammateStatuses', () => {
test('preserves in-process backend type for lifecycle actions', () => {
writeTeamConfig('alpha', {
name: 'alpha',
createdAt: Date.now(),
leadAgentId: 'team-lead@alpha',
members: [
{
agentId: 'team-lead@alpha',
name: 'team-lead',
joinedAt: Date.now(),
tmuxPaneId: '',
cwd: tempHome,
subscriptions: [],
},
{
agentId: 'worker@alpha',
name: 'worker',
joinedAt: Date.now(),
tmuxPaneId: 'in-process',
cwd: tempHome,
subscriptions: [],
backendType: 'in-process',
},
],
})
expect(getTeammateStatuses('alpha')).toEqual([
expect.objectContaining({
agentId: 'worker@alpha',
backendType: 'in-process',
}),
])
})
})

View File

@@ -30,6 +30,18 @@ mock.module("src/services/tokenEstimation.ts", () => ({
countTokensViaHaikuFallback: async () => 0,
}));
// Mock slowOperations to avoid bun:bundle import
mock.module('src/utils/slowOperations.ts', () => ({
jsonStringify: JSON.stringify,
jsonParse: JSON.parse,
slowLogging: { enabled: false },
clone: (v: any) => structuredClone(v),
cloneDeep: (v: any) => structuredClone(v),
callerFrame: () => '',
SLOW_OPERATION_THRESHOLD_MS: 100,
writeFileSync_DEPRECATED: () => {},
}))
const {
getTokenCountFromUsage,
getTokenUsage,

View File

@@ -89,6 +89,7 @@ export function getExperimentAdvisorModels():
export function modelSupportsAdvisor(model: string): boolean {
const m = model.toLowerCase()
return (
m.includes('opus-4-7') ||
m.includes('opus-4-6') ||
m.includes('sonnet-4-6') ||
process.env.USER_TYPE === 'ant'
@@ -99,6 +100,7 @@ export function modelSupportsAdvisor(model: string): boolean {
export function isValidAdvisorModel(model: string): boolean {
const m = model.toLowerCase()
return (
m.includes('opus-4-7') ||
m.includes('opus-4-6') ||
m.includes('sonnet-4-6') ||
process.env.USER_TYPE === 'ant'

View File

@@ -536,9 +536,25 @@ export type Attachment =
}
| {
type: 'skill_discovery'
skills: { name: string; description: string; shortId?: string }[]
skills: {
name: string
description: string
shortId?: string
score?: number
autoLoaded?: boolean
content?: string
path?: string
}[]
signal: DiscoverySignal
source: 'native' | 'aki' | 'both'
gap?: {
key: string
status: 'pending' | 'draft' | 'active'
draftName?: string
draftPath?: string
activeName?: string
activePath?: string
}
}
| {
type: 'queued_command'

View File

@@ -75,7 +75,7 @@ export function getAttributionTexts(): AttributionTexts {
const modelName =
isInternalModelRepoCached() || isKnownPublicModel
? getPublicModelName(model)
: 'Claude Opus 4.6'
: 'Claude Opus 4.7'
const defaultAttribution = `🤖 Generated with [Claude Code](${PRODUCT_URL})`
const defaultCommit = `Co-Authored-By: ${modelName} <noreply@anthropic.com>`

View File

@@ -514,7 +514,6 @@ async function _runAndCache(
} catch (e) {
if (epoch !== _apiKeyHelperEpoch) return ' '
const detail = e instanceof Error ? e.message : String(e)
// biome-ignore lint/suspicious/noConsole: user-configured script failed; must be visible without --debug
console.error(chalk.red(`apiKeyHelper failed: ${detail}`))
logForDebugging(`Error getting API key from apiKeyHelper: ${detail}`, {
level: 'error',
@@ -690,7 +689,6 @@ export function refreshAwsAuth(awsAuthRefresh: string): Promise<boolean> {
: chalk.red(
'Error running awsAuthRefresh (in settings or ~/.claude.json):',
)
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.error(message)
authStatusManager.endAuthentication(false)
void resolve(false)
@@ -769,10 +767,8 @@ async function getAwsCredsFromCredentialExport(): Promise<{
'Error getting AWS credentials from awsCredentialExport (in settings or ~/.claude.json):',
)
if (e instanceof Error) {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.error(message, e.message)
} else {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.error(message, e)
}
return null
@@ -958,7 +954,6 @@ export function refreshGcpAuth(gcpAuthRefresh: string): Promise<boolean> {
: chalk.red(
'Error running gcpAuthRefresh (in settings or ~/.claude.json):',
)
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.error(message)
authStatusManager.endAuthentication(false)
void resolve(false)
@@ -1779,6 +1774,7 @@ export function getOtelHeadersFromHelper(): Record<string, string> {
const debounceMs = parseInt(
process.env.CLAUDE_CODE_OTEL_HEADERS_HELPER_DEBOUNCE_MS ||
DEFAULT_OTEL_HEADERS_DEBOUNCE_MS.toString(),
10,
)
if (
cachedOtelHeaders &&

View File

@@ -81,7 +81,6 @@ export async function assertMinVersion(): Promise<void> {
versionConfig.minVersion &&
lt(MACRO.VERSION, versionConfig.minVersion)
) {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.error(`
It looks like your version of Claude Code (${MACRO.VERSION}) needs an update.
A newer version (${versionConfig.minVersion} or higher) is required to continue.
@@ -478,7 +477,6 @@ export async function installGlobalPackage(
currentVersion:
MACRO.VERSION as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.error(`
Error: Windows NPM detected in WSL

View File

@@ -421,6 +421,7 @@ export const createAndSaveSnapshot = async (
logForDebugging(`Creating shell snapshot for ${shellType} (${binShell})`)
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: async needed for sequential awaits inside executor
return new Promise(async resolve => {
try {
const configFile = getConfigFile(binShell)

View File

@@ -251,6 +251,7 @@ const BRACE_EXPANSION_RE = /\{[^{}\s]*(,|\.\.)[^{}\s]*\}/
* word boundaries.
*/
// eslint-disable-next-line no-control-regex
// biome-ignore lint/suspicious/noControlCharactersInRegex: intentional control character detection regex
const CONTROL_CHAR_RE = /[\x00-\x08\x0B-\x1F\x7F]/
/**
@@ -1899,6 +1900,7 @@ function walkVariableAssignment(
return {
kind: 'too-complex',
reason:
// biome-ignore lint/suspicious/noTemplateCurlyInString: ${VAR} is bash syntax documentation, not a JS template literal
'PS4 value outside safe charset — only ${VAR} refs and [A-Za-z0-9 _+:.=/[]-] allowed',
nodeType: 'variable_assignment',
}

View File

@@ -153,6 +153,7 @@ export function sanitizeSurfaceKey(surfaceKey: string): string {
*/
export function sanitizeModelName(shortName: string): string {
// Map internal variants to public equivalents based on model family
if (shortName.includes('opus-4-7')) return 'claude-opus-4-7'
if (shortName.includes('opus-4-6')) return 'claude-opus-4-6'
if (shortName.includes('opus-4-5')) return 'claude-opus-4-5'
if (shortName.includes('opus-4-1')) return 'claude-opus-4-1'

View File

@@ -525,8 +525,8 @@ export type GlobalConfig = {
// Permission explainer configuration
permissionExplainerEnabled?: boolean // Enable Haiku-generated explanations for permission requests (default: true)
// Teammate spawn mode: 'auto' | 'tmux' | 'in-process'
teammateMode?: 'auto' | 'tmux' | 'in-process' // How to spawn teammates (default: 'auto')
// Teammate spawn mode: 'auto' | 'tmux' | 'windows-terminal' | 'in-process'
teammateMode?: 'auto' | 'tmux' | 'windows-terminal' | 'in-process' // How to spawn teammates (default: 'auto')
// Model for new teammates when the tool call doesn't pass one.
// undefined = hardcoded Opus (backward-compat); null = leader's model; string = model alias/ID.
teammateDefaultModel?: string | null

View File

@@ -46,7 +46,11 @@ export function modelSupports1M(model: string): boolean {
return false
}
const canonical = getCanonicalName(model)
return canonical.includes('claude-sonnet-4') || canonical.includes('opus-4-6')
return (
canonical.includes('claude-sonnet-4') ||
canonical.includes('opus-4-6') ||
canonical.includes('opus-4-7')
)
}
export function getContextWindowForModel(
@@ -171,7 +175,10 @@ export function getModelMaxOutputTokens(model: string): {
const m = getCanonicalName(model)
if (m.includes('opus-4-6')) {
if (m.includes('opus-4-7')) {
defaultTokens = 64_000
upperLimit = 128_000
} else if (m.includes('opus-4-6')) {
defaultTokens = 64_000
upperLimit = 128_000
} else if (m.includes('sonnet-4-6')) {

View File

@@ -0,0 +1,93 @@
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
const mockParseDeepLink = mock((uri: string) => {
if (uri === null || uri === undefined || uri === 'bad-uri') {
throw new Error('invalid deep link')
}
return { query: 'hello', cwd: 'E:/Source_code/Claude-code-bast-test' }
})
const mockLaunchInTerminal = mock(async () => true)
mock.module('../parseDeepLink.js', () => ({
parseDeepLink: mockParseDeepLink,
}))
mock.module('../registerProtocol.js', () => ({
MACOS_BUNDLE_ID: 'com.anthropic.claude-code-url-handler',
}))
mock.module('../terminalLauncher.js', () => ({
launchInTerminal: mockLaunchInTerminal,
}))
mock.module('../banner.js', () => ({
readLastFetchTime: async () => undefined,
buildDeepLinkBanner: () => '',
}))
mock.module('../../githubRepoPathMapping.js', () => ({
updateGithubRepoPathMapping: async () => {},
getKnownPathsForRepo: () => [],
filterExistingPaths: async () => [],
validateRepoAtPath: async () => false,
removePathFromRepo: () => {},
}))
const { handleDeepLinkUri, handleUrlSchemeLaunch } = await import(
'../protocolHandler.js'
)
const originalBundleId = process.env.__CFBundleIdentifier
const originalUrlEvent = process.env.CLAUDE_CODE_URL_EVENT
beforeEach(() => {
mockParseDeepLink.mockClear()
mockLaunchInTerminal.mockClear()
process.env.__CFBundleIdentifier = undefined
delete process.env.CLAUDE_CODE_URL_EVENT
})
afterEach(() => {
process.env.__CFBundleIdentifier = originalBundleId
if (originalUrlEvent === undefined) {
delete process.env.CLAUDE_CODE_URL_EVENT
} else {
process.env.CLAUDE_CODE_URL_EVENT = originalUrlEvent
}
})
describe('handleUrlSchemeLaunch', () => {
test('returns null without calling url-handler-napi when bundle id does not match', async () => {
process.env.__CFBundleIdentifier = 'other.bundle'
await expect(handleUrlSchemeLaunch()).resolves.toBeNull()
expect(mockParseDeepLink).not.toHaveBeenCalled()
})
test('returns null for a matching bundle id when no URL event arrives', async () => {
process.env.__CFBundleIdentifier = 'com.anthropic.claude-code-url-handler'
await expect(handleUrlSchemeLaunch()).resolves.toBeNull()
expect(mockParseDeepLink).not.toHaveBeenCalled()
})
test('handles a URL event after waiting for url-handler-napi', async () => {
process.env.__CFBundleIdentifier = 'com.anthropic.claude-code-url-handler'
process.env.CLAUDE_CODE_URL_EVENT = 'claude-cli://prompt?q=hello'
await expect(handleUrlSchemeLaunch()).resolves.toBe(0)
expect(mockParseDeepLink).toHaveBeenCalledWith(
'claude-cli://prompt?q=hello',
)
})
})
describe('handleDeepLinkUri', () => {
test('returns 1 when parsing fails', async () => {
await expect(handleDeepLinkUri('bad-uri')).resolves.toBe(1)
expect(mockLaunchInTerminal).not.toHaveBeenCalled()
})
test('returns 0 when parsing succeeds and terminal launch succeeds', async () => {
await expect(
handleDeepLinkUri('claude-cli://prompt?q=hello'),
).resolves.toBe(0)
expect(mockLaunchInTerminal).toHaveBeenCalled()
})
})

View File

@@ -94,11 +94,13 @@ export async function handleUrlSchemeLaunch(): Promise<number | null> {
try {
const { waitForUrlEvent } = await import('url-handler-napi')
const url = (waitForUrlEvent as any)(5000)
const url = await (
waitForUrlEvent as (timeoutMs?: number) => Promise<string | null>
)(5000)
if (!url) {
return null
}
return await handleDeepLinkUri(await url as string)
return await handleDeepLinkUri(url)
} catch {
// NAPI module not available, or handleDeepLinkUri rejected — not a URL launch
return null

View File

@@ -14,7 +14,8 @@ export function isBilledAsExtraUsage(
.toLowerCase()
.replace(/\[1m\]$/, '')
.trim()
const isOpus46 = m === 'opus' || m.includes('opus-4-6')
const isOpus46 =
m === 'opus' || m.includes('opus-4-6') || m.includes('opus-4-7')
const isSonnet46 = m === 'sonnet' || m.includes('sonnet-4-6')
if (isOpus46 && isOpus1mMerged) return false

View File

@@ -140,7 +140,7 @@ export function getFastModeUnavailableReason(): string | null {
}
// @[MODEL LAUNCH]: Update supported Fast Mode models.
export const FAST_MODE_MODEL_DISPLAY = 'Opus 4.6'
export const FAST_MODE_MODEL_DISPLAY = 'Opus 4.7'
export function getFastModeModel(): string {
return 'opus' + (isOpus1mMergeEnabled() ? '[1m]' : '')
@@ -172,7 +172,10 @@ export function isFastModeSupportedByModel(
}
const model = modelSetting ?? getDefaultMainLoopModelSetting()
const parsedModel = parseUserSpecifiedModel(model)
return parsedModel.toLowerCase().includes('opus-4-6')
return (
parsedModel.toLowerCase().includes('opus-4-7') ||
parsedModel.toLowerCase().includes('opus-4-6')
)
}
// --- Fast mode runtime state ---

View File

@@ -1109,7 +1109,6 @@ async function readFileAsyncOrNull(path: string): Promise<string | null> {
const ENABLE_DUMP_STATE = false
function maybeDumpStateForDebug(state: FileHistoryState): void {
if (ENABLE_DUMP_STATE) {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.error(inspect(state, false, 5))
}
}

View File

@@ -35,7 +35,7 @@ export type FrontmatterData = {
// Values are arrays of matcher configurations with hooks
// Validated by HooksSchema in loadSkillsDir.ts
hooks?: HooksSettings | null
// Effort level for agents (e.g., 'low', 'medium', 'high', 'max', or an integer)
// Effort level for agents (e.g., 'low', 'medium', 'high', 'xhigh', 'max', or an integer)
// Controls the thinking effort used by the agent's model
effort?: string | null
// Execution context for skills: 'inline' (default) or 'fork' (run as sub-agent)

View File

@@ -22,7 +22,9 @@ export async function returnValue<A>(
}
type QueuedGenerator<A> = {
// biome-ignore lint/suspicious/noConfusingVoidType: void matches AsyncGenerator<A, void> return type
done: boolean | void
// biome-ignore lint/suspicious/noConfusingVoidType: void matches AsyncGenerator<A, void> yield type
value: A | void
generator: AsyncGenerator<A, void>
promise: Promise<QueuedGenerator<A>>

View File

@@ -75,6 +75,7 @@ function urlMatchesPattern(url: string, pattern: string): boolean {
*/
function sanitizeHeaderValue(value: string): string {
// eslint-disable-next-line no-control-regex
// biome-ignore lint/suspicious/noControlCharactersInRegex: intentional control character sanitization
return value.replace(/[\r\n\x00]/g, '')
}

View File

@@ -379,7 +379,7 @@ async function readIdeLockfile(path: string): Promise<IdeLockfileInfo | null> {
return {
workspaceFolders,
port: parseInt(port),
port: parseInt(port, 10),
pid,
ideName,
useWebSocket,
@@ -669,7 +669,7 @@ export async function detectIDEs(
try {
// Get the CLAUDE_CODE_SSE_PORT if set
const ssePort = process.env.CLAUDE_CODE_SSE_PORT
const envPort = ssePort ? parseInt(ssePort) : null
const envPort = ssePort ? parseInt(ssePort, 10) : null
// Get the current working directory, normalized to NFC for consistent
// comparison. macOS returns NFD paths (decomposed Unicode), while IDEs
@@ -1006,7 +1006,7 @@ function getVSCodeIDECommandByParentProcess(): string | null {
if (!ppidStr) {
break
}
pid = parseInt(ppidStr.trim())
pid = parseInt(ppidStr.trim(), 10)
}
return null

View File

@@ -158,7 +158,6 @@ const isHardFailMode = memoize((): boolean => {
export function logError(error: unknown): void {
const err = toError(error)
if (feature('HARD_FAIL') && isHardFailMode()) {
// biome-ignore lint/suspicious/noConsole:: intentional crash output
console.error('[HARD FAIL] logError called with:', err.stack || err.message)
// eslint-disable-next-line custom-rules/no-process-exit
process.exit(1)

View File

@@ -3555,14 +3555,40 @@ Read the team config to discover your teammates' names. Check the task list peri
// be gated, but this pattern can — same approach as teammate_mailbox above.
if (feature('EXPERIMENTAL_SKILL_SEARCH')) {
if (attachment.type === 'skill_discovery') {
if (attachment.skills.length === 0) return []
const lines = attachment.skills.map(s => `- ${s.name}: ${s.description}`)
if (attachment.skills.length === 0 && !attachment.gap) return []
const loaded = attachment.skills.filter(s => s.autoLoaded && s.content)
const recommended = attachment.skills.filter(s => !s.autoLoaded)
const loadedSections = loaded.map(
s =>
`<${COMMAND_NAME_TAG}>${s.name}</${COMMAND_NAME_TAG}>\n` +
`<loaded-skill name="${s.name}" path="${s.path ?? ''}">\n${s.content}\n</loaded-skill>`,
)
const recommendationLines = recommended.map(
s => `- ${s.name}: ${s.description}`,
)
const gapText = attachment.gap
? [
'No high-confidence active skill was auto-loaded for this request.',
attachment.gap.activePath
? `A learned skill was promoted for future turns: ${attachment.gap.activeName} (${attachment.gap.activePath}).`
: attachment.gap.draftPath
? `A draft learned skill candidate was created: ${attachment.gap.draftName} (${attachment.gap.draftPath}).`
: `The skill gap was recorded for future learning: ${attachment.gap.key}.`,
].join('\n')
: ''
return wrapMessagesInSystemReminder([
createUserMessage({
content:
`Skills relevant to your task:\n\n${lines.join('\n')}\n\n` +
`These skills encode project-specific conventions. ` +
`Invoke via Skill("<name>") for complete instructions.`,
content: [
loadedSections.length > 0
? `The following skills are auto-loaded for this task. Apply their instructions now; do not call Skill("<name>") again for these loaded skills.\n\n${loadedSections.join('\n\n')}`
: '',
recommendationLines.length > 0
? `Additional relevant skills were found but not auto-loaded:\n\n${recommendationLines.join('\n')}\n\nInvoke via Skill("<name>") only if you need their complete instructions.`
: '',
gapText,
]
.filter(Boolean)
.join('\n\n'),
isMeta: true,
}),
])
@@ -3570,7 +3596,6 @@ Read the team config to discover your teammates' names. Check the task list peri
}
// eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check -- teammate_mailbox/team_context/skill_discovery/bagel_console handled above
// biome-ignore lint/nursery/useExhaustiveSwitchCases: teammate_mailbox/team_context/max_turns_reached/skill_discovery/bagel_console handled above, can't add case for dead code elimination
switch (attachment.type) {
case 'directory': {
return wrapMessagesInSystemReminder([

View File

@@ -11,14 +11,7 @@ export function prewarmModifiers(): void {
return
}
prewarmed = true
// Load module in background
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { prewarm } = require('modifiers-napi') as { prewarm: () => void }
prewarm()
} catch {
// Ignore errors during prewarm
}
void import('modifiers-napi').then(({ prewarm }) => prewarm()).catch(() => {})
}
/**
@@ -28,9 +21,12 @@ export function isModifierPressed(modifier: ModifierKey): boolean {
if (process.platform !== 'darwin') {
return false
}
// Dynamic import to avoid loading native module at top level
const { isModifierPressed: nativeIsModifierPressed } =
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports
require('modifiers-napi') as { isModifierPressed: (m: string) => boolean }
return nativeIsModifierPressed(modifier)
const { isModifierPressed: nativeIsModifierPressed } =
require('modifiers-napi') as { isModifierPressed: (m: string) => boolean }
return nativeIsModifierPressed(modifier)
} catch {
return false
}
}

View File

@@ -799,6 +799,10 @@ export function initialPermissionModeFromCLI({
result = { mode: 'default', notification }
}
if (!result) {
result = { mode: 'default', notification }
}
if (feature('TRANSCRIPT_CLASSIFIER') && result.mode === 'auto') {
autoModeStateModule?.setAutoModeActive(true)
}
@@ -923,7 +927,6 @@ export async function initializeToolPermissionContext({
})
}
// Bypass permissions mode is available to all users
const isBypassPermissionsModeAvailable = true
const settings = getSettings_DEPRECATED() || {}
@@ -1061,54 +1064,131 @@ export function getAutoModeUnavailableNotification(
* kicking the user out of a mode they've already left during the await.
*/
export async function verifyAutoModeGateAccess(
_currentContext: ToolPermissionContext,
currentContext: ToolPermissionContext,
// Runtime AppState.fastMode — passed from callers with AppState access so
// the disableFastMode circuit breaker reads current state, not stale
// settings.fastMode (which is intentionally sticky across /model auto-
// downgrades). Optional for callers without AppState (e.g. SDK init paths).
fastMode?: boolean,
): Promise<AutoModeGateCheckResult> {
// Only fast-mode circuit breaker remains. All other gates (GrowthBook,
// settings, model support, opt-in) have been removed.
// Auto-mode config — runs in ALL builds (circuit breaker, carousel, kick-out)
// Fresh read of tengu_auto_mode_config.enabled — this async check runs once
// after GrowthBook initialization and is the authoritative source for
// isAutoModeAvailable. The sync startup path uses stale cache; this
// corrects it. Circuit breaker (enabled==='disabled') takes effect here.
const autoModeConfig = await getDynamicConfig_BLOCKS_ON_INIT<{
enabled?: AutoModeEnabledState
disableFastMode?: boolean
}>('tengu_auto_mode_config', {})
const enabledState = parseAutoModeEnabledState(autoModeConfig?.enabled)
const disabledBySettings = isAutoModeDisabledBySettings()
// Treat settings-disable the same as GrowthBook 'disabled' for circuit-breaker
// semantics — blocks SDK/explicit re-entry via isAutoModeGateEnabled().
autoModeStateModule?.setAutoModeCircuitBroken(
enabledState === 'disabled' || disabledBySettings,
)
// Carousel availability: not circuit-broken, not disabled-by-settings,
// model supports it, disableFastMode breaker not firing, and (enabled or opted-in)
const mainModel = getMainLoopModel()
// Temp circuit breaker: tengu_auto_mode_config.disableFastMode blocks auto
// mode when fast mode is on. Checks runtime AppState.fastMode (if provided)
// and, for ants, model name '-fast' substring (ant-internal fast models
// like capybara-v2-fast[1m] encode speed in the model ID itself).
// Remove once auto+fast mode interaction is validated.
const disableFastModeBreakerFires =
!!autoModeConfig?.disableFastMode &&
(!!fastMode ||
(process.env.USER_TYPE === 'ant' &&
mainModel.toLowerCase().includes('-fast')))
// If fast-mode breaker fires, circuit-break auto mode
autoModeStateModule?.setAutoModeCircuitBroken(disableFastModeBreakerFires)
const modelSupported =
modelSupportsAutoMode(mainModel) && !disableFastModeBreakerFires
let carouselAvailable = false
if (enabledState !== 'disabled' && !disabledBySettings && modelSupported) {
carouselAvailable =
enabledState === 'enabled' || hasAutoModeOptInAnySource()
}
// canEnterAuto gates explicit entry (--permission-mode auto, defaultMode: auto)
// — explicit entry IS an opt-in, so we only block on circuit breaker + settings + model
const canEnterAuto =
enabledState !== 'disabled' && !disabledBySettings && modelSupported
logForDebugging(
`[auto-mode] verifyAutoModeGateAccess: disableFastModeBreakerFires=${disableFastModeBreakerFires}`,
`[auto-mode] verifyAutoModeGateAccess: enabledState=${enabledState} disabledBySettings=${disabledBySettings} model=${mainModel} modelSupported=${modelSupported} disableFastModeBreakerFires=${disableFastModeBreakerFires} carouselAvailable=${carouselAvailable} canEnterAuto=${canEnterAuto}`,
)
if (!disableFastModeBreakerFires) {
// Auto mode available — no kick-out needed
return { updateContext: ctx => ctx }
// Capture CLI-flag intent now (doesn't depend on context).
const autoModeFlagCli = autoModeStateModule?.getAutoModeFlagCli() ?? false
// Return a transform function that re-evaluates context-dependent conditions
// against the CURRENT context at setAppState time. The async GrowthBook
// results above (canEnterAuto, carouselAvailable, enabledState, reason) are
// closure-captured — those don't depend on context. But mode, prePlanMode,
// and isAutoModeAvailable checks MUST use the fresh ctx or a mid-await
// shift-tab gets reverted (or worse, the user stays in auto despite the
// circuit breaker if they entered auto DURING the await — which is possible
// because setAutoModeCircuitBroken above runs AFTER the await).
const setAvailable = (
ctx: ToolPermissionContext,
available: boolean,
): ToolPermissionContext => {
if (ctx.isAutoModeAvailable !== available) {
logForDebugging(
`[auto-mode] verifyAutoModeGateAccess setAvailable: ${ctx.isAutoModeAvailable} -> ${available}`,
)
}
return ctx.isAutoModeAvailable === available
? ctx
: { ...ctx, isAutoModeAvailable: available }
}
// Fast-mode breaker fired — kick out of auto if currently in it
const notification = getAutoModeUnavailableNotification('circuit-breaker')
if (canEnterAuto) {
return { updateContext: ctx => setAvailable(ctx, carouselAvailable) }
}
// Gate is off or circuit-broken — determine reason (context-independent).
let reason: AutoModeUnavailableReason
if (disabledBySettings) {
reason = 'settings'
logForDebugging('auto mode disabled: disableAutoMode in settings', {
level: 'warn',
})
} else if (enabledState === 'disabled') {
reason = 'circuit-breaker'
logForDebugging(
'auto mode disabled: tengu_auto_mode_config.enabled === "disabled" (circuit breaker)',
{ level: 'warn' },
)
} else {
reason = 'model'
logForDebugging(
`auto mode disabled: model ${getMainLoopModel()} does not support auto mode`,
{ level: 'warn' },
)
}
const notification = getAutoModeUnavailableNotification(reason)
// Unified kick-out transform. Re-checks the FRESH ctx and only fires
// side effects (setAutoModeActive(false), setNeedsAutoModeExitAttachment)
// when the kick-out actually applies. This keeps autoModeActive in sync
// with toolPermissionContext.mode even if the user changed modes during
// the await: if they already left auto on their own, handleCycleMode
// already deactivated the classifier and we don't fire again; if they
// ENTERED auto during the await (possible before setAutoModeCircuitBroken
// landed), we kick them out here.
const kickOutOfAutoIfNeeded = (
ctx: ToolPermissionContext,
): ToolPermissionContext => {
const inAuto = ctx.mode === 'auto'
logForDebugging(
`[auto-mode] kickOutOfAutoIfNeeded (fast-mode): ctx.mode=${ctx.mode}`,
`[auto-mode] kickOutOfAutoIfNeeded applying: ctx.mode=${ctx.mode} ctx.prePlanMode=${ctx.prePlanMode} reason=${reason}`,
)
// Plan mode with auto active: either from prePlanMode='auto' (entered
// from auto) or from opt-in (strippedDangerousRules present).
const inPlanWithAutoActive =
ctx.mode === 'plan' &&
(ctx.prePlanMode === 'auto' || !!ctx.strippedDangerousRules)
if (!inAuto && !inPlanWithAutoActive) {
return { ...ctx, isAutoModeAvailable: false }
return setAvailable(ctx, false)
}
if (inAuto) {
autoModeStateModule?.setAutoModeActive(false)
@@ -1122,6 +1202,8 @@ export async function verifyAutoModeGateAccess(
isAutoModeAvailable: false,
}
}
// Plan with auto active: deactivate auto, restore permissions, defuse
// prePlanMode so ExitPlanMode goes to default.
autoModeStateModule?.setAutoModeActive(false)
setNeedsAutoModeExitAttachment(true)
return {
@@ -1131,23 +1213,62 @@ export async function verifyAutoModeGateAccess(
}
}
return { updateContext: kickOutOfAutoIfNeeded, notification }
// Notification decisions use the stale context — that's OK: we're deciding
// WHETHER to notify based on what the user WAS doing when this check started.
// (Side effects and mode mutation are decided inside the transform above,
// against the fresh ctx.)
const wasInAuto = currentContext.mode === 'auto'
// Auto was used during plan: entered from auto or opt-in auto active
const autoActiveDuringPlan =
currentContext.mode === 'plan' &&
(currentContext.prePlanMode === 'auto' ||
!!currentContext.strippedDangerousRules)
const wantedAuto = wasInAuto || autoActiveDuringPlan || autoModeFlagCli
if (!wantedAuto) {
// User didn't want auto at call time — no notification. But still apply
// the full kick-out transform: if they shift-tabbed INTO auto during the
// await (before setAutoModeCircuitBroken landed), we need to evict them.
return { updateContext: kickOutOfAutoIfNeeded }
}
if (wasInAuto || autoActiveDuringPlan) {
// User was in auto or had auto active during plan — kick out + notify.
return { updateContext: kickOutOfAutoIfNeeded, notification }
}
// autoModeFlagCli only: defaultMode was auto but sync check rejected it.
// Suppress notification if isAutoModeAvailable is already false (already
// notified on a prior check; prevents repeat notifications on successive
// unsupported-model switches).
return {
updateContext: kickOutOfAutoIfNeeded,
notification: currentContext.isAutoModeAvailable ? notification : undefined,
}
}
/**
* Bypass permissions is always available — no remote gate check needed.
* Core logic to check if bypassPermissions should be disabled based on Statsig gate
*/
export function shouldDisableBypassPermissions(): Promise<boolean> {
return Promise.resolve(false)
return checkSecurityRestrictionGate('tengu_disable_bypass_permissions_mode')
}
function isAutoModeDisabledBySettings(): boolean {
const settings = getSettings_DEPRECATED() || {}
return (
(settings as { disableAutoMode?: 'disable' }).disableAutoMode ===
'disable' ||
(settings.permissions as { disableAutoMode?: 'disable' } | undefined)
?.disableAutoMode === 'disable'
)
}
/**
* Checks if auto mode can be entered: only fast-mode circuit breaker remains.
* Synchronous.
* Checks if auto mode can be entered: circuit breaker is not active and settings
* have not disabled it. Synchronous.
*/
export function isAutoModeGateEnabled(): boolean {
// Auto mode is available to all users — only fast-mode circuit breaker remains
if (autoModeStateModule?.isAutoModeCircuitBroken() ?? false) return false
return true
}
@@ -1156,9 +1277,11 @@ export function isAutoModeGateEnabled(): boolean {
* Synchronous — uses state populated by verifyAutoModeGateAccess.
*/
export function getAutoModeUnavailableReason(): AutoModeUnavailableReason | null {
if (isAutoModeDisabledBySettings()) return 'settings'
if (autoModeStateModule?.isAutoModeCircuitBroken() ?? false) {
return 'circuit-breaker'
}
if (!modelSupportsAutoMode(getMainLoopModel())) return 'model'
return null
}
@@ -1172,7 +1295,11 @@ export function getAutoModeUnavailableReason(): AutoModeUnavailableReason | null
*/
export type AutoModeEnabledState = 'enabled' | 'disabled' | 'opt-in'
const AUTO_MODE_ENABLED_DEFAULT: AutoModeEnabledState = 'enabled'
const AUTO_MODE_ENABLED_DEFAULT: AutoModeEnabledState = feature(
'TRANSCRIPT_CLASSIFIER',
)
? 'enabled'
: 'disabled'
function parseAutoModeEnabledState(value: unknown): AutoModeEnabledState {
if (value === 'enabled' || value === 'disabled' || value === 'opt-in') {
@@ -1222,15 +1349,27 @@ export function getAutoModeEnabledStateIfCached():
* dialog or by IDE/Desktop settings toggle)
*/
export function hasAutoModeOptInAnySource(): boolean {
return true
if (autoModeStateModule?.getAutoModeFlagCli() ?? false) return true
return hasAutoModeOptIn()
}
/**
* Checks if bypassPermissions mode is currently disabled by Statsig gate or settings.
* Always returns false — bypass is available to all users.
* This is a synchronous version that uses cached Statsig values.
*/
export function isBypassPermissionsModeDisabled(): boolean {
return false
const growthBookDisableBypassPermissionsMode =
checkStatsigFeatureGate_CACHED_MAY_BE_STALE(
'tengu_disable_bypass_permissions_mode',
)
const settings = getSettings_DEPRECATED() || {}
const settingsDisableBypassPermissionsMode =
settings.permissions?.disableBypassPermissionsMode === 'disable'
return (
growthBookDisableBypassPermissionsMode ||
settingsDisableBypassPermissionsMode
)
}
/**
@@ -1255,12 +1394,29 @@ export function createDisabledBypassPermissionsContext(
}
/**
* No-op — bypass permissions is always available, no remote gate check needed.
* Asynchronously checks if the bypassPermissions mode should be disabled based on Statsig gate
* and returns an updated toolPermissionContext if needed
*/
export async function checkAndDisableBypassPermissions(
_currentContext: ToolPermissionContext,
currentContext: ToolPermissionContext,
): Promise<void> {
// Bypass permissions is always available — no gate check needed
// Only proceed if bypassPermissions mode is available
if (!currentContext.isBypassPermissionsModeAvailable) {
return
}
const shouldDisable = await shouldDisableBypassPermissions()
if (!shouldDisable) {
return
}
// Gate is enabled, need to disable bypassPermissions mode
logForDebugging(
'bypassPermissions mode is being disabled by Statsig gate (async check)',
{ level: 'warn' },
)
void gracefulShutdown(1, 'bypass_permissions_disabled')
}
export function isDefaultPermissionModeAuto(): boolean {
@@ -1278,7 +1434,11 @@ export function isDefaultPermissionModeAuto(): boolean {
*/
export function shouldPlanUseAutoMode(): boolean {
if (feature('TRANSCRIPT_CLASSIFIER')) {
return isAutoModeGateEnabled() && getUseAutoModeDuringPlan()
return (
hasAutoModeOptIn() &&
isAutoModeGateEnabled() &&
getUseAutoModeDuringPlan()
)
}
return false
}

32
src/utils/pipeStatus.ts Normal file
View File

@@ -0,0 +1,32 @@
import type { PipeRegistry } from './pipeRegistry.js'
import { readRegistry } from './pipeRegistry.js'
export async function formatPipeRegistryStatus(): Promise<string> {
return formatPipeRegistry(await readRegistry())
}
export function formatPipeRegistry(registry: PipeRegistry): string {
const lines = [
`Pipe registry: ${registry.main ? 1 : 0} main, ${registry.subs.length} sub(s)`,
]
if (registry.mainMachineId) {
lines.push(` main_machine=${registry.mainMachineId.slice(0, 8)}...`)
}
if (registry.main) {
lines.push(
` [main] ${registry.main.pipeName} pid=${registry.main.pid} host=${registry.main.hostname} tcp=${registry.main.tcpPort ?? 'none'}`,
)
}
for (const sub of registry.subs.slice(0, 10)) {
lines.push(
` [sub-${sub.subIndex}] ${sub.pipeName} pid=${sub.pid} host=${sub.hostname} bound=${sub.boundToMain ?? 'none'} tcp=${sub.tcpPort ?? 'none'}`,
)
}
if (!registry.main && registry.subs.length === 0) {
lines.push(' none')
}
if (registry.subs.length > 10) {
lines.push(` ... ${registry.subs.length - 10} more sub pipe(s)`)
}
return lines.join('\n')
}

View File

@@ -645,6 +645,7 @@ const PluginManifestUserConfigSchema = lazySchema(() =>
.describe(
'User-configurable values this plugin needs. Prompted at enable time. ' +
'Non-sensitive values saved to settings.json; sensitive values to secure storage ' +
// biome-ignore lint/suspicious/noTemplateCurlyInString: ${user_config.KEY} is plugin config syntax documentation, not a JS template literal
'(macOS keychain or .credentials.json). Available as ${user_config.KEY} in ' +
'MCP/LSP server config, hook commands, and (non-sensitive only) skill/agent content. ' +
'Note: sensitive values share a single keychain entry with OAuth tokens — keep ' +
@@ -690,6 +691,7 @@ const PluginManifestChannelsSchema = lazySchema(() =>
.optional()
.describe(
'Fields to prompt the user for when enabling this plugin in assistant mode. ' +
// biome-ignore lint/suspicious/noTemplateCurlyInString: ${user_config.KEY} is plugin config syntax documentation, not a JS template literal
'Saved values are substituted into ${user_config.KEY} references in the mcpServers env.',
),
})

View File

@@ -1702,6 +1702,7 @@ export function getPipelineSegments(
*/
export function isNullRedirectionTarget(target: string): boolean {
const t = target.trim().toLowerCase()
// biome-ignore lint/suspicious/noTemplateCurlyInString: ${null} is PowerShell syntax, not a JS template literal
return t === '$null' || t === '${null}'
}

View File

@@ -36,7 +36,6 @@ export function writeToStderr(data: string): void {
// Write error to stderr and exit with code 1. Consolidates the
// console.error + process.exit(1) pattern used in entrypoint fast-paths.
export function exitWithError(message: string): never {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.error(message)
// eslint-disable-next-line custom-rules/no-process-exit
process.exit(1)

View File

@@ -114,7 +114,7 @@ function recollapsePastedContent(
// Find pasted content in the edited text and re-collapse it
for (const [id, content] of Object.entries(pastedContents)) {
if (content.type === 'text') {
const pasteId = parseInt(id)
const pasteId = parseInt(id, 10)
const contentStr = content.content
// Check if this exact content exists in the edited prompt

View File

@@ -0,0 +1,23 @@
import {
getBridgeAccessToken,
getBridgeBaseUrl,
isSelfHostedBridge,
} from '../bridge/bridgeConfig.js'
export function formatRemoteControlLocalStatus(): string {
try {
const selfHosted = isSelfHostedBridge()
const token = getBridgeAccessToken()
return [
`Remote Control: ${selfHosted ? 'self-hosted' : 'official'}`,
` base_url=${getBridgeBaseUrl()}`,
` token=${token ? 'present' : 'missing'}`,
' entitlement=checked at remote-control startup',
].join('\n')
} catch (error) {
return [
'Remote Control: unknown',
` reason=${error instanceof Error ? error.message : String(error)}`,
].join('\n')
}
}

View File

@@ -0,0 +1,91 @@
import { randomUUID } from 'crypto'
import { mkdir, readFile, appendFile } from 'fs/promises'
import { dirname, join } from 'path'
import { getProjectRoot } from '../bootstrap/state.js'
const REMOTE_TRIGGER_AUDIT_REL = join('.claude', 'remote-trigger-audit.jsonl')
const MAX_AUDIT_RECORDS = 200
export type RemoteTriggerAuditRecord = {
auditId: string
action: string
triggerId?: string
ok: boolean
status?: number
error?: string
createdAt: number
}
export function resolveRemoteTriggerAuditPath(
rootDir: string = getProjectRoot(),
): string {
return join(rootDir, REMOTE_TRIGGER_AUDIT_REL)
}
export async function appendRemoteTriggerAuditRecord(
record: Omit<RemoteTriggerAuditRecord, 'auditId' | 'createdAt'> & {
auditId?: string
createdAt?: number
},
rootDir: string = getProjectRoot(),
): Promise<RemoteTriggerAuditRecord> {
const fullRecord: RemoteTriggerAuditRecord = {
auditId: record.auditId ?? randomUUID(),
action: record.action,
...(record.triggerId ? { triggerId: record.triggerId } : {}),
ok: record.ok,
...(record.status !== undefined ? { status: record.status } : {}),
...(record.error ? { error: record.error } : {}),
createdAt: record.createdAt ?? Date.now(),
}
const path = resolveRemoteTriggerAuditPath(rootDir)
await mkdir(dirname(path), { recursive: true })
await appendFile(path, `${JSON.stringify(fullRecord)}\n`, 'utf-8')
return fullRecord
}
export async function listRemoteTriggerAuditRecords(
rootDir: string = getProjectRoot(),
): Promise<RemoteTriggerAuditRecord[]> {
let raw: string
try {
raw = await readFile(resolveRemoteTriggerAuditPath(rootDir), 'utf-8')
} catch {
return []
}
const records: RemoteTriggerAuditRecord[] = []
for (const line of raw.split('\n')) {
if (!line.trim()) continue
try {
const parsed = JSON.parse(line) as Partial<RemoteTriggerAuditRecord>
if (
parsed &&
typeof parsed.auditId === 'string' &&
typeof parsed.action === 'string' &&
typeof parsed.ok === 'boolean' &&
typeof parsed.createdAt === 'number'
) {
records.push(parsed as RemoteTriggerAuditRecord)
}
} catch {
// Ignore malformed historical lines.
}
}
return records
.sort((a, b) => b.createdAt - a.createdAt)
.slice(0, MAX_AUDIT_RECORDS)
}
export function formatRemoteTriggerAuditStatus(
records: RemoteTriggerAuditRecord[],
): string {
const failures = records.filter(r => !r.ok)
const latest = records[0]
return [
`RemoteTrigger audit records: ${records.length}`,
`Failures: ${failures.length}`,
latest
? `Latest: ${latest.action}${latest.triggerId ? ` ${latest.triggerId}` : ''} ${latest.ok ? 'ok' : 'failed'} (${new Date(latest.createdAt).toLocaleString()})`
: 'Latest: none',
].join('\n')
}

View File

@@ -52,7 +52,6 @@ function spawnSecurity(serviceName: string): Promise<SpawnResult> {
// Exit 44 (entry not found) is a valid "no key" result and safe to
// prime as null. But timeout (err.killed) means the keychain MAY have
// a key we couldn't fetch — don't prime, let sync spawn retry.
// biome-ignore lint/nursery/noFloatingPromises: resolve() is not a floating promise
resolve({
stdout: err ? null : stdout?.trim() || null,
timedOut: Boolean(err && 'killed' in err && err.killed),

View File

@@ -39,7 +39,6 @@ function execFilePromise(
args,
{ encoding: 'utf-8', timeout: MDM_SUBPROCESS_TIMEOUT_MS },
(err, stdout) => {
// biome-ignore lint/nursery/noFloatingPromises: resolve() is not a floating promise
resolve({ stdout: stdout ?? '', code: err ? 1 : 0 })
},
)

View File

@@ -710,8 +710,8 @@ export const SettingsSchema = lazySchema(() =>
effortLevel: z
.enum(
process.env.USER_TYPE === 'ant'
? ['low', 'medium', 'high', 'max']
: ['low', 'medium', 'high'],
? ['low', 'medium', 'high', 'xhigh', 'max']
: ['low', 'medium', 'high', 'xhigh'],
)
.optional()
.catch(undefined)

View File

@@ -203,7 +203,6 @@ async function getCommandPrefixImpl(
if (nonInteractive) {
process.stderr.write(jsonStringify({ level: 'warn', message }) + '\n')
} else {
// biome-ignore lint/suspicious/noConsole: intentional warning
console.warn(chalk.yellow(`⚠️ ${message}`))
}
},

View File

@@ -6,7 +6,6 @@ import {
fsyncSync,
openSync,
} from 'fs'
// biome-ignore lint: This file IS the cloneDeep wrapper - it must import the original
import lodashCloneDeep from 'lodash-es/cloneDeep.js'
import { addSlowOperation } from '../bootstrap/state.js'
import { logForDebugging } from './debug.js'
@@ -132,6 +131,7 @@ function slowLoggingAnt(
..._values: unknown[]
): AntSlowLogger {
// eslint-disable-next-line prefer-rest-params
// biome-ignore lint/complexity/noArguments: intentional use of arguments object for AntSlowLogger
return new AntSlowLogger(arguments)
}

View File

@@ -793,7 +793,7 @@ function processedStatsToClaudeCodeStats(
hourEntries.length > 0
? parseInt(
hourEntries.reduce((max, [hour, count]) =>
count > parseInt(max[1].toString()) ? [hour, count] : max,
count > parseInt(max[1].toString(), 10) ? [hour, count] : max,
)[0],
10,
)

View File

@@ -5,7 +5,7 @@
* Used by the Teams UI in the footer to show team status.
*/
import { isPaneBackend, type PaneBackendType } from './swarm/backends/types.js'
import { type BackendType } from './swarm/backends/types.js'
import { readTeamFile } from './swarm/teamHelpers.js'
export type TeamSummary = {
@@ -28,7 +28,7 @@ export type TeammateStatus = {
cwd: string
worktreePath?: string
isHidden?: boolean // Whether the pane is currently hidden from the swarm view
backendType?: PaneBackendType // The backend type used for this teammate
backendType?: BackendType // The backend type used for this teammate
mode?: string // Current permission mode for this teammate
}
@@ -67,10 +67,7 @@ export function getTeammateStatuses(teamName: string): TeammateStatus[] {
cwd: member.cwd,
worktreePath: member.worktreePath,
isHidden: hiddenPaneIds.has(member.tmuxPaneId),
backendType:
member.backendType && isPaneBackend(member.backendType)
? member.backendType
: undefined,
backendType: member.backendType,
mode: member.mode,
})
}

View File

@@ -262,7 +262,6 @@ export function waitForTeammatesToBecomeIdle(
const onIdle = (): void => {
remaining--
if (remaining === 0) {
// biome-ignore lint/nursery/noFloatingPromises: resolve is a callback, not a Promise
resolve()
}
}

View File

@@ -132,6 +132,7 @@ async function getOtlpReaders() {
const exportInterval = parseInt(
process.env.OTEL_METRIC_EXPORT_INTERVAL ||
DEFAULT_METRICS_EXPORT_INTERVAL_MS.toString(),
10,
)
const exporters = []
@@ -527,6 +528,7 @@ export async function initializeTelemetry() {
const shutdownTelemetry = async () => {
const timeoutMs = parseInt(
process.env.CLAUDE_CODE_OTEL_SHUTDOWN_TIMEOUT_MS || '2000',
10,
)
try {
endInteractionSpan()
@@ -589,6 +591,7 @@ export async function initializeTelemetry() {
scheduledDelayMillis: parseInt(
process.env.OTEL_LOGS_EXPORT_INTERVAL ||
DEFAULT_LOGS_EXPORT_INTERVAL_MS.toString(),
10,
),
}),
),
@@ -635,6 +638,7 @@ export async function initializeTelemetry() {
scheduledDelayMillis: parseInt(
process.env.OTEL_TRACES_EXPORT_INTERVAL ||
DEFAULT_TRACES_EXPORT_INTERVAL_MS.toString(),
10,
),
}),
)
@@ -654,6 +658,7 @@ export async function initializeTelemetry() {
const shutdownTelemetry = async () => {
const timeoutMs = parseInt(
process.env.CLAUDE_CODE_OTEL_SHUTDOWN_TIMEOUT_MS || '2000',
10,
)
try {
@@ -712,6 +717,7 @@ export async function flushTelemetry(): Promise<void> {
const timeoutMs = parseInt(
process.env.CLAUDE_CODE_OTEL_FLUSH_TIMEOUT_MS || '5000',
10,
)
try {

View File

@@ -118,10 +118,14 @@ export function modelSupportsAdaptiveThinking(model: string): boolean {
}
const canonical = getCanonicalName(model)
// Supported by a subset of Claude 4 models
if (canonical.includes('opus-4-6') || canonical.includes('sonnet-4-6')) {
if (
canonical.includes('opus-4-7') ||
canonical.includes('opus-4-6') ||
canonical.includes('sonnet-4-6')
) {
return true
}
// Exclude any other known legacy models (allowlist above catches 4-6 variants first)
// Exclude any other known legacy models (allowlist above catches 4-6+ variants first)
if (
canonical.includes('opus') ||
canonical.includes('sonnet') ||

View File

@@ -46,7 +46,7 @@ information. Do not blow your cover.
NEVER include in commit messages or PR descriptions:
- Internal model codenames (animal names like Capybara, Tengu, etc.)
- Unreleased model version numbers (e.g., opus-4-7, sonnet-4-8)
- Unreleased model version numbers (e.g., sonnet-4-8)
- Internal repo or project names (e.g., claude-cli-internal, anthropics/…)
- Internal tooling, Slack channels, or short links (e.g., go/cc, #claude-code-…)
- The phrase "Claude Code" or any mention that you are an AI
@@ -64,8 +64,10 @@ GOOD:
BAD (never write these):
- "Fix bug found while testing with Claude Capybara"
- "1-shotted by claude-opus-4-6"
- "1-shotted by claude-opus-4-7"
- "Generated with Claude Code"
- "Co-Authored-By: Claude Opus 4.6 <…>"
- "Co-Authored-By: Claude Opus 4.7 <…>"
`
}
return ''

View File

@@ -99,7 +99,6 @@ export const findGitBashPath = memoize((): string => {
if (checkPathExists(process.env.CLAUDE_CODE_GIT_BASH_PATH)) {
return process.env.CLAUDE_CODE_GIT_BASH_PATH
}
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.error(
`Claude Code was unable to find CLAUDE_CODE_GIT_BASH_PATH path "${process.env.CLAUDE_CODE_GIT_BASH_PATH}"`,
)
@@ -115,7 +114,6 @@ export const findGitBashPath = memoize((): string => {
}
}
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.error(
'Claude Code on Windows requires git-bash (https://git-scm.com/downloads/win). If installed but not in PATH, set environment variable pointing to your bash.exe, similar to: CLAUDE_CODE_GIT_BASH_PATH=C:\\Program Files\\Git\\bin\\bash.exe',
)

160
src/utils/workflowRuns.ts Normal file
View File

@@ -0,0 +1,160 @@
import { readdir, readFile } from 'fs/promises'
import { join } from 'path'
import { getProjectRoot } from '../bootstrap/state.js'
import { safeParseJSON } from './json.js'
const WORKFLOW_RUNS_REL = join('.claude', 'workflow-runs')
const MAX_WORKFLOW_RUNS = 200
const WORKFLOW_RUN_STATUSES = ['running', 'completed', 'cancelled'] as const
const WORKFLOW_STEP_STATUSES = [
'pending',
'running',
'completed',
'cancelled',
] as const
type WorkflowRunStatus = (typeof WORKFLOW_RUN_STATUSES)[number]
type WorkflowStepStatus = (typeof WORKFLOW_STEP_STATUSES)[number]
export type WorkflowRunStepRecord = {
name: string
prompt?: string
status: WorkflowStepStatus
startedAt?: number
completedAt?: number
}
export type WorkflowRunRecord = {
runId: string
workflow: string
args?: string
status: WorkflowRunStatus
createdAt: number
updatedAt: number
currentStepIndex: number
steps: WorkflowRunStepRecord[]
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
function isWorkflowRunStatus(value: unknown): value is WorkflowRunStatus {
return (
typeof value === 'string' &&
WORKFLOW_RUN_STATUSES.includes(value as WorkflowRunStatus)
)
}
function isWorkflowStepStatus(value: unknown): value is WorkflowStepStatus {
return (
typeof value === 'string' &&
WORKFLOW_STEP_STATUSES.includes(value as WorkflowStepStatus)
)
}
function normalizeWorkflowStep(value: unknown): WorkflowRunStepRecord | null {
if (!isRecord(value)) return null
if (typeof value.name !== 'string') return null
if (!isWorkflowStepStatus(value.status)) return null
return {
name: value.name,
...(typeof value.prompt === 'string' ? { prompt: value.prompt } : {}),
status: value.status,
...(typeof value.startedAt === 'number'
? { startedAt: value.startedAt }
: {}),
...(typeof value.completedAt === 'number'
? { completedAt: value.completedAt }
: {}),
}
}
function normalizeWorkflowRun(value: unknown): WorkflowRunRecord | null {
if (!isRecord(value)) return null
if (typeof value.runId !== 'string') return null
if (typeof value.workflow !== 'string') return null
if (!isWorkflowRunStatus(value.status)) return null
if (typeof value.createdAt !== 'number') return null
if (typeof value.updatedAt !== 'number') return null
if (typeof value.currentStepIndex !== 'number') return null
if (!Array.isArray(value.steps)) return null
const steps = value.steps
.map(normalizeWorkflowStep)
.filter((step): step is WorkflowRunStepRecord => step !== null)
if (steps.length !== value.steps.length) return null
return {
runId: value.runId,
workflow: value.workflow,
...(typeof value.args === 'string' ? { args: value.args } : {}),
status: value.status,
createdAt: value.createdAt,
updatedAt: value.updatedAt,
currentStepIndex: value.currentStepIndex,
steps,
}
}
async function readWorkflowRun(
rootDir: string,
runId: string,
): Promise<WorkflowRunRecord | null> {
try {
const parsed = safeParseJSON(
await readFile(
join(rootDir, WORKFLOW_RUNS_REL, `${runId}.json`),
'utf-8',
),
false,
)
return normalizeWorkflowRun(parsed)
} catch {
return null
}
}
export async function listWorkflowRuns(
rootDir: string = getProjectRoot(),
): Promise<WorkflowRunRecord[]> {
let files: string[]
try {
files = await readdir(join(rootDir, WORKFLOW_RUNS_REL))
} catch {
return []
}
const jsonFiles = files.filter(file => file.endsWith('.json'))
const runs = await Promise.all(
jsonFiles
.slice(0, MAX_WORKFLOW_RUNS)
.map(file => readWorkflowRun(rootDir, file.slice(0, -'.json'.length))),
)
return runs
.filter((run): run is WorkflowRunRecord => run !== null)
.sort((a, b) => b.updatedAt - a.updatedAt)
}
export function formatWorkflowRunsStatus(runs: WorkflowRunRecord[]): string {
if (runs.length === 0) {
return ['Workflow runs: 0', ' none'].join('\n')
}
const running = runs.filter(run => run.status === 'running').length
const completed = runs.filter(run => run.status === 'completed').length
const cancelled = runs.filter(run => run.status === 'cancelled').length
const lines = [
`Workflow runs: ${runs.length}`,
` Running: ${running}`,
` Completed: ${completed}`,
` Cancelled: ${cancelled}`,
]
for (const run of runs.slice(0, 10)) {
const currentStep = run.steps[run.currentStepIndex]
lines.push(
` ${run.runId}: ${run.workflow}: ${run.status} step=${currentStep?.name ?? 'none'} updated=${new Date(run.updatedAt).toLocaleString()}`,
)
}
if (runs.length > 10) {
lines.push(` ... ${runs.length - 10} more workflow run(s)`)
}
return lines.join('\n')
}

View File

@@ -1268,7 +1268,6 @@ export async function execIntoTmuxWorktree(args: string[]): Promise<{
}
}
repoName = basename(findCanonicalGitRoot(getCwd()) ?? getCwd())
// biome-ignore lint/suspicious/noConsole: intentional console output
console.log(`Using worktree via hook: ${worktreeDir}`)
} else {
// Get main git repo root (resolves through worktrees)
@@ -1291,7 +1290,6 @@ export async function execIntoTmuxWorktree(args: string[]): Promise<{
prNumber !== null ? { prNumber } : undefined,
)
if (!result.existed) {
// biome-ignore lint/suspicious/noConsole: intentional console output
console.log(
`Created worktree: ${worktreeDir} (based on ${(result as any).baseBranch})`,
)
@@ -1383,7 +1381,6 @@ export async function execIntoTmuxWorktree(args: string[]): Promise<{
// Print hint about iTerm2 preferences when using control mode
if (useControlMode && !sessionExists) {
const y = chalk.yellow
// biome-ignore lint/suspicious/noConsole: intentional user guidance
console.log(
`\n${y('╭─ iTerm2 Tip ────────────────────────────────────────────────────────╮')}\n` +
`${y('│')} To open as a tab instead of a new window: ${y('│')}\n` +