Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion .github/workflows/e2e-isolated.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:
required: true
default: "gemini-cli"
type: choice
options: [claude-code, opencode, gemini-cli, factoryai-droid]
options: [claude-code, opencode, gemini-cli, factoryai-droid, kiro]
test:
description: "Test name filter (regex)"
required: true
Expand Down Expand Up @@ -38,6 +38,7 @@ jobs:
claude-code) curl -fsSL https://claude.ai/install.sh | bash ;;
opencode) curl -fsSL https://opencode.ai/install | bash ;;
gemini-cli) npm install -g @google/gemini-cli ;;
kiro) curl -fsSL https://cli.kiro.dev/install | bash ;;
factoryai-droid) curl -fsSL https://app.factory.ai/cli | sh ;;
esac
echo "$HOME/.local/bin" >> $GITHUB_PATH
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
strategy:
fail-fast: false
matrix:
agent: [claude-code, opencode, gemini-cli, factoryai-droid]
agent: [claude-code, opencode, gemini-cli, factoryai-droid, kiro]

steps:
- name: Checkout repository
Expand All @@ -36,6 +36,7 @@ jobs:
claude-code) curl -fsSL https://claude.ai/install.sh | bash ;;
opencode) curl -fsSL https://opencode.ai/install | bash ;;
gemini-cli) npm install -g @google/gemini-cli ;;
kiro) curl -fsSL https://cli.kiro.dev/install | bash ;;
factoryai-droid) curl -fsSL https://app.factory.ai/cli | sh ;;
esac
echo "$HOME/.local/bin" >> $GITHUB_PATH
Comment on lines 20 to 42
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding kiro to the E2E matrix will cause the main-branch workflow to run Kiro E2E on every push. The Kiro runner’s Bootstrap() fails on CI when not authenticated (kiro-cli whoami), and this workflow doesn’t provide any Kiro credentials/env to log in, so the kiro job is likely to fail consistently. Consider gating the kiro matrix entry behind a repo secret/variable (or making the job conditional), and/or setting E2E_AGENT=${{ matrix.agent }} for the bootstrap step so bootstrap only runs for the selected agent.

Copilot uses AI. Check for mistakes.
Expand Down
132 changes: 132 additions & 0 deletions cmd/entire/cli/agent/kiro/AGENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# Kiro Agent Integration

Implementation one-pager for the Kiro (Amazon AI coding CLI) agent integration.

## Identity

| Field | Value |
|-------|-------|
| Package | `kiro` |
| Registry Key | `kiro` |
| Agent Type | `Kiro` |
| Binary | `kiro-cli` |
| Preview | Yes |
| Protected Dir | `.kiro` |

## Hook Events (5 total)

| Kiro Hook (camelCase) | CLI Subcommand (kebab-case) | EventType | Notes |
|----------------------|----------------------------|-----------|-------|
| `agentSpawn` | `agent-spawn` | `SessionStart` | Agent initializes |
| `userPromptSubmit` | `user-prompt-submit` | `TurnStart` | stdin includes `prompt` field |
| `preToolUse` | `pre-tool-use` | `nil, nil` | Pass-through |
| `postToolUse` | `post-tool-use` | `nil, nil` | Pass-through |
| `stop` | `stop` | `TurnEnd` | Checkpoint trigger |

No `SessionEnd` hook exists — sessions end implicitly (similar to Cursor).

## Hook Configuration

**File:** `.kiro/agents/entire.json`

