Skip to content
Merged
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
**Upgrading: no `task-init` re-run needed.** All seven installers already symlink `$SCRIPT_DIR/../bin/task-done` and never their own copy — the drift-guarded `install-shared-symlinks` stanza — so no installer-written artifact changes and no repo needs refreshing.

### Fixed
- **kiro: `task-init`, `task-remove` and `task-config set` now handle an agent config kiro-cli has upgraded to its "universal" format (#267).** kiro-cli's V3 engine rewrites `.kiro/agents/*.json` into that format in three cases: when you accept the "upgrade your agent configs" dialog, when `chat.enableAutoAgentUpgrade` is `true`, and when you run `kiro-cli agent upgrade`. The rewrite turns `hooks` from an object keyed by trigger into an array of `{name, trigger, action: {type, command}, timeout}`. The radio hooks survived the rewrite, but nothing that edits them afterwards could read the array. Re-running `task-init <kiro loadout>` warned `could not merge radio hooks` for every agent, because jq failed with `Cannot index array with string "agentSpawn"`. Removal was worse because it was silent: `lib/loadout-artifacts.sh`'s strip skipped any `hooks` that was not an object, so `task-remove` and `task-config set` reported the file as kept because it "may be yours", and left every radio hook in it.
- **The merge** (all three kiro installers, `kiro-radio-hooks-jq-merge` and `kiro-radio-stray-hook-verify`, still drift-guarded) now recognises radio's entries by their command in either shape (`.command` in task-init's own format, `.action.command` in the universal one). It adds a missing entry in the shape the file already uses. In the universal format that entry is `{name: "radio-<trigger>", trigger, action: {type: "command", command}, timeout: 10}`, which is the timeout the upgrade gives an entry that had none. It never converts a config from one format to the other. A universal config with no `hooks` key at all gets an array too. That case is detected by `permissions`, the field kiro-cli's upgrade keys the hooks shape on: an object `hooks` beside `permissions` is the hybrid that kiro-cli reports `out of sync (not rebuilt)`. `kiroUpgrade` cannot be the marker, because a V3-only config carries no fingerprint. The `radio-<trigger>` name can repeat one of the user's hook names. kiro-cli 2.32.0 accepts duplicate hook names, so the merge does nothing to avoid them. Verified on kiro-cli 2.32.0: a merged config passes `kiro-cli agent validate`, and a second `kiro-cli agent upgrade --dry-run` still reports it `already universal`, so the `kiroUpgrade` fingerprint is not invalidated. The stale-radio-command warning reads the array too.
- **The strip** drops radio-owned entries from the array, keeps the user's own entries exactly as kiro-cli wrote them, and drops `hooks` only once it is empty.
- An upgraded config is never the copy `task-init` ships, because kiro-cli also rewrites `tools` and adds `permissions` and `kiroUpgrade`. So removal keeps the file, with only the radio hooks stripped, rather than deleting it. That is the existing keep-bias (#219), and `--purge` still deletes it.
- The #257 launch warning no longer warns you off `true`. It still names `false` as the fix, and the kiro steering templates and the README now say the upgraded format works.

Tests run against fixtures that are real `kiro-cli agent upgrade` output, not hand-written JSON. They live in `tests/fixtures/kiro-universal/`, together with the inputs and a regeneration recipe. The new tests cover a no-op re-run over an upgraded config, a hook-less upgraded config (real fixture, plus a variant without `kiroUpgrade` that pins the `permissions` marker), entries added in the array shape (name, trigger, command and timeout), idempotence, object-format configs beside it staying objects, the stray warning on the array, each kiro loadout's own `--loadout` default, the strip through `task-remove` (apply and `--dry-run`) and through `task-config set`, and the warning losing its caveat. Each new installer test was run against the pre-fix `kiro-gh/bin/task-init` and each strip test against the pre-fix lib, and all of them failed. The one exception is "object-format configs stay objects", which passes on the old code by design. It guards the opposite mistake, a merge that normalises every config to the universal format, and a merge mutated to do that turned it red. **Upgrading: no `task-init` re-run is needed for removal**, since `lib/` takes effect through the existing symlinks. **Re-run `task-init <kiro loadout>`** to get the merge fix (the warning goes away on that same run) and the updated steering doc.

- **`task-remove` and the removal phase of `task-config set` no longer hang at 100% CPU on a large `CLAUDE.md` under bash 3.2 with a UTF-8 locale (#266).** Two "is anything left?" checks in `lib/loadout-artifacts.sh` stripped whitespace with `${s//[[:space:]]/}`: `_la_strip_line`, which decides whether `CLAUDE.md` (or `.gitignore`) held only task-init's line, and `_la_workflow_doc_tail`, which decides whether the workflow doc needs a `<doc>.bak`. Bash 3.2's pattern matcher is superlinear on a bracket class under a multibyte locale. On macOS's `/bin/bash` an 8KB UTF-8 file took 25s, and a ~30KB `CLAUDE.md` never finished. Both now go through a new `_la_has_content`, a `grep -q '[^[:space:]]'` on a here-string. It is not a pipe, because under the callers' `pipefail` a `grep -q` that exits early would SIGPIPE the writer and a non-blank file would read as blank, so the user's `CLAUDE.md` would be deleted. `_la_workflow_doc_tail` now returns the tail verbatim, not whitespace-stripped. Its only caller tests it for emptiness. The repo has no other `${var//[[:class:]]/}` uses. New tests drive both functions on a ~40KB UTF-8 file under a UTF-8 locale with a 20s alarm, and pin the whitespace-only cases. On the old code both large-file tests hit the alarm, and forcing `_la_has_content` to either answer turned tests red. The large-file tests only discriminate on bash 3.2; elsewhere they pin behaviour. **Upgrading: no `task-init` re-run needed.** `lib/` takes effect through the existing `~/.local/bin` symlinks. Workaround on an older checkout: `LC_ALL=C task-remove …`.

- **kiro-cli's V3 engine holds a dispatched worker's first turn with an agent-config upgrade dialog (#257): the launchers now warn.** With the V3 engine selected, kiro-cli opens "Your agent configs are still in the V2 format. Upgrade them to run on V3?" before the positional first turn. Observed on kiro-cli 2.32.0 on the exact launch line `task-work` builds, `--tui` included. Today V3 means `chat.agentEngine = v3`. kiro-cli's help calls v2 "the pre-3.0 default", so V3 is expected to become the default with 3.0. What the probes established, each in a pty against a real kiro-cli 2.32.0:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1138,7 +1138,7 @@ Radio auto-submit and focus, on every combo (#246, #254):

A kiro worker's first turn is the task reference, passed as `kiro-cli chat`'s positional `[INPUT]`. kiro-cli runs it as soon as it starts, unless a startup prompt gets there first, and in a dispatched tab nobody answers one, so the worker shows its welcome message and sits idle as if it had never been given a ref (#157). Two such prompts are within reach. The classic UI's deprecation dialog is avoided by pinning `--tui` on the launch line when the installed kiro-cli accepts it, which also overrides a `chat.ui = legacy` setting. kiro-cli also documents a trust-all confirmation at startup for `-a/--trust-all`. That one was not observed here, and it is not ours to skip, so `task-work` and the kiro `task-reviewer`, which is trust-all by default, warn on stderr unless `chat.disableTrustAllConfirmation` is true. Set it with `kiro-cli settings chat.disableTrustAllConfirmation true`, or answer the prompt in the worker's tab.

A third prompt comes with kiro-cli's V3 engine (#257): "Your agent configs are still in the V2 format. Upgrade them to run on V3?" It was observed on kiro-cli 2.32.0 holding the first turn on the exact launch line `task-work` builds, `--tui` included, whenever the launch runs on V3: today that means `chat.agentEngine = v3`. kiro-cli's own help calls v2 "the pre-3.0 default", so V3 is expected to become the default with 3.0, and then every kiro launch is exposed. The dialog is the `chat.enableAutoAgentUpgrade` setting. When the setting is unset it prompts, `true` rewrites the configs without asking, and `false` never asks. kiro-cli has no flag or env var that sets it for one launch. So `task-work`, `task-recreate-worker` and the kiro `task-reviewer` warn on stderr when the launch would run on V3 and the setting is unset. None of them warns under `--no-launch`. Set it once with `kiro-cli settings chat.enableAutoAgentUpgrade false`. With `false`, V3 loads the agent configs `task-init` wrote as they are, and the radio hooks fire. `true` skips the dialog too, but kiro-cli then rewrites `.kiro/agents/*.json` into its universal format, where `hooks` is an array. The rewrite keeps the radio hooks, but `task-init` cannot yet merge hooks into that shape, and a re-run warns `could not merge radio hooks` for every agent. The same applies to answering "Enable auto-upgrade" in the dialog, or running `kiro-cli agent upgrade` yourself.
A third prompt comes with kiro-cli's V3 engine (#257): "Your agent configs are still in the V2 format. Upgrade them to run on V3?" It was observed on kiro-cli 2.32.0 holding the first turn on the exact launch line `task-work` builds, `--tui` included, whenever the launch runs on V3: today that means `chat.agentEngine = v3`. kiro-cli's own help calls v2 "the pre-3.0 default", so V3 is expected to become the default with 3.0, and then every kiro launch is exposed. The dialog is the `chat.enableAutoAgentUpgrade` setting. When the setting is unset it prompts, `true` rewrites the configs without asking, and `false` never asks. kiro-cli has no flag or env var that sets it for one launch. So `task-work`, `task-recreate-worker` and the kiro `task-reviewer` warn on stderr when the launch would run on V3 and the setting is unset. None of them warns under `--no-launch`. Set it once with `kiro-cli settings chat.enableAutoAgentUpgrade false`. With `false`, V3 loads the agent configs `task-init` wrote as they are, and the radio hooks fire. `true` skips the dialog too, but kiro-cli then rewrites `.kiro/agents/*.json` into its universal format, where `hooks` is an array of `{name, trigger, action: {type, command}, timeout}` entries. The same rewrite happens when you answer "Enable auto-upgrade" in the dialog or run `kiro-cli agent upgrade` yourself. The radio hooks survive it. Since #267, `task-init` and `task-remove` handle that format too. A re-run of `task-init` recognises radio's entries by their command and adds any that are missing in the array shape, with the name `radio-<trigger>` and the 10-second timeout the upgrade itself gives an entry. `task-remove` and `task-config set` strip only those entries. Neither one converts a config from one format to the other.

---

Expand Down
71 changes: 57 additions & 14 deletions kiro-gh/bin/task-init
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,29 @@ _radio_install_hooks() {
fi

# region:kiro-radio-hooks-jq-merge
# Two shapes of `hooks` reach this merge (#267). task-init writes an object
# keyed by trigger, {"agentSpawn": [{"command": …}], …}. kiro-cli's V3 engine
# can rewrite the file into its "universal" format, where `hooks` is an array
# of {name, trigger, action: {type, command}, timeout} — on the upgrade
# dialog, under chat.enableAutoAgentUpgrade=true, or by `kiro-cli agent
# upgrade`. The merge keeps whichever shape the file already has and never
# converts one into the other: a radio entry is recognised by its command in
# either shape, and a missing one is added in the shape around it, with the
# name / timeout the upgrade itself gives an entry (verified on 2.32.0;
# `kiro-cli agent validate` accepts the result, and a re-run of the upgrade
# still reports the file "already universal").
#
# A universal config with no hooks at all has no `hooks` to read the shape
# off, so its shape comes from `permissions`. That is the field kiro-cli's
# upgrade keys the hooks shape on (probed on 2.32.0): an object `hooks` in a
# file that has `permissions` is reported "out of sync (not rebuilt)", with or
# without the `kiroUpgrade` fingerprint, and an array is accepted in every
# case. `kiroUpgrade` alone would miss a V3-only config, which has no
# fingerprint, and give it the hybrid kiro-cli refuses to rebuild.
#
# The `radio-<trigger>` name can repeat a user entry's name. kiro-cli 2.32.0
# does not require unique names (`agent validate`, `agent upgrade` and
# `agent list` all accept duplicates), so nothing here avoids one.
local target tmp merged=0
for target in "$agents_dir"/*.json; do
[[ -f "$target" ]] || continue
Expand All @@ -300,22 +323,36 @@ _radio_install_hooks() {
--arg ups "$busy_cmd" \
--arg stop "$ready_cmd" \
'
def is_radio: (.command // .action.command? // "") | startswith("radio ");
def add_radio(arr; cmd):
if (arr // []) | any((.command // "") | startswith("radio ")) then arr
if (arr // []) | any(is_radio) then arr
else (arr // []) + [{command: cmd}]
end;
def add_radio_u(trig; cmd):
if any(.[]; .trigger == trig and is_radio) then .
else . + [{name: "radio-\(trig)", trigger: trig,
action: {type: "command", command: cmd}, timeout: 10}]
end;
# `agentStop` is not a valid kiro-cli trigger — an agent config carrying
# one fails to load wholesale, taking every other hook with it. Drop any
# radio-owned entry there; leave anything we did not write alone rather
# than silently editing a key that is not ours.
def drop_radio(arr):
[ (arr // [])[] | select(((.command // "") | startswith("radio ")) | not) ];
.hooks //= {}
| .hooks.agentSpawn = add_radio(.hooks.agentSpawn; $spawn)
| .hooks.userPromptSubmit = add_radio(.hooks.userPromptSubmit; $ups)
| .hooks.stop = add_radio(.hooks.stop; $stop)
| .hooks.agentStop = drop_radio(.hooks.agentStop)
| if (.hooks.agentStop | length) == 0 then del(.hooks.agentStop) else . end
[ (arr // [])[] | select(is_radio | not) ];
if (.hooks | type) == "array" or (.hooks == null and has("permissions")) then
.hooks |= ( (. // [])
| add_radio_u("agentSpawn"; $spawn)
| add_radio_u("userPromptSubmit"; $ups)
| add_radio_u("stop"; $stop)
| map(select((.trigger == "agentStop" and is_radio) | not)) )
else
.hooks //= {}
| .hooks.agentSpawn = add_radio(.hooks.agentSpawn; $spawn)
| .hooks.userPromptSubmit = add_radio(.hooks.userPromptSubmit; $ups)
| .hooks.stop = add_radio(.hooks.stop; $stop)
| .hooks.agentStop = drop_radio(.hooks.agentStop)
| if (.hooks.agentStop | length) == 0 then del(.hooks.agentStop) else . end
end
' "$target" >"$tmp"; then
rm -f "$tmp"
echo "Warning: could not merge radio hooks into $target (invalid JSON?)" >&2
Expand All @@ -330,20 +367,26 @@ _radio_install_hooks() {
# A radio-prefixed command that isn't the one we install blocks add_radio from
# installing the right one, so the repo would keep a stale hook while
# task-init prints success — the same trap #172 found on the claude side.
# Warn loudly rather than silently rewriting a command someone chose.
# Warn loudly rather than silently rewriting a command someone chose. Both
# `hooks` shapes are read, as in the merge above (#267).
local stray="" one
for target in "$agents_dir"/*.json; do
[[ -f "$target" ]] || continue
one=$(jq -r \
--arg spawn "$register_cmd" --arg ups "$busy_cmd" --arg stop "$ready_cmd" \
--arg name "${target##*/}" '
def stray(arr; evt; expected):
[ (arr // [])[] | .command // ""
def entries(evt):
if (.hooks | type) == "array"
then [ .hooks[] | select(.trigger == evt) | .action.command? // "" ]
else [ (.hooks[evt]? // [])[] | .command // "" ]
end;
def stray(evt; expected):
[ entries(evt)[]
| select(startswith("radio ") and . != expected)
| "\($name) \(evt): \(.) — replace with: \(expected)" ];
( stray(.hooks.agentSpawn; "agentSpawn"; $spawn)
+ stray(.hooks.userPromptSubmit; "userPromptSubmit"; $ups)
+ stray(.hooks.stop; "stop"; $stop)
( stray("agentSpawn"; $spawn)
+ stray("userPromptSubmit"; $ups)
+ stray("stop"; $stop)
) | .[]
' "$target" 2>/dev/null || true)
if [[ -n "$one" ]]; then stray+="$one"$'\n'; fi
Expand Down
2 changes: 1 addition & 1 deletion kiro-gh/steering/gh-workflow.example.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ Use the issue title as prefix: `<Issue title>: <short description>`
- `--focus` — switch to the new tab. By default focus stays on the tab you ran `task-work` from, so the PM can dispatch without being pulled away (#254)
- `--auto` — accepted for symmetry with claude, and a no-op here: the permission model stays `-a/--trust-all` (#206), and auto-submit is already on

Under kiro-cli's V3 engine (`chat.agentEngine = v3` today; kiro-cli's help calls v2 "the pre-3.0 default"), every dispatched kiro launch — `task-work`, `task-recreate-worker`, `task-reviewer` — would first stop at a dialog offering to upgrade your agent configs to the V3 format. Nobody answers it in a dispatched tab, so the first turn never runs. The launchers warn when the launch would run on V3 and `chat.enableAutoAgentUpgrade` is unset. Set it once with `kiro-cli settings chat.enableAutoAgentUpgrade false`. With `false` the radio hooks keep working under V3. `true`, or accepting the upgrade, rewrites `.kiro/agents/*.json` into a shape `task-init` cannot merge radio hooks into yet (#257).
Under kiro-cli's V3 engine (`chat.agentEngine = v3` today; kiro-cli's help calls v2 "the pre-3.0 default"), every dispatched kiro launch — `task-work`, `task-recreate-worker`, `task-reviewer` — would first stop at a dialog offering to upgrade your agent configs to the V3 format. Nobody answers it in a dispatched tab, so the first turn never runs. The launchers warn when the launch would run on V3 and `chat.enableAutoAgentUpgrade` is unset. Set it once with `kiro-cli settings chat.enableAutoAgentUpgrade false`. With `false` the radio hooks keep working under V3 (#257). `true`, or accepting the upgrade, rewrites `.kiro/agents/*.json` into kiro-cli's universal format instead; the radio hooks keep working there too, and `task-init` and `task-remove` handle that format since #267.

If local `<base>` is strictly behind `origin/<base>`, `task-work` auto-refreshes and forks the new worktree from `origin/<base>` instead of the stale local tip. Pass `--from` to override.

Expand Down
Loading
Loading