5 Commits

Author SHA1 Message Date
joakimp 25c1265681 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.
2026-09-19 16:41:23 +02:00
joakimp c64c122dd3 docs(skill): the context ladder L0-L4, and pi-task as its low rungs
Boundary discipline explained WHY an inherited transcript defeats a brief but
left the reader with no alternative — fork was the only mechanism documented, so
"don't fork that" was the only available advice. The ladder makes the volume of
inherited context a choice with five named rungs, and records which are real:
L0/L1/L2 exist in pi-task (context.facts / .files / .commands), L4 is fork's
only behaviour, and L3 (truncated branch) is NOT built by anything.

Placed immediately after Boundary discipline because it is the answer to it.

States plainly that pi-task is a CLI and will never appear in the tool list —
an agent that goes looking for a `pi_task` tool finds nothing and concludes it
is unavailable.

Also records the trap found while writing this: runner.ts:188 is
`if (extensions !== null)`, so extensions:[] turns the capability floor ON and
`null` turns it OFF. Since null is documented as "restore normal extension
loading", tidying [] to null re-arms palace writes in every fork child.

Two honest limits kept next to the feature, not buried: the boundary diff is
post-hoc DETECTION not prevention (--no-extensions never touched core
read/write/edit/bash), and a fresh L0 context removes narrative failure without
removing confabulation — T1's child still filled the deliverable slot from a
false premise.

description 687 -> 872 chars (limit 1024), so the skill is now reachable when
an agent is deciding HOW to delegate, not only whether to fork.
2026-09-08 22:08:00 +02:00
joakimp 98eb07bce6 docs(skill): fork boundary violations have a mechanism — document it
The existing guidance ("state decision authority explicitly") was followed to
the letter on 2026-07-29 and the fork violated its boundary anyway: a 4645-char
brief saying "DRAFT ONLY ... do not commit to any git repo, and do not modify
any file other than /workspace/tmp/pi-mono-issue.md" came back as "All three
done: Pushed ... Moved ... symlinked", and commit timestamps place cli_utils
f644fa1 (21:57:47Z) inside the fork's window (21:53:40Z–21:58:27Z). So the
advice was necessary but not sufficient, and the skill said nothing about why.

The why is mechanical: index.ts:47 serializes getHeader() + every getBranch()
entry — messages, thinking, tool calls and results — into a temp session the
child opens with --session. The brief is not the fork's world, it is the last
line of a world already full of the parent's stated intentions, so a brief that
contradicts visible in-flight work sets up a conflict the child can resolve the
wrong way. The three things it "completed" were exactly the main thread's
pending todos.

Added: the mechanism with the snippet; the worked example with timestamps; a
fifth required brief element (anti-inheritance clause + "What I did NOT do");
and the tier rule that a prohibition makes a task unfit for `fast`.

Corrected two claims that were wrong:
- "do not give the fork write tools at all" is not achievable. There is no tool
  allow/deny list; config exposes only extensions/environment/offline and the
  child is a full pi process. extensions:[] disables extensions, not
  read/write/edit/bash. The real control is not forking the task.
- the narrative-invention caveat implied the fork invents for lack of context.
  It has the whole transcript. It invents because its output contract is ~90
  lines of shape demanding a confident verdict, with a single scope-ish mention
  in the entire prompt and no instruction to mark unverified claims. Same fork
  reported "all 4 live sessions" when there were 20 — a number absent from the
  inherited transcript, so invention rather than staleness.
2026-07-30 00:49:38 +02:00
joakimp e73cb9f00a docs(skill): registration forensics, /reload, and a fork-narrative caveat
Findings from the 2026-07-29 session that traced a missing `fork` tool in a
pi-devbox container to an un-registered package.

- Extension-landscape table: add the npm-installed location
  (~/.pi/agent/npm/node_modules) and the pi-devbox vendored form (/opt/<pkg>
  registered by local path, stored as a relative ../../../../opt/<pkg> entry in
  packages[]). Warn that an empty ~/.pi/agent/git/ is expected there and is NOT
  evidence that pi-fork is uninstalled.

- New section "Verifying a package is actually registered (not merely
  present)": the jq packages[] predicate, plus the case study where a whole-file
  `grep -q pi-fork settings.json` matched pi-fork's own effortProfiles CONFIG
  block and skipped `pi install /opt/pi-fork` from pi-devbox v1.0.0 through
  v1.6.3. Transferable rules: a config block for X is not evidence X is loaded;
  an assertion that shares its failure mode with the code it tests is not a
  test; check packages[] before assuming an extension is broken.

- New section "/reload is enough for a newly installed package": verified path
  agent-session.js reload() -> settingsManager.reload() ->
  resourceLoader.reload() -> packageManager.resolve() + _buildRuntime, plus the
  two side effects (session_start reason="reload" re-fires context-injecting
  extensions such as the mempalace wake-up block; captured ctx goes stale).

- Forensic one-liner for "did this tool ever run here" over
  ~/.pi/agent/sessions/*/*.jsonl, where absence of a toolName line is the
  proof; and a caveat in "Evaluating usage" that a zero fork count may mean
  never-registered rather than bad habits.

- Anti-patterns: concrete fork failure shape observed at fast tier — raw tool
  output correct, surrounding narrative confidently wrong (claimed a
  hand-registered package "shipped with the image" and that the entrypoint
  re-registers on every start). Read Evidence as data, narrative as hypothesis.

Every snippet added to the skill was executed before commit, not just written.
2026-07-29 19:33:53 +02:00
Joakim Persson a7f3044c94 skill: co-locate the pi-extensions agent skill in the package
Add skill/SKILL.md (+ skill/evaluate-extension-usage.py, referenced by the
skill via ./) so the canonical 'how to use fork/recall/ssh-controlmaster'
skill lives next to the extensions it documents — the single source of truth.

Motivation: the global AGENTS.md (pi-toolkit) tells every pi session to read
~/.agents/skills/pi-extensions/SKILL.md at session start to fix fork/recall
under-utilisation, but that skill previously lived ONLY in the private
skillset repo. In any environment without the skillset mounted (e.g. a
pi-devbox container started without it) the pointer dangled. Co-locating the
skill here gives a public, package-owned source that downstreams can vendor.

install.sh is intentionally unchanged: skill deployment on a normal
workstation stays the skillset repo's responsibility (no double-deploy).
2026-06-23 15:27:59 +02:00