fix(kiro): report status on Kiro's V3 engine, including the built-in default agent (#33660)

Kiro's V3 engine (default from Kiro CLI 3.0) now reports status in Orca: Orca installs a global hooks file under KIRO_HOME/hooks (covering the built-in default agent, local and remote, never overwriting user files) and recognizes V3's PascalCase hook events.

Fixes #31577
Supersedes #24937

Co-authored-by: Dongho Lee <126236161+LDH1103@users.noreply.github.com>
This commit is contained in:
Neil
2026-10-25 16:07:05 -07:00
committed by GitHub
co-authored by Dongho Lee
parent d9a7aa89c8
commit 12cc6c5fff
11 changed files with 531 additions and 27 deletions
@@ -84,7 +84,11 @@ export function isNewTurnEvent(source: AgentHookSource, eventName: unknown): boo
return eventName === 'turn_start'
case 'kiro':
// Why: agentSpawn opens a fresh session in the pane, so stale tool/prompt caches go too.
return eventName === 'agentSpawn' || eventName === 'userPromptSubmit'
return (
eventName === 'agentSpawn' ||
eventName === 'userPromptSubmit' ||
eventName === 'UserPromptSubmit'
)
}
}
@@ -1,6 +1,7 @@
import { beforeEach, describe, expect, it } from 'vitest'
import { createHookListenerState, type HookListenerState } from '../listener-state'
import { normalizeAndAccept } from '../../agent-hook-listener-test-harness'
import { isNewTurnEvent } from '../provider-event-routing'
// Payload shapes copied from a live kiro-cli 2.28 TUI run with hooks on a custom agent.
const base = { cwd: '/tmp/ws', session_id: 'f6fe70d7-a1b2-4c3d-9e8f-001122334455' }
@@ -66,7 +67,81 @@ describe('normalizeKiroEvent', () => {
expect(post?.payload.state).toBe('working')
})
it('ignores event names Kiro does not document', () => {
expect(normalizeAndAccept(state, 'kiro', event('Stop'))).toBeNull()
it("ignores names neither engine fires, and V3's lazy SessionStart", () => {
// Why: Kiro's docs spell V3 triggers PromptSubmit/AgentStop, but 2.27 never fires them.
for (const name of ['PromptSubmit', 'AgentStop', 'SessionStart', 'SomethingElse']) {
expect(normalizeAndAccept(state, 'kiro', event(name))).toBeNull()
}
})
})
// Shapes captured from kiro-cli 2.27.0 (`chat --tui --v3`) running Orca's standalone
// `~/.kiro/hooks/*.json` file: Claude's PascalCase names, a `sess_` id, and no final reply.
const V3_SESSION_ID = 'sess_966a9c85-10a0-4a38-8fd0-91dbeffb032b'
function v3Event(name: string, extra: Record<string, unknown> = {}): Record<string, unknown> {
return {
session_id: V3_SESSION_ID,
hook_event_name: name,
cwd: 'C:\\Users\\dev\\project',
...extra
}
}
describe('normalizeKiroEvent with the V3 engine', () => {
let state: HookListenerState
beforeEach(() => {
state = createHookListenerState()
})
it('opens a turn on UserPromptSubmit, not on the SessionStart just before it', () => {
expect(isNewTurnEvent('kiro', 'UserPromptSubmit')).toBe(true)
expect(isNewTurnEvent('kiro', 'SessionStart')).toBe(false)
expect(normalizeAndAccept(state, 'kiro', v3Event('SessionStart'))).toBeNull()
const submitted = normalizeAndAccept(
state,
'kiro',
v3Event('UserPromptSubmit', { prompt: 'Run the shell command: echo probe-ok' })
)
expect(submitted?.payload).toMatchObject({
state: 'working',
agentType: 'kiro',
prompt: 'Run the shell command: echo probe-ok'
})
})
it('names the running tool and keeps a string tool_response as tool output', () => {
normalizeAndAccept(state, 'kiro', v3Event('UserPromptSubmit', { prompt: 'echo it' }))
const pre = normalizeAndAccept(
state,
'kiro',
v3Event('PreToolUse', { tool_name: 'execute_pwsh', tool_input: { command: 'echo probe-ok' } })
)
expect(pre?.payload).toMatchObject({ state: 'working', toolName: 'execute_pwsh' })
const post = normalizeAndAccept(
state,
'kiro',
v3Event('PostToolUse', {
tool_name: 'execute_pwsh',
tool_input: { command: 'echo probe-ok' },
tool_response: 'probe-ok'
})
)
expect(post?.payload).toMatchObject({
state: 'working',
lastAssistantMessage: 'probe-ok',
lastAssistantMessageIsToolOutput: true
})
})
it('reports done on Stop and SessionEnd, which carry no final reply', () => {
normalizeAndAccept(state, 'kiro', v3Event('UserPromptSubmit', { prompt: 'finish' }))
for (const name of ['Stop', 'SessionEnd']) {
const event = normalizeAndAccept(state, 'kiro', v3Event(name))
expect(event?.payload).toMatchObject({ state: 'done', agentType: 'kiro', prompt: 'finish' })
}
})
})
@@ -19,12 +19,18 @@ export function normalizeKiroEvent(
case 'userPromptSubmit':
case 'preToolUse':
case 'postToolUse':
case 'UserPromptSubmit':
case 'PreToolUse':
case 'PostToolUse':
stateName = 'working'
break
case 'agentSpawn':
case 'stop':
case 'Stop':
case 'SessionEnd':
stateName = 'done'
break
// V3 fires SessionStart lazily, just before the first prompt; done would flash idle mid-turn.
default:
return null
}
@@ -2,13 +2,17 @@ import type { ToolSnapshot } from '../listener-event'
import { readString } from '../tool-input-preview'
import { extractClaudeToolFields } from './claude-tool-fields'
/** Kiro's camelCase hook names mapped onto the Claude names its payload fields mirror. */
/** Kiro V2 camelCase hook names mapped onto Claude's; V3 already sends Claude's names. */
export const KIRO_CLAUDE_EVENT_NAMES: Readonly<Record<string, string>> = Object.freeze({
agentSpawn: 'SessionStart',
userPromptSubmit: 'UserPromptSubmit',
preToolUse: 'PreToolUse',
postToolUse: 'PostToolUse',
stop: 'Stop'
stop: 'Stop',
UserPromptSubmit: 'UserPromptSubmit',
PreToolUse: 'PreToolUse',
PostToolUse: 'PostToolUse',
Stop: 'Stop'
})
/** Kiro keeps Claude tool fields, maps camelCase events and sends final text in assistant_response. */