Offer PATH-detected cc-harness agents at /kickoff (work-system 1.12.0) - #52
Open
gering wants to merge 7 commits into
Open
Offer PATH-detected cc-harness agents at /kickoff (work-system 1.12.0)#52gering wants to merge 7 commits into
gering wants to merge 7 commits into
Conversation
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:<id> to `cc-harness-agents exec <id> -- claude [-n <s>] /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:<id> 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DE9DzwzzsMMu84KxC1cB5R
Applies the swarm review's agreed findings on the 1.12.0 feature. - /continue's reopen degrades for a harness worker too. `herdr-launch.sh resume` always sends a bare `claude -c`, which resumes the correct transcript WITHOUT the routing env — silently continuing on the user's default Claude model. That is worse than the codex/grok/kimi degrade, which is visibly a new session. Documented inline with the manual form (`cc-harness-agents exec <id> -- claude -c`) instead of claiming unqualified lifecycle parity. - Sanitize helper fields at ingest: strip C0 controls + DEL and cap the length. A note is rendered as an authoritative hint and enters the picker's context — same risk class the `--session` guard already covers. Unicode bidi/zero-width overrides remain a documented residual. - Split the helper's TSV explicitly. `IFS=$'\t' read` treats tab as IFS whitespace and collapses consecutive tabs, so one empty cell shifted every later column (empty model => `available` read as the model, fail-closing a working agent). The trap bit twice: on the helper's output, and where resolve/row_for_name re-read the lookup's own line. - Give `list` and `resolve` one namespace gate. A row missing the `cc-harness:` prefix was rejected by the list builder but still matched the lookup's bare-id clause — invisible yet launchable and storable as a committed default. - Make the picker's 4-option cap satisfiable: the aggregate reserves a slot, natives fill the rest one option per CLI, overflow is named with the `--agent` hint. Classify from `list --json`, not the `column -t` padded table whose note cells contain spaces. - Tests: regressions for each fix; stub emits rows via printf (echo's escape expansion is not POSIX-guaranteed); no-helper cases pin the seam at an absent path instead of trusting the host PATH. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DE9DzwzzsMMu84KxC1cB5R
The resume shim that will close it is planned in the helper's own project, not here — so the previous "needs per-task worker persistence" note named the wrong lever and would have invited rebuilding this in work-system. Keep the workaround minimal and say so. Also drop the docs' pointer to the helper's current source project: the contract is PATH-only, so the binary can relocate without touching this page. Per-task worker persistence stays the open idea for codex/grok/kimi resume dispatch, which is unaffected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DE9DzwzzsMMu84KxC1cB5R
Rebasing onto 1.11.1 turned a textual conflict into a real one: the
launcher no longer infers transport from argv[0], every entry declares
it. A harness argv is `cc-harness-agents exec <id> -- claude …`, so
argv[0] is the helper, not herdr's canonical `claude` — agent-start
(which asserts argv[0] == kind) cannot express it. The entry now
declares pane-run + herdr_kind=claude, which is what the helper execs
into. This is the "dynamically-registered wrapper" 1.11.1 anticipated,
so it needed no launcher change.
Also folded into the rebase:
- resolve/row_for_name emit the 6-field record (mode+kind) and the
harness arm emits herdr_mode=/herdr_kind=.
- herdr-launch.sh takes main's version wholesale; our only changes there
were comments, one of which ("argv[0] need not be claude") the new
contract makes false.
- Over-long helper notes are elided in the middle rather than
tail-truncated: notes read "<what broke> (<path>) — <what to do>", so
the actionable half sits at the end. Defensive only — an earlier
report of a note landing exactly on the cap was retracted (it came
from a fixture with HOME pointed at a worktree); real notes measure
~140 chars. The shape argument holds regardless, so the elision stays.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DE9DzwzzsMMu84KxC1cB5R
gering
force-pushed
the
task/offer-cc-harness-agents-at-kickoff
branch
from
August 16, 2026 13:02
c40a63c to
1148467
Compare
The docs said a forking helper would break agent-state detection and /close teardown. Since 1.11.1 the failure is earlier and sharper: the harness entry is pane-run + herdr_kind=claude, so the launcher polls until herdr sees `claude` in that pane. A wrapper left as the pane root never satisfies that poll and the launch reports blocked=unverified — a tab with no confirmed worker, not a later lifecycle glitch. Both sides now document the requirement (the helper's own header has carried it since its first version), so it is not a verbal agreement. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DE9DzwzzsMMu84KxC1cB5R
A scoped re-review of the rebased tree found the previous round's own hardening had broken the invariant it added: sanitizing ran on the LIST output while the lookup matched the RAW name, so a name carrying a control byte (or one past the length cap) was shown in the picker and then exited 2 on resolve. Reproduced live. Root cause was two pipelines kept in step by convention. They now share one: harness_rows() emits canonical sanitized, namespace-gated rows, and harness_lookup filters that output, so "listed" and "resolvable" are the same predicate by construction. Also from the same round: - The bare namespace `cc-harness:` passed the gate — listable, resolvable and storable as a committed default while emitting an empty argv word that only failed at launch. Requires a non-empty id now. - The tab-collapse fix had not reached the renderer: `column -t -s TAB` also folds consecutive separators, so an empty model still collapsed on screen (MODEL showing the availability value). Every empty cell is placeholdered before column sees it, and a test asserts the table, not just --json. - One splitter (harness_split) now serves the helper's 4-column rows, the canonical 5-column rows and the display loop. - Helper output is size-bounded, not only time-bounded. - Picker: deterministic page-1 ordering (registry order; claude never dropped) instead of leaving the over-cap choice to improvisation, an explicit page-2 cap rule, and a documented fallback for `list --json`, which hard-requires python3 on a path that had no such dependency. - Dropped the false "no change when the helper is absent" claim: the page-1 grouping applies to everyone. - Shared record emitter across resolve's two arms; the existence-check record carries the id emit_argv would actually need; fork-free sanitizing; knowledge frontmatter refreshed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DE9DzwzzsMMu84KxC1cB5R
gering
force-pushed
the
task/offer-cc-harness-agents-at-kickoff
branch
from
August 19, 2026 12:29
be025cf to
22b8f6b
Compare
Third review round on this feature; the critical finding was again a consequence of the previous round's fix. Sanitizing the name made `list` and `resolve` agree with each OTHER while disagreeing with the HELPER, which knows only the real id — the picker offered a scrubbed selector and the launch then failed after herdr had already opened the tab. Reproduced live. The rule is now explicit: keys must survive verbatim. A name that sanitizing would alter is dropped at list time (like the empty-id guard); only display fields (model, note) are scrubbed. Non-injectivity goes away with it — two ids can no longer collapse onto one label that routes to whichever came first — and exact duplicates are deduped. Also from this round: - Helper output is bounded at read time via `head -c` instead of truncating an already-buffered capture, which materialized a multi-GB writer in shell memory first. A byte cut can land mid-row, so a trailing partial line is dropped rather than parsed. - Invalid UTF-8 no longer aborts `list --json`; one bad helper cell used to take down the whole listing, native rows included. - New `list --tsv` gives the picker a machine-readable fallback without python3. The old fallback told the skill to whitespace-split the padded table — the exact ambiguous parse the JSON path exists to avoid, since names and notes may contain spaces. - The aggregate picker slot is reserved only when a harness row is actually available, so unavailable agents cannot evict an available native CLI. - Sanitizing returns through a variable, so the per-field fork the parameter-expansion rewrite claimed to remove is actually gone; row_for_name stops fabricating an unread record; the single-call-site rename wrapper is inlined; `default get`'s live-probe asymmetry is documented where it happens. Left untouched: the PATH-resolution finding, same class as the one refuted in round one — every worker is invoked by bare PATH name. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DE9DzwzzsMMu84KxC1cB5R
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
/kickoffcan now launch cc-harness foreign agents — full Claude Code sessions driven by a foreign model (e.g.cc-harness:grok,cc-harness:kimi,cc-harness:sol) through a local gateway. Strictly more capable than the nativegrok/codexCLI voice, which has no work-system skills.command -v cc-harness-agents. Helper absent → no behavior change at all.list/exec). No gateway URL, no token path, no model table, no per-agent code path — whateverlistprints becomes a picker entry.Changes
scripts/agent-registry.shname/model/available/note) into the 5-column list ascli=cc-harness. Availability and note come straight from the helper — no re-probing of gateway or credentials.available=yescounts as available (unknownfails closed for launch).resolve cc-harness:<id>emitscc-harness-agents exec <id> -- claude [-n <session>] /work-system:continue. No--model: the helper sets it via env, thenexecs intoclaude, so the herdr pane roots atclaudeandagent_status+/closeteardown stay intact.resolveexit 3 with the helper's fix hint).Picker (
skills/kickoff/SKILL.md, referenced by/adopt)cc-harness agents ▸aggregate, shown only when the helper printed rows. Page 2: the concrete harness agents.AskUserQuestion's hard 4-options-per-question cap — which the 7 native entries already exceeded.SELECTOR; 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 <name>reaches any entry) — never silently truncate.Lifecycle parity
supports=is the full claude set —/continue,/closeScenario A/B and tab glyphs work unchanged. Nothing assumesargv[0] == claude.cc-harness:<id>default validates through the helper and falls back to the picker when the helper is gone (same path as a stale native name).Bug fixed while demoing
list's human table dropped the last harness row:$( )strips the trailing newline off the merged block, and the table'swhile readthen discards the final newline-less line — silently, and only in the table, since--jsonparses it fine. That table is exactly what the picker reads, so the lost row was an agent the user could never choose.Docs
plugins/work-system/docs/cc-harness-agents.md— the full contract (both subcommands, TSV columns, exec semantics, exit codes), a setup sketch, and security notes for helper authors.CLAUDE.md; version bumped to 1.12.0 inplugin.json+marketplace.json.Readiness
main— up-to-datekickoff-agent-selection.md+_index.mdupdatedcheck-structure.pygreen (0 errors), incl.test_agent_registry.pyDependency status
The paired dotfiles helper (
gering/dotfilesPR #12) is still open and not installed on PATH here. Everything in this PR is built against the documented contract and verified with mocks — no live integration has been exercised. Once the helper lands, a real/kickoff --pickrun is the remaining verification.Test plan
python3 plugins/work-system/scripts/test_agent_registry.py— passespython3 scripts/check-structure.py— 0 errorsagent-registry.sh listis unchanged, andresolve cc-harness:grokexits 2 with a hint naming the helperlist(including the last one),--jsonand the human table agree on the row countresolve cc-harness:<id> --session fooyieldscc-harness-agents exec <id> -- claude -n foo /work-system:continuewith no--modelavailable=nostill emits argv and exits 3 with the helper's note🤖 Generated with Claude Code
https://claude.ai/code/session_01DE9DzwzzsMMu84KxC1cB5R