-
Notifications
You must be signed in to change notification settings - Fork 232
Add Kiro agent integration with E2E-first TDD #554
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
85afb2d
c58064c
9b48a83
3e70f85
c38e217
19acfef
526070a
09d3006
237ba90
bf4dac1
df1609d
846147e
ec25180
3e19f03
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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
|
||
| **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). | ||
| 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 | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding
kiroto 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 thekirojob is likely to fail consistently. Consider gating thekiromatrix entry behind a repo secret/variable (or making the job conditional), and/or settingE2E_AGENT=${{ matrix.agent }}for the bootstrap step so bootstrap only runs for the selected agent.