f89439e667
`fork` passes the child getHeader()+getBranch() -- the whole untrimmed parent
branch -- so in a long session it continues the parent's narrative instead of
doing the task (4/4 dispatches on 2026-09-06 ignored their brief; one filed a
diary entry as the parent). Upstream considers that by design.
bin/pi-task inverts the defaults: context is an explicit, default-empty JSON
spec; the child is a fresh isolated session with --no-extensions (so the
mempalace bridge, an extension, cannot file anything under our identity); and
the answer must parse as a declared envelope or the task is recorded FAILED
regardless of how fluent the prose was. Adds a post-hoc boundary diff over
roots[], a per-run audit dir, wall-clock kill and post-hoc cost accounting.
`pi-task selftest` feeds the validator 1 known-good + 6 known-bad envelopes and
a two-sided boundary check, and aborts if any pair fails to discriminate.
Measured while building, and documented in the README rather than smoothed over:
* a fresh session removes the parent's VOICE but not slot-filling -- given a
self-contradictory spec, a zero-context child invented a task, read the
README and returned a well-formed envelope nobody asked for. Fresh context
fixes continuation, not confabulation.
* read_only is VERIFIED, not enforced: pi has no tool allow/deny list, so
--no-extensions leaves core read/write/edit/bash in place.
* a pointer is checked for presence, not checkability ("arithmetic fact" passes).
* budget.usd is post-hoc; only wall_s is enforced.
Deliberately NOT wired into install.sh: per the 2026-09-06 decision, bake only
after the envelope has been beaten up on real work. First real run is committed
as examples/task-mempalace-pi-adapter.json.
290 lines
13 KiB
Markdown
290 lines
13 KiB
Markdown
# pi-toolkit
|
|
|
|
Harness-side bring-up for the [pi coding-agent](https://github.com/earendil-works/pi).
|
|
|
|
**What's here:**
|
|
- `pi-env.zsh` — loads `~/.config/pi/.env` into every shell so pi's Bedrock provider has `AWS_PROFILE` / `AWS_REGION` available.
|
|
- `keybindings.json` — mosh/tmux-friendly newline bindings (`shift+enter`, `ctrl+j`, `alt+j`).
|
|
- `pi-atelier.json` — Status Rail defaults for the [pi-atelier](https://github.com/michaelmjhhhh/pi-atelier) extension (rail segments, context warning thresholds, sidebar tool names). Inert if that extension isn't installed.
|
|
- `settings.example.json` — template for `~/.pi/agent/settings.json` so `pi` starts without having to pass `--provider`/`--model` on every invocation.
|
|
- `install.sh` — idempotent installer wiring these into place.
|
|
- `bin/pi-task` — **prototype** headless subtask runner (spec in, verified envelope out). Deliberately **not** installed by `install.sh` yet; run it by path. See below.
|
|
|
|
**No dependency on MemPalace.** For the palace memory layer see [`mempalace-toolkit`](https://gitea.jordbo.se/joakimp/mempalace-toolkit) — it installs a pi↔mempalace MCP bridge on top of this toolkit. The two repos compose but don't require each other, same pattern as [`opencode-toolkit`](https://gitea.jordbo.se/joakimp/opencode-toolkit) ↔ mempalace.
|
|
|
|
---
|
|
|
|
## Why this exists
|
|
|
|
Pi, out of the box on a fresh machine, has three friction points that this toolkit fixes:
|
|
|
|
1. **No model selection without `--model`.** Every invocation needs `pi --provider ... --model ...` until `~/.pi/agent/settings.json` exists with defaults. The shipped `settings.example.json` is a working eu-west-1 Bedrock template — copy, edit for your region, done.
|
|
2. **`Shift+Enter` (newline in the TUI) doesn't work over mosh/tmux.** mosh's vt220-ish emulation strips modifier keys upstream of tmux, so pi never sees the Shift bit. The shipped `keybindings.json` adds `ctrl+j` and `alt+j` fallbacks that pass through as plain control/meta bytes.
|
|
3. **Bedrock provider needs `AWS_PROFILE` and `AWS_REGION` in the shell environment at spawn time.** The shipped `pi-env.zsh` sources `~/.config/pi/.env` into every shell (`set -a; source; set +a`), so you can edit credentials in one plaintext-on-disk, encrypted-in-git-repo file.
|
|
|
|
None of this talks to MemPalace; it's all pi's own configuration surface.
|
|
|
|
---
|
|
|
|
## Install
|
|
|
|
```bash
|
|
git clone ssh://git@gitea.jordbo.se:2222/joakimp/pi-toolkit.git ~/pi-toolkit
|
|
cd ~/pi-toolkit
|
|
./install.sh
|
|
```
|
|
|
|
Requires pi to be installed first (`~/.pi/agent/` must exist). Install via:
|
|
|
|
```bash
|
|
brew install pi-coding-agent # macOS
|
|
# or see https://github.com/earendil-works/pi for Linux
|
|
pi --help # first run creates ~/.pi/agent/
|
|
```
|
|
|
|
What `install.sh` does:
|
|
|
|
| Step | Action |
|
|
|---|---|
|
|
| Symlink `keybindings.json` | `~/.pi/agent/keybindings.json` → repo. Safe to symlink: pi doesn't rewrite it. |
|
|
| Install `pi-env.zsh` | `cp` into `~/.oh-my-zsh/custom/` if oh-my-zsh detected; otherwise print shell-specific `source` snippet for `~/.zshrc` or `~/.bashrc`. Does not auto-edit rc files. |
|
|
| Copy `pi-atelier.json` | `~/.pi/agent/pi-atelier.json`, **only if absent**. NOT symlinked: pi-atelier rewrites this path on menu saves via write-temp-then-`rename`, and `rename(2)` replaces a symlink rather than following it — so a link would silently detach. An existing file is left alone with a `diff` hint. |
|
|
| `settings.example.json` | **Not installed.** Installer warns if `~/.pi/agent/settings.json` is missing and prints the `cp` command. NOT symlinked because pi rewrites `settings.json` at runtime (`lastChangelogVersion` bumps) and a symlink would dirty the repo. |
|
|
| AWS env probe | If `settings.json` selects `amazon-bedrock`, warn if `AWS_PROFILE` / `AWS_REGION` not set. Silent otherwise. |
|
|
|
|
Everything is non-destructive: existing real files get backed up with a timestamp before symlinking; shell loader copy refuses to clobber local edits (prints `diff` hint and moves on). Re-runs are idempotent.
|
|
|
|
## Uninstall
|
|
|
|
```bash
|
|
./install.sh --uninstall
|
|
```
|
|
|
|
---
|
|
|
|
## `bin/pi-task` — headless subtask runner (prototype)
|
|
|
|
`fork` hands its child `getHeader()+getBranch()` — the **whole untrimmed parent
|
|
branch** — with the brief appended as the last turn. In a long session the parent
|
|
narrative outweighs the task: measured 2026-09-06 on mbp-m1-2020, 4 of 4
|
|
dispatches ignored their brief, answered in the operator's voice, fabricated
|
|
self-referential measurements, and one filed a diary entry under the parent
|
|
identity. That is upstream's intended design for a *young* session, not a bug to
|
|
wait out.
|
|
|
|
`pi-task` inverts the defaults:
|
|
|
|
| property | how |
|
|
|---|---|
|
|
| context **explicit and default-empty** | a JSON **spec**, not a chat message; `context.facts` / `.files` / `.commands` are enumerated by name. "Inherit the session" is not expressible. |
|
|
| fresh identity | `--session-id pitask-<id>-<stamp>` in a private `--session-dir`; no parent transcript is passed |
|
|
| capability floor | `--no-extensions`, so the mempalace bridge (an extension) is absent and palace writes are impossible **by construction** |
|
|
| machine-checkable result | the child must emit a fenced `json` envelope (`status`/`deliverable`/`evidence`/`unsure`/`did_not_do`). **If it does not parse, the task FAILED**, however fluent the prose |
|
|
| claims carry pointers | every `evidence[]` entry needs a `pointer`; the parent is told to spot-check them |
|
|
| post-hoc boundary diff | git `HEAD`+porcelain (or a sha256 manifest) of every `roots[]` entry, before and after; a `read_only` task that mutates a root FAILS |
|
|
| audit trail | `~/.pi/agent/pi-task/<stamp>-<id>/` keeps `spec.json`, `prompt.txt`, `argv.json`, `raw.ndjson`, `result.json`, both boundary snapshots, and the child's session |
|
|
| budgets | `budget.wall_s` (hard kill) and `budget.usd` (post-hoc, summed from `agent_end.messages[].usage.cost.total`) |
|
|
|
|
```bash
|
|
./bin/pi-task schema # spec fields
|
|
./bin/pi-task selftest # two-sided validator check
|
|
./bin/pi-task run examples/task-*.json --dry-run # print the exact prompt
|
|
./bin/pi-task run examples/task-*.json # exit 0 = PASS, 1 = FAIL
|
|
```
|
|
|
|
`selftest` is not decoration: it feeds the validator one known-good and six
|
|
known-bad envelopes plus a two-sided boundary check, and **aborts** if any pair
|
|
fails to discriminate. A validator that has only ever returned PASS has not been
|
|
shown to validate anything.
|
|
|
|
### Honest limits — read before trusting it
|
|
|
|
1. **`read_only` is verified, not enforced.** pi has no tool allow/deny list;
|
|
`--no-extensions` removes *extensions*, never core `read`/`write`/`edit`/`bash`.
|
|
The boundary diff catches a violation *after* it happens, and only inside
|
|
`roots[]`. A child can still write anywhere you can.
|
|
2. **A pointer is checked for presence, not checkability.** `"pointer":
|
|
"arithmetic fact"` passes. The parent still has to open a sample.
|
|
3. **The cost ceiling is post-hoc.** pi takes no spend limit, so `budget.usd`
|
|
reports an overrun, it cannot prevent one. `wall_s` *is* enforced (SIGKILL).
|
|
4. **A fresh session does not stop confabulation — it only stops *continuation*.**
|
|
Measured while building this: given a self-contradictory spec (goal said "emit
|
|
no json", the template requires an envelope), a zero-context child resolved the
|
|
conflict by *inventing a task* — it read this README and returned a well-formed
|
|
envelope about pi-toolkit that nobody asked for, with real pointers. Removing
|
|
inherited context removes the parent's *voice*; the envelope contract still
|
|
pressures the child to fill the slot. **Under-specify the goal and you will get
|
|
a confident answer to a question you did not ask.**
|
|
|
|
|
|
Removes the keybindings and `AGENTS.md` symlinks, plus the shell-loader and `pi-atelier.json` copies — the copies only if their content still matches the repo, so local edits (including pi-atelier menu saves) survive. Your `settings.json` is never touched.
|
|
|
|
---
|
|
|
|
## Deploying pi on a new machine
|
|
|
|
Full recipe from a clean macOS or Linux box to a working pi install. Follow in order.
|
|
|
|
### 0. Prerequisites
|
|
|
|
- Shell: zsh + oh-my-zsh recommended (the `pi-env.zsh` loader installs into `~/.oh-my-zsh/custom/`). Bash or plain zsh works too — installer prints a manual source snippet.
|
|
- `git`, pi (installed upstream), optional: `tmux` ≥ 3.2 for CSI-u extended keys on non-mosh paths.
|
|
- AWS credentials reachable via `AWS_PROFILE` (either `aws configure sso` cache or static keys in `~/.aws/credentials`) — only if using `amazon-bedrock` as pi's provider.
|
|
|
|
### 1. Dotfiles (if you keep one)
|
|
|
|
Your dotfiles repo should ship `~/.config/pi/.env` (git-crypt encrypted, containing `AWS_PROFILE` and `AWS_REGION`). Provision it first so the loader has something to source:
|
|
|
|
```bash
|
|
git clone <your-dotfiles> ~/src/dotfiles
|
|
cd ~/src/dotfiles
|
|
git-crypt unlock <key>
|
|
# Run your dotfiles provisioner (example: myconfigs)
|
|
./provision.sh --profile <profile>
|
|
```
|
|
|
|
If you don't use a dotfiles repo, create the file manually after step 3 (installer will point you at the right path).
|
|
|
|
### 2. Install pi
|
|
|
|
```bash
|
|
brew install pi-coding-agent # macOS
|
|
# or see https://github.com/earendil-works/pi for Linux
|
|
pi --help # creates ~/.pi/agent/
|
|
```
|
|
|
|
### 3. Install this toolkit
|
|
|
|
```bash
|
|
git clone ssh://git@gitea.jordbo.se:2222/joakimp/pi-toolkit.git ~/pi-toolkit
|
|
cd ~/pi-toolkit && ./install.sh
|
|
```
|
|
|
|
This symlinks `keybindings.json`, copies `pi-env.zsh`, and prints the `settings.json` bootstrap command.
|
|
|
|
### 4. Bootstrap pi settings
|
|
|
|
```bash
|
|
cp ~/pi-toolkit/settings.example.json ~/.pi/agent/settings.json
|
|
$EDITOR ~/.pi/agent/settings.json
|
|
```
|
|
|
|
Adjust the inference-profile prefix to match your AWS region:
|
|
|
|
| Region | Prefix | Example model ID |
|
|
|---|---|---|
|
|
| eu-west-1 | `eu.` | `eu.anthropic.claude-sonnet-5` |
|
|
| us-east-1 | `us.` | `us.anthropic.claude-sonnet-5` |
|
|
| non-Bedrock | (none) | `anthropic:claude-sonnet-5` |
|
|
|
|
Run `pi --list-models` to confirm what your credentials can actually invoke.
|
|
|
|
### 5. Verify AWS env vars in your shell
|
|
|
|
If step 1 provisioned `~/.config/pi/.env` and step 3 installed `pi-env.zsh`:
|
|
|
|
```bash
|
|
exec zsh
|
|
echo "$AWS_PROFILE $AWS_REGION" # should print your values
|
|
```
|
|
|
|
If empty, check that `~/.config/pi/.env` decrypted (plain text, not binary) — `git-crypt unlock` in step 1 is the usual culprit.
|
|
|
|
### 6. First run
|
|
|
|
```bash
|
|
pi # should start with the default model, no --model needed
|
|
```
|
|
|
|
### 7. (Optional) Add MemPalace memory
|
|
|
|
If you want pi to have persistent memory across sessions, install [`mempalace-toolkit`](https://gitea.jordbo.se/joakimp/mempalace-toolkit):
|
|
|
|
```bash
|
|
uv tool install mempalace
|
|
git clone ssh://git@gitea.jordbo.se:2222/joakimp/mempalace-toolkit.git ~/mempalace-toolkit
|
|
cd ~/mempalace-toolkit && ./install.sh
|
|
```
|
|
|
|
It detects pi and symlinks a `mempalace.ts` extension into `~/.pi/agent/extensions/` that wires pi's MCP client to the palace.
|
|
|
|
### Verification checklist
|
|
|
|
```bash
|
|
# keybindings symlinked into this repo
|
|
ls -la ~/.pi/agent/keybindings.json
|
|
|
|
# shell loader installed
|
|
ls -la ~/.oh-my-zsh/custom/pi-env.zsh
|
|
|
|
# env vars loaded in a fresh shell
|
|
zsh -ic 'echo $AWS_PROFILE $AWS_REGION'
|
|
|
|
# pi starts with its defaults
|
|
pi --version
|
|
|
|
# Re-run is idempotent
|
|
./install.sh --yes # all rows should say "already linked/installed"
|
|
```
|
|
|
|
---
|
|
|
|
## Settings template reference
|
|
|
|
`settings.example.json`:
|
|
|
|
```json
|
|
{
|
|
"defaultProvider": "amazon-bedrock",
|
|
"defaultModel": "eu.anthropic.claude-opus-5",
|
|
"enabledModels": [
|
|
"eu.anthropic.claude-opus-5",
|
|
"eu.anthropic.claude-sonnet-5",
|
|
"eu.anthropic.claude-opus-4-8",
|
|
"eu.anthropic.claude-haiku-4-5-20251001-v1:0"
|
|
]
|
|
}
|
|
```
|
|
|
|
- `defaultProvider` — most common values: `amazon-bedrock`, `anthropic`, `openai`.
|
|
- `defaultModel` — Bedrock IDs have region-specific prefixes (`eu.`, `us.`). Bare Anthropic uses `anthropic:claude-...`. Run `pi --list-models` to see what you can actually call.
|
|
- `enabledModels` — optional; restricts the model picker inside pi to this subset.
|
|
|
|
pi rewrites `settings.json` at runtime (`lastChangelogVersion` bumps on upgrade), so don't symlink this file.
|
|
|
|
## Keybindings reference
|
|
|
|
`keybindings.json`:
|
|
|
|
```json
|
|
{
|
|
"tui.input.newLine": ["shift+enter", "ctrl+j", "alt+j"]
|
|
}
|
|
```
|
|
|
|
Over direct `kitty` / `iTerm2` / `WezTerm` with tmux configured for CSI-u extended keys, `Shift+Enter` works natively. The `ctrl+j` / `alt+j` fallbacks kick in when modifier forwarding is stripped (most commonly: mosh). Pi reads this file once at startup — restart pi after editing.
|
|
|
|
## Environment file reference
|
|
|
|
`~/.config/pi/.env` format (plaintext on disk, chmod 600, encrypt in your dotfiles repo):
|
|
|
|
```bash
|
|
AWS_PROFILE=YourBedrockProfile
|
|
AWS_REGION=eu-west-1
|
|
```
|
|
|
|
Loaded by `pi-env.zsh` via `set -a; source; set +a` — plain `KEY=VALUE`, no `export` needed. Add any other pi-scoped env vars here as the surface grows.
|
|
|
|
---
|
|
|
|
## Related repos
|
|
|
|
- [`mempalace-toolkit`](https://gitea.jordbo.se/joakimp/mempalace-toolkit) — MemPalace memory layer. Installs a pi↔mempalace MCP bridge on top of this toolkit. Optional.
|
|
- [`opencode-toolkit`](https://gitea.jordbo.se/joakimp/opencode-toolkit) — sibling repo for the opencode coding-agent. Same shape (shell loader + install.sh). Independent of this repo.
|
|
- [`opencode-devbox`](https://gitea.jordbo.se/joakimp/opencode-devbox) — Docker containers with coding agents preinstalled. Can compose pi-toolkit and mempalace-toolkit as independent layers.
|
|
- [`myconfigs`](https://gitea.jordbo.se/joakimp/myconfigs) — dotfiles repo where `~/.config/pi/.env` is tracked (git-crypt encrypted).
|
|
|
|
## License
|
|
|
|
MIT — see [`LICENSE`](LICENSE).
|