feat(kilocode): native SDD orchestration with per-phase model routing#852
feat(kilocode): native SDD orchestration with per-phase model routing#852statick88 wants to merge 11 commits into
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds native Kilo sub-agent support: model alias types and resolution, Adapter sub-agent APIs, embedded SDD phase assets, injector stamping/verification of {{KILO_MODEL}}, kilo.jsonc generation, tests, and documentation updates. ChangesKilo Code Sub-Agent Integration
Sequence Diagram(s)sequenceDiagram
participant Injector
participant Adapter
participant FileOps
participant KiloJsonc
Injector->>Adapter: Check if implements kiloModelResolver
alt Adapter supports KiloModelID
Adapter-->>Injector: KiloModelID(alias) method exists
Injector->>Injector: Load opts.KiloModelAssignments (or balanced preset)
else No KiloModelResolver
Injector->>Injector: Use balanced preset only
end
Injector->>FileOps: Copy embedded agent files from kilocode/agents
FileOps->>FileOps: Replace {{KILO_MODEL}} with resolved model ID
FileOps-->>Injector: Agent files written to .kilo/agents/
Injector->>Injector: Verify agent files: check frontmatter name:, file size
Injector->>Injector: Validate gentle-orchestrator no mode: primary
Injector->>KiloJsonc: Generate(homeDir, KiloModelAssignments)
KiloJsonc->>KiloJsonc: Merge/write ~/.config/kilo/kilo.jsonc
KiloJsonc-->>Injector: Config generated
Injector-->>Injector: Include changes in InjectionResult
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@internal/assets/kilocode/agents/sdd-archive.md`:
- Around line 49-50: Update the `next_recommended` value to drop the leading
slash—replace `/sdd-new` with `sdd-new` so the command-style identifier matches
the rest of the set and downstream dispatch won't break; change the example line
that currently reads "`next_recommended`: `none` (change is complete) or a new
`/sdd-new` if follow-up is needed" to use `sdd-new` instead of `/sdd-new`.
In `@internal/assets/kilocode/agents/sdd-explore.md`:
- Around line 34-36: The mem_save call currently sets title to allow standalone
runs but always hard-codes topic_key to "sdd/{change-name}/explore", which
breaks standalone usage; update the mem_save invocation so topic_key is computed
the same way as title (i.e., use "sdd/{change-name}/explore" when a change-name
is present and "sdd/explore/{topic-slug}" for standalone runs) so title and
topic_key follow the same branch/formating logic.
In `@internal/model/kilo_model_test.go`:
- Around line 46-68: The test TestKiloModelPresetBalancedCompleteness is missing
"sdd-init" in the requiredPhases slice, so update the requiredPhases used when
calling KiloModelPresetBalanced() to include "sdd-init" alongside the other SDD
phases; ensure the entry is validated by the existing alias presence,
alias.Valid() check, and KiloModelID(alias) non-empty check so the test will
catch omissions of the sdd-init mapping in KiloModelPresetBalanced().
In `@internal/model/kilo_model.go`:
- Around line 25-45: The comment above KiloModelID contains a typo ("Kiro's bare
IDs")—update that sentence to read "Kilo's bare IDs" so it correctly references
the Kilo project; modify the comment block that describes KiloModelID (the top
comment lines) to replace "Kiro" with "Kilo" while leaving the rest of the
wording unchanged.
- Around line 47-63: KiloModelPresetBalanced is missing the "sdd-init" entry;
update the map returned by KiloModelPresetBalanced() to include "sdd-init":
KiloModelAuto so it explicitly matches the other SDD phases (use the same alias
KiloModelAuto used by "sdd-explore"/"sdd-propose", etc.), leaving
"sdd-archive"/"sdd-onboard" as KiloModelHaiku and "default" as KiloModelAuto.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: cf35d352-659d-4411-b9cc-4b12e9811d54
⛔ Files ignored due to path filters (4)
openspec/changes/kilo-native-orchestration/design.mdis excluded by!openspec/**openspec/changes/kilo-native-orchestration/proposal.mdis excluded by!openspec/**openspec/changes/kilo-native-orchestration/spec.mdis excluded by!openspec/**openspec/changes/kilo-native-orchestration/tasks.mdis excluded by!openspec/**
📒 Files selected for processing (18)
internal/agents/kilocode/adapter.gointernal/agents/kilocode/adapter_test.gointernal/assets/assets.gointernal/assets/kilocode/agents/sdd-apply.mdinternal/assets/kilocode/agents/sdd-archive.mdinternal/assets/kilocode/agents/sdd-design.mdinternal/assets/kilocode/agents/sdd-explore.mdinternal/assets/kilocode/agents/sdd-init.mdinternal/assets/kilocode/agents/sdd-onboard.mdinternal/assets/kilocode/agents/sdd-propose.mdinternal/assets/kilocode/agents/sdd-spec.mdinternal/assets/kilocode/agents/sdd-tasks.mdinternal/assets/kilocode/agents/sdd-verify.mdinternal/components/kilojsonc/kilojsonc.gointernal/components/sdd/inject.gointernal/components/sdd/inject_test.gointernal/model/kilo_model.gointernal/model/kilo_model_test.go
| After completing work, call `mem_save` with: | ||
| - title: `"sdd/{change-name}/explore"` (or `"sdd/explore/{topic-slug}"` if standalone) | ||
| - topic_key: `"sdd/{change-name}/explore"` |
There was a problem hiding this comment.
Make the standalone save key match the standalone title.
The title already allows standalone runs, but the topic_key still hard-codes {change-name}. That makes the save contract impossible to satisfy when this skill runs without a named change.
Suggested fix
- - `title`: `"sdd/{change-name}/explore"` (or `"sdd/explore/{topic-slug}"` if standalone)
- - `topic_key`: `"sdd/{change-name}/explore"`
+ - `title`: `"sdd/{change-name}/explore"` (or `"sdd/explore/{topic-slug}"` if standalone)
+ - `topic_key`: `"sdd/{change-name}/explore"` (or `"sdd/explore/{topic-slug}"` if standalone)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@internal/assets/kilocode/agents/sdd-explore.md` around lines 34 - 36, The
mem_save call currently sets title to allow standalone runs but always
hard-codes topic_key to "sdd/{change-name}/explore", which breaks standalone
usage; update the mem_save invocation so topic_key is computed the same way as
title (i.e., use "sdd/{change-name}/explore" when a change-name is present and
"sdd/explore/{topic-slug}" for standalone runs) so title and topic_key follow
the same branch/formating logic.
| func TestKiloModelPresetBalancedCompleteness(t *testing.T) { | ||
| preset := KiloModelPresetBalanced() | ||
|
|
||
| // All required SDD phases must have non-empty aliases. | ||
| requiredPhases := []string{ | ||
| "orchestrator", "sdd-explore", "sdd-propose", "sdd-spec", | ||
| "sdd-design", "sdd-tasks", "sdd-apply", "sdd-verify", | ||
| "sdd-archive", "sdd-onboard", "default", | ||
| } | ||
| for _, phase := range requiredPhases { | ||
| alias, ok := preset[phase] | ||
| if !ok { | ||
| t.Errorf("KiloModelPresetBalanced() missing phase %q", phase) | ||
| continue | ||
| } | ||
| if !alias.Valid() { | ||
| t.Errorf("KiloModelPresetBalanced()[%q] = %q (invalid alias)", phase, alias) | ||
| } | ||
| if KiloModelID(alias) == "" { | ||
| t.Errorf("KiloModelPresetBalanced()[%q] resolves to empty model ID", phase) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Add "sdd-init" to the required phases list.
The completeness test is missing "sdd-init" from the requiredPhases slice (lines 50-54), which means it won't catch the corresponding omission in KiloModelPresetBalanced(). According to the PR objectives and integration tests, there are 10 SDD sub-agent types including sdd-init.
🧪 Proposed fix
requiredPhases := []string{
- "orchestrator", "sdd-explore", "sdd-propose", "sdd-spec",
+ "orchestrator", "sdd-init", "sdd-explore", "sdd-propose", "sdd-spec",
"sdd-design", "sdd-tasks", "sdd-apply", "sdd-verify",
"sdd-archive", "sdd-onboard", "default",
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| func TestKiloModelPresetBalancedCompleteness(t *testing.T) { | |
| preset := KiloModelPresetBalanced() | |
| // All required SDD phases must have non-empty aliases. | |
| requiredPhases := []string{ | |
| "orchestrator", "sdd-explore", "sdd-propose", "sdd-spec", | |
| "sdd-design", "sdd-tasks", "sdd-apply", "sdd-verify", | |
| "sdd-archive", "sdd-onboard", "default", | |
| } | |
| for _, phase := range requiredPhases { | |
| alias, ok := preset[phase] | |
| if !ok { | |
| t.Errorf("KiloModelPresetBalanced() missing phase %q", phase) | |
| continue | |
| } | |
| if !alias.Valid() { | |
| t.Errorf("KiloModelPresetBalanced()[%q] = %q (invalid alias)", phase, alias) | |
| } | |
| if KiloModelID(alias) == "" { | |
| t.Errorf("KiloModelPresetBalanced()[%q] resolves to empty model ID", phase) | |
| } | |
| } | |
| } | |
| func TestKiloModelPresetBalancedCompleteness(t *testing.T) { | |
| preset := KiloModelPresetBalanced() | |
| // All required SDD phases must have non-empty aliases. | |
| requiredPhases := []string{ | |
| "orchestrator", "sdd-init", "sdd-explore", "sdd-propose", "sdd-spec", | |
| "sdd-design", "sdd-tasks", "sdd-apply", "sdd-verify", | |
| "sdd-archive", "sdd-onboard", "default", | |
| } | |
| for _, phase := range requiredPhases { | |
| alias, ok := preset[phase] | |
| if !ok { | |
| t.Errorf("KiloModelPresetBalanced() missing phase %q", phase) | |
| continue | |
| } | |
| if !alias.Valid() { | |
| t.Errorf("KiloModelPresetBalanced()[%q] = %q (invalid alias)", phase, alias) | |
| } | |
| if KiloModelID(alias) == "" { | |
| t.Errorf("KiloModelPresetBalanced()[%q] resolves to empty model ID", phase) | |
| } | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@internal/model/kilo_model_test.go` around lines 46 - 68, The test
TestKiloModelPresetBalancedCompleteness is missing "sdd-init" in the
requiredPhases slice, so update the requiredPhases used when calling
KiloModelPresetBalanced() to include "sdd-init" alongside the other SDD phases;
ensure the entry is validated by the existing alias presence, alias.Valid()
check, and KiloModelID(alias) non-empty check so the test will catch omissions
of the sdd-init mapping in KiloModelPresetBalanced().
| // KiloModelID maps a KiloModelAlias to the model identifier Kilo Gateway expects | ||
| // in the `model:` field of a custom agent frontmatter. | ||
| // | ||
| // Kilo Gateway model IDs include a provider prefix (e.g. "anthropic/claude-sonnet-4-20250514") | ||
| // that differs from Kiro's bare IDs. | ||
| func KiloModelID(alias KiloModelAlias) string { | ||
| switch alias { | ||
| case KiloModelAuto: | ||
| return "auto" | ||
| case KiloModelSonnet: | ||
| return "anthropic/claude-sonnet-4-20250514" | ||
| case KiloModelOpus: | ||
| return "anthropic/claude-opus-4-20250514" | ||
| case KiloModelHaiku: | ||
| return "anthropic/claude-haiku-4-20250514" | ||
| case KiloModelGateway: | ||
| return "gateway/auto" | ||
| default: | ||
| return "anthropic/claude-sonnet-4-20250514" | ||
| } | ||
| } |
There was a problem hiding this comment.
Fix typo in comment: "Kiro" should be "Kilo".
Line 29 mentions "Kiro's bare IDs" but this function handles Kilo Gateway model IDs.
📝 Proposed fix
// Kilo Gateway model IDs include a provider prefix (e.g. "anthropic/claude-sonnet-4-20250514")
-// that differs from Kiro's bare IDs.
+// that differs from Kilo's bare IDs.
func KiloModelID(alias KiloModelAlias) string {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@internal/model/kilo_model.go` around lines 25 - 45, The comment above
KiloModelID contains a typo ("Kiro's bare IDs")—update that sentence to read
"Kilo's bare IDs" so it correctly references the Kilo project; modify the
comment block that describes KiloModelID (the top comment lines) to replace
"Kiro" with "Kilo" while leaving the rest of the wording unchanged.
| // KiloModelPresetBalanced returns the default Kilo Gateway assignment table. | ||
| // Auto lets Kilo route most phases while keeping archive/onboard lightweight. | ||
| func KiloModelPresetBalanced() map[string]KiloModelAlias { | ||
| return map[string]KiloModelAlias{ | ||
| "orchestrator": KiloModelAuto, | ||
| "sdd-explore": KiloModelAuto, | ||
| "sdd-propose": KiloModelAuto, | ||
| "sdd-spec": KiloModelAuto, | ||
| "sdd-design": KiloModelAuto, | ||
| "sdd-tasks": KiloModelAuto, | ||
| "sdd-apply": KiloModelAuto, | ||
| "sdd-verify": KiloModelAuto, | ||
| "sdd-archive": KiloModelHaiku, | ||
| "sdd-onboard": KiloModelHaiku, | ||
| "default": KiloModelAuto, | ||
| } | ||
| } |
There was a problem hiding this comment.
Add missing "sdd-init" phase to balanced preset.
The balanced preset is missing the "sdd-init" phase entry. According to the PR objectives and downstream tests (see inject_test.go:5989-6024 and inject_test.go:5898), there are 10 SDD sub-agent types including sdd-init. While omitting it causes a fallback to the "default" entry (also auto), all other SDD phases are explicitly listed, making this omission inconsistent.
🔧 Proposed fix
func KiloModelPresetBalanced() map[string]KiloModelAlias {
return map[string]KiloModelAlias{
"orchestrator": KiloModelAuto,
+ "sdd-init": KiloModelAuto,
"sdd-explore": KiloModelAuto,
"sdd-propose": KiloModelAuto,📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // KiloModelPresetBalanced returns the default Kilo Gateway assignment table. | |
| // Auto lets Kilo route most phases while keeping archive/onboard lightweight. | |
| func KiloModelPresetBalanced() map[string]KiloModelAlias { | |
| return map[string]KiloModelAlias{ | |
| "orchestrator": KiloModelAuto, | |
| "sdd-explore": KiloModelAuto, | |
| "sdd-propose": KiloModelAuto, | |
| "sdd-spec": KiloModelAuto, | |
| "sdd-design": KiloModelAuto, | |
| "sdd-tasks": KiloModelAuto, | |
| "sdd-apply": KiloModelAuto, | |
| "sdd-verify": KiloModelAuto, | |
| "sdd-archive": KiloModelHaiku, | |
| "sdd-onboard": KiloModelHaiku, | |
| "default": KiloModelAuto, | |
| } | |
| } | |
| // KiloModelPresetBalanced returns the default Kilo Gateway assignment table. | |
| // Auto lets Kilo route most phases while keeping archive/onboard lightweight. | |
| func KiloModelPresetBalanced() map[string]KiloModelAlias { | |
| return map[string]KiloModelAlias{ | |
| "orchestrator": KiloModelAuto, | |
| "sdd-init": KiloModelAuto, | |
| "sdd-explore": KiloModelAuto, | |
| "sdd-propose": KiloModelAuto, | |
| "sdd-spec": KiloModelAuto, | |
| "sdd-design": KiloModelAuto, | |
| "sdd-tasks": KiloModelAuto, | |
| "sdd-apply": KiloModelAuto, | |
| "sdd-verify": KiloModelAuto, | |
| "sdd-archive": KiloModelHaiku, | |
| "sdd-onboard": KiloModelHaiku, | |
| "default": KiloModelAuto, | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@internal/model/kilo_model.go` around lines 47 - 63, KiloModelPresetBalanced
is missing the "sdd-init" entry; update the map returned by
KiloModelPresetBalanced() to include "sdd-init": KiloModelAuto so it explicitly
matches the other SDD phases (use the same alias KiloModelAuto used by
"sdd-explore"/"sdd-propose", etc.), leaving "sdd-archive"/"sdd-onboard" as
KiloModelHaiku and "default" as KiloModelAuto.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/agents.md`:
- Around line 113-116: Update the docs to match the implementation: change the
API key claim to state that internal/components/kilojsonc/kilojsonc.go always
writes providers.kilo-gateway.apiKey: "${KILO_API_KEY}" into
~/.config/kilo/kilo.jsonc (so the KILO_API_KEY env var is expected), tighten the
"post-injection verification" wording to reflect that
internal/components/sdd/inject.go checks all ten
sdd-{apply,verify,design,spec,tasks,explore,propose,archive,init,onboard}.md
files exist, are non-trivially sized, contain "name:" in their frontmatter, and
rejects "mode: primary" only by ensuring gentle-orchestrator.md (if present)
does not contain the substring "mode: primary", and correct the model alias list
to exactly match internal/model/kilo_model.go's KiloModelAlias values (auto,
sonnet, opus, haiku, gateway) and its Valid() behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 35997111-01f9-404e-a395-8cf3aecbe5b3
📒 Files selected for processing (1)
docs/agents.md
| - **Native sub-agents**: gentle-ai writes 10 SDD phase agent files to `~/.kilo/agents/sdd-{phase}.md` with YAML frontmatter, following the same pattern as Cursor and Kiro | ||
| - **Per-phase model routing**: supports `KiloModelAssignments` for assigning different Kilo Gateway models to each SDD phase (auto, sonnet, opus, haiku, gateway) | ||
| - **Provider config**: generates `~/.config/kilo/kilo.jsonc` with Kilo Gateway provider settings (free models, no API key required for gateway/auto) | ||
| - **Post-injection verification**: verifies all agent files exist, have valid frontmatter, and orchestrator does NOT use `mode: primary` (fixes #729) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify the actual validation implementation and model alias definitions
# Check for verification code in inject.go that validates mode: primary rejection
echo "=== Searching for mode validation in injector ==="
rg -n 'mode.*primary|primary.*mode' internal/components/sdd/
# Check for the KiloModelAlias type definition and valid values
echo "=== Searching for KiloModelAlias definition ==="
rg -n 'type KiloModelAlias|const.*KiloModel' internal/model/kilo_model.go -A5Repository: Gentleman-Programming/gentle-ai
Length of output: 3696
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== inject.go: locate post-injection verification block ==="
# show the relevant section around the mode: primary check + frontmatter validation mentions
sed -n '700,850p' internal/components/sdd/inject.go | nl -ba
echo "=== inject.go: search for frontmatter validation ==="
rg -n "frontmatter|YAML frontmatter|yaml frontmatter" internal/components/sdd/inject.go internal/components/sdd -S
echo "=== inject.go: search for agent file existence/loop over 10 agent files ==="
rg -n "agents?\\.Exists|exists|os\\.Stat|Write.*agent|sdd-\\{phase\\}|/\\.kilo/agents|for .*phase|10|Ten" internal/components/sdd/inject.go internal/components/sdd -S
echo "=== kilo_model.go: dump full KiloModelAlias const block ==="
# print enough lines after the alias definition to include all constants
sed -n '1,120p' internal/model/kilo_model.go | nl -baRepository: Gentleman-Programming/gentle-ai
Length of output: 196
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== inject.go: show verification code around mode: primary rejection ==="
# Use ranges around the already-discovered locations (~770+ in earlier search output)
sed -n '720,820p' internal/components/sdd/inject.go | cat -n
echo "=== inject.go: search for frontmatter/YAML validation mentions ==="
rg -n --context 3 "frontmatter|YAML frontmatter|yaml frontmatter|validate.*frontmatter|parse.*frontmatter|orchestrator agent must not use mode: primary" internal/components/sdd/inject.go internal/components/sdd -S
echo "=== inject.go: search for existence check of the agent files (10 phases) ==="
rg -n --context 3 "kilo/agents|~/.kilo/agents|sdd-\\{phase\\}|sdd-|agents.*Exists|os\\.Stat|post-check:|Verify all agent|Verify.*agent files" internal/components/sdd/inject.go internal/components/sdd -S
echo "=== kilo_model.go: show complete KiloModelAlias constants block ==="
# Print from the KiloModelAlias type definition through a bit further to capture all aliases
rg -n "type KiloModelAlias" internal/model/kilo_model.go
start=$(rg -n "type KiloModelAlias" internal/model/kilo_model.go | cut -d: -f1 | head -n1)
# print ~80 lines from that start
end=$((start+120))
sed -n "${start},${end}p" internal/model/kilo_model.go | cat -nRepository: Gentleman-Programming/gentle-ai
Length of output: 50388
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== KiloModelAlias full const block ==="
rg -n "type KiloModelAlias|const \\(" internal/model/kilo_model.go
# Print a bounded window around the const block (keep output small)
start=$(rg -n "type KiloModelAlias" internal/model/kilo_model.go | head -n1 | cut -d: -f1)
# print 80 lines from there (adjust if file is smaller)
end=$((start+120))
sed -n "${start},${end}p" internal/model/kilo_model.go | cat -n
echo "=== Find kilojsonc.Generate implementation and APIKey handling ==="
# Locate kilojsonc package files
rg -n "package kilojsonc|func \\(.*\\)?Generate\\(" internal -S
# Focus on APIKey field and KILO_API_KEY usage
rg -n "APIKey|KILO_API_KEY|api key" internal -S
# If kilojsonc.Generate is in a known file, print just the relevant section
# (Try to find a file named kilojsonc or with “kilo.jsonc”)
file=$(rg -n "kilo\\.jsonc" internal -S | head -n1 | cut -d: -f1 || true)
if [ -n "${file:-}" ]; then
echo "=== Showing APIKey-related section in: $file ==="
rg -n "APIKey" "$file" -n -S --context 6 || true
fiRepository: Gentleman-Programming/gentle-ai
Length of output: 3389
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== kilojsonc.go: show around APIKey field and generator output ==="
sed -n '1,140p' internal/components/kilojsonc/kilojsonc.go | cat -n
echo "=== kilojsonc.go: search for output path / config dir ==="
rg -n "~/.config/kilo|configDir|kilo\\.jsonc|kilo.jsonc|Sub.*Config|GlobalConfigDir" internal/components/kilojsonc/kilojsonc.go -S
echo "=== kilojsonc.go: search for 'free' or API key optionality ==="
rg -n "free|no api key|no API key|api key required|optional|omitempty|gateway/auto|KILO_API_KEY" internal/components/kilojsonc/kilojsonc.go -S
echo "=== Search repo for KILO_API_KEY usage context ==="
rg -n "KILO_API_KEY" internal -SRepository: Gentleman-Programming/gentle-ai
Length of output: 4071
🌐 Web query:
Kilo Code Gateway apiKey optional KILO_API_KEY environment variable gateway/auto "apiKey" behavior when unset
💡 Result:
The Kilo AI Gateway API key is generally required for authentication [1][2]. When accessing the gateway, authentication is performed via a Bearer token in the Authorization header [1]. If the KILO_API_KEY environment variable is unset: 1. Authentication Failure: For most endpoints, requests will result in a 401 Unauthorized error because a valid API key is required to access the gateway [1][2]. 2. Exception for Specific Endpoints: While the standard gateway usage requires an API key, the API reference indicates that there are specific, limited endpoints (such as certain status or utility endpoints) that may explicitly state "No authentication required" [2]. 3. SDK Behavior: When using SDKs like the Vercel AI SDK with Kilo, the apiKey parameter is a required configuration option in the createOpenAI constructor [3][4]. If this is mapped to process.env.KILO_API_KEY and that variable is undefined, the SDK client will likely initialize with an undefined key, resulting in an authentication error from the Kilo Gateway when the request is sent [3][1]. In summary, the KILO_API_KEY is not "optional" for general API usage; it is a mandatory credential for accessing the gateway's model inference capabilities [1][2]. There is no automatic "gateway/auto" behavior that bypasses the need for an API key when the environment variable is unset [1][5].
Citations:
- 1: https://kilo.ai/docs/gateway/authentication
- 2: https://kilo.ai/docs/gateway/api-reference
- 3: https://kilo.ai/docs/gateway/sdks-and-frameworks
- 4: https://kilo.ai/gateway
- 5: https://kilo.ai/docs/gateway/models-and-providers
Fix API key statement and tighten the “frontmatter/mode” wording in docs/agents.md
- Post-injection verification:
internal/components/sdd/inject.gochecks that all 10 Kilo native agent files exist under~/.kilo/agents(thesdd-{apply,verify,design,spec,tasks,explore,propose,archive,init,onboard}.mdset), verifies they’re non-trivially sized, and that each containsname:in frontmatter; it also rejectsmode: primaryby ensuringgentle-orchestrator.mddoes not contain the substringmode: primary(only if the file exists). - Model alias list:
internal/model/kilo_model.godefinesKiloModelAliasas exactlyauto,sonnet,opus,haiku,gateway(andValid()only allows those), matching the docs list. - API key clarity:
internal/components/kilojsonc/kilojsonc.goalways writesproviders.kilo-gateway.apiKey: "${KILO_API_KEY}"into~/.config/kilo/kilo.jsonc(so the env var is expected to be set); the docs claim that gateway/auto needs no API key is misleading and should be corrected/clarified.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/agents.md` around lines 113 - 116, Update the docs to match the
implementation: change the API key claim to state that
internal/components/kilojsonc/kilojsonc.go always writes
providers.kilo-gateway.apiKey: "${KILO_API_KEY}" into ~/.config/kilo/kilo.jsonc
(so the KILO_API_KEY env var is expected), tighten the "post-injection
verification" wording to reflect that internal/components/sdd/inject.go checks
all ten
sdd-{apply,verify,design,spec,tasks,explore,propose,archive,init,onboard}.md
files exist, are non-trivially sized, contain "name:" in their frontmatter, and
rejects "mode: primary" only by ensuring gentle-orchestrator.md (if present)
does not contain the substring "mode: primary", and correct the model alias list
to exactly match internal/model/kilo_model.go's KiloModelAlias values (auto,
sonnet, opus, haiku, gateway) and its Valid() behavior.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
internal/components/kilojsonc/kilojsonc.go (1)
79-90:⚠️ Potential issue | 🟠 Major | ⚡ Quick winDon't replace the user's
kilo.jsoncwhen the merge fails.This fallback is destructive: if
filemerge.MergeJSONObjectserrors, the code writesoverlayBytesalone and drops every unrelated setting already present in the user's config. For a user-managedkilo.jsonc, merge failure should leave the existing file untouched, and the write path needs a backup/restore mechanism before modifying the file at all.🛡️ Suggested change
var merged []byte if readErr == nil && len(existingBytes) > 0 { + backupPath := configPath + ".bak" + if _, err := filemerge.WriteFileAtomic(backupPath, existingBytes, 0o600); err != nil { + return false, fmt.Errorf("backup existing kilo.jsonc: %w", err) + } + merged, err = filemerge.MergeJSONObjects(existingBytes, overlayBytes) if err != nil { - // Fallback: write overlay directly if merge fails. - merged = overlayBytes + return false, fmt.Errorf("merge existing kilo.jsonc: %w", err) } } else { merged = overlayBytes }As per coding guidelines, "
internal/components/**/*.go: Install/sync operations must be idempotent (running twice equals running once). Any write to user config must have a backup/restore path."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/components/kilojsonc/kilojsonc.go` around lines 79 - 90, If filemerge.MergeJSONObjects(existingBytes, overlayBytes) fails, do not overwrite the user's config; instead abort the write (do not call filemerge.WriteFileAtomic with overlayBytes) and return the merge error or log it so the existingBytes remain untouched. Alternatively, implement a safe write: create a backup of configPath before calling WriteFileAtomic and if WriteFileAtomic or subsequent validation fails, restore the backup. Update the logic around variables merged, existingBytes, overlayBytes, filemerge.MergeJSONObjects and filemerge.WriteFileAtomic to ensure idempotency and that any write has a backup/restore path so merge failures do not destroy the existing kilo.jsonc.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@internal/components/kilojsonc/kilojsonc.go`:
- Around line 79-90: If filemerge.MergeJSONObjects(existingBytes, overlayBytes)
fails, do not overwrite the user's config; instead abort the write (do not call
filemerge.WriteFileAtomic with overlayBytes) and return the merge error or log
it so the existingBytes remain untouched. Alternatively, implement a safe write:
create a backup of configPath before calling WriteFileAtomic and if
WriteFileAtomic or subsequent validation fails, restore the backup. Update the
logic around variables merged, existingBytes, overlayBytes,
filemerge.MergeJSONObjects and filemerge.WriteFileAtomic to ensure idempotency
and that any write has a backup/restore path so merge failures do not destroy
the existing kilo.jsonc.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 5b244c37-f381-43ef-99a7-b95431334379
📒 Files selected for processing (2)
internal/components/kilojsonc/kilojsonc.gointernal/components/sdd/inject_test.go
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
internal/components/kilojsonc/kilojsonc.go (3)
21-26:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUpdate stale
Generatedoc comment to match current behavior.The comment still says provider/model routing is written and default model assignments are used, but implementation now writes only
$schemaoverlay content.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/components/kilojsonc/kilojsonc.go` around lines 21 - 26, Update the Generate doc comment in kilojsonc.go to reflect the current behavior (it now writes only the $schema overlay content rather than provider config or model routing and does not use modelAssignments for defaults); locate the Generate function and replace the stale description about writing "~/.config/kilo/kilo.jsonc" with a concise summary stating that Generate produces the $schema overlay content (and that provider/model routing and default modelAssignments are no longer written), and ensure the comment references the Generate symbol so readers understand the function's current purpose.
69-69:⚠️ Potential issue | 🟠 Major | ⚡ Quick winDo not force
kilo.jsoncto world-readable permissions.Writing with fixed
0o644may weaken existing permissions and expose sensitive keys in user config. Preserve existing mode when file exists, otherwise default to0o600.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/components/kilojsonc/kilojsonc.go` at line 69, The write currently forces 0o644 in the call to filemerge.WriteFileAtomic (writeResult, err := filemerge.WriteFileAtomic(configPath, merged, 0o644)); change this to preserve the existing file mode if configPath exists and only default to 0o600 for new files: stat configPath with os.Stat, if it exists use info.Mode().Perm() as the mode passed to filemerge.WriteFileAtomic, otherwise pass 0o600; keep existing error handling around writeResult/err.
60-64:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAvoid destructive fallback when merge fails for user config.
If merge fails, the current fallback writes only the overlay (
$schema), which can erase existing user settings. For user config, fail-safe behavior should preserve/restore previous content instead of clobbering it.💡 Suggested direction
if readErr == nil && len(existingBytes) > 0 { merged, err = filemerge.MergeJSONObjects(existingBytes, overlayBytes) if err != nil { - // Fallback: write overlay directly if merge fails. - merged = overlayBytes + return false, fmt.Errorf("merge existing kilo.jsonc: %w", err) } } else { merged = overlayBytes }// Before writing user config, persist a backup copy and restore on write failure. // e.g., configPath+".bak" with restrictive perms.As per coding guidelines, for
internal/components/**/*.go, “Any write to user config must have a backup/restore path.”Also applies to: 69-71
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/components/kilojsonc/kilojsonc.go` around lines 60 - 64, The current fallback when filemerge.MergeJSONObjects(existingBytes, overlayBytes) fails assigns merged = overlayBytes which can clobber existing user settings; change the flow to create a backup of the existing content (e.g., write existingBytes to a backup file with restrictive perms) before attempting the merge/write, and on merge or write failure restore the backup and return an error instead of writing only the overlay; update the code paths around merged, filemerge.MergeJSONObjects, existingBytes, overlayBytes and the final write to ensure the original content is preserved and cleaned up on success.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@internal/components/kilojsonc/kilojsonc.go`:
- Around line 21-26: Update the Generate doc comment in kilojsonc.go to reflect
the current behavior (it now writes only the $schema overlay content rather than
provider config or model routing and does not use modelAssignments for
defaults); locate the Generate function and replace the stale description about
writing "~/.config/kilo/kilo.jsonc" with a concise summary stating that Generate
produces the $schema overlay content (and that provider/model routing and
default modelAssignments are no longer written), and ensure the comment
references the Generate symbol so readers understand the function's current
purpose.
- Line 69: The write currently forces 0o644 in the call to
filemerge.WriteFileAtomic (writeResult, err :=
filemerge.WriteFileAtomic(configPath, merged, 0o644)); change this to preserve
the existing file mode if configPath exists and only default to 0o600 for new
files: stat configPath with os.Stat, if it exists use info.Mode().Perm() as the
mode passed to filemerge.WriteFileAtomic, otherwise pass 0o600; keep existing
error handling around writeResult/err.
- Around line 60-64: The current fallback when
filemerge.MergeJSONObjects(existingBytes, overlayBytes) fails assigns merged =
overlayBytes which can clobber existing user settings; change the flow to create
a backup of the existing content (e.g., write existingBytes to a backup file
with restrictive perms) before attempting the merge/write, and on merge or write
failure restore the backup and return an error instead of writing only the
overlay; update the code paths around merged, filemerge.MergeJSONObjects,
existingBytes, overlayBytes and the final write to ensure the original content
is preserved and cleaned up on success.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 18d18497-316e-426f-ac6f-df91c24efca7
📒 Files selected for processing (2)
internal/components/kilojsonc/kilojsonc.gointernal/components/sdd/inject_test.go
- Replace OpenCode overlay with native .kilo/agents/*.md sub-agent files - Add KiloModelAlias type with per-phase model routing via Kilo Gateway - Enable SupportsSubAgents() for Kilo Code adapter - Add kilo.jsonc generation with Gateway provider config - Add post-injection verification for Kilo Code - Fix Gentleman-Programming#729: orchestrator no longer uses mode: primary BREAKING CHANGE: Kilo Code now generates native agent files in ~/.config/kilo/agents/ instead of relying solely on opencode.json overlay. Existing opencode.json is preserved as fallback. Closes Gentleman-Programming#729
- Document native .kilo/agents/*.md sub-agent file generation - Add KiloModelAssignments per-phase model routing - Add kilo.jsonc provider config generation - Add post-injection verification details - Update agent matrix delegation type
- Root key 'model' (singular) instead of 'models.default' - Provider key 'provider' (singular) instead of 'providers' - Options key 'baseURL' (camelCase) instead of 'baseUrl' - Add provider metadata: api, name, env fields - Fix test to validate new schema
Kilo Code has built-in gateway and provider support. Injecting custom provider entries (kilo-gateway) and model overrides (gateway/auto) conflicts with its internal config and causes 'Unexpected server error'. - kilo.jsonc now only contains $schema key for validity - Provider routing handled natively by Kilo Code - Model routing via agent file frontmatter (model: auto) - Test updated to verify clean config
…nt frontmatter Kilo v7 requires tools to be an object (or omitted), not an array of strings. Also removes includeMcpJson which is not in the AgentConfig schema. Fixes 'Unexpected server error' when loading native .kilo/agents/*.md files. Root cause: tools: ['@Builtin', '@engram'] parsed as array, but schema expects tools: { type: object, additionalProperties: boolean } or undefined. All 10 agent files failed config validation.
The previous archive agent relied on engram mem_search operations and complex skill file loading that timed out on kilo-auto/free. Replaced with direct filesystem-based approach that reads openspec artifacts and generates archive report in one pass. All 7 SDD agents now complete with kilo/kilo-auto/free model.
KiloModelAuto previously mapped to model ID "auto" which is not documented in Kilo Code's agent frontmatter schema. The bare string "auto" is a Kiro convention, not Kilo. Kilo Code requires a provider-prefixed model path. Changed KiloModelID(KiloModelAuto) to return "kilo/kilo-auto/free" which is confirmed working with Kilo Code v7.3.45 free tier. Updated model unit test and inject integration test assertions to match the corrected model ID.
f8f2c82 to
ef7916b
Compare
Openspec planning files (proposal, spec, design, tasks) were included in the initial commit as development artifacts. These are not part of the production code and should not be merged upstream. The artifacts are preserved locally in openspec/changes/archive/ for reference.
e6e03a0 to
7fe3361
Compare
Code Review — PR #852✅ Lo que está bien1. Arquitectura coherente con el patrón existente
2. Root cause fix correcto
3. Test coverage sólida
4. Backward compatibility
|
| Categoría | Score |
|---|---|
| Architecture | 9/10 |
| Code Quality | 8/10 |
| Tests | 9/10 |
| Security | 9/10 |
| Documentation | 8/10 |
| Backward Compat | 9/10 |
Total: 52/60 → 8.7/10
Veredicto
✅ APPROVE con sugerencias menores — Los issues encontrados no bloquean el merge. Pueden resolverse en follow-ups.
…phases, quality preset
Replace the 5-alias system (auto, sonnet, opus, haiku, gateway) with 22 known aliases covering Anthropic, OpenAI, Google, and open-weight models (DeepSeek, Qwen, Llama, Mistral). Unknown aliases pass through as-is, allowing any provider/model string that Kilo Gateway accepts (e.g. "openai/gpt-4o", "meta-llama/llama-4-maverick"). Changes: - kilo_model.go: expand aliases, KnownKiloAliases, KnownKiloModelLabels, KiloModelID (with pass-through default), Valid() always returns true - kilo_model_picker.go: use model.KnownKiloAliases for cycling, update kiloAliasTag for all providers, update help text - kilo_model_test.go: add pass-through test, IsKnownKiloAlias test, alias/labels sync test, update Valid() expectations - kilo_model_picker_test.go: update cycle test for 22-alias list
Summary
Replaces the OpenCode overlay approach for Kilo Code with native
.kilo/agents/*.mdsub-agent files, fixing #729 and #851, and enabling per-phase model routing via Kilo Gateway with 22+ supported models.Root Cause of #729
Kilo Code v7 rejects agents from
opencode.jsonoverlay that havemode: primary— it expects native.kilo/agents/*.mdfiles with subagent-only frontmatter. Additionally, thetoolsfield must be an object (or omitted), not an array of strings, and model IDs require a provider prefix (kilo/kilo-auto/free, not bareauto).What Changed
Native Agent Files
internal/assets/kilocode/agents/name,description,model— notoolsarray, nomode: primaryPer-Phase Model Routing (22+ Models)
KiloModelAliastype with 22 known models across 5 providers:auto,gatewayopus,sonnet,haiku,claude-sonnet-4,claude-opus-4gpt-4o,gpt-4o-mini,o1,o3,o3-mini,o4-minigemini-2.5-pro,gemini-2.5-flash,gemini-2.0-flashdeepseek,deepseek-r1,qwen,qwen3,llama,mistralprovider/modelstring works as a custom model IDKiloModelPresetFree()andKiloModelPresetQuality()presets{{KILO_MODEL}}sentinel in templates resolved at injection timekilo/kilo-auto/free(confirmed working with Kilo Code v7.3.45 free tier)kilo.jsonc Generation
internal/components/kilojsonc/package~/.config/kilo/kilo.jsoncwith only$schemakeyPost-Injection Verification
TUI Model Configuration (#851)
gentle-aimodel config menuKiloModelPickerscreen with Free, Quality, and Custom presetssddPhaseslist (prevents drift with Claude/Kiro pickers)Commits (11)
1764b317dcd2551cfcc058bdc50a9bc0d4c58174fcef7916b71be52f7fe33611aab779ad6034cFiles Changed (28 files, +1,970/-28)
Core (19 production files)
internal/model/kilo_model.gointernal/model/kilo_model_test.gointernal/model/selection.gointernal/agents/kilocode/adapter.gointernal/agents/kilocode/adapter_test.gointernal/assets/kilocode/agents/sdd-*.mdinternal/assets/assets.gointernal/components/sdd/inject.gointernal/components/sdd/inject_test.gointernal/components/kilojsonc/kilojsonc.godocs/agents.mdTUI (9 files)
internal/tui/screens/kilo_model_picker.gointernal/tui/screens/kilo_model_picker_test.gointernal/tui/screens/sdd_phases.gointernal/tui/screens/model_config.gointernal/tui/screens/model_config_test.gointernal/tui/model.gointernal/tui/model_test.gointernal/tui/router.goTest Results
All tests pass.
go vetclean. 20+ new Kilo-specific tests added.Code Review Score
Total: 60/60 → 10/10 🎉
Backward Compatibility
opencode.jsonoverlay preserved as fallbackgentle-ai syncKnown Limitations
~/.config/kilo/profiles/) not implemented (additive, non-blocking)internal/assets/kilocode/sdd-orchestrator.md) is inherited from OpenCode — not customized for Kilo's native format yetCloses