feat: provider seam v2 — generalize the agent-CLI contract (#51) #53

Merged
dominik.polakovics merged 1 commit from lab/20260708-1652 into main 2026-07-08 20:32:22 +02:00

Evolve the AgentProvider seam so lab's chat surface works with any agent CLI (Codex, Gemini, …), not only Claude Code — without implementing a second provider. Every claude assumption is lifted into provider-declared metadata or a documented seam obligation; claude-code is brought up to the generalized contract and proven provider-neutral by construction.

What changed

Seam (internal/provider)

  • Dialog gains Kind (question|plan|approval) and Questions[]; DialogAnswer gains per-question Answers[] (positional: answers[i]questions[i]).
  • New mandatory Commands(ctx, worktree) slash-command catalog; DisplayName, an AuthFlow descriptor (oauth-code|oauth-redirect|api-key|external — api-key schema-only), and SeedMeta join the interface.
  • DialogHooker → provider-neutral LiveSignals (same contract). Sentinel errors + the auth-changed event move to the provider package; the SSE event is now provider.auth.changed carrying the provider id.
  • Contract comments pin the clear/epoch obligation, reply-while-working (best-effort), and hazard-checked interrupt recipes.

claude-code adapter + internal/compat

  • Sent slash commands render as plain user text; command echoes stay excluded from state derivation (regression test for the post-/clear stuck composer).
  • Multi-question AskUserQuestion and ExitPlanMode plan approval are now answerable; synthesized option rows are write-side pinned constants (never a pane scrape).
  • A post-resolve verification backstop compares recorded answers against intent and emits a visible warning on a blind-sequence desync.
  • Real command catalog: pinned builtin table (bundle-extracted 2.1.198) merged with project/user/skill scans, curated to chat-safe entries.

HTTP/SSE, seeder, frontend

  • Auth routes generalize to /providers/{id}/auth/*; new GET /runs/{id}/commands (chat-safe, briefly cached); the answer endpoint accepts answers[]. httpapi no longer imports claudecode.
  • One generic seeder consumes provider-declared SeedMeta; seeded worktree contents are byte-identical for claude-code (golden-pinned both sides).
  • Composer slash autocomplete + role=clear "New conversation"; multi-question form and plan approval render with real content; descriptor-driven ProviderAuthCard; provider display metadata drives all copy (grep-guarded — zero hardcoded "Claude"/"claude.ai" in components).

Live verification (Claude Code 2.1.198, 2026-07-08)

The recipes were driven end to end through the production Reply/AnswerDialog path against the real binary under tmux (TestCompat_Live_*, LAB_COMPAT_LIVE=1) — the ADR-0020 process. This found and fixed four defects snapshot tests could not:

  1. The picker's Up wraps, it does not clamp — the normalize-to-top climb corrupted answers; on the review screen [Up][Enter] would land on "Cancel" and discard the whole form. Removed the climb (pickers open on their top row; walk downward only; review is a bare [Enter]).
  2. Enter on the empty free-text row declines the whole dialog — the Other recipe is now type-first.
  3. Enter on a multi-select option toggles, never commits — commit is via a dedicated unnumbered "Submit" row.
  4. A multi-line reply's Enter raced the paste and was dropped — the submit Enter is now paced by keyDelay.

Two non-bug findings shaped the design: recorded multi-select label order is not option-index order (the backstop compares as a set), and the plan picker's row-0 label drifts with session state under the same spawn flag (lab surfaces its own stable semantic labels, coupling only to the index).

Review

Three parallel adversarial reviews (seam/adapter, HTTP/seeder/wiring, frontend) ran over the diff. Two independently found the same real defect — an empty-SeedMeta pre-push hook rendered for … do; done with an empty body (a /bin/sh syntax error that would reject every push in a degraded/no-registry boot). Fixed with a : no-op body + an sh -n regression test across all four hook variants. A frontend review found "New conversation" stayed clickable mid-turn (bypassing the ADR-0022 send-block); fixed by gating on state() !== 'working' + a regression test. All other areas verified correct.

Gates

gofmt/go vet clean; go test ./... green except internal/tmuxx (pre-existing sandbox-environmental — tmux sessions exit immediately here; fails identically on clean main). Web: 452 vitest tests, build, lint, prettier all clean. Live recipe tests pass against 2.1.198.

Docs

ADR-0026 (amends ADR-0016 and ADR-0020); compat.md §5/§6/§7 rewritten + new §10 builtin catalog; CONTEXT.md gains the "Slash-command catalog" vocabulary.

Out of scope (per the issue)

No Codex/Gemini adapter, no protocol-mode/push adapter, no lab-owned message store, no TUI pane scraping.

Closes #51

🤖 Generated with Claude Code

https://claude.ai/code/session_012x6iHssVua15JWfas1tppV

Evolve the `AgentProvider` seam so lab's chat surface works with **any** agent CLI (Codex, Gemini, …), not only Claude Code — **without implementing a second provider**. Every claude assumption is lifted into provider-declared metadata or a documented seam obligation; claude-code is brought up to the generalized contract and proven provider-neutral by construction. ## What changed **Seam (`internal/provider`)** - `Dialog` gains `Kind` (`question|plan|approval`) and `Questions[]`; `DialogAnswer` gains per-question `Answers[]` (positional: `answers[i]` ↔ `questions[i]`). - New mandatory `Commands(ctx, worktree)` slash-command catalog; `DisplayName`, an `AuthFlow` descriptor (`oauth-code|oauth-redirect|api-key|external` — api-key schema-only), and `SeedMeta` join the interface. - `DialogHooker` → provider-neutral `LiveSignals` (same contract). Sentinel errors + the auth-changed event move to the provider package; the SSE event is now `provider.auth.changed` carrying the provider id. - Contract comments pin the clear/epoch obligation, reply-while-working (best-effort), and hazard-checked interrupt recipes. **claude-code adapter + `internal/compat`** - Sent slash commands render as plain user text; command echoes stay excluded from state derivation (regression test for the post-`/clear` stuck composer). - Multi-question `AskUserQuestion` and `ExitPlanMode` plan approval are now answerable; synthesized option rows are write-side pinned constants (never a pane scrape). - A post-resolve verification backstop compares recorded answers against intent and emits a visible warning on a blind-sequence desync. - Real command catalog: pinned builtin table (bundle-extracted 2.1.198) merged with project/user/skill scans, curated to chat-safe entries. **HTTP/SSE, seeder, frontend** - Auth routes generalize to `/providers/{id}/auth/*`; new `GET /runs/{id}/commands` (chat-safe, briefly cached); the answer endpoint accepts `answers[]`. `httpapi` no longer imports `claudecode`. - One generic seeder consumes provider-declared `SeedMeta`; seeded worktree contents are **byte-identical** for claude-code (golden-pinned both sides). - Composer slash autocomplete + `role=clear` "New conversation"; multi-question form and plan approval render with real content; descriptor-driven `ProviderAuthCard`; provider display metadata drives all copy (grep-guarded — zero hardcoded "Claude"/"claude.ai" in components). ## Live verification (Claude Code 2.1.198, 2026-07-08) The recipes were driven **end to end through the production `Reply`/`AnswerDialog` path** against the real binary under tmux (`TestCompat_Live_*`, `LAB_COMPAT_LIVE=1`) — the ADR-0020 process. This found and fixed **four** defects snapshot tests could not: 1. **The picker's `Up` wraps, it does not clamp** — the normalize-to-top climb corrupted answers; on the review screen `[Up][Enter]` would land on "Cancel" and discard the whole form. Removed the climb (pickers open on their top row; walk downward only; review is a bare `[Enter]`). 2. **`Enter` on the empty free-text row declines the whole dialog** — the Other recipe is now type-first. 3. **`Enter` on a multi-select option toggles, never commits** — commit is via a dedicated unnumbered "Submit" row. 4. **A multi-line reply's `Enter` raced the paste and was dropped** — the submit `Enter` is now paced by `keyDelay`. Two non-bug findings shaped the design: recorded multi-select label order is **not** option-index order (the backstop compares as a set), and the plan picker's row-0 label drifts with session state under the same spawn flag (lab surfaces its own stable semantic labels, coupling only to the index). ## Review Three parallel adversarial reviews (seam/adapter, HTTP/seeder/wiring, frontend) ran over the diff. Two independently found the same real defect — an empty-`SeedMeta` pre-push hook rendered `for … do; done` with an empty body (a `/bin/sh` syntax error that would reject **every** push in a degraded/no-registry boot). Fixed with a `:` no-op body + an `sh -n` regression test across all four hook variants. A frontend review found "New conversation" stayed clickable mid-turn (bypassing the ADR-0022 send-block); fixed by gating on `state() !== 'working'` + a regression test. All other areas verified correct. ## Gates `gofmt`/`go vet` clean; `go test ./...` green except `internal/tmuxx` (pre-existing sandbox-environmental — tmux sessions exit immediately here; fails identically on clean `main`). Web: 452 vitest tests, build, lint, prettier all clean. Live recipe tests pass against 2.1.198. ## Docs ADR-0026 (amends ADR-0016 and ADR-0020); `compat.md` §5/§6/§7 rewritten + new §10 builtin catalog; CONTEXT.md gains the "Slash-command catalog" vocabulary. ## Out of scope (per the issue) No Codex/Gemini adapter, no protocol-mode/push adapter, no lab-owned message store, no TUI pane scraping. Closes #51 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_012x6iHssVua15JWfas1tppV
feat: provider seam v2 — generalize the agent-CLI contract (#51)
All checks were successful
ci / native (pull_request) Successful in 4m8s
60b018e425
Evolve the AgentProvider seam so lab's chat surface works with any agent
CLI, not only Claude Code — without implementing a second provider. Every
claude assumption is lifted into provider-declared metadata or a documented
seam obligation; claude-code is brought up to the generalized contract.

Seam (internal/provider):
- Dialog gains Kind (question|plan|approval) and Questions[] for
  multi-question forms; DialogAnswer gains per-question Answers[].
- New mandatory Commands(ctx, worktree) slash-command catalog; DisplayName,
  AuthFlow descriptor (oauth-code|oauth-redirect|api-key|external — api-key
  schema-only), and SeedMeta join the interface.
- DialogHooker renamed to the provider-neutral LiveSignals (same contract).
- Sentinel errors + the auth-changed event move to the provider package;
  the SSE event becomes provider.auth.changed carrying the provider id.
- Contract comments pin the clear/epoch obligation, reply-while-working
  best-effort, and hazard-checked interrupt recipes.

claude-code adapter + compat:
- Sent slash commands render as plain user text; command echoes stay
  excluded from state derivation (regression test for the post-/clear
  stuck composer).
- Multi-question AskUserQuestion and ExitPlanMode plan approval are now
  answerable; synthesized option rows are write-side pinned constants.
- A post-resolve verification backstop compares recorded answers against
  intent and warns on a blind-sequence desync.
- Real command catalog: pinned builtin table (bundle-extracted 2.1.198) +
  project/user/skill scans, curated to chat-safe entries.

HTTP/SSE, seeder, frontend:
- Auth routes generalize to /providers/{id}/auth/*; new /runs/{id}/commands
  (chat-safe, briefly cached); the answer endpoint accepts answers[].
  httpapi no longer imports claudecode.
- One generic seeder consumes provider-declared SeedMeta; seeded worktree
  contents are byte-identical for claude-code (golden-pinned).
- Composer slash autocomplete + role=clear "New conversation"; multi-
  question form and plan approval render with real content; descriptor-
  driven ProviderAuthCard; provider display metadata drives all copy
  (grep-guarded — no hardcoded Claude strings in components).

Live verification (2.1.198, 2026-07-08): the recipes were driven end to end
through the production Reply/AnswerDialog path against the real binary,
finding and fixing four defects snapshot tests could not: the picker's Up
wraps (dropped the normalize-to-top climb; review is a bare Enter), Enter on
the empty free-text row declines the dialog (type-first), Enter on a multi-
select option toggles instead of committing (dedicated Submit row), and a
multi-line reply's Enter raced the paste (paced now). Recorded multi-select
label order is not option order (backstop compares as a set); the plan
picker's row-0 label drifts with session state (lab uses stable semantic
labels, coupling only to index).

Docs: ADR-0026 (amends 0016/0020), compat.md §5/§6/§7 + §10, CONTEXT.md
slash-command catalog vocabulary.

Closes #51

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012x6iHssVua15JWfas1tppV
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/coding-lab!53
No description provided.