Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
adc7455
feat(usage): add Kiro subscription credits
guanbear Aug 12, 2026
1bc0fa1
fix(usage): distinguish compact Kiro label
guanbear Aug 12, 2026
04eb691
fix(usage): use compact Kiro Q label
guanbear Aug 12, 2026
bba016d
fix(usage): avoid inventing Kiro reset time
guanbear Aug 14, 2026
10affdc
fix(usage): follow split shared UI types
guanbear Aug 14, 2026
136ae26
fix(usage): keep non-date Kiro reset descriptions
guanbear Aug 14, 2026
3fcd012
fix(usage): launch Kiro .cmd shims through cmd.exe
guanbear Aug 14, 2026
9996044
fix(usage): align provider search with current lint gates
guanbear Aug 15, 2026
2118b79
test(usage): mock the Kiro fetcher in the Antigravity service test
guanbear Aug 24, 2026
e685ce3
fix(usage): thread the reset-countdown clock into the Kiro reset label
guanbear Aug 31, 2026
651b4a6
fix(usage): resolve Kiro review findings
guanbear Sep 2, 2026
b28b50e
fix(usage): complete Kiro rebase integration
guanbear Sep 28, 2026
1fcf401
Merge remote-tracking branch 'upstream/main' into codex/pr-14001-repair
guanbear Sep 28, 2026
2a1ab9d
fix(usage): narrow Kiro CLI errors without assertions
guanbear Sep 28, 2026
0149ead
Merge remote-tracking branch 'upstream/main' into codex/pr-14001-repair
guanbear Sep 29, 2026
8b1d748
fix: adapt Kiro usage to latest provider refreshes
guanbear Oct 8, 2026
b9af9b9
fix(kiro): isolate reset label coverage and preserve upstream formatting
guanbear Oct 8, 2026
4a278ea
feat(kiro): report agent status and list Kiro sessions in history
w-felipe360 Oct 8, 2026
08f0a6f
fix(kiro): honor KIRO_HOME for agent hooks and session history
w-felipe360 Oct 8, 2026
e0a9f71
Merge remote-tracking branch 'upstream/main' into feat/kiro-agent-sta…
w-felipe360 Oct 8, 2026
2baadd7
test(usage): mock the Kiro fetcher in the Cursor and OpenCode Go serv…
w-felipe360 Oct 8, 2026
edd4fc3
fix(kiro): install SSH hooks under the remote host's KIRO_HOME
w-felipe360 Oct 8, 2026
1dfca61
fix(kiro): name the actual agents directory in the no-agents hook status
w-felipe360 Oct 8, 2026
9cc7e7c
fix(usage): detect KIRO_CLI_PATH and reject truncated Kiro usage output
w-felipe360 Oct 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions config/tsconfig.cli.json
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,8 @@
"../src/main/jcode/hook-service.ts",
"../src/main/kimi/hook-service.ts",
"../src/main/kimi/kimi-hook-config-toml.ts",
"../src/main/kiro/hook-service.ts",
"../src/main/kiro/hook-settings.ts",
"../src/main/dsh/dsh-home-patch.ts",
"../src/main/dsh/hook-service.ts",
"../src/main/dsh/hook-settings.ts",
Expand Down
15 changes: 15 additions & 0 deletions src/main/agent-hooks/installer-utils-remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,21 @@ export async function readTextFileRemote(
}
}

/** Entry names in a remote directory, or null when the directory does not exist. */
export async function listRemoteDirectory(
sftp: SFTPWrapper,
remotePath: string
): Promise<string[] | null> {
try {
return (await readdir(sftp, remotePath)).map((entry) => entry.filename)
} catch (err) {
if (isNoEntryError(err)) {
return null
}
throw err
}
}

