fix: separate Pi ACP and passthrough commands (#2708)

This commit is contained in:
Carlos Florêncio
2026-08-16 17:39:51 +01:00
committed by GitHub
parent 0de77f7871
commit 9558d8b5ee
11 changed files with 321 additions and 20 deletions
+1 -1
View File
@@ -85,7 +85,7 @@ Connect Kandev to the tools your team already uses — pull issues into the kanb
| **Factory Droid** | `droid` |
| **iFlow (beta)** | `@iflow-ai/iflow-cli` |
| **Kilocode** | `@kilocode/cli` |
| **Pi** | `pi-acp` |
| **Pi** | ACP adapter: `pi-acp`; CLI passthrough: `pi` |
| **Kimi** | `kimi` *(install Kimi CLI from Moonshot AI)* |
| **Kiro** | `kiro-cli-chat` *(install Kiro CLI from AWS)* |
| **Qoder** | `qodercli` *(install Qoder CLI)* |
@@ -2,7 +2,10 @@ package agents
import (
"context"
"os"
"os/exec"
"path/filepath"
"runtime"
"slices"
"strings"
"testing"
@@ -21,8 +24,8 @@ type acpAgentSpec struct {
// detectBinaries lists every binary name IsInstalled accepts via
// WithCommand. Used by TestNewACPAgents_DetectionRequiresGlobalBinary
// to skip the test whenever ANY of these is on PATH — otherwise an
// agent with multiple WithCommand fallbacks (e.g. Pi accepts both
// `pi-acp` and `pi`) flakes when the secondary binary is present.
// agent with multiple WithCommand fallbacks flakes when a secondary binary
// is present.
detectBinaries []string
expectedArgv []string // BuildCommand and Runtime.Cmd
inferenceArgv []string // InferenceConfig.Command
@@ -76,11 +79,12 @@ var newACPAgentSpecs = []struct {
sessionDirTemplate: "{home}/.kilocode",
}},
{func() Agent { return NewPiACP() }, acpAgentSpec{
id: "pi-acp", displayName: "Pi", detectBinaries: []string{"pi-acp", "pi"},
id: "pi-acp", displayName: "Pi", detectBinaries: []string{"pi"},
expectedArgv: []string{"npx", "-y", "pi-acp"},
inferenceArgv: []string{"npx", "-y", "pi-acp"},
passthroughArgv: []string{"npx", "-y", "pi-acp"},
passthroughArgv: []string{"pi"},
installViaNpm: true,
installScript: "npm install -g --ignore-scripts @earendil-works/pi-coding-agent",
sessionDirTemplate: "{home}/.pi",
}},
{func() Agent { return NewCursorACP() }, acpAgentSpec{
@@ -287,7 +291,7 @@ func TestNewACPAgents_InstallScript(t *testing.T) {
// to use. detectBinaries comes from the spec table so npx-launched agents
// (whose argv[0] is "npx") are still verified against their real detection
// targets (qwen, iflow, droid, …), and agents with multiple WithCommand
// fallbacks (e.g. Pi accepts both `pi-acp` and `pi`) skip the test
// fallbacks skip the test
// whenever ANY of those is on PATH so the test doesn't flake on CI hosts
// that happen to have a generic `pi` (Raspberry Pi tooling) installed.
func TestNewACPAgents_DetectionRequiresGlobalBinary(t *testing.T) {
@@ -310,6 +314,27 @@ func TestNewACPAgents_DetectionRequiresGlobalBinary(t *testing.T) {
}
}
func TestPiACPDetectionRejectsPiBinaryWithoutVersionSupport(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("shell executable fixture is Unix-specific")
}
binDir := t.TempDir()
piPath := filepath.Join(binDir, "pi")
if err := os.WriteFile(piPath, []byte("#!/bin/sh\nexit 1\n"), 0o755); err != nil {
t.Fatalf("write fake pi executable: %v", err)
}
t.Setenv("PATH", binDir)
result, err := NewPiACP().IsInstalled(context.Background())
if err != nil {
t.Fatalf("IsInstalled error: %v", err)
}
if result.Available {
t.Fatalf("Available=true for pi executable that fails --version")
}
}
// TestNewACPAgents_LogosNonEmpty guards against agents shipping with empty
// embedded SVGs (which renders as a broken <img> in the UI).
func TestNewACPAgents_LogosNonEmpty(t *testing.T) {
+12 -10
View File
@@ -16,7 +16,11 @@ var piACPLogoLight []byte
//go:embed logos/pi_acp_dark.svg
var piACPLogoDark []byte
const piACPPkg = "pi-acp"
const (
piACPPkg = "pi-acp"
piCLIBin = "pi"
piCLIPkg = "@earendil-works/pi-coding-agent"
)
var (
_ Agent = (*PiACP)(nil)
@@ -37,7 +41,7 @@ func NewPiACP() *PiACP {
Supported: true,
Label: "CLI Passthrough",
Description: "Show terminal directly instead of chat interface",
PassthroughCmd: NewCommand("npx", "-y", piACPPkg),
PassthroughCmd: NewCommand(piCLIBin),
ModelFlag: NewParam("--model", "{model}"),
IdleTimeout: 3 * time.Second,
BufferMaxBytes: DefaultBufferMaxBytes,
@@ -64,13 +68,11 @@ func (a *PiACP) Logo(v LogoVariant) []byte {
}
func (a *PiACP) IsInstalled(ctx context.Context) (*DiscoveryResult, error) {
// "pi" is short and could in theory collide with unrelated tooling, but
// the pi-mono coding agent (https://github.com/badlogic/pi-mono) ships
// its CLI as `pi`, and a kandev user with `pi` on PATH is overwhelmingly
// likely to be the pi coding-agent installation we're targeting.
// Checking `pi-acp` first lets a dedicated wrapper take precedence
// when both are present.
result, err := Detect(ctx, WithCommand("pi-acp"), WithCommand("pi"))
// The Pi package publishes the short `pi` binary used by passthrough. A
// non-interactive version check is a best-effort filter for unrelated tools
// with the same name; it does not prove package identity. The separate ACP
// probe validates the structured `npx -y pi-acp` surface, not this binary.
result, err := Detect(ctx, WithCommandCheck(piCLIBin, "--version"))
if err != nil {
return result, err
}
@@ -111,7 +113,7 @@ func (a *PiACP) Runtime() *RuntimeConfig {
func (a *PiACP) RemoteAuth() *RemoteAuth { return nil }
func (a *PiACP) InstallScript() string {
return "npm install -g " + piACPPkg
return "npm install -g --ignore-scripts " + piCLIPkg
}
func (a *PiACP) PermissionSettings() map[string]PermissionSetting {
@@ -536,9 +536,13 @@ func TestPassthroughPiWritesProjectFile(t *testing.T) {
mgr, execution, profile := newPassthroughMCPTestManager(t, "pi-acp")
piPath := filepath.Join(execution.WorkspacePath, ".pi", "mcp.json")
if _, _, _, _, err := mgr.passthroughAgentCommand(context.Background(), execution, profile); err != nil {
_, _, _, cmd, err := mgr.passthroughAgentCommand(context.Background(), execution, profile)
if err != nil {
t.Fatalf("passthroughAgentCommand returned error: %v", err)
}
if got, want := cmd.Args(), []string{"pi", "--model", "default"}; !slices.Equal(got, want) {
t.Fatalf("passthrough command = %v, want %v", got, want)
}
data, err := os.ReadFile(piPath)
if err != nil {
@@ -276,7 +276,7 @@ test("@search reveals and flashes a cached preview content match selected with E
await expect(dialog).toBeVisible();
await dialog.getByRole("combobox").fill(CACHED_PREVIEW_MARKER);
const targetResult = dialog.getByTestId("content-search-result").filter({ hasText: targetPath });
await expect(targetResult).toHaveCount(1);
await expect(targetResult).toHaveCount(1, { timeout: 30_000 });
await testPage.keyboard.press("Enter");
await expect(dialog).not.toBeVisible();
@@ -1,6 +1,6 @@
# ADR-2026-08-12-validated-managed-runtime-version-selection: Validate and Persist Managed Runtime Version Selection
**Status:** accepted
**Status:** accepted (amended 2026-08-16)
**Date:** 2026-08-12
**Area:** backend, frontend, protocol
**Supersedes:**
@@ -45,6 +45,12 @@ continue to resolve their own runtime. When no selection record exists, Kandev
retains the legacy unversioned host behavior until the first candidate is
successfully activated.
The managed package and exact-version override belong only to an agent's ACP
command surfaces. They do not replace `PassthroughConfig.PassthroughCmd`, an
interactive authentication helper, or either surface's install recipe. Agents
such as Pi can therefore use an ACP adapter package for structured execution
and a separately distributed native CLI for terminal passthrough.
The selection is install-wide and per built-in agent. A saved selection applies
only while its stored package identity matches the agent's current trusted
package metadata; a package change starts with no selection and requires fresh
@@ -85,3 +91,6 @@ capabilities.
transactional selection without reimplementing package storage.
- Applying the host selection to remote executors was rejected because each
remote runtime has separate platform, cache, credentials, and ownership.
- Reusing the managed ACP package for terminal passthrough was rejected because
an ACP JSON-RPC stdio adapter is not an interactive PTY application, and some
integrations distribute those surfaces as different packages and binaries.
+90
View File
@@ -0,0 +1,90 @@
---
spec: docs/specs/cli-mode-parity/spec.md
created: 2026-08-16
status: complete
---
# Implementation Plan: Separate Pi Runtime Surfaces
## Overview
Pi's built-in definition assigns `npx -y pi-acp` to both structured ACP
execution and interactive passthrough. Registry metadata confirms that
`pi-acp` publishes the ACP adapter binary, while
`@earendil-works/pi-coding-agent` publishes the interactive `pi` binary. The
repair keeps the ACP adapter on structured and inference paths, launches `pi`
for passthrough, and makes installation and discovery agree on that executable.
The durable managed-runtime boundary is recorded in
[`docs/decisions/2026-08-12-validated-managed-runtime-version-selection.md`](../../decisions/2026-08-12-validated-managed-runtime-version-selection.md),
and the observable behavior is specified in
[`docs/specs/cli-mode-parity/spec.md`](../../specs/cli-mode-parity/spec.md) and
[`docs/specs/agents/runtime-updates.md`](../../specs/agents/runtime-updates.md).
---
## Backend
### Pi agent definition
- Update `apps/backend/internal/agent/agents/pi_acp.go` so
`BuildCommand`, `Runtime().Cmd`, and `InferenceConfig().Command` remain
`npx -y pi-acp`.
- Change `PassthroughConfig.PassthroughCmd` to the globally installed `pi`
executable. Do not route the passthrough path through managed ACP package or
version resolution.
- Make `IsInstalled` probe `pi`, the executable provisioned for users and
required by passthrough, with a non-interactive `--version` check instead of
accepting an adapter-only `pi-acp` installation or an unrelated `pi`
executable that cannot satisfy the full integration.
- Change `InstallScript` to the supplied safe install recipe:
`npm install -g --ignore-scripts @earendil-works/pi-coding-agent`.
### Public documentation
- Update `docs/public/agents-and-profiles.md` to identify `pi` as Pi's detected
and passthrough executable, document the exact install command, and explain
that structured sessions continue through `pi-acp`.
- Clarify the Pi entry in `README.md` so the supported-agent table does not
imply that the ACP adapter is also the interactive CLI.
## Tests
- **What:** Structured Pi chat and inference remain on `npx -y pi-acp`, while
`PassthroughCmd` is exactly `pi`; the install recipe and detection binary
match the interactive package.
**File:** `apps/backend/internal/agent/agents/new_acp_agents_test.go`.
**How:** Change the Pi row in the shared command-surface contract before the
production edit. Confirm the command, installation, and detection tests fail
for the reported mismatch, then pass after the repair.
- **What:** The lifecycle's real Pi passthrough resolution, including MCP
project-file materialization, returns `pi` rather than the ACP adapter.
**File:**
`apps/backend/internal/agent/runtime/lifecycle/manager_passthrough_test.go`.
**How:** Extend `TestPassthroughPiWritesProjectFile` to assert the resolved
command argv as well as the existing `.pi/mcp.json` contract. This covers the
registry-to-lifecycle passthrough path where a future managed-runtime change
could otherwise reintroduce the conflation.
No browser E2E is needed because the settings UI and passthrough terminal UI do
not change; the behavior is the backend command selected behind the existing
profile mode.
## Verification Results
- Red: the focused Go contract run failed on Pi's old passthrough command and
install script, as expected.
- Green: the focused Go command, including the `pi --version` collision
regression, passed with 43 tests across the agents and lifecycle packages.
- PR fixup: the detection probe now uses `WithCommandCheck("pi", "--version")`
and restores the rationale comment for the remaining binary-name tradeoff.
- Public docs: `node --test scripts/validate-public-docs.test.mjs` passed all
61 tests, `node scripts/validate-public-docs.mjs` validated 41 pages, and
`git diff --check` passed.
## Implementation Tasks
- [x] [task-01-correct-pi-execution-surfaces](task-01-correct-pi-execution-surfaces.md)
Execution is sequential in the primary conversation. There are no parallel
candidates for this single vertical repair.
@@ -0,0 +1,112 @@
---
id: "01-correct-pi-execution-surfaces"
title: "Correct Pi execution surfaces"
status: done
wave: 1
depends_on: []
plan: "plan.md"
spec: "../../specs/cli-mode-parity/spec.md"
---
# Task 01: Correct Pi Execution Surfaces
## Intent
Keep Pi's ACP JSON-RPC adapter on structured execution surfaces while launching
the separately distributed interactive `pi` CLI for passthrough. Align host
installation and discovery with the executable users actually run.
## Inputs
- `docs/specs/cli-mode-parity/spec.md`, sections **ACP and passthrough commands
remain distinct** and the three Pi scenarios.
- `docs/specs/agents/runtime-updates.md`, managed-runtime command-routing
boundary.
- `docs/decisions/2026-08-12-validated-managed-runtime-version-selection.md`,
amended ACP-only ownership decision.
- Existing agent pattern: `apps/backend/internal/agent/agents/claude_acp.go`
keeps its managed ACP package separate from its native passthrough CLI.
## Dependencies
None.
## Files Likely Touched
- `apps/backend/internal/agent/agents/pi_acp.go`
- `apps/backend/internal/agent/agents/new_acp_agents_test.go`
- `apps/backend/internal/agent/runtime/lifecycle/manager_passthrough_test.go`
- `docs/public/agents-and-profiles.md`
- `README.md`
- `docs/plans/pi-runtime-surfaces/plan.md`
- `docs/plans/pi-runtime-surfaces/task-01-correct-pi-execution-surfaces.md`
## TDD Sequence
1. Change the Pi expectations in `new_acp_agents_test.go` to require
`PassthroughCmd == ["pi"]`, detection through `pi --version`, and the exact install
script
`npm install -g --ignore-scripts @earendil-works/pi-coding-agent`.
2. Extend `TestPassthroughPiWritesProjectFile` to require the lifecycle-resolved
passthrough argv to be `["pi"]` while preserving the MCP file assertions.
3. Run the targeted Go command and confirm it fails because production still
returns `npx -y pi-acp` and the old install/detection contract.
4. Apply the minimal `pi_acp.go` changes. Do not change structured ACP or
inference argv.
5. Update the public Pi installation and command-surface documentation.
6. Run the exact verification block and record every result below and in
`plan.md`.
## Acceptance Criteria
- Structured `BuildCommand`, `Runtime().Cmd`, and one-shot inference remain
`npx -y pi-acp`.
- Passthrough lifecycle command resolution returns `pi`, including the path
that materializes Pi's project MCP configuration.
- Pi discovery requires `pi`, and installation runs exactly
`npm install -g --ignore-scripts @earendil-works/pi-coding-agent`; discovery
rejects a same-named executable that fails the non-interactive version check.
- Public docs explain the separate ACP adapter and interactive CLI without
implying that managed ACP version selection controls passthrough.
## Verification
Run from the repository root:
```bash
cd apps/backend && go test ./internal/agent/agents ./internal/agent/runtime/lifecycle -run 'Test(NewACPAgents_(AllCommandSurfaces|InstallScript|DetectionRequiresGlobalBinary)|PiACPDetectionRejectsPiBinaryWithoutVersionSupport|PassthroughPiWritesProjectFile)$' -count=1 && cd ../../ && node --test scripts/validate-public-docs.test.mjs && node scripts/validate-public-docs.mjs && git diff --check
```
## Parallelism
`sequential`. The agent definition, shared contract fixture, lifecycle
regression, and documentation describe one coupled behavior.
## Risks
- Hosts that have only a global `pi-acp` adapter will become correctly
unavailable until the interactive Pi package is installed; this is required
because adapter-only installation cannot run passthrough.
- The package is installed with `--ignore-scripts` exactly as reported. The
implementation must not relax or remove that safety flag.
- No package version-management support is added for `pi-acp` in this repair.
## Output Contract
Report the root-cause fix, files changed, red/green command results, public-doc
validation, blockers, remaining risks, and synchronized task/plan status in the
primary conversation.
## Results
- Red: the focused Go contract run failed on the old Pi passthrough argv and
install script before the production change.
- Green: the focused Go command passed with 43 tests across the agents and
lifecycle packages, including the collision regression for `pi --version`.
- Public docs validation passed: 61 validation tests, 41 published pages, and
`git diff --check`.
- The lifecycle regression preserves Pi's existing `--model default` profile
flag while proving the executable is `pi` and the `.pi/mcp.json` file is
materialized.
- PR fixup: restored the `pi` collision rationale and validated the package's
non-interactive version flag with the repository's `WithCommandCheck` helper.
+13
View File
@@ -24,6 +24,19 @@ The production registry currently shows Auggie, Claude, Codex, Copilot, Gemini,
Hermes launches with `hermes acp`. Install the required `hermes` executable from its **Settings > Agents** card, which runs the official Hermes installer. Hermes currently supports task and workspace sessions. Office-assigned skill injection is not yet supported.
### Pi command surfaces
Pi uses separate executables for its two Kandev modes:
- Structured ACP sessions and one-shot inference use `npx -y pi-acp`.
- CLI Passthrough starts the globally installed `pi` executable.
- The Pi install action runs `npm install -g --ignore-scripts @earendil-works/pi-coding-agent`.
After installation, select **Rescan**. Kandev detects Pi when `pi` is on the
`PATH` of the backend process and responds to the non-interactive `--version`
check. The `pi-acp` adapter is not the interactive terminal CLI, and managed
ACP runtime selection does not replace the passthrough command.
1. Select **Rescan** after installing or updating a CLI.
2. If the card offers an install action, review the command before running it. Installation runs on the Kandev host.
3. If the card reports that login is required, open its login terminal or authenticate the CLI as the same operating-system user that runs Kandev.
+11 -1
View File
@@ -1,7 +1,7 @@
---
status: approved
created: 2026-07-26
updated: 2026-08-12
updated: 2026-08-16
owner: Kandev
---
@@ -40,6 +40,10 @@ broken release again.
metadata. A caller can submit only a version returned by the trusted
package's npm metadata. Tags, prereleases, package specs, registry URLs, and
shell text are rejected.
- Managed runtime resolution changes only the structured ACP command surfaces.
When an agent's interactive passthrough CLI is distributed separately from
its ACP adapter, passthrough keeps its own package, executable, and install
recipe.
- Jobs for one agent are idempotent while queued or running. Installation and
version management for the same agent cannot run concurrently.
@@ -197,6 +201,8 @@ version while its current probe is unknown produces a repair job.
sessionless utility prompts use the same active-version resolver.
- Native-binary preference continues to win when an agent deliberately selects
its supported native binary path.
- Passthrough command construction does not receive or apply the active managed
ACP version. It continues to use the agent's declared interactive CLI.
- SSH and container command builders receive no host version override.
- A saved selection read error fails the new host command with an actionable
error. It does not fall back to an unversioned package.
@@ -271,6 +277,10 @@ version while its current probe is unknown produces a repair job.
and does not run a second candidate concurrently.
- **GIVEN** an SSH or container session, **WHEN** the host active version
changes, **THEN** that executor's command remains unchanged.
- **GIVEN** an agent whose interactive passthrough CLI is separate from its
managed ACP adapter, **WHEN** the host active ACP version changes, **THEN**
later passthrough sessions still launch the declared interactive CLI and do
not launch the ACP package under a PTY.
- **GIVEN** a phone viewport and a long version catalogue or process log,
**WHEN** the operator selects and activates a version, **THEN** the drawer
remains contained and the primary action remains touch-reachable.
+36
View File
@@ -1,6 +1,7 @@
---
status: draft
created: 2026-05-16
updated: 2026-08-16
owner: cfl
issue: https://github.com/kdlbs/kandev/issues/906
needs-upgrade: true
@@ -22,6 +23,21 @@ This spec brings CLI-passthrough mode up to feature parity with ACP for the **ka
## What
### ACP and passthrough commands remain distinct
A built-in agent that uses a separate ACP adapter and interactive CLI keeps
those execution surfaces separate. Structured chat and one-shot inference use
the ACP adapter. CLI passthrough launches the interactive CLI directly under
the PTY; managed ACP runtime resolution does not replace that passthrough
command.
For Pi, structured chat and inference use `npx -y pi-acp`, while CLI
passthrough launches the globally installed `pi` executable. Kandev's Pi
install action runs
`npm install -g --ignore-scripts @earendil-works/pi-coding-agent`, and agent
discovery treats a `pi` executable on the Kandev process `PATH` that passes its
non-interactive `--version` check as the installation signal.
### Prompt allowed at task creation in CLI mode
The task-create dialog's prompt textarea is **enabled** when a CLI/passthrough-capable agent is selected. Users can write the prompt the same way they would for ACP. The control flow that today gates this off when `cli_passthrough` is true is removed.
@@ -58,6 +74,25 @@ Users can still press Ctrl-C directly inside the xterm terminal. A dedicated too
## Scenarios
### Pi uses its interactive CLI in passthrough mode
- GIVEN a Pi profile with `cli_passthrough: true` and a `pi` executable on the
Kandev process `PATH` that passes `pi --version`
- WHEN Kandev starts its passthrough PTY
- THEN the command starts with `pi`
- AND Kandev does not launch `pi-acp` or `npx` as the passthrough process
### Pi keeps its ACP adapter for structured execution
- GIVEN a Pi profile with `cli_passthrough: false`
- WHEN Kandev starts structured chat or one-shot inference
- THEN the command remains `npx -y pi-acp`
### Pi installation provisions the passthrough executable
- GIVEN Pi is unavailable on the Kandev host
- WHEN the operator runs the Pi install action and rescans agents
- THEN Kandev runs
`npm install -g --ignore-scripts @earendil-works/pi-coding-agent`
- AND discovery finds the installed `pi` executable after its version check
### CLI-mode task can have a prompt
- GIVEN a Claude profile with `cli_passthrough: true`
- WHEN the user opens the task-create dialog and types in the prompt field
@@ -161,6 +196,7 @@ Pressing Ctrl-C inside the passthrough terminal writes `\x03` to PTY stdin. A fu
- Parsing PTY output into `task_messages` (transcriber). The terminal panel shows live output; the chat transcript only shows user-sent text in CLI mode.
- Billing-type / subscription-quota badge or DTO surface. That belongs to `subscription-usage.md`.
- Adding passthrough support to agents that don't have it (`Supported: false`).
- Adding `pi-acp` to the managed runtime update catalogue.
- Migrating away from the current ACP bridge.
- Headless `-p` mode for Claude (drains API credit; we deliberately do not use it).