Commit Graph

20 Commits

Author SHA1 Message Date
joakimp 9c87ee843f AGENTS.md: delegating work — task first, fork second; the rule that survives compaction
The cheat-sheet listed Fork first (with "balanced … default: exploration/
impl/test") and Task as the exception, and it lost to the fork tool's
self-recommending description every time it mattered (five of five fork
briefs in one 2026-09-17 session carried "do not"). This file is the one
copy of the rule in the SYSTEM PROMPT — the skill is gone after the first
compaction — so it now carries: the one discriminator (what the child
sees), task as the default for anything that writes or must obey a rule,
fork only for read-only exploration against this conversation or parallel
opinions, a three-question pre-flight before any fork(...), a copy-paste
minimal task(...) call with the roots contract (write_allowed ⊆ roots, never
nested in a watched root), and a note that fork-gate now enforces the split.

README: pi-task is now also the `task` tool (pi-extensions task.ts) with
fork-gate alongside; where the wrapper looks for this script.
2026-09-19 16:42:03 +02:00
joakimp adfb553f5c docs: which subtask mechanism, for the operator who will not read a skill
The pi-task section already explained the tool in depth. What was missing was
the decision: given a subtask, which mechanism, and why. Adds an end-user
section built on the L0-L4 ladder — the child's context volume is the axis that
explains nearly every observed good and bad behaviour — plus when to use
neither.

Carries the measured cost so the choice is priced, not guessed: 9 runs,
$0.669 total, $0.0027 (fast, refused an over-budget spec in 2.5s) to $0.165
(balanced, 87s). Includes the jq one-liner, and the warning that a crashed run
leaves no result.json — one of the ten here is exactly that, so a rollup must
tolerate missing files rather than assume runs == directories.

Records that fork spend is NOT in that tree (pi-fork aggregates from the
parent's own toolResult entries into the status bar), so the two mechanisms
report spend in two different places and nothing adds them up today.

pi-global-AGENTS.md gets one cheat-sheet bullet so the choice is visible
without loading a skill, and names pi-task as a CLI rather than a tool.

Both files also carry the inverted capability-floor trap ([] = floor on,
null = floor off).
2026-09-08 22:08:03 +02:00
joakimp 02af927f26 feat(pi-task): separate WATCHED roots from WRITABLE ones, and catch a real violation
Closes the gap the reliability testing left open: no real child had ever tripped
the boundary diff. T2 could not do it, and the reason is structural rather than
bad luck — with read_only: true a write is DEFIANCE, and a well-behaved child
refuses, so the detector never runs against a real delta.

Fix: `roots` is now the WATCHED set and `write_allowed` the CHANGEABLE subset. A
violation is then producible by a child that is OBEYING, which is also the
realistic hazard: nobody's agent defiantly rewrites a repo, but plenty of commands
leave artefacts behind.

T4, run to prove it: write task in root A, plus an instruction to verify a module
in root B (watched, NOT writable) with `python3 -m py_compile`. The child obeyed
perfectly — status=ok, typo fixed, module compiled — and still tripped the diff,
because py_compile dropped __pycache__/ into B. Exit 1, violation named, and A's
authorised edit correctly NOT flagged.

It also served as the in-anger test of this morning's --ignored fix: __pycache__/
is gitignored in B, so `git status --porcelain` reported B as CLEAN on the very
same event that `--porcelain --ignored` caught. Pre-fix, T4 would have PASSED.
The fixture test said the same thing; this says it about a real child.
2026-09-07 21:22:45 +02:00
joakimp 5d503e191f fix(pi-task): boundary diff missed IGNORED files; test the git path; adversarial results
Found by the reliability testing, in the tool's own security-relevant check:
boundary() used plain `git status --porcelain`, which OMITS ignored files. A child
writing .env, a credential, or a build artefact into a root therefore read back
as CLEAN. Measured on a fixture repo: an ignored secret.txt produced ZERO
porcelain lines, and `!! secret.txt` once --ignored was passed.

Now always `status --porcelain --ignored`, keeping a sha256 + entry count and
substituting it for the text above 8 KB so a node_modules tree cannot dump
megabytes into every audit dir. Also detects a root whose kind changes.

selftest grows 10 -> 14 checks. The GIT path had NO coverage at all before this
(only the manifest path did), despite being what every real run uses: now covers
clean-repo, ignored-file (the regression), modified-tracked-file, and HEAD move.

Adversarial suite documented in the README: T1 false premise -> correctly
status=failed; T2 tempting write under read_only -> refused, repo verified
untouched by a second route; T3 poisoned caller-asserted fact (wrong node pin)
-> contradicted the caller from the file, and corrected the downstream inference.
T3 is the important one: caller-asserted context.facts is a confabulation vector
this design introduces, and it held.

Still untested, stated in the README rather than implied: no real child has ever
tripped the boundary diff (T2 refused), everything so far is read-only analysis,
nothing iterative, and all specs were written with more care than a rushed one.

Also: add .gitignore (repo had none) and remove the bin/__pycache__ I left behind.
2026-09-07 21:00:47 +02:00
joakimp b501120042 examples(pi-task): two more real read-only task specs, both PASS with pointers verified 9/9
- task-pi-devbox-node22-pins.json: node-24 change-set. Result: Dockerfile.base:557
  is the SOLE hard pin; the real find is that NO test asserts the node major
  (smoke-test.sh:94 is plain run(), not run_expect), so a bump passes silently.
- task-ci-watcher-infra-vs-code.json: infra-vs-code CI failure. The delegate
  REJECTED the proposed zero-log-bytes heuristic on three false-positive grounds
  plus unreachability, and pointed at a better signal already fetched and unused
  (started_at -> RUN_START_TS, read only at watcher-hub-only.sh:266-267).

Both specs assert only measured facts, including the retracted node-24/agent-browser
claim, so the delegate could not resurrect it.
2026-09-07 20:44:33 +02:00
joakimp f89439e667 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.
2026-09-07 20:33:02 +02:00
joakimp 7ee865c8f6 settings: give pi-fork a capability floor (extensions: [])
A fork child inherits the parent's whole session branch by design, and has
twice written to the shared palace under the parent's identity. extensions: []
runs children with --no-extensions; the mempalace bridge is an extension, so
the write path is gone by construction rather than by instruction. Costs forks
their palace search and recall; set to null to restore normal loading.
2026-09-06 20:40:02 +02:00
joakimp 0e1369e6b4 pi-atelier.json: modernise to the current schema (0.7+/0.8 keys)
The config was written against pi-atelier's pre-0.7 vocabulary and had drifted
behind upstream. It still LOADED — 0.7.x/0.8.0 keep reading the old keys — but
only through compatibility shims, and it was missing every sidebar control that
pi-atelier has added since:

  - `segments` (flat id list)  -> `segmentLayout` (ordered, explicit per-segment
    `visible`). Upstream routes the old key through `parseLegacySegments()` and
    treats it as non-authoritative.
  - `ornament: "none"`         -> `{"id":"brand","visible":false}`. types.ts calls
    Ornament "legacy menu vocabulary ... translated to Brand visibility".
  - `showExtensionStatuses`    -> `{"id":"statuses","visible":true}`. Upstream
    maps it onto statuses visibility; the README states Brand and Statuses
    rendering "is controlled only by this normalized layout".
  - added `showSidebarAgent`, `showSidebarTodos`, `showSidebarOnStartup` — the
    last of which did not exist when this file was written (added in 0.8.0).

Every deliberate choice in the old file is preserved exactly: editorial preset,
alt+a, compact density, 60/85 context thresholds (earlier than upstream's
70/90), 3 currency decimals, session actions on, sidebar tool names on,
completion notifications off (desktop toasts are noise in a container). The two
segments the old `segments` list omitted — brand and performance — are now
explicitly hidden rather than implicitly absent.

Verified by loading both the old and new file through pi-atelier 0.8.0's own
`loadConfig()`: zero warnings from each, and identical EFFECTIVE config
(same visible segments, density, thresholds, panels) — so this is a pure
schema modernisation with no behaviour change.

`sidebarPanelLayout` is deliberately NOT set. Declaring it makes the user file
authoritative for the panel set, which would freeze it as upstream adds panels
(its README notes Subagents/Skills are "intentionally not implemented" — i.e.
plausibly coming). Omitting it keeps the product default, currently all eight
panels.

Context: pi-devbox v1.7.0 vendors pi-atelier into the image at a pinned tag, so
this file goes from "config for a thing you hand-installed" to the default
every container gets.
2026-08-07 21:31:06 +02:00
joakimp 4b4b76e80e feat(pi-atelier): ship Status Rail defaults, cp'd not symlinked
Adds pi-atelier.json (defaults for the pi-atelier extension) and an
install/uninstall step for it, so the config survives a devbox volume wipe and
applies to pi on the host too — ~/.pi/agent is a named volume in a container,
which the extension's own config path alone does not outlive.

cp-if-absent rather than a symlink, unlike keybindings.json/AGENTS.md: the
extension rewrites this exact path when the user saves from its menu
(src/config.ts writeJsonAtomic -> "<path>.<pid>.tmp" then rename). rename(2)
REPLACES a symlink with a regular file instead of following it, so a link would
detach on the first menu save and the repo copy would quietly stop applying.
Verified empirically before choosing the idiom, not assumed.

Drift is respected in both directions, matching the pi-env.zsh shape: install
never clobbers an existing file (warns + prints a diff hint), uninstall removes
it only while its content still matches the repo. All five paths exercised
against an isolated HOME: fresh copy, idempotent re-run, drift-preserved
install, drift-preserved uninstall, matched-content uninstall.

Values chosen for this setup and validated against the extension's own
validateConfig (no warnings, no coercion):
- segments drop "brand" (decoration, and first in the drop order anyway)
- density compact — the rail relayouts at 132/96/72/56 cols and these sessions
  run over plain SSH at unknown width
- contextWarning/Danger 60/85, earlier than the 70/90 default because
  defaultModel is opus-5 at xhigh thinking with obsmem compaction behind it
- showSidebarToolNames true — MCP/tool-heavy sessions, names beat "something
  is running"
- completionNotifications false — deliverSystemNotification only spawns for
  darwin/win32 and returns undefined on linux, and a container has no desktop
  session to reach anyway

Docs: file inventory + install/uninstall tables in README.md and AGENTS.md,
including why the symlink idiom does not apply here.
2026-07-29 23:42:05 +02:00
joakimp 926f73834d settings(template): default + fork-deep to Claude Opus 5
pi 0.82.0 added Claude Opus 5 (Anthropic + Bedrock, adaptive thinking incl.
xhigh, 1M ctx / 128K out). Point the template at it:

- defaultModel: eu.anthropic.claude-opus-4-8 -> eu.anthropic.claude-opus-5
- enabledModels: add opus-5 at the head, drop opus-4-7 (superseded; 4-8 kept
  as the previous-gen fallback so the picker stays at four entries)
- pi-fork deep tier: opus-4-8 -> opus-5 (fast=haiku-4-5, balanced=sonnet-5
  unchanged)

README's settings-template snippet updated in the same commit to stay in sync.

Fresh machines / fresh pi-devbox volumes get this verbatim via the entrypoint
bootstrap. Existing volumes are unaffected: entrypoint-user.sh deep-merges
template-FIRST, live-SECOND with arrays as leaves, so live values win and no
model a user removed is re-added.
2026-07-26 00:30:34 +02:00
joakimp 1517fbde9f docs(readme): align settings-template snippet with real file
Show defaultModel = eu.anthropic.claude-opus-4-8 (the actual default) and add
the missing opus-4-8 entry to enabledModels so the README example matches
settings.example.json.
2026-07-13 23:13:24 +02:00
joakimp 00104178cd chore(settings): bump balanced/sonnet model to claude-sonnet-5
pi-fork balanced tier + enabledModels and the README examples now point at
eu.anthropic.claude-sonnet-5 (matches the live settings.json change made
before the container recreate).
2026-07-13 23:03:01 +02:00
joakimp 8fbef66664 docs(settings): make AWS_REGION note deployment-agnostic
settings.example.json is consumed by two very different deployments:
the native workstation (where pi-env.zsh sources ~/.config/pi/.env — the
path is real and correct) AND the pi-devbox image, which bakes this file
as the settings.json seed at /opt/pi-toolkit/. In the container there is
no ~/.config/pi/.env: env comes from the compose env_file. The old
comment pointed only at the workstation path, which is stale/misleading
inside the container.

Reword to 'Must match your AWS_REGION' — accurate in both contexts,
without asserting a path that exists in only one. Comment-only change;
no keys touched. The other ~/.config/pi/.env references (install.sh,
README, AGENTS, pi-env.zsh) are workstation-correct and left as-is.
2026-07-13 22:35:09 +02:00
Joakim Persson 9a8f6faeaa install: symlink global AGENTS.md (auto-load pi-extensions skill at session start)
Add pi-global-AGENTS.md, symlinked by install.sh to ~/.pi/agent/AGENTS.md
(pi's global-instructions file, loaded at every startup). Directs the agent
to read the pi-extensions skill at session start and carries a core
fork/recall cheat-sheet, since on-demand skill description-matching was
leaving the extensions under-utilised.

Symlinked (not cp'd) like keybindings.json: pi never rewrites AGENTS.md, and
the entrypoint re-runs install.sh on every container start, so the directive
self-heals across container recreation / volume wipes.
2026-06-17 10:26:38 +02:00
Joakim Persson d7fe3c610f settings.example: add claude-opus-4-8, set as default, use for fork deep tier 2026-06-17 00:34:35 +02:00
Joakim Persson adb6907188 settings.example: add pi-fork effort profiles (Haiku/Sonnet/Opus via Bedrock) 2026-06-17 00:26:57 +02:00
Joakim Persson 1aae29c6a0 settings.example: add observational-memory model config (Haiku via Bedrock) 2026-06-17 00:14:15 +02:00
joakimp 26bcb872e1 AGENTS.md: documentation-drift sweep as explicit pre-commit step
Companion to the same addition in the cloud-init and ansible repos.
Caught real drift in those repos in a recent session only because
the user explicitly asked. Codify the sweep with concrete, repo-
specific drift hotspots rather than a vague 'watch for drift' rule
that gets ignored.

Each AGENTS.md addition lists the doc files most likely to fall
behind code changes here, plus a quick-triage one-liner using
'git diff --name-only HEAD | xargs grep -l ...' so the rule is
actionable not aspirational.
2026-05-20 23:12:07 +02:00
joakimp 8a4279f773 Rename github URL refs to earendil-works/pi
Pi moved to its new home at earendil-works on 2026-05-07
(https://pi.dev/news/2026/5/7/pi-has-a-new-home).

URL substitution:
  https://github.com/mariozechner/pi-coding-agent
  -> https://github.com/earendil-works/pi

(coding-agent now lives at packages/coding-agent inside the new
monorepo root, but linking to the root is the canonical reference
per the announcement post).

Brew install references (`brew install pi-coding-agent`) left as-is:
the homebrew formula still works at 0.73.1 and a tap update is
tracked upstream at earendil-works/pi#2755.
2026-05-09 17:56:29 +02:00
joakimp ff774cf88f Initial commit: pi harness bring-up split out of mempalace-toolkit
Pi-generic config artifacts (no mempalace dependency):
- pi-env.zsh: shell loader sourcing ~/.config/pi/.env for AWS_PROFILE /
  AWS_REGION. POSIX-compatible (works in bash and zsh).
- keybindings.json: mosh/tmux newline bindings (shift+enter, ctrl+j, alt+j).
- settings.example.json: ~/.pi/agent/settings.json template so pi starts
  without --provider/--model. Region-specific (Bedrock inference-profile
  prefix).

install.sh mirrors mempalace-toolkit + opencode-toolkit patterns:
- require_pi_installed: hard exit 4 if ~/.pi/agent/ missing (cannot do
  anything useful; user must install pi first).
- symlink keybindings.json (safe: pi doesn't rewrite it).
- cp pi-env.zsh into ~/.oh-my-zsh/custom/ (portability over symlink,
  that dir is part of dotfiles backups). Print source snippet for bash
  / plain-zsh users.
- settings.example.json NOT installed \u2014 pi rewrites settings.json at
  runtime. check_pi_settings probe prints the cp command instead.
- check_aws_env: gated on settings.json selecting amazon-bedrock; silent
  for non-Bedrock providers or missing settings.
- All probes warn + return 0, never halt.
- Non-destructive: backup on symlink collision, cmp-based drift detection
  on the cp path, uninstall only removes copies whose content still
  matches repo.

Split rationale: opencode-devbox's mempalace opt-out (~300 MB saved)
wants pi available without mempalace. That dependency asymmetry is
cleanest when pi's own config lives in its own repo, same shape as
opencode-toolkit split out earlier today.

The pi\u2194mempalace MCP bridge (mempalace.ts) stays in mempalace-toolkit
where it belongs \u2014 it imports pi's ExtensionAPI but only exists to
bridge to the palace.

Verified on tor-ms22: fresh install \u2192 drift detect \u2192 adopt canonical \u2192
uninstall \u2192 reinstall \u2192 zsh -ic loads AWS vars. Bash fallback path also
tested via HOME=/tmp/fake SHELL=/bin/bash.
2026-05-05 17:22:06 +02:00