export async function writeTextFileRemoteAtomic(
sftp: SFTPWrapper,
remotePath: string,
Expand Down
13 changes: 9 additions & 4 deletions src/main/agent-hooks/managed-agent-hook-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { grokHookService } from '../grok/hook-service'
import { hermesHookService } from '../hermes/hook-service'
import { jcodeHookService } from '../jcode/hook-service'
import { kimiHookService } from '../kimi/hook-service'
import { kiroHookService } from '../kiro/hook-service'
import { museHookService } from '../muse/hook-service'
import { openClaudeHookService } from '../openclaude/hook-service'
import { zcodeHookService } from '../zcode/hook-service'
Expand Down Expand Up @@ -65,7 +66,8 @@ export const MANAGED_AGENT_HOOK_INSTALLERS: readonly ManagedAgentHookInstaller[]
['muse', () => museHookService.install()],
['zcode', () => zcodeHookService.install()],
['dsh', () => dshHookService.install()],
['jcode', () => jcodeHookService.install()]
['jcode', () => jcodeHookService.install()],
['kiro', () => kiroHookService.install()]
]

// Why: covers the shared launcher/statusline scripts under ~/.orca/agent-hooks — the files a
Expand Down Expand Up @@ -94,7 +96,8 @@ export const MANAGED_AGENT_HOOK_SCRIPT_REFRESHERS: readonly ManagedAgentHookScri
['muse', () => museHookService.refreshManagedScripts()],
['zcode', () => zcodeHookService.refreshManagedScripts()],
['dsh', () => dshHookService.refreshManagedScripts()],
['jcode', () => jcodeHookService.refreshManagedScripts()]
['jcode', () => jcodeHookService.refreshManagedScripts()],
['kiro', () => kiroHookService.refreshManagedScripts()]
]

export const MANAGED_AGENT_HOOK_REMOVERS: readonly ManagedAgentHookRemover[] = [
Expand All @@ -119,7 +122,8 @@ export const MANAGED_AGENT_HOOK_REMOVERS: readonly ManagedAgentHookRemover[] = [
['muse', () => museHookService.remove()],
['zcode', () => zcodeHookService.remove()],
['dsh', () => dshHookService.remove()],
['jcode', () => jcodeHookService.remove()]
['jcode', () => jcodeHookService.remove()],
['kiro', () => kiroHookService.remove()]
]

export const MANAGED_AGENT_HOOK_ASYNC_REMOVERS: readonly ManagedAgentHookAsyncRemover[] = [
Expand Down Expand Up @@ -148,5 +152,6 @@ export const MANAGED_AGENT_HOOK_STATUS_READERS: readonly ManagedAgentHookStatusR
['muse', () => museHookService.getStatus()],
['zcode', () => zcodeHookService.getStatus()],
['dsh', () => dshHookService.getStatus()],
['jcode', () => jcodeHookService.getStatus()]
['jcode', () => jcodeHookService.getStatus()],
['kiro', () => kiroHookService.getStatus()]
]
8 changes: 8 additions & 0 deletions src/main/agent-hooks/managed-hook-command-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { getMuseManagedCommand, getMuseRemoteManagedCommand } from '../muse/hook
import { getDshManagedCommand, getDshRemoteManagedCommand } from '../dsh/hook-settings'
import { getZCodeManagedCommand, getZCodeRemoteManagedCommand } from '../zcode/hook-settings'
import { getJcodeManagedCommand, getJcodeRemoteManagedCommand } from '../jcode/hook-settings'
import { getKiroManagedCommand, getKiroRemoteManagedCommand } from '../kiro/hook-settings'
import {
wrapPosixHookCommand,
wrapWindowsCmdHookCommand,
Expand Down Expand Up @@ -227,6 +228,13 @@ const buildersByAgent = new Map<string, CommandBuilders>([
local: (path) => [getJcodeManagedCommand(path)],
remote: (path) => [getJcodeRemoteManagedCommand(path)]
}
],
[
'kiro',
{
local: (path) => [getKiroManagedCommand(path)],
remote: (path) => [getKiroRemoteManagedCommand(path)]
}
]
])