We own the entire file — no round-trip preservation needed (unlike Cursor's shared `hooks.json`).

Comment on lines +28 to +33
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description mentions Kiro hook installation via .kiro/settings/hooks.json, but the integration (and this doc) uses .kiro/agents/entire.json. Please reconcile this so the PR description matches the actual implementation (or adjust the implementation if .kiro/settings/hooks.json is the intended target).

Copilot uses AI. Check for mistakes.
**Format:**
```json
{
"name": "entire",
"tools": ["read", "write", "shell", "grep", "glob", "aws", "report",
"introspect", "knowledge", "thinking", "todo", "delegate"],
"hooks": {
"agentSpawn": [{"command": "entire hooks kiro agent-spawn"}],
"userPromptSubmit": [{"command": "entire hooks kiro user-prompt-submit"}],
"preToolUse": [{"command": "entire hooks kiro pre-tool-use"}],
"postToolUse": [{"command": "entire hooks kiro post-tool-use"}],
"stop": [{"command": "entire hooks kiro stop"}]
}
}
```

Note: The file is a Kiro agent definition. Hooks must be nested under the `hooks` field.
Required top-level fields: `name`. Optional: `$schema`, `description`, `prompt`, `mcpServers`,
`tools`, `toolAliases`, `allowedTools`, `resources`, `hooks`, `toolsSettings`, `model`, etc.

**Important:** The `tools` array must include all default Kiro tools. Without it, `--agent entire`
restricts the model to zero tools. The tool names come from `~/.kiro/agents/agent_config.json.example`.

## Agent Activation

Hooks only fire when `--agent entire` is passed to `kiro-cli chat`. Without this flag,
`.kiro/agents/entire.json` is not loaded and hooks do not execute.

**`--no-interactive` mode:** Does not fire agent hooks. All E2E tests use interactive (tmux) mode.

**TUI prompt indicator:** `!>` in trust-all mode (with `-a` flag). The `Credits:` line
appears after each agent response and serves as a reliable completion marker.

## Hook Stdin Format

All hooks receive the same JSON structure on stdin:
```json
{
"hook_event_name": "userPromptSubmit",
"cwd": "/path/to/repo",
"prompt": "user message",
"tool_name": "fs_write",
"tool_input": "...",
"tool_response": "..."
}
```

Fields are populated based on the hook event — `prompt` only for `userPromptSubmit`, tool fields only for tool hooks.

## Transcript Storage

**Source:** SQLite database at `~/Library/Application Support/kiro-cli/data.sqlite3` (macOS)
or `~/.local/share/kiro-cli/data.sqlite3` (Linux).

**Table:** `conversations_v2`
- `key` column: CWD path (used for lookup)
- `value` column: JSON blob with conversation data
- `updated_at` column: timestamp for ordering

**Conversation JSON structure:**
```json
{
"conversation_id": "uuid-v4",
"history": [
{"role": "user", "content": [{"type": "text", "text": "..."}]},
{"role": "assistant", "content": [{"type": "text", "text": "..."}, {"type": "tool_use", "name": "fs_write", "input": {...}}]},
{"role": "request_metadata", "input_tokens": 150, "output_tokens": 80}
]
}
```

## Session ID Discovery

Hook stdin does not include session ID. We query SQLite by CWD:
```sql
SELECT json_extract(value, '$.conversation_id')
FROM conversations_v2
WHERE key = '<cwd>'
ORDER BY updated_at DESC LIMIT 1
```

## SQLite Access Strategy

Uses `sqlite3` CLI (pre-installed on macOS/Linux) rather than a Go library to avoid CGO dependencies. Same approach as OpenCode's `opencode export` CLI.

**Test mock:** `ENTIRE_TEST_KIRO_MOCK_DB=1` env var causes the agent to skip SQLite queries and use pre-written mock files.

## File Modification Tools

Tools that modify files on disk:
- `fs_write` — write file content
- `str_replace` — string replacement in files
- `create_file` — create new files
- `write_file` — write/overwrite files
- `edit_file` — edit existing files

## Caching

Transcript data is cached to `.entire/tmp/<sessionID>.json` (same pattern as OpenCode).
183 changes: 183 additions & 0 deletions cmd/entire/cli/agent/kiro/hooks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package kiro

import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"

"github.com/entireio/cli/cmd/entire/cli/agent"
"github.com/entireio/cli/cmd/entire/cli/jsonutil"
"github.com/entireio/cli/cmd/entire/cli/paths"
)

// Ensure KiroAgent implements HookSupport
var _ agent.HookSupport = (*KiroAgent)(nil)

// HooksFileName is the config file for Kiro hooks.
const HooksFileName = "entire.json"

// hooksDir is the directory within .kiro where agent hook configs live.
const hooksDir = "agents"

// localDevCmdPrefix is the command prefix used for local development builds.
const localDevCmdPrefix = "go run ${KIRO_PROJECT_DIR}/cmd/entire/main.go "

// entireHookPrefixes identify Entire hooks in the config file.
var entireHookPrefixes = []string{
"entire ",
localDevCmdPrefix,
}

