Skip to content

Pi local-model executor: model profiles + board-completion parity#616

Open
bborn wants to merge 1 commit into
mainfrom
task/4404-local-model-executor-via-pi-model-profil
Open

Pi local-model executor: model profiles + board-completion parity#616
bborn wants to merge 1 commit into
mainfrom
task/4404-local-model-executor-via-pi-model-profil

Conversation

@bborn

@bborn bborn commented Jun 23, 2026

Copy link
Copy Markdown
Owner

Goal

Make the existing Pi executor usable as a real "local (or hosted) coding model" executor with Kanban-board parity — a Pi task running on a local Qwen (or any OpenAI-compatible endpoint) selects its model and reports completion back to the board, hands-off, the way Claude tasks do.

The gap this closes

runPi calls neither setupClaudeHooks nor writeWorkflowMCPConfig, so Pi tasks get no taskyou MCP server and no hooks. A hands-off Pi process exit defaulted to NeedsInput, never Done — so a Pi task could never report completion. Completion is not MCP-locked: the taskyou_complete MCP tool and the CLI task status done <id> both call the same db.UpdateTaskStatus. So any executor can complete a task via the CLI, no MCP protocol work required.

Three additive changes (Pi path only — Claude path untouched)

1. Model profiles / selection

  • New named-profile config at ~/.config/taskyou/pi-models.json (internal/executor/pi_profiles.go), keyed by name to {provider, model, base_url?, api_key_env?}.
  • Per-task selector: new model_profile DB column + task create --model-profile <name> CLI flag, threaded through mirroring the existing EffortLevel pattern.
  • Built-in providers (e.g. OpenRouter): injects --provider <p> --model <m>; the API key is resolved by Pi from its conventional env var and never appears on the command line.
  • Custom OpenAI-compatible endpoints (Ollama / vLLM / LM Studio): Pi has no base-URL CLI flag, so the provider is registered in Pi's models.json via EnsurePiCustomProvider (atomic write, preserves existing providers, idempotent), then selected with the same --provider/--model flags.
  • provider/model values are validated against a safe token charset, so a profile can never inject shell.

2. Completion signalling (primary)

Every Pi task gets a completion instruction appended via --append-system-prompt, telling the agent to run task status $WORKTREE_TASK_ID done when finished (or ... blocked when it needs input) through Pi's built-in bash tool. $WORKTREE_TASK_ID is already exported into the pane. This rides the existing CLI -> UpdateTaskStatus path — no MCP.

3. Completion backstop (belt-and-suspenders)

windowDeathResult() now maps a clean Pi exit with no self-report to agent-success (-> Backlog / "awaiting human review", exactly how Claude's agent-success is handled) instead of stranding it in NeedsInput. A Pi task that self-reported blocked via the CLI keeps that state. The Claude path is unchanged — a Claude clean exit with no recorded status still surfaces as NeedsInput.

The two-command-builder sync hazard

The Pi command is built in two places that must stay identical — the daemon (runPi / runPiResume) and the TUI (PiExecutor.BuildCommand). Both now derive their model+completion flag segment from a single shared helper piExtraFlags(task) (and the daemon side from buildPiDaemonScript). TestPiCommandBuildersStayInSync asserts both builders embed the identical segment, guarding against the divergence class that previously bit the Claude CLAUDE_CONFIG_DIR path.

Testing

Unit tests are the core deliverable (internal/executor/pi_profiles_test.go):

  • Exact command strings for buildPiDaemonScript (fresh + resume).
  • piExtraFlags with/without a profile; asserts the API key/env never leaks onto the command line.
  • The two builders stay in sync.
  • commandFlags rejects shell-metachar provider/model values.
  • EnsurePiCustomProvider writes the right schema, preserves existing providers, is idempotent, and is a no-op for built-in providers.
  • windowDeathResult status mapping across Claude vs Pi.

go build ./..., go vet, and golangci-lint run (v2.8.0, matching CI) are clean; internal/db and internal/executor packages green.

Schema / flag verification (no live key needed)

Validated against the locally-installed pi CLI without an API call:

  • --provider, --model, --append-system-prompt, --session, --continue all confirmed in pi --help; PI_CODING_AGENT_DIR default ~/.pi/agent matches piAgentDir().
  • The models.json provider/model object we write matches Pi's actual typebox validator (ModelDefinitionSchema / ProviderConfigSchema in model-registry.js) — baseUrl+apiKey+models[] full-replacement form with all required model fields.
  • task status <task-id> <status> arg order confirmed against the cobra command.

Live smoke test — not run

No OPENROUTER_API_KEY in the environment and no local model server available, so the end-to-end "task lands as done on the board" run was not executed (per the task, this is best-effort and must not block).

Manual smoke-test steps:

  1. Create ~/.config/taskyou/pi-models.json with a profile like:
    { "profiles": { "qwen-openrouter": { "provider": "openrouter", "model": "qwen/qwen3-coder", "api_key_env": "OPENROUTER_API_KEY" } } }
  2. export OPENROUTER_API_KEY=...
  3. task create --executor pi --model-profile qwen-openrouter -x "Make a trivial change and mark the task done"
  4. Confirm the card lands in Backlog/Done on the board (primary: agent runs task status ... done; backstop: clean exit -> Backlog).

For a local Ollama profile, add "base_url": "http://localhost:11434/v1" to the profile and confirm ~/.pi/agent/models.json gets the provider registered on launch.

Follow-ups (out of scope)

  • --mode rpc live status streaming for processing/blocked parity (currently only terminal state is reported).
  • Same treatment for the OpenCode executor.
  • A TUI profile-picker UI.

🤖 Generated with Claude Code

…letion parity

Make the Pi executor usable as a real local/hosted coding-model executor with
Kanban-board parity: a Pi task selects its model AND reports completion back to
the board hands-off, the way Claude tasks do.

Three additive changes to the Pi path only (Claude path untouched):

1. Model profiles (internal/executor/pi_profiles.go)
   - Named provider+model(+base_url+api_key_env) config at
     ~/.config/taskyou/pi-models.json, selected per-task via a new
     `model_profile` column (CLI: `task create --model-profile <name>`).
   - Built-in providers (OpenRouter etc.) -> just `--provider/--model`; key
     resolved by Pi from its conventional env var, never on the command line.
   - Custom OpenAI-compatible endpoints (Ollama/vLLM/LM Studio) are registered
     in Pi's models.json (EnsurePiCustomProvider); schema verified against Pi's
     actual typebox validator.
   - provider/model validated to a safe token charset (no shell injection).

2. Completion signalling (primary): every Pi task gets a completion
   system prompt via --append-system-prompt instructing the agent to run
   `task status $WORKTREE_TASK_ID done|blocked` through its bash tool — rides
   the existing CLI -> db.UpdateTaskStatus path, no MCP server needed.

3. Completion backstop: windowDeathResult() now treats a clean Pi process
   exit (no self-report) as agent-success -> Backlog/awaiting-review instead of
   stranding it in NeedsInput. Claude path behavior is unchanged; a Pi task that
   self-reported `blocked` keeps that state.

Sync hazard: the Pi command is built in two places (daemon runPi/runPiResume and
TUI PiExecutor.BuildCommand). Both now derive their flag segment from the single
shared piExtraFlags()/buildPiDaemonScript() helpers, with
TestPiCommandBuildersStayInSync guarding against divergence.

Follow-ups (not in scope): --mode rpc live processing/blocked status streaming;
the same treatment for the OpenCode executor; a TUI profile-picker UI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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