diff --git a/.claude/gh-workflow.md b/.claude/gh-workflow.md index e50f8ac..3ca9e39 100644 --- a/.claude/gh-workflow.md +++ b/.claude/gh-workflow.md @@ -234,6 +234,7 @@ side and committed once. Which one runs is then a local choice, not a commit: task-config add assistant kiro # install the other one beside this, settings carried task-config use kiro # the active assistant: local git config, no tracked file task-config sync # make the two docs' tracker settings agree again +task-config remove assistant kiro # drop one again: the mirror of add, the kept one goes active ``` `add` keeps the assistant you are on active, so nothing changes until you `use` @@ -249,8 +250,13 @@ none active, every dispatcher refuses and names `task-config use`. `show` marks active loadout and flags **drift**, a tracker setting the two workflow docs disagree on. `sync` resolves it: the active assistant's doc wins, or `--from ` picks the other one, and only the other doc is re-rendered. On a dual repo, -`set assistant A` keeps A and removes the other, `set loadout` on another tracker -replaces both, and `set tracker` is refused with the two steps that do it. +`set assistant A` is refused and changes nothing, `--yes` or not (#303): A is +already installed, so the switch is `use A` and dropping the other is +`remove assistant `, which previews its deletions and confirms on a TTY. +`set loadout` onto the same tracker is refused the same way; onto another tracker +it replaces both, and `set tracker` is refused with the two steps that do it. +`show` prints the switch in one line: `task-config use `, or +`task-work … --agent ` for one launch. `task-remove` removes both and clears the setting. jira cannot be dual until a kiro-jira loadout exists (#92). diff --git a/CHANGELOG.md b/CHANGELOG.md index b33ea4a..5b30142 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **`task-monitor-snapshot` works on jq 1.6 again: `$label` is a keyword there (#296).** On a machine with jq 1.6, `task-monitor` showed `snapshot failed` in every jira or notion repo. `_aw_tracker_item_describe_default` in `lib/trackers/_default.sh` bound `--arg label` and read `$label`, and in jq 1.6 `label` is a keyword, so the program failed to compile (`unexpected label, expecting IDENT`). jq 1.7 relaxed this. The empty output then reached the next `--argjson` and failed the whole snapshot. gh and local repos took the same path whenever the forge did not answer, including every `--local` run: 16 of the 51 snapshot tests failed under jq 1.6. The variable is now `$lbl`, and the output is byte-identical. A new test pins the describer's JSON for a key, a URL and a bare slug. With the old name it fails under jq 1.6 and passes under 1.8, so a new CI job, `Bats tests (jq 1.6)`, runs the whole bats suite with the official jq-1.6 binary first on `PATH`. The download is checked against jqlang's sha256 from `sig/v1.6/sha256sum.txt` in the jq repo. The first 1.6 run of the whole suite found a second difference. `jq -e` on empty input exits 0 on 1.6 and 4 on 1.7+, so an empty `zellij action list-tabs --json` answer passed as an array. `task-done` then said `no tab has id=N any more (zellij restarted?)` where it should have said the id could not be verified. It still fails closed, so no tab was ever closed wrongly. The same check is in `aw_zellij_session_running`, where an empty answer read as a running session, and in the snapshot's per-repo check, where an empty repo file passed. All three now go through a slurped check, and both jq versions agree on empty input, `[]`, non-JSON and `{}`. The zellij checks use a new `aw_jq_is_array` in `lib/zellij-tab.sh`, which is stricter than before: it requires exactly one value, where the old check passed a stream whose last value was an array. The snapshot uses `jq -se 'length > 0 and all(type == "object")'`, so a `null` or stray-string repo document is skipped too. New tests cover `aw_jq_is_array` on seven inputs, a live zellij session whose `list-tabs` prints nothing (not running), and `--all` with one repo whose document is empty or a string (skipped, with the warning). Under jq 1.6, putting back the old check turned each of them red. jq 1.6 is now the stated floor, in the README's requirements and in `lib/require-jq.sh`. Not done: the optional per-item degradation, where one item's describe failure blanks only that item. With 1.6 in CI, it would only cover a class of bug CI now catches. **Upgrading:** nothing to run, since `lib/` and `bin/` are read from the checkout. The workaround for anyone not on this version is `brew upgrade jq`. ### Changed +- **`task-config set assistant ` in a dual repo is refused, and `task-config remove assistant` drops one explicitly (#303).** Behaviour change from #293. There, `set assistant A` in a repo with both assistants meant "keep A, remove the other". A user testing the dual setup ran `task-config set assistant claude` expecting a switch, confirmed the prompt, and lost `.kiro/agents/*.json` and the kiro workflow doc, so `task-work … --agent kiro` could no longer launch. `--yes` skipped even that prompt. Now, in a dual repo, `set assistant A` (both are always installed there) exits non-zero and changes nothing, `--yes` or not, and the message names both loadouts, `task-config use A` to switch while keeping the other, and `task-config remove assistant ` to drop it. `set loadout -` is refused the same way; `set loadout` onto another tracker still replaces both, and `set tracker` stays refused. The explicit path is the new `task-config remove assistant `, the mirror of `add`: it previews what it deletes (with `--dry-run` it stops there), confirms on a TTY, removes only B's artifacts (on the local tracker the shared `tasks/` scaffolding stays, as before), and makes the kept assistant active. It refuses the only assistant installed, an assistant that is not installed, and a repo whose two loadouts are on different trackers. Single-assistant repos are unchanged: `set assistant` still swaps. `task-config show` on a dual repo with an active assistant adds one line, `switch: task-config use · launch one: task-work … --agent `. Tests in `tests/dual_assistant.bats` cover both `set assistant` directions and `set loadout claude-gh` refused with a clean `git status` and the active setting unchanged, the refusal with no `--yes` and no prompt, `remove assistant` touching only `.kiro/` and setting the active assistant, its `--dry-run`, its refusals, a single-assistant swap after a remove, and the `show` hint both ways; the two former "keep A, remove the other" tests became `remove assistant` tests. Skipping the refusal, dropping its `exit`, and dropping `remove`'s active-assistant write each turned tests red. Docs: the README's dual-repo section, `.claude/gh-workflow.md`, all seven steering templates, and `task-config --help`. **Upgrading:** nothing is required, since `bin/` is read from the checkout; re-run `task-init ` only for the new wording in your workflow doc. - **The local-loadout PM reads `tasks/_board.md` for the backlog, and the board's Done section is bounded (#294).** The PM's "Showing the backlog" instruction said to read the frontmatter of every `tasks/*.md`, so showing the backlog cost context in proportion to the whole backlog, done tasks included. Both local PM prompts (`claude-local/commands/pm.md`, the kiro-local `agents/pm.json`) now run `task-board` and read the one file it writes, present it as the board has it (grouped by status, priority on each line), and open a task file only when the work is about that task. Staleness is handled by always running `task-board` first rather than by comparing timestamps: the render answers in one line, and an mtime check would miss a change to the `state.json` overlay. `task-board` itself still lists every open task (todo and in-progress) in full, but Done now lists only the 10 newest, highest id first since ids are allocated max+1, followed by one `+ N older done tasks` line. A done task whose id is not a number sorts after all the numbered ones, so it is the first the cap trims. The cap is set by `--done-limit N` or `TASK_BOARD_DONE_LIMIT=N`, the flag winning; `0` lists none and still counts them, and a value that is not a non-negative integer is refused before the board is touched. Todo and In Progress keep their ascending-id order. The planner prompts read only their own task file, so they needed no change. New tests in `tests/task_board.bats` cover all open tasks listed past the cap, the default cap with newest-first order and the count line, the flag, the variable and their precedence, a leading-zero cap read as decimal, a non-numeric id sorting last in Done, a cap of 0, no count line below the cap, and a refused cap; raising the default, sorting ascending, dropping the count line, dropping the base-10 read, keying a non-numeric done id high and ignoring the flag's value each turned at least one of them red. Docs: the README's claude-local section and both local steering templates. **Upgrading: re-run `task-init `** on claude-local and kiro-local, since the copied `commands/pm.md` / `agents/pm.json` changed; `task-board` is picked up through the existing symlink, and the next run trims an existing board. The other loadouts need nothing. - **task-monitor's overview lists every agent under its repo and marks the one that needs you (#287, epic #272).** The overview showed one row per repo, so seeing who was doing what meant pressing ⏎ into each repo in turn. Now each repo row has one line per agent under it: the PM, each in-flight worker, and each reviewer nested under the item whose PR it reviews. Every line gives the agent (`claude` / `kiro`), the state icon (`▶` / `⏸` / `❓`), the state or status words, and the title. `← you` marks the agent behind each attention signal rather than the repo: the PM for `unread-inbox` and `review-waiting-merge`, and the worker that the `worker-awaiting` signal names (by role or slug) for `awaiting`. The signal decides, not the session's `STATE=`, so an `awaiting` left behind by a crashed tab marks nobody. `!` filters the overview to the marked agents under their repo rows, and says `nothing needs you` when nothing does. `t` on an agent line goes to that agent's tab: the worker's sidecar id and slug as in the repo view, and a reviewer's session id with task-reviewer's `review-pr` slug, under the same session and #260 name checks as before. ⏎ on an agent line opens the repo view on its item. **`t` now crosses zellij sessions** (the user's scope addition to #287), reversing the epic's own-session-only rule, in the overview and the repo view alike. For a tab in another session, `goto_tab` lists that session's tabs with `zellij -s action list-tabs --json`, applies the #260 name check, and runs `zellij action switch-session --tab-position `. That moves the client, and the monitor stays in its own session. These were probed on zellij 0.44.2 using throwaway sessions and a pty-attached client. `go-to-tab-by-id` sent to a session with no client attached is ignored: the client that attaches afterwards lands on that session's previous tab. So the switch carries the tab itself. `--tab-position` is 1-based: 1, 2 and 3 focused list positions 0, 1 and 2. The v0.44.2 source agrees: `Screen::go_to_tab` calls `switch_active_tab(tab_index.saturating_sub(1), …)` on the field list-tabs reports as `position`. `switch-session` to a missing session creates it and exits 0, while `list-tabs` against one prints plain text and exits 0. So a `list-tabs` answer that does not parse is refused as `session '' is not running`, and no failed check ever reaches `switch-session`. Tests pin the exact command sequence and that a stale id or a dead session issues no switch. Breaking the 1-based offset, the session comparison, the dead-session refusal and the name check each turned a test red. A narrow terminal cuts the title and keeps `← you`. The overview's selection now follows its row across refreshes and filter toggles. It is keyed by repo name, worker slug and reviewer role, and falls back to the row's index when one is missing, so two nameless rows never share a key. Front end only: `snapshot.rs` now reads the fields the collector already emits for each signal (`count`, `prs`, `workers`). The `multi.json` fixture gains the `worker-awaiting` workers the collector emits, a kiro PM and task-force's own `worker-awaiting` signal. New Rust tests cover the row order, which agent each signal marks, the filter, `t` and ⏎ on agent rows, selection across a reorder, and rendering at 120 and 60 columns. Each of the nine mutations tried (dropping a PM signal, the role match or the slug match, the empty-repo skip, the reviewer slug, the reviewer rows, `← you` on narrow rows, the awaiting words, and ⏎'s item selection) turned a test red. Docs: the README `task-monitor` section (an overview mockup, its keys and the cross-session `t`), the `task-monitor` paragraph in `.claude/gh-workflow.md` and all seven steering templates, and `task-monitor --help`. **Upgrading:** re-run your loadout's `install.sh` to rebuild the binary. Re-run `task-init ` only if you want the refreshed wording in your workflow doc. diff --git a/README.md b/README.md index dafde50..1e1048a 100644 --- a/README.md +++ b/README.md @@ -318,6 +318,7 @@ task-config add assistant kiro # install kiro- beside claude- merge ` it needs, and say so when the new loadout is not committed yet, since a merge brings only what is (#304). It is a warning, not a refusal. `task-recreate-worker` refuses to launch there, and `task-work --from` refuses to fork from such a ref. A clone that has both committed but no local choice yet is refused by every dispatcher, with a pointer to `task-config use`. Linked worktrees share the repo's git config, so a worker's worktree resolves the same way its main checkout does. @@ -334,7 +335,7 @@ Two loadouts on different trackers stay refused, whatever the active assistant s **Drift.** The tracker settings are written into both workflow docs. `add` and `set` keep them identical, but a hand edit to one leaves the two assistants pointed at different boards. `task-config show` compares them on a dual repo, names every field that differs with both values, and exits non-zero. There is one way to resolve it: `task-config sync`. The active assistant's doc wins, or `sync --from ` picks the other one. Only the other doc is re-rendered, through its own `task-init --force --workflow` with the settings as flags. -**`set` and `task-remove` on a dual repo.** `set assistant A` keeps A and removes the other. `set loadout` on another tracker removes both and installs the one named. `set tracker` is refused with the two commands that do it (`set loadout -`, then `add assistant`), because it would have to reinstall both. `task-remove` removes both, as it removes every detected loadout, and clears the active-assistant setting. On the local tracker the pair shares the `tasks/` scaffolding and the `.gitignore` entry, so those stay while either loadout does and go with the last. +**`set`, `remove` and `task-remove` on a dual repo.** `set assistant A` is refused and changes nothing, `--yes` or not (#303). A is already installed, so the command can only mean one of two things, and #293 guessed the destructive one: a user testing the dual setup ran `set assistant claude` expecting a switch and lost every kiro file. The refusal names both: `task-config use A` switches and keeps the other, and `task-config remove assistant ` drops the other. `remove assistant` is the mirror of `add`: it previews what it deletes, confirms on a TTY, and makes the kept assistant active. It refuses the only assistant installed, since that would leave no loadout (use `set assistant` or `task-remove`). `set loadout` onto the same tracker is refused like `set assistant`; onto another tracker it removes both and installs the one named. `task-config show` on a dual repo prints the switch in one line: `switch: task-config use · launch one: task-work … --agent `. In a single-assistant repo `set assistant` still swaps the one for the other. `set tracker` is refused with the two commands that do it (`set loadout -`, then `add assistant`), because it would have to reinstall both. `task-remove` removes both, as it removes every detected loadout, and clears the active-assistant setting. On the local tracker the pair shares the `tasks/` scaffolding and the `.gitignore` entry, so those stay while either loadout does and go with the last. jira cannot be dual until a kiro-jira loadout exists (#92): `add assistant kiro` on a claude-jira repo is refused with the same message as `set`. diff --git a/bin/task-config b/bin/task-config index f1ba10a..63aac59 100755 --- a/bin/task-config +++ b/bin/task-config @@ -23,6 +23,7 @@ Usage: task-config [show] task-config set tracker [--dry-run] [--yes] task-config set loadout [--dry-run] [--yes] task-config add assistant [--dry-run] [--yes] + task-config remove assistant [--dry-run] [--yes] task-config use [--dry-run] task-config sync [--from ] [--dry-run] [--yes] @@ -42,6 +43,10 @@ add assistant The assistant you are on now becomes the active one, so nothing changes until you say so. jira cannot be dual until a kiro-jira loadout exists (#92). +remove assistant + The mirror of add: uninstall one of the two assistants, keep the other, and + make it the active one. Previews what it deletes and confirms on a TTY. + use Pick the active assistant in a repo with both installed. It is written to the repo's local git config (task-force.assistant), never to a tracked file, and @@ -82,6 +87,7 @@ Examples task-config set loadout claude-notion --yes task-config add assistant kiro task-config use kiro + task-config remove assistant kiro task-config sync --from claude EOF exit 0 @@ -285,7 +291,11 @@ _show_dual() { printf 'Dual: %s are installed on %s.\n' \ "$(printf '%s\n' "${MATCHES[@]}" | sed 's/-.*//' | paste -sd'+' - | sed 's/+/ and /g')" "$tracker" if [[ -n "$active" ]] && _in_list "$active-$tracker" "${MATCHES[@]}"; then - printf 'active : %s (git config %s — local, not committed)\n\n' "$active" "$AW_ACTIVE_ASSISTANT_KEY" + printf 'active : %s (git config %s — local, not committed)\n' "$active" "$AW_ACTIVE_ASSISTANT_KEY" + # `set assistant` is not the switch here (#303), so say what is. + local other + other=$(printf '%s\n' "${MATCHES[@]}" | sed 's/-.*//' | grep -vxF "$active" | head -1) + printf 'switch: task-config use %s · launch one: task-work … --agent %s\n\n' "$other" "$other" else if [[ -n "$active" ]]; then printf 'active : %s — not installed here, so none is active\n' "$active" @@ -566,9 +576,12 @@ _set_target() { } # _set_dual — a `set` on a repo with both assistants -# installed on (#293). Every outcome is a single loadout: +# installed on (#293). Every outcome that changes anything is a +# single loadout: # -# set assistant A keep A, remove the other — "stop being dual" +# set assistant A refused (#303): A is installed, so this is either a +# switch (`use A`) or a removal (`remove assistant B`), +# and guessing the second deleted a user's kiro files # set loadout - the same when T is this tracker; otherwise remove both # and install the one named # set tracker T refused: it would have to reinstall both, and the second @@ -602,17 +615,23 @@ _set_dual() { for m in "${MATCHES[@]}"; do [[ "$m" == "$target" ]] || drop+=("$m"); done if ((${#drop[@]} < ${#MATCHES[@]})); then - # The target is already installed: this is just the other one going. - printf '%s → %s (keeping %s, removing %s)\n' \ - "$(printf '%s+' "${MATCHES[@]}" | sed 's/+$//')" "$target" "$target" "${drop[*]}" - if _previewing; then - for m in "${drop[@]}"; do printf '\nremoving %s:\n' "$m"; la_plan "$REPO_ROOT" "$m"; done - fi - _go_ahead - for m in "${drop[@]}"; do printf '\n'; la_remove "$REPO_ROOT" "$m"; done - _set_active "$(la_assistant "$target")" - printf '\nNow on %s alone.\nVerify with: task-config show\n' "$target" - return 0 + # The target is already installed. Before #303 this kept it and removed the + # other, and `set assistant claude` reads as "switch to claude": a user + # testing the dual setup confirmed it and lost every kiro file. A switch is + # `use`; dropping an assistant is `remove assistant`, which says so. Refused + # whatever --yes says, since the refusal is about meaning, not consent. + local keep other + keep=$(la_assistant "$target") + other=$(la_assistant "${drop[0]}") + { + echo "Error: both assistants are installed here (${MATCHES[*]}), so $target is" + echo " already installed and there is nothing to switch to. Nothing was changed." + echo " Make $keep the active one, keeping $other:" + echo " task-config use $keep" + echo " Or uninstall $other and keep $keep alone:" + echo " task-config remove assistant $other" + } >&2 + exit 1 fi # A loadout on another tracker: both go, and the target is a fresh install. @@ -764,6 +783,64 @@ cmd_add() { return 0 } +# remove assistant — the mirror of `add` (#303): uninstall B from a repo +# with both assistants on one tracker, keep the other, and make it active. The +# explicit form of what `set assistant ` used to do by surprise. +cmd_remove() { + local axis="$1" agent="$2" + [[ "$axis" == assistant && -n "$agent" ]] || { + echo "Error: expected: task-config remove assistant <$(IFS='|'; echo "${ASSISTANTS[*]}")>" >&2; exit 1; } + _in_list "$agent" "${ASSISTANTS[@]}" || { + echo "Error: unknown assistant '$agent' — known: ${ASSISTANTS[*]}" >&2; exit 1; } + [[ -z "$PIN" ]] || { echo "Error: remove takes no --impl (and ignores no \$AW_IMPL): unset it first" >&2; exit 1; } + + _collect_matches + local tracker + case "${#MATCHES[@]}" in + 0) + echo "Error: no loadout configured in $REPO_ROOT — there is no assistant to remove." >&2 + exit 1 ;; + 1) + if [[ "$(la_assistant "${MATCHES[0]}")" == "$agent" ]]; then + echo "Error: $agent is the only assistant installed here (${MATCHES[0]}); removing it" >&2 + echo " would leave no loadout. Switch it with task-config set assistant ," >&2 + echo " or take task-force out entirely with task-remove." >&2 + else + echo "Error: $agent is not installed here (configured: ${MATCHES[0]}) — nothing to remove." >&2 + fi + exit 1 ;; + *) + tracker=$(_dual_tracker) || { + echo "Error: the loadouts here are on different trackers:" >&2 + printf ' - %s\n' "${MATCHES[@]}" >&2 + echo " remove assistant drops one of two assistants on ONE tracker;" >&2 + echo " remove a loadout with task-config set loadout --impl ." >&2 + exit 1; } ;; + esac + + local gone="$agent-$tracker" keep m + _in_list "$gone" "${MATCHES[@]}" || { + echo "Error: $gone is not installed here (configured: ${MATCHES[*]}) — nothing to remove." >&2; exit 1; } + for m in "${MATCHES[@]}"; do [[ "$m" == "$gone" ]] || keep="$m"; done + + printf '%s → %s (removing %s, keeping %s)\n' \ + "$(printf '%s+' "${MATCHES[@]}" | sed 's/+$//')" "$keep" "$gone" "$keep" + if _previewing; then + printf '\nremoving %s:\n' "$gone" + la_plan "$REPO_ROOT" "$gone" + printf '\n%s is left untouched.\n' "$keep" + printf 'active assistant → %s (git config %s; local, nothing tracked)\n' \ + "$(la_assistant "$keep")" "$AW_ACTIVE_ASSISTANT_KEY" + fi + _go_ahead + printf '\n' + la_remove "$REPO_ROOT" "$gone" + _set_active "$(la_assistant "$keep")" + printf '\nNow on %s alone; active assistant: %s.\n' "$keep" "$(la_assistant "$keep")" + printf 'Put it back with: task-config add assistant %s\n' "$agent" + return 0 +} + cmd_use() { local agent="$1" [[ -n "$agent" ]] || { @@ -864,10 +941,11 @@ cmd_sync() { } case "$SUBCMD" in - show) cmd_show ;; - set) cmd_set "$AXIS" "$VALUE" ;; - add) cmd_add "$AXIS" "$VALUE" ;; - use) cmd_use "$AXIS" ;; - sync) cmd_sync ;; - *) echo "Error: unknown subcommand '$SUBCMD' — expected show, set, add, use or sync" >&2; exit 1 ;; + show) cmd_show ;; + set) cmd_set "$AXIS" "$VALUE" ;; + add) cmd_add "$AXIS" "$VALUE" ;; + remove) cmd_remove "$AXIS" "$VALUE" ;; + use) cmd_use "$AXIS" ;; + sync) cmd_sync ;; + *) echo "Error: unknown subcommand '$SUBCMD' — expected show, set, add, remove, use or sync" >&2; exit 1 ;; esac diff --git a/claude-gh/steering/gh-workflow.example.md b/claude-gh/steering/gh-workflow.example.md index 5cbd933..e5c64f7 100644 --- a/claude-gh/steering/gh-workflow.example.md +++ b/claude-gh/steering/gh-workflow.example.md @@ -230,6 +230,7 @@ side and committed once. Which one runs is then a local choice, not a commit: task-config add assistant kiro # install the other one beside this, settings carried task-config use kiro # the active assistant: local git config, no tracked file task-config sync # make the two docs' tracker settings agree again +task-config remove assistant kiro # drop one again: the mirror of add, the kept one goes active ``` `add` keeps the assistant you are on active, so nothing changes until you `use` @@ -245,8 +246,13 @@ none active, every dispatcher refuses and names `task-config use`. `show` marks active loadout and flags **drift**, a tracker setting the two workflow docs disagree on. `sync` resolves it: the active assistant's doc wins, or `--from ` picks the other one, and only the other doc is re-rendered. On a dual repo, -`set assistant A` keeps A and removes the other, `set loadout` on another tracker -replaces both, and `set tracker` is refused with the two steps that do it. +`set assistant A` is refused and changes nothing, `--yes` or not (#303): A is +already installed, so the switch is `use A` and dropping the other is +`remove assistant `, which previews its deletions and confirms on a TTY. +`set loadout` onto the same tracker is refused the same way; onto another tracker +it replaces both, and `set tracker` is refused with the two steps that do it. +`show` prints the switch in one line: `task-config use `, or +`task-work … --agent ` for one launch. `task-remove` removes both and clears the setting. jira cannot be dual until a kiro-jira loadout exists (#92). diff --git a/claude-jira/steering/jira-workflow.example.md b/claude-jira/steering/jira-workflow.example.md index 29bf87c..9382d83 100644 --- a/claude-jira/steering/jira-workflow.example.md +++ b/claude-jira/steering/jira-workflow.example.md @@ -230,6 +230,7 @@ side and committed once. Which one runs is then a local choice, not a commit: task-config add assistant kiro # install the other one beside this, settings carried task-config use kiro # the active assistant: local git config, no tracked file task-config sync # make the two docs' tracker settings agree again +task-config remove assistant kiro # drop one again: the mirror of add, the kept one goes active ``` `add` keeps the assistant you are on active, so nothing changes until you `use` @@ -245,8 +246,13 @@ none active, every dispatcher refuses and names `task-config use`. `show` marks active loadout and flags **drift**, a tracker setting the two workflow docs disagree on. `sync` resolves it: the active assistant's doc wins, or `--from ` picks the other one, and only the other doc is re-rendered. On a dual repo, -`set assistant A` keeps A and removes the other, `set loadout` on another tracker -replaces both, and `set tracker` is refused with the two steps that do it. +`set assistant A` is refused and changes nothing, `--yes` or not (#303): A is +already installed, so the switch is `use A` and dropping the other is +`remove assistant `, which previews its deletions and confirms on a TTY. +`set loadout` onto the same tracker is refused the same way; onto another tracker +it replaces both, and `set tracker` is refused with the two steps that do it. +`show` prints the switch in one line: `task-config use `, or +`task-work … --agent ` for one launch. `task-remove` removes both and clears the setting. jira cannot be dual until a kiro-jira loadout exists (#92). diff --git a/claude-local/steering/local-workflow.example.md b/claude-local/steering/local-workflow.example.md index 8a0b483..ebf55b2 100644 --- a/claude-local/steering/local-workflow.example.md +++ b/claude-local/steering/local-workflow.example.md @@ -263,6 +263,7 @@ side and committed once. Which one runs is then a local choice, not a commit: task-config add assistant kiro # install the other one beside this, settings carried task-config use kiro # the active assistant: local git config, no tracked file task-config sync # make the two docs' tracker settings agree again +task-config remove assistant kiro # drop one again: the mirror of add, the kept one goes active ``` `add` keeps the assistant you are on active, so nothing changes until you `use` @@ -278,8 +279,13 @@ none active, every dispatcher refuses and names `task-config use`. `show` marks active loadout and flags **drift**, a tracker setting the two workflow docs disagree on. `sync` resolves it: the active assistant's doc wins, or `--from ` picks the other one, and only the other doc is re-rendered. On a dual repo, -`set assistant A` keeps A and removes the other, `set loadout` on another tracker -replaces both, and `set tracker` is refused with the two steps that do it. +`set assistant A` is refused and changes nothing, `--yes` or not (#303): A is +already installed, so the switch is `use A` and dropping the other is +`remove assistant `, which previews its deletions and confirms on a TTY. +`set loadout` onto the same tracker is refused the same way; onto another tracker +it replaces both, and `set tracker` is refused with the two steps that do it. +`show` prints the switch in one line: `task-config use `, or +`task-work … --agent ` for one launch. `task-remove` removes both and clears the setting. jira cannot be dual until a kiro-jira loadout exists (#92). diff --git a/claude-notion/steering/notion-workflow.example.md b/claude-notion/steering/notion-workflow.example.md index 9e171f6..1563e54 100644 --- a/claude-notion/steering/notion-workflow.example.md +++ b/claude-notion/steering/notion-workflow.example.md @@ -239,6 +239,7 @@ side and committed once. Which one runs is then a local choice, not a commit: task-config add assistant kiro # install the other one beside this, settings carried task-config use kiro # the active assistant: local git config, no tracked file task-config sync # make the two docs' tracker settings agree again +task-config remove assistant kiro # drop one again: the mirror of add, the kept one goes active ``` `add` keeps the assistant you are on active, so nothing changes until you `use` @@ -254,8 +255,13 @@ none active, every dispatcher refuses and names `task-config use`. `show` marks active loadout and flags **drift**, a tracker setting the two workflow docs disagree on. `sync` resolves it: the active assistant's doc wins, or `--from ` picks the other one, and only the other doc is re-rendered. On a dual repo, -`set assistant A` keeps A and removes the other, `set loadout` on another tracker -replaces both, and `set tracker` is refused with the two steps that do it. +`set assistant A` is refused and changes nothing, `--yes` or not (#303): A is +already installed, so the switch is `use A` and dropping the other is +`remove assistant `, which previews its deletions and confirms on a TTY. +`set loadout` onto the same tracker is refused the same way; onto another tracker +it replaces both, and `set tracker` is refused with the two steps that do it. +`show` prints the switch in one line: `task-config use `, or +`task-work … --agent ` for one launch. `task-remove` removes both and clears the setting. jira cannot be dual until a kiro-jira loadout exists (#92). diff --git a/kiro-gh/steering/gh-workflow.example.md b/kiro-gh/steering/gh-workflow.example.md index 9edae44..dab1ea0 100644 --- a/kiro-gh/steering/gh-workflow.example.md +++ b/kiro-gh/steering/gh-workflow.example.md @@ -229,6 +229,7 @@ side and committed once. Which one runs is then a local choice, not a commit: task-config add assistant kiro # install the other one beside this, settings carried task-config use kiro # the active assistant: local git config, no tracked file task-config sync # make the two docs' tracker settings agree again +task-config remove assistant kiro # drop one again: the mirror of add, the kept one goes active ``` `add` keeps the assistant you are on active, so nothing changes until you `use` @@ -244,8 +245,13 @@ none active, every dispatcher refuses and names `task-config use`. `show` marks active loadout and flags **drift**, a tracker setting the two workflow docs disagree on. `sync` resolves it: the active assistant's doc wins, or `--from ` picks the other one, and only the other doc is re-rendered. On a dual repo, -`set assistant A` keeps A and removes the other, `set loadout` on another tracker -replaces both, and `set tracker` is refused with the two steps that do it. +`set assistant A` is refused and changes nothing, `--yes` or not (#303): A is +already installed, so the switch is `use A` and dropping the other is +`remove assistant `, which previews its deletions and confirms on a TTY. +`set loadout` onto the same tracker is refused the same way; onto another tracker +it replaces both, and `set tracker` is refused with the two steps that do it. +`show` prints the switch in one line: `task-config use `, or +`task-work … --agent ` for one launch. `task-remove` removes both and clears the setting. jira cannot be dual until a kiro-jira loadout exists (#92). diff --git a/kiro-local/steering/local-workflow.example.md b/kiro-local/steering/local-workflow.example.md index 0147dee..7a9ff4f 100644 --- a/kiro-local/steering/local-workflow.example.md +++ b/kiro-local/steering/local-workflow.example.md @@ -262,6 +262,7 @@ side and committed once. Which one runs is then a local choice, not a commit: task-config add assistant kiro # install the other one beside this, settings carried task-config use kiro # the active assistant: local git config, no tracked file task-config sync # make the two docs' tracker settings agree again +task-config remove assistant kiro # drop one again: the mirror of add, the kept one goes active ``` `add` keeps the assistant you are on active, so nothing changes until you `use` @@ -277,8 +278,13 @@ none active, every dispatcher refuses and names `task-config use`. `show` marks active loadout and flags **drift**, a tracker setting the two workflow docs disagree on. `sync` resolves it: the active assistant's doc wins, or `--from ` picks the other one, and only the other doc is re-rendered. On a dual repo, -`set assistant A` keeps A and removes the other, `set loadout` on another tracker -replaces both, and `set tracker` is refused with the two steps that do it. +`set assistant A` is refused and changes nothing, `--yes` or not (#303): A is +already installed, so the switch is `use A` and dropping the other is +`remove assistant `, which previews its deletions and confirms on a TTY. +`set loadout` onto the same tracker is refused the same way; onto another tracker +it replaces both, and `set tracker` is refused with the two steps that do it. +`show` prints the switch in one line: `task-config use `, or +`task-work … --agent ` for one launch. `task-remove` removes both and clears the setting. jira cannot be dual until a kiro-jira loadout exists (#92). diff --git a/kiro-notion/steering/notion-workflow.example.md b/kiro-notion/steering/notion-workflow.example.md index 18eb893..07ff195 100644 --- a/kiro-notion/steering/notion-workflow.example.md +++ b/kiro-notion/steering/notion-workflow.example.md @@ -218,6 +218,7 @@ side and committed once. Which one runs is then a local choice, not a commit: task-config add assistant kiro # install the other one beside this, settings carried task-config use kiro # the active assistant: local git config, no tracked file task-config sync # make the two docs' tracker settings agree again +task-config remove assistant kiro # drop one again: the mirror of add, the kept one goes active ``` `add` keeps the assistant you are on active, so nothing changes until you `use` @@ -233,8 +234,13 @@ none active, every dispatcher refuses and names `task-config use`. `show` marks active loadout and flags **drift**, a tracker setting the two workflow docs disagree on. `sync` resolves it: the active assistant's doc wins, or `--from ` picks the other one, and only the other doc is re-rendered. On a dual repo, -`set assistant A` keeps A and removes the other, `set loadout` on another tracker -replaces both, and `set tracker` is refused with the two steps that do it. +`set assistant A` is refused and changes nothing, `--yes` or not (#303): A is +already installed, so the switch is `use A` and dropping the other is +`remove assistant `, which previews its deletions and confirms on a TTY. +`set loadout` onto the same tracker is refused the same way; onto another tracker +it replaces both, and `set tracker` is refused with the two steps that do it. +`show` prints the switch in one line: `task-config use `, or +`task-work … --agent ` for one launch. `task-remove` removes both and clears the setting. jira cannot be dual until a kiro-jira loadout exists (#92). diff --git a/tests/dual_assistant.bats b/tests/dual_assistant.bats index f556932..0a25ca3 100644 --- a/tests/dual_assistant.bats +++ b/tests/dual_assistant.bats @@ -15,7 +15,9 @@ # different trackers; # - the two docs' tracker settings are checked for drift, and `sync` fixes it; # - `set` and `task-remove` handle the dual state, keeping what the pair shares -# until the last of them goes; +# until the last of them goes; `set assistant` (or `set loadout` on the same +# tracker) is refused there, and `remove assistant` is the explicit way to +# drop one (#303); # - kiro-jira does not exist (#92), so jira cannot be dual. bats_load_library bats-support @@ -448,18 +450,123 @@ _sum() { # set on a dual repo # --------------------------------------------------------------------------- -@test "set assistant on a dual repo keeps that assistant and removes the other" { +# A dual repo committed clean, claude active: the state a refused command must +# leave exactly as it found it. +_dual_committed() { _init claude-gh --owner acme --repo widgets --project 7 run "$TASK_CONFIG" add assistant kiro --yes - run "$TASK_CONFIG" set assistant kiro --yes assert_success - assert_output --partial "keeping kiro-gh, removing claude-gh" - assert [ ! -e "$MAIN_REPO/.claude/gh-workflow.md" ] + _commit_all dual +} + +_assert_untouched() { + run git -C "$MAIN_REPO" status --porcelain + assert_output "" + assert_equal "$(_active)" claude + assert [ -f "$MAIN_REPO/.claude/gh-workflow.md" ] assert [ -f "$MAIN_REPO/.kiro/steering/gh-workflow.md" ] + assert [ -f "$MAIN_REPO/.kiro/agents/worker.json" ] +} + +@test "set assistant on a dual repo is refused, changes nothing, and names use and remove (#303)" { + _dual_committed + local a + for a in claude kiro; do + run "$TASK_CONFIG" set assistant "$a" --yes + assert_failure + assert_output --partial "both assistants are installed here (claude-gh kiro-gh)" + assert_output --partial "task-config use $a" + assert_output --partial "task-config remove assistant $( [[ $a == claude ]] && echo kiro || echo claude )" + refute_output --partial "removed" + _assert_untouched + done +} + +@test "set loadout - on a dual repo is refused like set assistant (#303)" { + _dual_committed + run "$TASK_CONFIG" set loadout claude-gh --yes + assert_failure + assert_output --partial "task-config use claude" + assert_output --partial "task-config remove assistant kiro" + _assert_untouched +} + +@test "set assistant on a dual repo is refused without --yes too, before any prompt (#303)" { + _dual_committed + run bash -c "'$TASK_CONFIG' set assistant claude