// InstallHooks installs Entire hooks in .kiro/agents/entire.json.
// Since Entire owns this file entirely, we write it from scratch each time.
// Returns the number of hooks installed.
func (k *KiroAgent) InstallHooks(ctx context.Context, localDev bool, force bool) (int, error) {
worktreeRoot, err := paths.WorktreeRoot(ctx)
if err != nil {
worktreeRoot = "."
}

hooksPath := filepath.Join(worktreeRoot, ".kiro", hooksDir, HooksFileName)

// If hooks are already installed and not forcing, check if they're current
if !force {
if existing, readErr := os.ReadFile(hooksPath); readErr == nil { //nolint:gosec // path constructed from repo root
var file kiroAgentFile
if json.Unmarshal(existing, &file) == nil && allHooksPresent(file.Hooks, localDev) {
return 0, nil
}
}
}

var cmdPrefix string
if localDev {
cmdPrefix = localDevCmdPrefix + "hooks kiro "
} else {
cmdPrefix = "entire hooks kiro "
}

file := kiroAgentFile{
Name: "entire",
// Include all default Kiro tools so the agent profile doesn't restrict them.
Tools: []string{
"read", "write", "shell", "grep", "glob",
"aws", "report", "introspect", "knowledge",
"thinking", "todo", "delegate",
},
Hooks: kiroHooks{
AgentSpawn: []kiroHookEntry{{Command: cmdPrefix + HookNameAgentSpawn}},
UserPromptSubmit: []kiroHookEntry{{Command: cmdPrefix + HookNameUserPromptSubmit}},
PreToolUse: []kiroHookEntry{{Command: cmdPrefix + HookNamePreToolUse}},
PostToolUse: []kiroHookEntry{{Command: cmdPrefix + HookNamePostToolUse}},
Stop: []kiroHookEntry{{Command: cmdPrefix + HookNameStop}},
},
}

if err := os.MkdirAll(filepath.Dir(hooksPath), 0o750); err != nil {
return 0, fmt.Errorf("failed to create .kiro/agents directory: %w", err)
}

output, err := jsonutil.MarshalIndentWithNewline(file, "", " ")
if err != nil {
return 0, fmt.Errorf("failed to marshal hooks config: %w", err)
}

if err := os.WriteFile(hooksPath, output, 0o600); err != nil {
return 0, fmt.Errorf("failed to write hooks config: %w", err)
}

return len(k.HookNames()), nil
}

// UninstallHooks removes the Entire hooks config file.
func (k *KiroAgent) UninstallHooks(ctx context.Context) error {
worktreeRoot, err := paths.WorktreeRoot(ctx)
if err != nil {
worktreeRoot = "."
}

hooksPath := filepath.Join(worktreeRoot, ".kiro", hooksDir, HooksFileName)
if err := os.Remove(hooksPath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to remove hooks config: %w", err)
}
return nil
}

// AreHooksInstalled checks if Entire hooks are installed.
func (k *KiroAgent) AreHooksInstalled(ctx context.Context) bool {
worktreeRoot, err := paths.WorktreeRoot(ctx)
if err != nil {
worktreeRoot = "."
}

hooksPath := filepath.Join(worktreeRoot, ".kiro", hooksDir, HooksFileName)
data, err := os.ReadFile(hooksPath) //nolint:gosec // path constructed from repo root
if err != nil {
return false
}

var file kiroAgentFile
if err := json.Unmarshal(data, &file); err != nil {
return false
}

return hasEntireHook(file.Hooks.AgentSpawn) ||
hasEntireHook(file.Hooks.UserPromptSubmit) ||
hasEntireHook(file.Hooks.Stop)
}

// GetSupportedHooks returns the hook types Kiro supports.
func (k *KiroAgent) GetSupportedHooks() []agent.HookType {
return []agent.HookType{
agent.HookSessionStart,
agent.HookUserPromptSubmit,
agent.HookPreToolUse,
agent.HookPostToolUse,
agent.HookStop,
}
}

func allHooksPresent(hooks kiroHooks, localDev bool) bool {
var cmdPrefix string
if localDev {
cmdPrefix = localDevCmdPrefix + "hooks kiro "
} else {
cmdPrefix = "entire hooks kiro "
}

return hookCommandExists(hooks.AgentSpawn, cmdPrefix+HookNameAgentSpawn) &&
hookCommandExists(hooks.UserPromptSubmit, cmdPrefix+HookNameUserPromptSubmit) &&
hookCommandExists(hooks.PreToolUse, cmdPrefix+HookNamePreToolUse) &&
hookCommandExists(hooks.PostToolUse, cmdPrefix+HookNamePostToolUse) &&
hookCommandExists(hooks.Stop, cmdPrefix+HookNameStop)
}

func hookCommandExists(entries []kiroHookEntry, command string) bool {
for _, entry := range entries {
if entry.Command == command {
return true
}
}
return false
}

func isEntireHook(command string) bool {
for _, prefix := range entireHookPrefixes {
if strings.HasPrefix(command, prefix) {
return true
}
}
return false
}

func hasEntireHook(entries []kiroHookEntry) bool {
for _, entry := range entries {
if isEntireHook(entry.Command) {
return true
}
}
return false
}
Loading
Loading