From f2585342e8fc7ae239d44326beda0f0275180fed Mon Sep 17 00:00:00 2001 From: Robert Gering Date: Thu, 6 Aug 2026 11:43:31 +0200 Subject: [PATCH 1/2] Offer PATH-detected cc-harness agents at /kickoff (1.12.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a `cc-harness-agents` helper is on PATH, /kickoff merges its foreign agents into the picker: full Claude Code sessions driven by a foreign model (cc-harness:grok, :kimi, :sol) via a local gateway. Auto-detected via one `command -v`; helper absent means no change from today. - agent-registry.sh: merge the helper's 4-column TSV (name/model/available/ note) into the 5-column list as cli=cc-harness, map only literal `available=yes` to available (fail closed on `unknown`), and resolve cc-harness: to `cc-harness-agents exec -- claude [-n ] /work-system:continue` — no --model, the helper sets it via env before exec'ing into claude, so the herdr pane roots at claude and agent_status + /close stay intact. - Helper exit 3 (capability absent) is a silent degrade, distinct from a listed-but-unavailable provider (resolve exit 3 + the helper's fix hint). - No per-agent code: whatever `list` prints becomes a picker entry — tested with a name the plugin has never shipped. - Lifecycle parity: supports= is the full claude set; a committed cc-harness: default validates through the helper and falls back to the picker when it is gone. - Docs: plugins/work-system/docs/cc-harness-agents.md (contract + setup), knowledge, READMEs, CHANGELOG; kickoff/adopt picker prose. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01DE9DzwzzsMMu84KxC1cB5R --- .claude-plugin/marketplace.json | 4 +- .claude/knowledge/_index.md | 2 +- .../features/kickoff-agent-selection.md | 67 +++++- CHANGELOG.md | 9 + CLAUDE.md | 2 +- README.md | 2 +- .../work-system/.claude-plugin/plugin.json | 4 +- plugins/work-system/README.md | 37 +++- plugins/work-system/docs/cc-harness-agents.md | 124 +++++++++++ plugins/work-system/scripts/agent-registry.sh | 198 ++++++++++++++++-- plugins/work-system/scripts/herdr-launch.sh | 18 +- .../scripts/test_agent_registry.py | 171 ++++++++++++++- plugins/work-system/skills/adopt/SKILL.md | 20 +- plugins/work-system/skills/kickoff/SKILL.md | 87 +++++--- 14 files changed, 653 insertions(+), 92 deletions(-) create mode 100644 plugins/work-system/docs/cc-harness-agents.md diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index dc54678..5d03a96 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -17,8 +17,8 @@ { "name": "work-system", "source": "./plugins/work-system", - "description": "Generic task and worktree workflow system for Claude Code. Manage tasks as markdown files, run them in isolated git worktrees with a choice of worker agent (Claude, codex, grok, or kimi), and track progress across the define/kickoff/continue/status/close lifecycle.", - "version": "1.11.0" + "description": "Generic task and worktree workflow system for Claude Code. Manage tasks as markdown files, run them in isolated git worktrees with a choice of worker agent (Claude, codex, grok, kimi, or a PATH-detected cc-harness agent), and track progress across the define/kickoff/continue/status/close lifecycle.", + "version": "1.12.0" }, { "name": "pr-flow", diff --git a/.claude/knowledge/_index.md b/.claude/knowledge/_index.md index 30ad4f7..fe590f6 100644 --- a/.claude/knowledge/_index.md +++ b/.claude/knowledge/_index.md @@ -16,7 +16,7 @@ - `features/lane-registry.md` — `lanes.sh` + `herdr-agent.sh` (Wave 1): the one herdr-agent wrapper (degrade-not-block, bounded wait) + centralized `$HERDR_MATCH_PRELUDE` cwd↔worktree match (consumed by herdr-tab-glyph, regression-guarded via live snapshot); lanes.sh joins states+liveness keyed by worktree_path with a worktree-tab-state degrade tri-state; env test-seams for hermetic join tests - `features/herdr-close-automation.md` — `/close` in herdr: cwd-tab teardown, plugin SessionEnd hook, the one TUI-exit primitive, detached self-exit onto idle - `features/herdr-tab-glyphs.md` — Task-state glyphs (`○ ● ◇ ◆ ✓`) + main-root `◉` on herdr tab labels: `states` mode in the self-contained renderer, sync-vs-`--cached` PR refresh per caller, exact-cwd rename rules, soft pr-flow shim -- `features/kickoff-agent-selection.md` — `/kickoff` worker choice: single committed per-repo default (no global/fallback/ranking) else picker; `agent-registry.sh` as SoT; bounded model-aware grok/kimi probes (inconclusive→trust-auth); kimi's two-phase seed+continue argv + `argv_shell=`; non-claude "document, don't fake" degradation; announce-not-prompt for external defaults +- `features/kickoff-agent-selection.md` — `/kickoff` worker choice: single committed per-repo default (no global/fallback/ranking) else picker; `agent-registry.sh` as SoT; optional PATH-detected `cc-harness:` class (pure consumer of `list`/`exec`, no gateway hardcoding); bounded model-aware grok/kimi probes (inconclusive→trust-auth); kimi's two-phase seed+continue argv + `argv_shell=`; non-claude "document, don't fake" degradation; announce-not-prompt for external defaults - `features/task-archiving-on-close.md` — `/close` archives (not deletes) the task file; adaptive commit + ff-push to main; per-repo `.claude/work-system-close-autocommit` opt-in skips the ask - `features/swarm-backend-adapter.md` — 0.6.0 read+web posture: OS secret-jail (denylist, worktree-aware, git-config-safe), per-voice fail-closed degrade, `jail` verb, prompt egress guard + residual risks; plus verified codex/grok CLI facts (schema JSON, effort mapping, model-aware readiness) - `features/swarm-review-pipeline.md` — `/swarm:review` pipeline: skill↔Workflow wiring, family-consensus, 0.5.0 lens clusters + design-kind verify, `--fix`/`--loop` (deterministic close-out via `loop-closeout.py`), `--pr` publish via deterministic `pr-post.py` diff --git a/.claude/knowledge/features/kickoff-agent-selection.md b/.claude/knowledge/features/kickoff-agent-selection.md index 9a9551f..25d8844 100644 --- a/.claude/knowledge/features/kickoff-agent-selection.md +++ b/.claude/knowledge/features/kickoff-agent-selection.md @@ -3,8 +3,8 @@ title: "Kickoff Agent Selection: registry, per-repo default, honest degradation" createdAt: 2026-07-17 updatedAt: 2026-08-05 createdFrom: "session: 2026-07-17 (task/kickoff-agent-selection)" -updatedFrom: "session: 2026-08-05 (task/add-kimi-worker-support, post-swarm)" -pluginVersion: 1.11.0 +updatedFrom: "session: 2026-08-05 (task/offer-cc-harness-agents-at-kickoff)" +pluginVersion: 1.12.0 prime: false --- @@ -15,22 +15,67 @@ prime: false ## Single per-repo default, no global, no fallback The **only** persisted selection state is one committed -`/.claude/work-system-agent` (`default=`). No global per-user -default, no shipped fallback, no `--auto` ranking, no `--last`. With no flag: -use the repo default if set, else the **picker** — which offers (in the same -AskUserQuestion) to save the pick as the project default (applied only after a -successful launch). This was deliberately simplified *down* to this from an -earlier ranking/two-tier design — the user wanted "project default or picker," -nothing more. `--pick` forces the picker even when a default exists. +`/.claude/work-system-agent` (`default=` or +`default=cc-harness:`). No global per-user default, no shipped fallback, no +`--auto` ranking, no `--last`. With no flag: use the repo default if set, else +the **picker** — which offers (in the same AskUserQuestion) to save the pick as +the project default (applied only after a successful launch). This was +deliberately simplified *down* to this from an earlier ranking/two-tier design +— the user wanted "project default or picker," nothing more. `--pick` forces +the picker even when a default exists. ## Registry is the single source of truth `scripts/agent-registry.sh` owns aliases (`--fable`/`--opus`/`--codex`/`--sol`/ -`--grok`/`--kimi`/`--agent cli[:model]`), the launch argv per CLI, availability, and +`--grok`/`--kimi`/`--agent cli[:model]`), the optional PATH-detected +`cc-harness:` class, the launch argv per CLI, availability, and `default get`/`set`. `herdr-launch.sh` stays CLI-agnostic: it execs the resolved `argv=` words (argv-exec, no shell-typing race — same reason as the kickoff launch). Skills never hardcode the CLI list. `default get` **validates** its committed value against the registry — a stale/removed/attacker-supplied name -reads as "no default" (→ picker), never routes or bricks kickoff. +(including a `cc-harness:…` default when the helper is off PATH) reads as "no +default" (→ picker), never routes or bricks kickoff. + +## Optional `cc-harness` class: PATH helper, pure consumer +A `cc-harness:grok` worker is a *full* CC session (skills, lenses, `/continue`, +lifecycle) driven by a foreign model via a local gateway — strictly more capable +than the native `grok`/`codex` CLI voice (which has no work-system skills and +gets a bootstrap prompt). Its subagents also run on the foreign model. + +Detection is one `command -v cc-harness-agents`. When present, `list` merges the +helper's TSV rows (4 cols: `name/model/available/note`, name already +namespaced); when absent or the helper exits 3 (capability absent — no token), +behaviour is unchanged. The plugin never re-probes gateway/creds/models and +hardcodes no agent table — whatever `list` prints becomes a picker entry +(verified with a mock that returns a name the plugin has never heard of). +Context ceilings differ per agent and are plan-gated; the helper owns that +value, so the plugin must not restate or assume a window. + +Resolve shape (no `--model` — the helper sets it via env, then `exec`s into +claude so the herdr pane roots at claude and agent_status + `/close` stay +intact): + + cc-harness-agents exec -- claude [-n ] /work-system:continue + +`supports=` is the full claude set (`continue,close-exit,statusline,commit,pr`) +— lifecycle parity is the whole point. Full contract + setup sketch: +`plugins/work-system/docs/cc-harness-agents.md`. Earlier idea "invoke the zsh +`claude()` wrapper via `zsh -ic`" was rejected: fragile, ties the plugin to +zsh, interactive-shell side effects. + +## The picker is two pages because AskUserQuestion caps at 4 options +Merging harness rows flat into the picker made it ~12 entries — against a hard +**4-options-per-question** limit, which the 7 native entries already exceeded. +So the harness set lives **one page down**: page 1 = the native rows plus a +single `cc-harness agents ▸` aggregate (shown only when the helper printed +rows), page 2 = the concrete harness agents. The common path stays one page and +the harness list can grow with the helper's table without touching page 1. + +Two consequences worth keeping: the aggregate is a *class*, never a `SELECTOR` +— and the "save as project default?" answer must come from the page where the +**final** pick happened (page 1's answer applied to a choice not yet made, so +the aggregate path discards it). Where a set still exceeds 4, the rule is +*consolidate and say what you left out* (`--agent ` reaches any entry) — +never silently truncate. ## grok availability is model-aware and bounded grok drops/renames models between releases (composer `grok-composer-2.5-fast` diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f5f217..174513d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,15 @@ entries are grouped per plugin, newest first. ## work-system +### 1.12.0 — 2026-08-05 +- `/kickoff` offers **cc-harness foreign agents** when a `cc-harness-agents` helper is on `PATH`: full Claude Code sessions driven by a foreign model (e.g. `cc-harness:grok`, `cc-harness:kimi`, `cc-harness:sol`) via a local gateway. Auto-detected — one `command -v`; helper absent → no change from today. The plugin is a pure consumer of a small contract (`list` / `exec`); it hardcodes no gateway, no models, no agent table — whatever `list` prints becomes a picker entry (covered by a mock that returns a name the plugin has never heard of). +- `agent-registry.sh` merges the helper's 4-column TSV (`name/model/available/note`, name already namespaced) into its 5-column list as `cli=cc-harness`, maps only literal `available=yes` to available (fail-closed on `unknown`), and resolves `cc-harness:` to `cc-harness-agents exec -- claude [-n ] /work-system:continue` — no `--model` (the helper sets it via env before `exec`ing into claude, so the herdr pane roots at claude and agent_status + `/close` stay intact). Exit 3 from the helper ("capability absent") is a silent degrade, distinct from a listed-but-unavailable provider (exit 3 from resolve, with the helper's fix hint). +- Lifecycle parity: a cc-harness worker is a real CC session, so `/continue`, `/close` Scenario A/B, and tab glyphs work unchanged. `supports=` is the full claude set. A committed `cc-harness:` default is accepted when the helper lists it and falls through to the picker when the helper is gone (same validation path as a stale native name). +- The picker is now **two pages**: page 1 is the familiar worker list plus a single `cc-harness agents ▸` aggregate entry (shown only when the helper printed rows), and choosing it opens page 2 with the concrete harness agents. Merging them flat would have made ~12 entries against AskUserQuestion's hard 4-options-per-question cap, which the 7 native entries already exceeded. The common path stays one page; the harness list can grow with the helper's table. The aggregate is a class, never a selector, and the "save as project default?" answer is taken from the page where the final pick happened. Where a set still exceeds 4, consolidate and name what was left out (`--agent ` reaches any entry) — never silently truncate. Unavailable rows stay visible with the helper's fix hint, available first. +- Contract + setup sketch: `plugins/work-system/docs/cc-harness-agents.md`. Earlier "invoke the zsh `claude()` wrapper via `zsh -ic`" idea was rejected: fragile, ties the plugin to zsh, interactive-shell side effects. +- Fix: `list`'s human table dropped the **last** harness row. `$( )` strips the trailing newline off the merged block, and the table's `while read` then discards the final newline-less line — silently, and only in the table, since `--json` parses it fine. That table is exactly what the picker reads, so the lost row was an agent the user could never choose. Now covered by a test that asserts the table (not just `--json`) and that both views agree on the row count. +- Covered by `test_agent_registry.py`: merge list, never-seen agent, argv shape (no `--model`), clean degrade when absent, exit-3 capability-absent, `available=unknown` → no, harness default set/get. + ### 1.11.0 — 2026-08-03 - `/kickoff` can launch the **kimi CLI** (kimi-code) as a worker: `--kimi` → `kimi:kimi-code/k3-256k`, joining claude/codex/grok in `agent-registry.sh`. It appears in the picker and can be saved as the repo default, where it announces like the other third-party workers. - kimi is the first worker without a ` -m ` launch form — it has no positional launch prompt, no initial-prompt env var, and piped stdin only prefills the input box (and would steal the TUI's tty). Its `-p` flag is the only entry point, but it cannot be combined with `--auto`/`-y` and exits after one answer. Since `-p` does run tools unattended and `kimi -c` inherits its history, the launch is two-phase: `sh -c 'kimi -m "$1" -p "$2" || ; exec kimi -c --auto' …` — the seed works the task through once, then `exec` hands over to the interactive autonomous session. So a kimi tab has already made progress by the time you switch to it. diff --git a/CLAUDE.md b/CLAUDE.md index 90113b5..09a9a51 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,7 +19,7 @@ This is a **Claude Code plugin marketplace** (monorepo) containing plugins that ## Current Plugins - **knowledge-system** (v1.9.x) — Knowledge management with three layers: Rules, Knowledge, Memory. Skills: `/init`, `/query`, `/curate`, `/reindex`, `/backfill-knowledge`, `/migrate`, `/statusline` -- **work-system** (v1.11.x) — Task and worktree workflow (workers: Claude/codex/grok/kimi). Skills: `/define`, `/kickoff`, `/adopt`, `/continue`, `/status`, `/close`, `/list`, `/statusline` +- **work-system** (v1.12.x) — Task and worktree workflow (workers: Claude/codex/grok/kimi, or PATH-detected cc-harness). Skills: `/define`, `/kickoff`, `/adopt`, `/continue`, `/status`, `/close`, `/list`, `/statusline` - **pr-flow** (v1.3.x) — PR review feedback loop. Skills: `/open`, `/cycle`, `/check`, `/fix`, `/rebase`, `/merge` - **swarm** (v0.7.x) — Local mixture-of-agents code review (external `codex`/`grok` CLIs — grok-4.5 — plus Claude lenses: 11 in 4 clusters). Every voice fans out per gated cluster; externals get file-read + web research under an OS secret-jail. P2: `/swarm:review` pipeline (scope→fan-out→merge→verify); P5: `--fix`/`--loop` apply the findings you agreed with. Skills: `/swarm:review`, `/swarm:agents` - **settings** (v0.1.x) — Per-plugin TOML config resolved over schema defaults; each plugin owns its `schema/settings.schema.json`. Skill: `/settings` (list/show/get/set/validate). Phase 1: config surface only. diff --git a/README.md b/README.md index 1df7b9f..a9778a1 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Lightweight, native knowledge management for Claude Code projects. Three layers ### Work System -Generic task and worktree workflow system. Manage tasks as markdown files, work in isolated git worktrees, and track progress through the full lifecycle. `/kickoff` runs the repo's default worker agent (Claude, codex, grok, or kimi — a single committed per-project default), or, when none is set, shows a picker and offers to save your choice; override per run with flags like `--opus`/`--sol` or `--pick`. Inside a [herdr](plugins/work-system/README.md#herdr-integration) session it auto-opens a tab (named after the task, shortened for the sidebar and prefixed with the task's state glyph — `●` active, `◇` in review, `◆` approved, `✓` merged) with the worktree as cwd, starts the chosen worker, and — for a Claude worker — runs `/work-system:continue` for you (plugin-qualified, since a Claude Code built-in `/continue` shadows the bare skill); `/adopt` auto-opens the same tab once it has built the worktree from an existing branch; `/work-system:continue ` from the main session reopens that tab and resumes it if a stray `/exit` closed it; and `/close` tears the tab down again when the task is merged. +Generic task and worktree workflow system. Manage tasks as markdown files, work in isolated git worktrees, and track progress through the full lifecycle. `/kickoff` runs the repo's default worker agent (Claude, codex, grok, kimi, or a PATH-detected cc-harness foreign agent — a single committed per-project default), or, when none is set, shows a picker and offers to save your choice; override per run with flags like `--opus`/`--sol` or `--pick`. Inside a [herdr](plugins/work-system/README.md#herdr-integration) session it auto-opens a tab (named after the task, shortened for the sidebar and prefixed with the task's state glyph — `●` active, `◇` in review, `◆` approved, `✓` merged) with the worktree as cwd, starts the chosen worker, and — for a Claude worker — runs `/work-system:continue` for you (plugin-qualified, since a Claude Code built-in `/continue` shadows the bare skill); `/adopt` auto-opens the same tab once it has built the worktree from an existing branch; `/work-system:continue ` from the main session reopens that tab and resumes it if a stray `/exit` closed it; and `/close` tears the tab down again when the task is merged. **Commands:** `/define`, `/kickoff`, `/adopt`, `/continue`, `/status`, `/close`, `/list`, `/statusline` diff --git a/plugins/work-system/.claude-plugin/plugin.json b/plugins/work-system/.claude-plugin/plugin.json index 428cd2e..f32599c 100644 --- a/plugins/work-system/.claude-plugin/plugin.json +++ b/plugins/work-system/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "work-system", - "description": "Generic task and worktree workflow system for Claude Code. Manage tasks as markdown files, run them in isolated git worktrees with a choice of worker agent (Claude, codex, grok, or kimi), and track progress across the define/kickoff/continue/status/close lifecycle.", - "version": "1.11.0", + "description": "Generic task and worktree workflow system for Claude Code. Manage tasks as markdown files, run them in isolated git worktrees with a choice of worker agent (Claude, codex, grok, kimi, or a PATH-detected cc-harness agent), and track progress across the define/kickoff/continue/status/close lifecycle.", + "version": "1.12.0", "author": { "name": "gering" }, diff --git a/plugins/work-system/README.md b/plugins/work-system/README.md index 5534680..c01ae2b 100644 --- a/plugins/work-system/README.md +++ b/plugins/work-system/README.md @@ -28,7 +28,7 @@ Generic task and worktree workflow system for Claude Code. Manage tasks as markd | Command | Description | |---------|-------------| | `/define` | Create a new task (markdown file with Goal/Context/Requirements) | -| `/kickoff` | Start a task in an isolated git worktree, with a choice of worker agent (Claude/codex/grok/kimi) | +| `/kickoff` | Start a task in an isolated git worktree, with a choice of worker agent (Claude/codex/grok/kimi, or a PATH-detected cc-harness agent) | | `/adopt` | Adopt an existing branch into the work system | | `/continue` | Resume the current task (in a worktree); or `/continue ` from the main session reopens the task's herdr tab and resumes it | | `/status` | Check task status (PRs, branches, commits) | @@ -190,6 +190,7 @@ flag picks another: | `--grok` | grok-4.5 | | `--kimi` | kimi-code on k3-256k (launches in two phases — see below) | | `--agent ` | any registry entry, e.g. `--agent claude:sonnet` or `--agent codex` | +| `--agent cc-harness:` | foreign model *inside* the CC harness (only when `cc-harness-agents` is on PATH) | **The default is a single per-repo setting** — no global default, no shipped fallback. It lives in a committed `.claude/work-system-agent` file, so it travels @@ -199,18 +200,32 @@ in a repo with no default yet. Everything is registry-driven — no ranking, no call; the default is a simple, explicit choice (the hook where future task-aware routing can plug in). +**Optional cc-harness agents (PATH-detected).** When a `cc-harness-agents` helper +is on `PATH`, `/kickoff` offers its foreign agents — e.g. `cc-harness:grok`, +`cc-harness:kimi`, `cc-harness:sol`. These are full Claude Code sessions driven by +a foreign model via a local gateway (skills, lenses, `/continue`, `/close` all +work), not the native CLI voice. The picker keeps them **one page down**: page 1 is +the usual worker list plus a single `cc-harness agents ▸` entry, and only choosing +that opens a second page with the concrete agents — so the familiar path stays one +page and the harness list can grow freely. The plugin is a pure consumer of a small +contract (`list` / `exec`); it hardcodes no gateway, no models, no agent table. +Helper absent → one `command -v`, no aggregate entry, no change. Setup + contract: +[docs/cc-harness-agents.md](docs/cc-harness-agents.md). + **Non-Claude workers degrade honestly.** codex/grok/kimi have no work-system skills, so a launched worker gets a bootstrap prompt (read `TASK.md`, commit, open -a PR) instead of `/continue`. Everything git/PR-derived (`/status`, `/list`, the -`[ws]` statusline, `/close`'s tab teardown) works for any worker; only -claude-session concepts differ. `/continue`'s reopen **always sends `claude -c`** -— the work-system doesn't persist which worker a task used (per-task agent memory -is a later idea), so it can't dispatch per CLI. That resumes a claude worker; for -a codex/grok/kimi task it's a *new* Claude session, so you resume the real worker -yourself in the tab (`codex resume --last` / `grok -c` / `kimi -c`) — `/continue` -surfaces this caveat inline. Since codex and grok read `AGENTS.md`, dropping a -short `AGENTS.md` note into the worktree is an optional way to give them standing -task guidance. +a PR) instead of `/continue`. A `cc-harness:…` worker is still a full CC session +(the helper only routes the model), so it does **not** degrade — lifecycle is +unchanged. Everything git/PR-derived (`/status`, `/list`, the `[ws]` statusline, +`/close`'s tab teardown) works for any worker; only claude-session concepts +differ. `/continue`'s reopen **always sends `claude -c`** — the work-system +doesn't persist which worker a task used (per-task agent memory is a later idea), +so it can't dispatch per CLI. That resumes a claude worker (including a +cc-harness one); for a codex/grok/kimi task it's a *new* Claude session, so you +resume the real worker yourself in the tab (`codex resume --last` / `grok -c` / +`kimi -c`) — `/continue` surfaces this caveat inline. Since codex and grok read +`AGENTS.md`, dropping a short `AGENTS.md` note into the worktree is an optional +way to give them standing task guidance. **kimi launches in two phases.** It has no positional launch prompt, and its one-shot `-p` flag can't be combined with the autonomous `--auto`/`-y` modes — so diff --git a/plugins/work-system/docs/cc-harness-agents.md b/plugins/work-system/docs/cc-harness-agents.md new file mode 100644 index 0000000..efb4197 --- /dev/null +++ b/plugins/work-system/docs/cc-harness-agents.md @@ -0,0 +1,124 @@ +# CC-Harness foreign agents at `/kickoff` + +When a `cc-harness-agents` helper is on `PATH`, `/kickoff` offers its foreign +agents as workers — a foreign model running *inside* the Claude Code harness +(full skills, lenses, `/continue`, `/close`), routed through a local gateway. +When the helper is absent, behaviour is unchanged (one `command -v`). + +This page is the **plugin-side contract**. The helper itself is machine-local +(gateway URL, credentials, model ceilings) and is *not* shipped with the plugin. +A reference implementation lives in the paired dotfiles change that extracts the +helper as a PATH binary; anything that implements the two subcommands below is +enough for auto-detect. + +## Why a PATH helper (and not a shell function) + +Earlier idea "invoke the interactive `claude()` wrapper via `zsh -ic`" was +rejected: fragile, ties the plugin to zsh, interactive-shell side effects. A +shell-agnostic PATH binary keeps detection a cheap `command -v`, keeps the +launch argv-exec-native (no shell), and keeps all machine-local config +(gateway / creds / models / context ceilings) out of the public plugin. + +## Contract + +### `cc-harness-agents list` + +Probe without starting a session. Print one TSV row per foreign agent, **exactly +four columns**: + +``` +namemodelavailablenote +``` + +Example: + +``` +cc-harness:grok grok-4.5 yes - +cc-harness:sol gpt-5.6-sol no run: cliproxyapi -codex-login +``` + +- `name` is already namespaced (`cc-harness:`). +- `available` is `yes` | `no` | `unknown`. The plugin treats only the literal + `yes` as available; everything else is fail-closed for launch. +- `note` is `-` (or empty) when there is nothing to say; otherwise a short fix + hint the picker shows next to a greyed entry. +- Unavailable agents are listed too. Order is the helper's table order. +- Exit codes (list): + - `0` — the probe ran (regardless of per-agent availability). + - `3` — capability absent (no usable gateway token at all). The plugin treats + this as "helper not configured here" and silently adds no rows — distinct + from "configured, but this provider is not logged in" (`available=no`). + - `2` — usage error. + +> **Not the same shape as `agent-registry.sh list`.** The registry emits five +> columns (`name/cli/model/available/note`). The helper has no `cli` field; the +> plugin maps deliberately when merging. Do not feed helper rows into a +> five-column parser. + +### `cc-harness-agents exec [--] ` + +Set the routing environment for ``, then **`exec "$@"`** so the calling +process *becomes* the target (typically `claude`) — no lingering wrapper in the +process tree. That is load-bearing: herdr's agent-state detection and +work-system's `/close` teardown both key on the pane's root process being +`claude`. + +- `` accepts either form: `grok` or `cc-harness:grok`. +- No `--model` on the `claude` side — the helper sets `ANTHROPIC_MODEL` (and the + tier defaults / context ceiling) via env before the exec. +- Exit codes (exec, *before* the target runs): + - never returns on success (process replaced). + - `1` — requested agent not available (not logged in / gateway down). + - `2` — usage / unknown agent. + - `3` — capability absent (no token). +- Once the target is running, its own status comes back unchanged; a consumer + that needs a reliable availability answer asks `list`, which never execs. + +### What the plugin does with this + +| surface | behaviour | +|---------|-----------| +| `agent-registry.sh list` | if `command -v cc-harness-agents` succeeds, run `list` (bounded) and merge rows as `cli=cc-harness`; helper absent or exit 3 → no change | +| `agent-registry.sh resolve cc-harness:` | availability + note from the helper (no re-probe); argv = `cc-harness-agents exec -- claude [-n ] /work-system:continue` | +| `/kickoff` picker | harness rows labelled "foreign model in the Claude Code harness, routed via a local gateway"; unavailable greyed with the helper's fix hint; available first | +| lifecycle | full CC session → `/continue`, `/close` Scenario A/B, tab glyphs unchanged. `supports=` is the same set as a native claude worker | +| default | a committed `cc-harness:` default is accepted when the helper lists it, and falls through to the picker when the helper is gone (same validation as a stale native name) | + +Nothing gateway-specific is hardcoded in the plugin. A sixth foreign model is a +new row in the helper's table — the plugin has no per-agent code path, which is +why the tests assert a name the plugin has never shipped (`cc-harness:never-seen`). + +## Setup sketch (reference) + +Exact install steps live with the helper. The shape is: + +1. Install and run a local Anthropic-compatible gateway (e.g. CLIProxyAPI) that + fronts the foreign providers, with a client token on disk. +2. Log each provider into the gateway so it holds OAuth / API credentials. +3. Put a `cc-harness-agents` binary on `PATH` that implements `list` / `exec` + against that gateway. +4. `/kickoff --pick` (or `agent-registry.sh list`) now shows the harness rows. + +Context ceilings differ per agent and are often plan-gated (e.g. a marketed 1M +window may serve 256k on the current plan). The helper owns that value; the +plugin never restates or assumes a window — another reason to stay a pure +consumer. + +## Security notes (for helper authors) + +- `exec` hands the gateway token to whatever argv it runs. Deny both the token + file *and* `cc-harness-agents exec` in Claude Code permission rules if you + don't want a session to exfiltrate the token; `list` is safe (prints no + secret). Those rules bind Claude Code sessions only. +- An argv allow-list was considered and rejected: it would break the contract + ("run this routed"), and the sanctioned target `claude` can run arbitrary + commands itself. The boundary is who may execute the helper at all. +- Gateway liveness should be a *plausibility* check that withholds the token + from a bare socket listener (e.g. an unauthenticated request must be + refused), not a bare TCP connect. + +## Related + +- `scripts/agent-registry.sh` — PATH detect, list merge, resolve/emit_argv +- `skills/kickoff/SKILL.md` step 12 — picker presentation +- `.claude/knowledge/features/kickoff-agent-selection.md` — design decisions diff --git a/plugins/work-system/scripts/agent-registry.sh b/plugins/work-system/scripts/agent-registry.sh index b9b94eb..312c385 100755 --- a/plugins/work-system/scripts/agent-registry.sh +++ b/plugins/work-system/scripts/agent-registry.sh @@ -33,11 +33,26 @@ # grok -> grok -m # kimi -> sh -c 'kimi -m "$1" -p "$2"; exec kimi -c --auto' \ # kimi-worker (seed+continue) +# cc-harness -> cc-harness-agents exec -- claude [-n ] \ +# /work-system:continue +# (foreign model *inside* the Claude Code harness, routed by a PATH +# helper — full CC session, so lifecycle skills work unchanged. No +# --model: the helper sets ANTHROPIC_MODEL via env before exec'ing. +# The helper `exec`s into claude, so the herdr pane roots at claude.) # The bootstrap prompt (codex/grok/kimi have no work-system skills) tells the # agent to read TASK.md and drive the task to a PR. `supports=` metadata records # which lifecycle hooks each agent honors, so /close and /continue can degrade # for non-claude workers instead of faking claude-only behavior. # +# Optional PATH helper `cc-harness-agents` (see plugins/work-system/docs/ +# cc-harness-agents.md). When present, `list` merges its rows; when absent, one +# `command -v` is the only cost and behaviour is unchanged. The helper owns +# gateway/creds/models — this registry never re-probes them. Contract: +# list → namemodelavailablenote (4 cols; name already +# namespaced, e.g. cc-harness:grok). available is yes|no|unknown. +# exec [--] → set env, exec into argv (pane root = claude). +# exit 3 on list = capability absent (no token) → silent degrade, no rows. +# # Why kimi needs the two-phase seed+continue shape (all probed live, 0.31.1): # kimi has NO positional launch prompt (`kimi "text"` -> "unknown command"), no # initial-prompt env var, and piped stdin only prefills the input box without @@ -88,6 +103,13 @@ GROK_AUTH_FILE="${GROK_AUTH_FILE:-$HOME/.grok/auth.json}" # probing that path would report every authenticated install as logged out. KIMI_CREDENTIALS_FILE="${KIMI_CREDENTIALS_FILE:-$HOME/.kimi-code/credentials/kimi-code.json}" +# Optional foreign-model-in-CC-harness helper (PATH binary; absent = no-op). +# Overridable for tests that want a specific path without PATH munging. +HARNESS_BIN="${WORK_SYSTEM_CC_HARNESS_AGENTS:-cc-harness-agents}" +HARNESS_NS="cc-harness" +# Full CC session → same lifecycle hooks as a native claude worker. +HARNESS_SUPPORTS="continue,close-exit,statusline,commit,pr" + # The bootstrap prompt for CLIs without work-system skills (codex, grok, kimi). One # argv word; the launch helper passes it verbatim. BOOTSTRAP_PROMPT='Read TASK.md in this worktree and continue the task. Commit on the current branch as you go, and open a PR when the work is complete.' @@ -139,13 +161,90 @@ usage() { # Emit one `flag|cli|model|supports` record per line (skips blank lines). registry_rows() { printf '%s\n' "$REGISTRY"; } +# ---------- optional cc-harness-agents helper ---------- +# Discovery is a cheap `command -v`. The helper owns availability (gateway + +# per-provider creds) and the routing env; we never re-probe those here. +# Names are already namespaced by the helper (`cc-harness:grok`) and do NOT +# follow the native `cli:model` shape — the id is an agent key, not a model. +harness_on_path() { command -v "$HARNESS_BIN" >/dev/null 2>&1; } + +# Run `cc-harness-agents list` bounded. Prints its stdout; returns its exit +# code (0 = ok, 3 = capability absent, 124 = timed out, other = fail). Callers +# treat non-zero as "no harness rows" (silent degrade). +harness_list_raw() { + harness_on_path || return 1 + run_bounded 10 "$HARNESS_BIN" list 2>/dev/null +} + +# Map a helper available cell to yes|no. Only the literal "yes" is available; +# "no", "unknown", empty, and anything else → no (fail closed for launch). +harness_map_avail() { + case "$1" in yes) printf 'yes' ;; *) printf 'no' ;; esac +} + +# Normalize helper note: a lone "-" means empty (the helper's "nothing to say"). +harness_map_note() { + case "$1" in -|"") printf '' ;; *) printf '%s' "$1" ;; esac +} + +# Look up one harness agent by selector (`cc-harness:grok` or bare `grok` after +# the ns strip). Prints `name\tmodel\tavailable\tnote` (already mapped) on hit; +# returns 1 if the helper is absent, list fails, or the name is unknown. +harness_lookup() { + local want="$1" bare full raw hrc=0 name model avail note + bare="${want#"$HARNESS_NS":}" + full="$HARNESS_NS:$bare" + raw="$(harness_list_raw)" || hrc=$? + [ "$hrc" -eq 0 ] || return 1 + while IFS=$'\t' read -r name model avail note; do + [ -n "$name" ] || continue + if [ "$name" = "$full" ] || [ "$name" = "$want" ] || [ "${name#"$HARNESS_NS":}" = "$bare" ]; then + printf '%s\t%s\t%s\t%s\n' "$name" "$model" "$(harness_map_avail "$avail")" "$(harness_map_note "$note")" + return 0 + fi + done <<<"$raw" + return 1 +} + +# Append harness rows to the list builder as name\tcli\tmodel\tavail\tnote. +# Silent on helper-absent / exit-3 / timeout — today's behaviour is unchanged. +harness_append_list_rows() { + local raw hrc=0 name model avail note + raw="$(harness_list_raw)" || hrc=$? + [ "$hrc" -eq 0 ] || return 0 + while IFS=$'\t' read -r name model avail note; do + [ -n "$name" ] || continue + # Reject a helper that forgot the namespace: names must be distinct from the + # native `cli:model` set, and the picker labels them as harness agents. + case "$name" in + "$HARNESS_NS":*) ;; + *) continue ;; + esac + printf '%s\t%s\t%s\t%s\t%s\n' \ + "$name" "$HARNESS_NS" "$model" \ + "$(harness_map_avail "$avail")" "$(harness_map_note "$note")" + done <<<"$raw" +} + # Print the whole record for a canonical name (cli:model), or nothing. +# Also accepts a harness name (`cc-harness:grok`) when the helper lists it — +# used by `default get`/`set` validation so a committed harness default is +# accepted when the helper is present and rejected (→ picker) when absent. row_for_name() { local want="$1" flag cli model supports while IFS='|' read -r flag cli model supports; do [ -n "$cli" ] || continue [ "$cli:$model" = "$want" ] && { printf '%s|%s|%s|%s\n' "$flag" "$cli" "$model" "$supports"; return 0; } done < <(registry_rows) + case "$want" in + "$HARNESS_NS":*) + local hname hmodel havail hnote + IFS=$'\t' read -r hname hmodel havail hnote < <(harness_lookup "$want") || return 1 + # flag `-` = no shorthand (dynamic entries are name/--agent only). + printf -- '-|%s|%s|%s\n' "$HARNESS_NS" "$hmodel" "$HARNESS_SUPPORTS" + return 0 + ;; + esac return 1 } @@ -170,10 +269,14 @@ row_for_cli_default() { # Resolve any selector to a registry record. Order: shorthand flag, canonical # cli:model name, bare CLI (its default model). Prints the record or fails (1). +# Harness selectors (`cc-harness:…`) are NOT resolved here — they don't fit the +# flag|cli|model|supports record (the name is an agent id, not cli:model). +# subcmd_resolve handles them via harness_lookup instead. row_for_selector() { local sel="$1" case "$sel" in --*) row_for_flag "$sel" && return 0 ;; + "$HARNESS_NS":*) return 1 ;; *:*) row_for_name "$sel" && return 0 ;; *) row_for_cli_default "$sel" && return 0 ;; esac @@ -352,9 +455,26 @@ shell_quote() { esac } +# Emit argv= lines + one argv_shell= line for an already-built words array. +# Shared by emit_argv (native CLIs) and the harness path so quoting lives once. +_emit_words() { + # Guard the expansion: under `set -u` a bash 3.2 `"${words[@]}"` on an EMPTY + # array is an unbound-variable error, which an unknown cli would hit. + [ "$#" -gt 0 ] || return 0 + printf 'argv=%s\n' "$@" + # A ready-to-paste command line. Quoting is load-bearing for kimi (`;`/`exec`) + # and for the harness wrapper (spaces in the session name). Skills print this + # verbatim instead of re-deriving it. + local shell_cmd="" w + for w in "$@"; do shell_cmd="$shell_cmd$(shell_quote "$w") "; done + printf 'argv_shell=%s\n' "${shell_cmd% }" +} + emit_argv() { # Print `argv=` lines for a resolved entry, then ONE `argv_shell=` line # with the same words shell-quoted. $1=cli $2=model $3=session. + # For cli=cc-harness, $2 is the bare agent id (e.g. grok), NOT a model slug — + # the helper sets ANTHROPIC_MODEL itself; we must not pass --model. local cli="$1" model="$2" session="$3" local words=() case "$cli" in @@ -374,19 +494,18 @@ emit_argv() { # flags or be absorbed by `-p`. words=(sh -c "$KIMI_LAUNCH_SCRIPT" kimi-worker "$model" "$BOOTSTRAP_PROMPT") ;; + cc-harness) + # Foreign model inside the CC harness. The helper sets the routing env + # then `exec`s into claude, so the herdr pane roots at claude (agent_status + # + /close Scenario B stay intact). No --model — the model is env-set. + # Bare id (not the namespaced form) for the helper's arg; it + # accepts both, but bare is the stable half of the contract. + words=("$HARNESS_BIN" exec "$model" -- claude) + [ -n "$session" ] && words+=(-n "$session") + words+=(/work-system:continue) + ;; esac - # Guard the expansion: under `set -u` a bash 3.2 `"${words[@]}"` on an EMPTY - # array is an unbound-variable error, which an unknown cli would hit. - [ "${#words[@]}" -gt 0 ] || return 0 - printf 'argv=%s\n' "${words[@]}" - # A ready-to-paste command line, quoted by printf %q rather than by whoever - # renders the manual-launch block. That rendering used to be a prose rule, and - # for kimi a mis-quote is not cosmetic: its argv carries `;` and `exec`, so an - # unquoted paste would replace the USER'S OWN interactive shell with an - # unattended agent. Skills print this verbatim instead of re-deriving it. - local shell_cmd="" w - for w in "${words[@]}"; do shell_cmd="$shell_cmd$(shell_quote "$w") "; done - printf 'argv_shell=%s\n' "${shell_cmd% }" + _emit_words "${words[@]+"${words[@]}"}" } subcmd_resolve() { @@ -410,12 +529,43 @@ subcmd_resolve() { done [ -n "$selector" ] || { echo "resolve: missing selector" >&2; exit 2; } + # Harness path: name is an agent id (cc-harness:grok), NOT cli:model. Availability + # comes from the helper — we never re-probe gateway/creds. Helper absent or the + # name not listed → "unknown selector" (exit 2), same as any other miss; an + # explicitly listed-but-unavailable agent still resolves and exits 3. + case "$selector" in + "$HARNESS_NS":*) + local hname hmodel havail hnote bare + if ! IFS=$'\t' read -r hname hmodel havail hnote < <(harness_lookup "$selector"); then + if harness_on_path; then + echo "Unknown agent selector: $selector" >&2 + echo "Try: cc-harness-agents list (or a native flag/name/cli)" >&2 + else + echo "Unknown agent selector: $selector" >&2 + echo "cc-harness agents require the \`cc-harness-agents\` helper on PATH — see plugins/work-system/docs/cc-harness-agents.md" >&2 + fi + exit 2 + fi + bare="${hname#"$HARNESS_NS":}" + printf 'name=%s\n' "$hname" + printf 'cli=%s\n' "$HARNESS_NS" + printf 'model=%s\n' "$hmodel" + printf 'available=%s\n' "$havail" + printf 'supports=%s\n' "$HARNESS_SUPPORTS" + [ -n "$hnote" ] && printf 'note=%s\n' "$hnote" + # emit_argv takes the bare agent id in the model slot for this cli. + emit_argv "$HARNESS_NS" "$bare" "$session" + [ "$havail" = yes ] || exit 3 + return 0 + ;; + esac + local record record="$(row_for_selector "$selector")" || { echo "Unknown agent selector: $selector" >&2 # Derive the flag list from REGISTRY rather than restating it — a new entry # must not need a second edit here to appear in the hint. - echo "Try: $(registry_rows | cut -d'|' -f1 | grep -v '^-$' | tr '\n' ' ')— a name (claude:opus), or a cli (codex)" >&2 + echo "Try: $(registry_rows | cut -d'|' -f1 | grep -v '^-$' | tr '\n' ' ')— a name (claude:opus), a cli (codex), or cc-harness: when the helper is on PATH" >&2 exit 2 } local flag cli model supports @@ -444,12 +594,27 @@ subcmd_list() { esac # Build rows: name cli model available note (TAB-separated internally). + # Native registry first, then optional harness rows (available first is a + # presentation concern for the picker skill, not for this raw table — keep + # registry order stable so tests and consumers can rely on it). local rows="" flag cli model supports avail note while IFS='|' read -r flag cli model supports; do [ -n "$cli" ] || continue IFS=$'\t' read -r avail note < <(entry_status "$cli" "$model") rows+="$cli:$model $cli $model $avail $note"$'\n' done < <(registry_rows) + # Append harness rows when the helper is present and healthy. A missing + # helper, exit 3 (no token), or a timed-out list is a silent no-op — one + # `command -v` is the only cost of the absent case. + # + # Re-add the trailing newline `$( )` strips, and only when there ARE rows (an + # unconditional append would add a blank line). Without it the LAST harness + # row has no newline, and the human table's `while read` drops it — silently, + # and only in the table, since the --json path parses it fine. That is the + # picker's input, so a dropped row is an agent the user can never choose. + local harness_rows + harness_rows="$(harness_append_list_rows)" + [ -n "$harness_rows" ] && rows+="$harness_rows"$'\n' if [ -n "$as_json" ]; then command -v python3 >/dev/null 2>&1 || { echo "python3 required for --json" >&2; exit 1; } @@ -470,8 +635,13 @@ print() fi # Human table. Use column when present; else a plain TSV still renders. + # `|| [ -n "$name" ]` keeps a final line that lacks a trailing newline: `read` + # returns non-zero there even though it filled the variables, so the plain + # loop would drop it. Belt-and-braces next to the newline fix above — this is + # the picker's input, and losing the last row loses a selectable agent. { printf 'NAME\tCLI\tMODEL\tAVAILABLE\tNOTE\n' - printf '%s' "$rows" | while IFS=$'\t' read -r name cli model avail note; do + printf '%s' "$rows" | while IFS=$'\t' read -r name cli model avail note || [ -n "$name" ]; do + [ -n "$name" ] || continue printf '%s\t%s\t%s\t%s\t%s\n' "$name" "$cli" "$model" "$avail" "${note:--}" done } | { command -v column >/dev/null 2>&1 && column -t -s $'\t' || cat; } diff --git a/plugins/work-system/scripts/herdr-launch.sh b/plugins/work-system/scripts/herdr-launch.sh index c76fa04..a15e3f8 100755 --- a/plugins/work-system/scripts/herdr-launch.sh +++ b/plugins/work-system/scripts/herdr-launch.sh @@ -11,10 +11,13 @@ # the kickoff knowledge entry) — a fresh interactive shell # can eat the leading keystrokes of a typed command. # The worker argv is resolved from an agent SELECTOR via -# agent-registry.sh (claude/codex/grok/kimi × model); with no -# selector it stays the legacy `claude … /work-system:continue`. The +# agent-registry.sh (claude/codex/grok/kimi × model, plus +# optional PATH-detected cc-harness:); with no selector +# it stays the legacy `claude … /work-system:continue`. The # registry owns every per-CLI launch detail, so this script -# is CLI-agnostic — it just execs the resolved argv. +# is CLI-agnostic — it just execs the resolved argv (so a +# harness worker's `cc-harness-agents exec … -- claude …` is +# fine; argv[0] need not be claude). # # resume (/continue) — reopen a tab at the worktree and run `claude -c` INSIDE a # shell pane, then focus it. Because Claude runs inside a @@ -39,7 +42,8 @@ # workspace-id herdr workspace to open the tab in (e.g. $HERDR_WORKSPACE_ID) # agent-selector (launch only) agent-registry selector: a shorthand flag # (--fable/--opus/--codex/--sol/--grok/--kimi), a name -# (claude:opus), or a bare cli. Empty → legacy claude default. +# (claude:opus / cc-harness:grok), or a bare cli. Empty → +# legacy claude default. # session-name (launch only) `claude -n` name; defaults to