task tool + fork-gate: put the "pi-task, not fork" rule where the decision is made
The rule was correct and written down twice (global AGENTS.md, this skill)
and was still violated by agents that had just read it: on 2026-09-17 all
five fork briefs in one session carried "do not", one returned confident
verbatim quotes that did not exist, and four had disjoint write boundaries
fork cannot enforce (re-run as pi-task, they passed). Three mechanisms,
none of them wording:
1. fork is a TOOL — its self-recommending description ("implementation,
testing, review…") is in the model's face every turn; pi-task was a CLI
to be remembered and reached through bash with a hand-written JSON.
2. The skill is gone after the first compaction; the tool list never is.
The asymmetry widens in exactly the long sessions where fork is worst.
3. Friction: one string vs a spec file + bash + reading result.json.
extensions/task.ts registers pi-task as the `task` tool. Flat parameters
build the spec; the decision rule sits in the description and in
promptGuidelines (appended to the system prompt, so compaction cannot remove
it). Before spending a model run it rejects the two spec errors that make a
boundary violation certain — write_allowed not an exact subset of roots, and
a writable root nested in a watched-only root (the parent's porcelain would
change every time; pi-task keys deltas by root string) — and it serialises
sibling tasks whose roots overlap (parallel siblings saw each other's writes
as violations, 2026-09-17). Returns the CLI's own parent-facing report; a
FAIL verdict is a result, only a CLI refusal is an error.
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 as the reason the exact task(...) to make instead.
Wording, not intent — the message says so and how to rephrase a genuinely
read-only brief. PI_FORK_GATE=off logs instead; /ext disables.
Evidence: test/fork-gate.test.mjs is two-sided (15 must-block incl. the real
shapes, 10 must-pass incl. "Write a summary…", "Report which files were
modified…", "Give me an update…"); the classifier redirects 5/5 of the real
briefs from the motivating session. test/task.test.mjs pins root overlap and
the pre-launch validation. Live in `pi -p`: the fork was intercepted before
any child spawned (no /tmp/pi-fork-* dir) and the model received the
redirect; task returned PASS with an evidence pointer and audit dir, a
budget-overrun returned a FAIL result (isError=false), and a nested-root spec
was rejected with no audit dir created.
skill/SKILL.md: Part 1 now opens with "decide the rung before the brief"
(the table, the three-question pre-flight, the roots contract, overlap,
what isolation does not fix); the ladder section and quick reference no
longer say pi-task "will never appear in your tool list". package.json gains
"type": "module" and a test script.
This commit is contained in:
+96
-20
@@ -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 <trimmed snapshot>` | **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 <spec.json>` (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 <spec.json>`. 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/<stamp>-<id>/result.json
|
||||
- CLI fallback: bash /opt/pi-toolkit/bin/pi-task run <spec.json> (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 <spec> # 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/<stamp>-<id>/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/<session>.ndjson (off by default)
|
||||
|
||||
Reference in New Issue
Block a user