Skip to content

Offer PATH-detected cc-harness agents at /kickoff (work-system 1.12.0) - #52

Open
gering wants to merge 7 commits into
mainfrom
task/offer-cc-harness-agents-at-kickoff
Open

Offer PATH-detected cc-harness agents at /kickoff (work-system 1.12.0)#52
gering wants to merge 7 commits into
mainfrom
task/offer-cc-harness-agents-at-kickoff

Conversation

@gering

@gering gering commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Summary

  • /kickoff can 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 native grok/codex CLI voice, which has no work-system skills.
  • Auto-detected via one command -v cc-harness-agents. Helper absent → no behavior change at all.
  • The plugin is a pure consumer of a small contract (list / exec). No gateway URL, no token path, no model table, no per-agent code path — whatever list prints becomes a picker entry.
  • The picker becomes two pages so the familiar path stays one page and the harness list can grow freely.

Changes

scripts/agent-registry.sh

  • PATH-detect the helper; merge its 4-column TSV (name/model/available/note) into the 5-column list as cli=cc-harness. Availability and note come straight from the helper — no re-probing of gateway or credentials.
  • Only literal available=yes counts as available (unknown fails closed for launch).
  • resolve cc-harness:<id> emits cc-harness-agents exec <id> -- claude [-n <session>] /work-system:continue. No --model: the helper sets it via env, then execs into claude, so the herdr pane roots at claude and agent_status + /close teardown stay intact.
  • Helper exit 3 ("capability absent" — no token) is a silent degrade, distinct from a listed-but-unavailable provider (resolve exit 3 with the helper's fix hint).

Picker (skills/kickoff/SKILL.md, referenced by /adopt)

  • Page 1: the familiar worker list plus a single cc-harness agents ▸ aggregate, shown only when the helper printed rows. Page 2: the concrete harness agents.
  • Merging flat would have meant ~12 entries against AskUserQuestion's hard 4-options-per-question cap — which the 7 native entries already exceeded.
  • The aggregate is a class, never a 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, /close Scenario A/B and tab glyphs work unchanged. Nothing assumes argv[0] == claude.
  • A committed 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'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.

Docs

  • New 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.
  • Knowledge, both READMEs, CHANGELOG, CLAUDE.md; version bumped to 1.12.0 in plugin.json + marketplace.json.

Readiness

  • ✅ Uncommitted changes — clean
  • ✅ Rebase vs main — up-to-date
  • ✅ README — root + plugin updated
  • ✅ Version — work-system 1.11.0 → 1.12.0 (minor), both files in sync
  • ✅ Changelog — 1.12.0 entry added
  • ✅ Knowledge — kickoff-agent-selection.md + _index.md updated
  • ✅ Tests — check-structure.py green (0 errors), incl. test_agent_registry.py
  • ➖ Linter / Build — N/A (declarative repo, no build step)

Dependency status

The paired dotfiles helper (gering/dotfiles PR #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 --pick run is the remaining verification.

Test plan

  • python3 plugins/work-system/scripts/test_agent_registry.py — passes
  • python3 scripts/check-structure.py — 0 errors
  • With no helper on PATH: agent-registry.sh list is unchanged, and resolve cc-harness:grok exits 2 with a hint naming the helper
  • With a stub helper on PATH: all its rows appear in list (including the last one), --json and the human table agree on the row count
  • resolve cc-harness:<id> --session foo yields cc-harness-agents exec <id> -- claude -n foo /work-system:continue with no --model
  • A stub row with available=no still emits argv and exits 3 with the helper's note

🤖 Generated with Claude Code

https://claude.ai/code/session_01DE9DzwzzsMMu84KxC1cB5R

gering and others added 4 commits August 16, 2026 14:53
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
gering force-pushed the task/offer-cc-harness-agents-at-kickoff branch from c40a63c to 1148467 Compare August 16, 2026 13:02
gering and others added 2 commits August 16, 2026 15:12
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
gering force-pushed the task/offer-cc-harness-agents-at-kickoff branch from be025cf to 22b8f6b Compare August 19, 2026 12:29
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant