Pi local-model executor: model profiles + board-completion parity#616
Open
bborn wants to merge 1 commit into
Open
Pi local-model executor: model profiles + board-completion parity#616bborn wants to merge 1 commit into
bborn wants to merge 1 commit into
Conversation
…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>
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.
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
runPicalls neithersetupClaudeHooksnorwriteWorkflowMCPConfig, 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: thetaskyou_completeMCP tool and the CLItask status done <id>both call the samedb.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
~/.config/taskyou/pi-models.json(internal/executor/pi_profiles.go), keyed by name to{provider, model, base_url?, api_key_env?}.model_profileDB column +task create --model-profile <name>CLI flag, threaded through mirroring the existingEffortLevelpattern.--provider <p> --model <m>; the API key is resolved by Pi from its conventional env var and never appears on the command line.models.jsonviaEnsurePiCustomProvider(atomic write, preserves existing providers, idempotent), then selected with the same--provider/--modelflags.2. Completion signalling (primary)
Every Pi task gets a completion instruction appended via
--append-system-prompt, telling the agent to runtask status $WORKTREE_TASK_ID donewhen finished (or... blockedwhen it needs input) through Pi's built-inbashtool.$WORKTREE_TASK_IDis already exported into the pane. This rides the existing CLI ->UpdateTaskStatuspath — 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-reportedblockedvia 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 helperpiExtraFlags(task)(and the daemon side frombuildPiDaemonScript).TestPiCommandBuildersStayInSyncasserts both builders embed the identical segment, guarding against the divergence class that previously bit the ClaudeCLAUDE_CONFIG_DIRpath.Testing
Unit tests are the core deliverable (
internal/executor/pi_profiles_test.go):buildPiDaemonScript(fresh + resume).piExtraFlagswith/without a profile; asserts the API key/env never leaks onto the command line.commandFlagsrejects shell-metachar provider/model values.EnsurePiCustomProviderwrites the right schema, preserves existing providers, is idempotent, and is a no-op for built-in providers.windowDeathResultstatus mapping across Claude vs Pi.go build ./...,go vet, andgolangci-lint run(v2.8.0, matching CI) are clean;internal/dbandinternal/executorpackages green.Schema / flag verification (no live key needed)
Validated against the locally-installed
piCLI without an API call:--provider,--model,--append-system-prompt,--session,--continueall confirmed inpi --help;PI_CODING_AGENT_DIRdefault~/.pi/agentmatchespiAgentDir().models.jsonprovider/model object we write matches Pi's actual typebox validator (ModelDefinitionSchema/ProviderConfigSchemainmodel-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_KEYin 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:
~/.config/taskyou/pi-models.jsonwith a profile like:{ "profiles": { "qwen-openrouter": { "provider": "openrouter", "model": "qwen/qwen3-coder", "api_key_env": "OPENROUTER_API_KEY" } } }export OPENROUTER_API_KEY=...task create --executor pi --model-profile qwen-openrouter -x "Make a trivial change and mark the task done"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.jsongets the provider registered on launch.Follow-ups (out of scope)
--mode rpclive status streaming for processing/blocked parity (currently only terminal state is reported).🤖 Generated with Claude Code