From 50153e65b75c4a266d8cc14a7f8a20b1375a9c60 Mon Sep 17 00:00:00 2001 From: Joakim Persson Date: Sat, 19 Sep 2026 16:42:06 +0200 Subject: [PATCH] skill floor: refresh vendored pi-extensions skill to pi-extensions@25c1265 (task tool + fork-gate) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit check-skill-floor.sh: OK, tree_sha256 9b85a633… matches the package at main (25c1265). Forces one base rebuild (base_tag hashes rootfs/), which is also what bakes extensions/task.ts and fork-gate.ts into /opt/pi-extensions via the floating PI_EXTENSIONS_REF=main; install.sh symlinks every extensions/*.ts on start. --- .../pi-devbox/skills/pi-extensions/SKILL.md | 116 +++++++++++++++--- 1 file changed, 96 insertions(+), 20 deletions(-) diff --git a/rootfs/usr/local/share/pi-devbox/skills/pi-extensions/SKILL.md b/rootfs/usr/local/share/pi-devbox/skills/pi-extensions/SKILL.md index e33b08f..7ec1750 100644 --- a/rootfs/usr/local/share/pi-devbox/skills/pi-extensions/SKILL.md +++ b/rootfs/usr/local/share/pi-devbox/skills/pi-extensions/SKILL.md @@ -1,17 +1,17 @@ --- name: pi-extensions description: >- - Use the pi extensions (pi-fork, pi-observational-memory, ssh-controlmaster) effectively in the pi coding agent harness. Load this skill only when running inside pi (detection - `fork` and `recall` are present in your tool list, or `pi --ssh` was used to start the session). pi-fork dispatches focused subtasks to forked agents at fast/balanced/deep effort tiers; pi-observational-memory compacts long sessions into recallable observations + reflections; ssh-controlmaster rewires pi's read/write/edit/bash tools to execute on a remote host over a multiplexed SSH connection. Also covers the context ladder L0-L4 and when to reach for the separate `pi-task` CLI instead of `fork` - isolated child, immutable spec, machine-checked envelope, write-boundary diff. This skill covers tier selection, task design, boundary discipline, when to use recall, and remote-pi mechanics. + Use the pi extensions (pi-fork, pi-observational-memory, ssh-controlmaster) effectively in the pi coding agent harness. Load this skill only when running inside pi (detection - `fork` and `recall` are present in your tool list, or `pi --ssh` was used to start the session). pi-fork dispatches focused subtasks to forked agents at fast/balanced/deep effort tiers; pi-observational-memory compacts long sessions into recallable observations + reflections; ssh-controlmaster rewires pi's read/write/edit/bash tools to execute on a remote host over a multiplexed SSH connection. Also covers the context ladder L0-L4 and the `task` tool (pi-task: isolated child, immutable spec, machine-checked envelope, write-boundary diff) that is the DEFAULT for delegated work, with `fork` reserved for read-only exploration and parallel opinions, plus the `fork-gate` hook that enforces the split. This skill covers rung and tier selection, task design, boundary discipline, when to use recall, and remote-pi mechanics. --- -# Pi Extensions: pi-fork, pi-observational-memory, ssh-controlmaster +# Pi Extensions: pi-fork + task/fork-gate, pi-observational-memory, ssh-controlmaster ## When to Load This Skill Load only when **both** of these are true: 1. You are running inside the **pi coding agent harness** (not Claude Code, not opencode, not any other harness). -2. The `fork` and/or `recall` tools appear in your available tool list, **or** the session was started with `pi --ssh ...`. +2. The `fork`, `task` and/or `recall` tools appear in your available tool list, **or** the session was started with `pi --ssh ...`. If you do not see those tools, this skill does not apply — skip it. Other harnesses do not have these extensions and the patterns below will not work there. @@ -71,7 +71,78 @@ ssh-controlmaster is orthogonal but composes cleanly: when pi is operating remot --- -## Part 1: pi-fork +## Part 1: delegating work — `task` and `fork` + +### Decide the rung BEFORE the brief (read this first) + +Two tools run a child agent. They differ in one thing, and it decides the +quality of what comes back: **what the child sees.** + +| tool | child sees | rung | gives you | use for | +|---|---|---|---|---| +| **`task`** (pi-extensions `task.ts`, wraps `pi-task`) | **only your spec** — goal, named files, curated facts | L0–L2 | immutable spec, PASS/FAIL envelope, per-root boundary diff, audit dir, budgets | **any delegated work that changes files or must obey a rule** — the default | +| **`fork`** (pi-fork) | **your entire branch**, brief appended last | L4 | prose report, effort tiers, parallel dispatch from one message | read-only exploration that needs this conversation; N independent opinions | + +**Pre-flight before any `fork(...)` — one *yes* makes it a `task`:** +1. Does the brief say *do not / only / never / must not*? +2. Will the child write, edit, commit or push anything? +3. Do I want a PASS/FAIL I can check, rather than prose? + +Why the text alone did not work (and why this is now enforced): the rule above +lived in this skill and in the global AGENTS.md for months and was still +violated by agents that had just read it — five fork briefs in one session on +2026-09-17, all carrying "do not", one of which returned confident verbatim +quotes that did not exist. `fork` is a *tool*: its self-recommending +description ("implementation, testing, review…") is in the tool list every turn +and survives compaction; this skill is gone after the first compaction, and +`pi-task` was a CLI to be remembered. Two structural fixes shipped 2026-09-19: + +- **`task` is a tool** (`extensions/task.ts`), so both rungs sit in the tool + list with the decision rule in their descriptions, and `promptGuidelines` + puts the rule in the system prompt where compaction cannot remove it. It + rejects, before spending a model run, the two spec errors that make a + violation certain (see "roots" below) and serialises overlapping tasks. +- **`fork-gate`** (`extensions/fork-gate.ts`) is a `tool_call` hook that BLOCKS + a fork whose brief contains a prohibition, a write boundary or a clause-initial + file-changing imperative, and returns the `task(...)` call to make instead. + It matches wording, not intent: a genuinely read-only exploration brief that + trips it is rephrased, and one that cannot be rephrased without its + prohibition needed `task` all along. `PI_FORK_GATE=off` logs instead of + blocking; `/ext` disables it entirely. + +Minimal call: + +``` +task(id="slug", goal="…verbatim; the child has NO other context…", + deliverable="…exact shape wanted…", effort="fast|balanced|deep", + read_only=false, + roots=["/abs/repo/docs", "/abs/repo/src"], # WATCHED, each diffed alone + write_allowed=["/abs/repo/docs"], # exact subset of roots + facts=["verified fact"], files=["/abs/path/to/read"]) +``` + +**Roots — the two errors the tool refuses up front.** Every root is diffed on +its own and a delta is allowed only if that *exact root string* is in +`write_allowed`. So (a) `write_allowed` must be a subset of `roots`, not a +subdirectory of one, and (b) a writable root must not lie inside a watched-only +root — the parent's porcelain would change and register a violation every time. +List the writable part as its own root and leave the enclosing repo out. This is +the shape the 2026-09-17 migration tasks used (sibling roots, `write_allowed` +naming four of them) and it passed cleanly. + +**Overlap.** Sibling `task` calls whose roots overlap would see each other's +writes as violations; the tool runs them one after another automatically. Do +not rely on that for ordering *semantics* — if B needs A's output, call B after +A returns. + +**What isolation does not fix.** L0 removes the *narrative* failures (parent +voice, invented continuity, ignored prohibitions). It does not remove +confabulation: an under-specified spec still gets a confident deliverable. The +report prints the evidence pointers under a "SPOT-CHECK THESE" heading for a +reason. + +Everything below about tiers, brief design and boundary discipline applies to +**both** tools — a `task` spec is a brief too. ### Effort tier mapping @@ -85,15 +156,15 @@ Configured in `~/.pi/agent/settings.json` under `pi-fork.effortProfiles`. The co **Rule of thumb:** start at `balanced` unless you have a specific reason to go up or down. Going too cheap on a deep task wastes a fork; going too expensive on a mechanical task is just slow. -### When to fork vs. do it yourself +### When to delegate vs. do it yourself -Fork when **any** of: +Having chosen the rung above, delegate (either tool) when **any** of: - The task requires reading many files whose contents you don't need to keep in your main context afterwards (the fork returns a dense summary; raw file contents stay in the fork's context and are discarded). - You want to run multiple analyses in **parallel** (especially: comparing N options, where independent reasoning is itself a signal — see "parallel forks" below). - The task is well-scoped enough to specify completely up front and well-bounded enough that returning a dense report is more useful than continuing the dialogue. - You are about to do something that would burn a lot of tokens on tool calls (long file reads, many bash invocations) whose output you will mostly discard. -Don't fork when: +Don't delegate when: - The work fits in your current context budget without crowding out what comes next. - The task is exploratory and you'll need to iterate based on what you find (forking turns iteration into round-trips with full task-spec rewrites). - You need to make decisions during the work that depend on context only the main thread has. @@ -175,11 +246,12 @@ sits at one extreme of it. Five rungs: | **L3** | a **truncated tail** of the parent branch | *nothing implements this* — would need a new spec key plus `--session ` | **no** | | **L4** | the **entire** parent branch | `fork(task=…)` — `getHeader()+getBranch()`, no offset or limit anywhere in the call chain | yes | -**`pi-task` is a CLI, not an extension — it will never appear in your tool list.** -Invoke it with `bash`: `/opt/pi-toolkit/bin/pi-task run ` (source at -`/workspace/pi-toolkit/bin/pi-task`, `schema` subcommand prints the spec fields). -It reads an immutable JSON spec, and "inherit the session" is not expressible in -that schema — the isolation is structural, not a request. +**`pi-task` is a CLI (`/opt/pi-toolkit/bin/pi-task`, `schema` prints the spec +fields); the `task` tool from pi-extensions wraps it** so it appears in your tool +list next to `fork`. If the tool is absent, invoke the CLI with `bash`: +`/opt/pi-toolkit/bin/pi-task run `. Either way it reads an immutable +JSON spec, and "inherit the session" is not expressible in that schema — the +isolation is structural, not a request. **Choose the lowest rung that can do the job:** @@ -292,7 +364,17 @@ When entries conflict, **the most recent observation reflects the latest known s ## Quick Reference ``` +task(id, goal, deliverable, effort, read_only, roots, write_allowed, facts, files, commands, wall_s, usd) + - L0-L2: isolated child sees ONLY the spec — DEFAULT for work that writes or has rules + - roots[] = WATCHED (each diffed alone); write_allowed[] = exact subset of roots, + never nested inside a watched-only root (the tool rejects both errors up front) + - envelope must parse or the run FAILED; spot-check evidence pointers + - overlapping-root tasks are serialised; audit: ~/.pi/agent/pi-task/-/result.json + - CLI fallback: bash /opt/pi-toolkit/bin/pi-task run (schema | selftest | run --dry-run) + fork(task=..., effort=fast|balanced|deep) # L4: child inherits your WHOLE branch + - ONLY for read-only exploration needing this conversation, or N parallel opinions + - fork-gate BLOCKS briefs with do-not/only/never, write boundaries, or "edit/commit/fix …" - state decision authority explicitly - pass verified context up front - specify deliverable shape @@ -302,13 +384,6 @@ fork(task=..., effort=fast|balanced|deep) # L4: child inherits your WHOLE b - write-capable? demand "What I did NOT do", then verify from git/fs, not the report - prohibition in the brief => not a `fast` task -bash: /opt/pi-toolkit/bin/pi-task run # L0-L2: isolated child, NOT a tool - - schema | selftest | run [--dry-run] - - context.facts (pasted) / .files (names only) / .commands - - roots[] = WATCHED, write_allowed[] = CHANGEABLE subset - - envelope must parse or the run FAILED - - audit + cost: ~/.pi/agent/pi-task/-/result.json - recall(id=<12-char-hex>) - only when stakes justify the cost - id must already be visible in your context @@ -317,8 +392,9 @@ recall(id=<12-char-hex>) ``` ~/.pi/agent/settings.json - pi-fork.effortProfiles — model + thinking-depth per tier + pi-fork.effortProfiles — model + thinking-depth per tier (used by BOTH fork and task) pi-fork.defaultEffort — usually "balanced" + env PI_FORK_GATE=off — fork-gate logs instead of blocking (default: block) observational-memory.* — token thresholds, model, agentMaxTurns observational-memory.debugLog: true — opt-in NDJSON telemetry at ~/.pi/agent/observational-memory/debug/.ndjson (off by default)