Expand Down
14 changes: 10 additions & 4 deletions src/main/agent-hooks/managed-hook-local-filesystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,16 @@ export function createManagedHookLocalFilesystem(): SFTPWrapper {
stat(path: string, callback: Callback<{ mode: number }>): void {
stat(path, (error, stats) => finish(callback, error, stats))
},
readdir(path: string, callback: Callback<[]>): void {
// Why: installers use readdir only as an existence check; names and
// attrs would allocate work that no caller consumes.
readdir(path, (error) => finish(callback, error, []))
readdir(path: string, callback: Callback<{ filename: string }[]>): void {
// Why names only: Kiro lists its agent configs by name; attrs have no consumer,
// matching the WSL adapter's `{ filename }` entries.
readdir(path, (error, names) =>
finish(
callback,
error,
names?.map((filename) => ({ filename }))
)
)
},
mkdir(path: string, callback: Callback): void {
mkdir(path, (error) => finish(callback, error))
Expand Down
22 changes: 21 additions & 1 deletion src/main/agent-hooks/managed-hook-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ const { execFile } = await import('node:child_process')
const execFileMock = vi.mocked(execFile)
const { execFile: actualExecFile } =
await vi.importActual<typeof NodeChildProcess>('node:child_process')
const { installManagedHooks, resolveRelayGrokHome } = await import('./managed-hook-runtime')
const { installManagedHooks, resolveRelayGrokHome, resolveRelayKiroHome } =
await import('./managed-hook-runtime')

type ExecFileCallback = (error: Error | null, result?: { stdout: string; stderr: string }) => void

Expand Down Expand Up @@ -120,6 +121,25 @@ describe.runIf(process.platform !== 'win32')('resolveRelayGrokHome', () => {
})
})

describe.runIf(process.platform !== 'win32')('resolveRelayKiroHome', () => {
it('reads KIRO_HOME from the login shell', async () => {
vi.stubEnv('SHELL', '/bin/sh')
stubProbeOutput('/srv/kiro/\n')

await expect(resolveRelayKiroHome('/home/orca')).resolves.toBe('/srv/kiro')
expect(execFileMock.mock.calls[0]?.[1]?.[1]).toContain('printenv KIRO_HOME')
})

it('falls back to ~/.kiro when KIRO_HOME is unset or the probe fails', async () => {
vi.stubEnv('SHELL', '/bin/sh')
stubProbeOutput('\n')
await expect(resolveRelayKiroHome('/home/orca')).resolves.toBe('/home/orca/.kiro')

stubProbeFailure(new Error('spawn failed'))
await expect(resolveRelayKiroHome('/home/orca')).resolves.toBe('/home/orca/.kiro')
})
})

describe.runIf(process.platform !== 'win32')('installManagedHooks', () => {
it.each([
['omitted', undefined],
Expand Down
45 changes: 30 additions & 15 deletions src/main/agent-hooks/managed-hook-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,16 @@ import {
} from './managed-hook-owner-identity'

const execFileAsync = promisify(execFile)
const GROK_HOME_MAX_LENGTH = 4096
const GROK_HOME_PROBE_TIMEOUT_MS = 8_000
const AGENT_HOME_MAX_LENGTH = 4096
const AGENT_HOME_PROBE_TIMEOUT_MS = 8_000

export type ManagedHookInstallSummary = {
installers: number
errors: number
}

function defaultGrokHome(home: string): string {
return `${home.replace(/\/+$/, '') || home}/.grok`
function defaultAgentHome(home: string, dirName: string): string {
return `${home.replace(/\/+$/, '') || home}/${dirName}`
}

function hasControlCharacter(value: string): boolean {
Expand All @@ -31,10 +31,10 @@ function hasControlCharacter(value: string): boolean {
})
}

function normalizeGrokHome(candidate: string): string | null {
function normalizeAgentHome(candidate: string): string | null {
if (
candidate.length === 0 ||
candidate.length > GROK_HOME_MAX_LENGTH ||
candidate.length > AGENT_HOME_MAX_LENGTH ||
candidate !== candidate.trim() ||
!candidate.startsWith('/') ||
candidate.includes('\\') ||
Expand All @@ -53,26 +53,38 @@ function resolveLoginShell(): string {
return candidate
}

export async function resolveRelayGrokHome(home: string, signal?: AbortSignal): Promise<string> {
const fallback = defaultGrokHome(home)
async function resolveRelayAgentHome(
envName: 'GROK_HOME' | 'KIRO_HOME',
fallback: string,
signal?: AbortSignal
): Promise<string> {
try {
const shell = resolveLoginShell()
const shellName = basename(shell)
const mode = shellName === 'sh' || shellName === 'dash' ? '-c' : '-lc'
// Why: agent PTYs start login shells, so read the same profile-derived
// GROK_HOME without opening two additional SSH exec channels.
// home override without opening two additional SSH exec channels.
const { stdout } = await execFileAsync(
shell,
[mode, `printenv GROK_HOME | head -c ${GROK_HOME_MAX_LENGTH + 1}`],
{ encoding: 'utf8', timeout: GROK_HOME_PROBE_TIMEOUT_MS, signal }
[mode, `printenv ${envName} | head -c ${AGENT_HOME_MAX_LENGTH + 1}`],
{ encoding: 'utf8', timeout: AGENT_HOME_PROBE_TIMEOUT_MS, signal }
)
return normalizeGrokHome(stdout.split(/\r?\n/, 1)[0] ?? '') ?? fallback
return normalizeAgentHome(stdout.split(/\r?\n/, 1)[0] ?? '') ?? fallback
} catch {
signal?.throwIfAborted()
return fallback
}
}

export function resolveRelayGrokHome(home: string, signal?: AbortSignal): Promise<string> {
return resolveRelayAgentHome('GROK_HOME', defaultAgentHome(home, '.grok'), signal)
}

/** `$KIRO_HOME` replaces `~/.kiro` outright, for agent configs and sessions alike. */
export function resolveRelayKiroHome(home: string, signal?: AbortSignal): Promise<string> {
return resolveRelayAgentHome('KIRO_HOME', defaultAgentHome(home, '.kiro'), signal)
}

export async function installManagedHooks(options?: {
signal?: AbortSignal
hostKeyFingerprint?: string
Expand All @@ -86,9 +98,11 @@ export async function installManagedHooks(options?: {
return { installers: 0, errors: 0 }
}
const home = homedir()
const grokHomeDir = agents.includes('grok')
? await resolveRelayGrokHome(home, options?.signal)
: undefined
// Why parallel: each probe may wait out its own login-shell timeout.
const [grokHomeDir, kiroHomeDir] = await Promise.all([
agents.includes('grok') ? resolveRelayGrokHome(home, options?.signal) : undefined,
agents.includes('kiro') ? resolveRelayKiroHome(home, options?.signal) : undefined
])
options?.signal?.throwIfAborted()
const hostIdentity = scopeManagedHookHostIdentity(
await readManagedHookHostIdentity(),
Expand All @@ -103,6 +117,7 @@ export async function installManagedHooks(options?: {
home,
{
grokHomeDir,
kiroHomeDir,
signal: options?.signal,
agents,
...(options?.claudeVersion ? { claudeVersion: options.claudeVersion } : {})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { grokHookService } from '../grok/hook-service'
import { hermesHookService } from '../hermes/hook-service'
import { kimiHookService } from '../kimi/hook-service'
import { dshHookService } from '../dsh/hook-service'
import { kiroHookService } from '../kiro/hook-service'
import { museHookService } from '../muse/hook-service'
import { openClaudeHookService } from '../openclaude/hook-service'
import { qwenCodeHookService } from '../qwen-code/hook-service'
Expand Down Expand Up @@ -61,6 +62,7 @@ describe('remote hook service registry coverage', () => {
['codebuddy', codebuddyHookService],
['zcode', zcodeHookService],
['dsh', dshHookService],
['kiro', kiroHookService],
['jcode', jcodeHookService]
])

Expand Down
10 changes: 9 additions & 1 deletion src/main/agent-hooks/remote-managed-hook-installers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { grokHookService } from '../grok/hook-service'
import { hermesHookService } from '../hermes/hook-service'
import { jcodeHookService } from '../jcode/hook-service'
import { kimiHookService } from '../kimi/hook-service'
import { kiroHookService } from '../kiro/hook-service'
import { dshHookService } from '../dsh/hook-service'
import { museHookService } from '../muse/hook-service'
import { zcodeHookService } from '../zcode/hook-service'
Expand All @@ -29,6 +30,8 @@ export type RemoteManagedHookInstallOptions = {
deferTrustUntilConfigToml?: boolean
/** Explicit GROK_HOME for remote runtimes that redirect Grok's config. */
grokHomeDir?: string
/** Login-shell KIRO_HOME on the execution host; it replaces ~/.kiro outright. */
kiroHomeDir?: string
/** Version reported by Claude on this execution host. */
claudeVersion?: string
/** Stops before starting the next installer when the owning relay request
Expand Down Expand Up @@ -87,7 +90,12 @@ const REMOTE_MANAGED_HOOK_INSTALLERS: readonly RemoteManagedHookInstaller[] = [
['muse', (sftp, remoteHome) => museHookService.installRemote(sftp, remoteHome)],
['zcode', (sftp, remoteHome) => zcodeHookService.installRemote(sftp, remoteHome)],
['dsh', (sftp, remoteHome) => dshHookService.installRemote(sftp, remoteHome)],
['jcode', (sftp, remoteHome) => jcodeHookService.installRemote(sftp, remoteHome)]
['jcode', (sftp, remoteHome) => jcodeHookService.installRemote(sftp, remoteHome)],
[
'kiro',
(sftp, remoteHome, options) =>
kiroHookService.installRemote(sftp, remoteHome, options?.kiroHomeDir)
]
]

/** Agents wired into the remote (SSH) hook installer. Exported so an invariant
Expand Down
3 changes: 2 additions & 1 deletion src/main/agent-hooks/server-retired-pane-new-turn.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ const NEW_TURN_EVENT: Record<AgentHookSource, string | null> = {
muse: 'UserPromptSubmit',
zcode: 'SessionStart',
dsh: 'SessionStart',
jcode: 'turn_start'
jcode: 'turn_start',
kiro: 'agentSpawn'
}

function reviveRetiredPane(source: unknown, hookEventName: string): boolean {
Expand Down
70 changes: 70 additions & 0 deletions src/main/ai-vault/remote-session-scanner-kiro-source.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import type { RemoteHostPlatform } from '../ssh/ssh-remote-platform'
import { joinRemotePath } from '../ssh/ssh-remote-platform'
import { throwIfAiVaultScanCancelled } from './ai-vault-scan-cancellation'
import { isMissingRemoteSessionPathError } from './remote-session-file-stat'
import {
remoteSessionContentLines,
streamedSessionContentLines
} from './remote-session-content-lines'
import type { RemoteScannerContext, RemoteSessionSource } from './remote-session-scanner-types'
import {
isKiroSessionMetadataPath,
kiroTranscriptPathForMetadata,
parseKiroSessionContent
} from './session-scanner-kiro-parser'

async function readTranscriptLines(
metadataPath: string,
context: RemoteScannerContext
): Promise<Iterable<string> | AsyncIterable<string> | null> {
const transcriptPath = kiroTranscriptPathForMetadata(metadataPath)
// Why: beside the execution host the transcript streams; megabyte tool outputs are common.
if (context.provider.readTranscriptBytes) {
return streamedSessionContentLines(
context.provider.readTranscriptBytes(transcriptPath, context.signal),
context.signal
)
}
try {
throwIfAiVaultScanCancelled(context.signal)
const read = await context.provider.readFile(transcriptPath)
throwIfAiVaultScanCancelled(context.signal)
return read.isBinary ? null : remoteSessionContentLines(read.content, context.signal)
} catch (error) {
throwIfAiVaultScanCancelled(context.signal)
if (isMissingRemoteSessionPathError(error)) {
return null
}
throw error
}
}

export function remoteKiroSource(
remoteHome: string,
hostPlatform: RemoteHostPlatform
): RemoteSessionSource {
return {
agent: 'kiro',
rootDir: joinRemotePath(hostPlatform, remoteHome, '.kiro', 'sessions', 'cli'),
extensions: ['.json'],
filePredicate: isKiroSessionMetadataPath,
contentDependencyPath: kiroTranscriptPathForMetadata,
directoryPredicate: () => false,
parse: async (file, content, context) => {
const options = {
executionHostId: context.executionHostId,
executionHostPlatform: context.hostPlatform.os
}
try {
const lines = await readTranscriptLines(file.path, context)
return await parseKiroSessionContent(file, content, lines, context.hostPlatform.os, options)
} catch (error) {
// A transcript that vanished mid-stream leaves a valid metadata-only session.
if (!isMissingRemoteSessionPathError(error)) {
throw error
}
return parseKiroSessionContent(file, content, null, context.hostPlatform.os, options)
}
}
}
}
Loading
Loading