Files
orca/src/main/copilot/copilot-managed-hook-definitions.ts
T
Neil bc28107864 refactor(hooks,relay): split agent hook services and relay under the max-lines budget (#14725)
The four agent hook services, the main hooks module, and the two relay modules
each carried a file-level `eslint-disable max-lines` and ran 365-628 counted
lines against a 300-line budget. AGENTS.md calls for splitting rather than
suppressing, and config/max-lines-baseline.txt is a shrink-only ratchet, so this
removes all seven suppressions and prunes their entries (341 -> 334).

Pure move, no behavior change. Each hook service splits into its managed script
source, its config/bundle serialization, and its remote-install path, keeping the
per-agent integrations independent: copilot, amp, antigravity and hermes each
retain their own getManagedScript rather than sharing one, because each emits a
different script body for a different agent. Merging them by name would have
been a behavior change, not a refactor.

For antigravity the suppression's stated rationale -- that local install, Windows
wrapper generation, status cleanup, and SSH remote install must share one event
list and managed-command matcher so stale-hook cleanup cannot drift by platform
-- is now enforced structurally instead: both install paths call
buildInstalledConfig + createAntigravityManagedCommandMatcher over the single
ANTIGRAVITY_EVENTS catalog, with the graph a strict DAG.

Also registers the six new antigravity/ and copilot/ modules in
config/tsconfig.cli.json. That project uses a curated `include` list rather than
a glob, so an unlisted module fails `tsc -p config/tsconfig.tc.cli.json` with
TS6307 even though the entire unit suite passes.

Verified: oxlint clean, ratchet passes, typecheck clean, full unit suite green
(remaining failures are pre-existing load flakes in untouched files, green when
re-run serially), no new runtime import cycles, and no lint suppression added.
2026-08-15 18:25:37 -07:00

74 lines
2.4 KiB
TypeScript

import {
wrapPosixHookCommand,
wrapWindowsHookCommand,
type HookDefinition
} from '../agent-hooks/installer-utils'
// Why: Copilot's user-level hook files can use VS Code-compatible PascalCase
// names, which match the event vocabulary already normalized by Orca's hook
// server and avoid wrapper-side event remapping.
export const COPILOT_EVENTS = [
'SessionStart',
'SessionEnd',
'UserPromptSubmit',
'PreToolUse',
'PostToolUse',
'PostToolUseFailure',
// Why: GitHub's current reference documents subagentStart with only the
// camelCase payload shape. The wrapper passes the event name separately, so
// Orca can normalize it without depending on a PascalCase payload.
'subagentStart',
'SubagentStop',
'PreCompact',
'Stop',
'ErrorOccurred',
'PermissionRequest',
'Notification'
] as const
export function getManagedCommand(scriptPath: string, eventName: string): string {
if (process.platform !== 'win32') {
return wrapPosixHookCommand(scriptPath, { ORCA_COPILOT_HOOK_EVENT: eventName })
}
return wrapWindowsHookCommand(scriptPath, { ORCA_COPILOT_HOOK_EVENT: eventName })
}
export function getManagedHookDefinition(command: string): HookDefinition {
return process.platform === 'win32'
? { type: 'command', powershell: command, timeoutSec: 5 }
: { type: 'command', bash: command, timeoutSec: 5 }
}
export function definitionHasCurrentCommand(definition: HookDefinition, command: string): boolean {
return (
definition.command === command ||
definition.bash === command ||
definition.powershell === command ||
(Array.isArray(definition.hooks) && definition.hooks.some((hook) => hook.command === command))
)
}
export function definitionHasStaleManagedCommand(
definition: HookDefinition,
currentCommand: string | null,
isManagedCommand: (command: string | undefined) => boolean
): boolean {
const commands = [definition.command, definition.bash, definition.powershell]
if (commands.some((command) => isManagedCommand(command) && command !== currentCommand)) {
return true
}
return (
Array.isArray(definition.hooks) &&
definition.hooks.some(
(hook) => isManagedCommand(hook.command) && hook.command !== currentCommand
)
)
}
export function definitionsChanged(before: HookDefinition[], after: HookDefinition[]): boolean {
return (
before.length !== after.length ||
after.some((definition, index) => definition !== before[index])
)
}