feat(pi-task): prototype headless subtask runner with a verified result envelope
`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.
This commit is contained in:
@@ -8,6 +8,7 @@ Harness-side bring-up for the [pi coding-agent](https://github.com/earendil-work
|
||||
- `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.
|
||||
|
||||
@@ -59,6 +60,63 @@ Everything is non-destructive: existing real files get backed up with a timestam
|
||||
./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.
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user