Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ pi-stack is installed for this user.
agents $HOME/.pi/agent/agents
backups $HOME/.pi/agent/backups/subagents
skills <count>
packages pi-web-access, pi-hashline-edit, pi-subagents, @narumitw/pi-goal
packages pi-web-access, pi-hashline-edit, pi-subagents
jig $HOME/.local/bin/jig
pstack $HOME/.local/bin/update-pstack
controller $HOME/.pi/agent/jig/bin/jigctl.py
Expand Down Expand Up @@ -86,21 +86,17 @@ The command fast-forwards only the independent pstack Git checkout. It then reru

## Required packages

Fresh installs get these packages. Existing rows remain unchanged.
Fresh installs get these packages. Refresh preserves unrelated package entries.

- `npm:pi-web-access` adds `web_search` and `fetch_content`. Librarian skills are filtered out.
- `npm:pi-hashline-edit` replaces the built-in `read` and `edit` tools with hash-anchored versions. The overlay keeps the built-in `grep`.
- `npm:pi-subagents` adds the `subagent` and `subagent_wait` tools. A running Pi agent does not start a child with `pi -p`.
- `npm:@narumitw/pi-goal` replaces the prose `/goal` prompt with settled-idle continuation and the `goal_complete`, `goal_blocked`, and `goal_wait` tools.

If `$HOME/.pi/agent/pi-goal.json` is absent, the installer creates it with unlimited automatic turns and a three-run no-progress guard. It preserves an existing file byte for byte.

The overlay does not install an MCP adapter, a todo tool, plan mode, pi-lens, an interactive browser, CDP, or Instant Grep. Those packages duplicate or conflict with its tools.

## Run process commands

- `/poteto` loads poteto-mode.
- `/goal <objective>` starts a session-scoped objective. Give it a checkable exit predicate. Arrange a wake message before you call `goal_wait` for an external wait.
- `/update-pstack` reviews and installs a pstack update while keeping the pi-stack revision unchanged.

## Configure one repository
Expand Down
55 changes: 2 additions & 53 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,7 @@ Copies the Jig launcher, controller, skill, and references into $HOME/.pi/agent/
Copies the pstack updater command and controller into $HOME/.pi/agent/update-pstack/.
Merges defaultTools, skills, and packages into settings.json without changing project trust.
Finds pi on PATH or under ~/.local/share/pi-node and installs
npm:pi-web-access, npm:pi-hashline-edit, npm:pi-subagents, and
npm:@narumitw/pi-goal.
Creates pi-goal.json with unlimited automatic turns when that file is absent.
npm:pi-web-access, npm:pi-hashline-edit, and npm:pi-subagents.
Rewrites cursor/* subagent models to inherit. Links jig and update-pstack into ~/.local/bin.
Never writes auth.json, models-store.json, private/, or sessions/.
Does not search for git repositories. Initialize one Git root later with jig init.
Expand Down Expand Up @@ -212,7 +210,6 @@ required_packages=(
pi-web-access
pi-hashline-edit
pi-subagents
@narumitw/pi-goal
)

plugins_root="$pi_stack/.plugins"
Expand Down Expand Up @@ -371,7 +368,6 @@ python3 - "${#pstack_skill_names[@]}" "${pstack_skill_names[@]}" "${required_pac
import json
import os
import sys
import tempfile
from pathlib import Path

agent = Path(os.environ["PI_AGENT_DIR"])
Expand Down Expand Up @@ -439,6 +435,7 @@ if packages is None:
if not isinstance(packages, list):
sys.exit("settings.json packages is not an array")

packages = [entry for entry in packages if npm_package_name(entry) != "@narumitw/pi-goal"]
by_name = {}
for i, package in enumerate(packages):
name = npm_package_name(package)
Expand Down Expand Up @@ -469,20 +466,6 @@ if not path.exists() or path.read_text() != text:
tmp.write_text(text)
tmp.replace(path)

goal_settings = agent / "pi-goal.json"
if not goal_settings.exists() and not goal_settings.is_symlink():
goal_text = json.dumps({
"continuationLimits": {"automaticTurns": None, "noProgressTurns": 3}
}, indent=2) + "\n"
with tempfile.NamedTemporaryFile("w", dir=agent, prefix=".pi-goal.", delete=False) as tmp:
tmp.write(goal_text)
goal_tmp = Path(tmp.name)
try:
os.link(goal_tmp, goal_settings)
except FileExistsError:
pass
finally:
goal_tmp.unlink(missing_ok=True)
PY

resolve_pi() {
Expand Down Expand Up @@ -525,40 +508,6 @@ if [[ "${PI_STACK_SKIP_PACKAGES:-}" != 1 ]]; then
fi
fi

goal_manifest="${npm_root}/@narumitw/pi-goal/package.json"
if [[ -f "$goal_manifest" ]]; then
python3 - "$goal_manifest" <<'PY'
import json
import sys
from pathlib import Path

path = Path(sys.argv[1])
data = json.loads(path.read_text())
if data.get("name") != "@narumitw/pi-goal":
raise SystemExit(f"pi-goal package identity mismatch in {path}")
PY
python3 - "$agent/prompts/goal.md" <<'PY'
import hashlib
import sys
from pathlib import Path

path = Path(sys.argv[1])
legacy_sha256 = "c17b0e11552afcc0de0fb894ec8f63ba036cce54afb31f3db79ad64c92b9275a"
if path.is_symlink():
print(f"{path} is not the generated pi-stack goal prompt; keeping it.", file=sys.stderr)
elif path.is_file():
if hashlib.sha256(path.read_bytes()).hexdigest() == legacy_sha256:
path.unlink()
else:
print(f"{path} is not the generated pi-stack goal prompt; keeping it.", file=sys.stderr)
elif path.exists():
print(f"{path} is not a regular file; keeping it.", file=sys.stderr)
PY
elif [[ "${PI_STACK_SKIP_PACKAGES:-}" != 1 ]]; then
printf 'pi-goal package manifest is missing: %s\n' "$goal_manifest" >&2
exit 1
fi

export PSTACK="$pstack"
export OVERLAY="$overlay"
export PI_AGENT_DIR="$agent"
Expand Down
3 changes: 1 addition & 2 deletions overlay/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ Plan mode is `PLAN.md` in the working tree.

Public web is `web_search` and `fetch_content`. `gh`, `git`, and `curl` cover private or authenticated URLs those tools cannot reach. Never MCP.

`/loop` is a one-shot prompt template, not a timer. `/goal` comes from `@narumitw/pi-goal`. For pstack autonomous runs, replace the Cursor `/loop` wake step with `/goal` settled continuation. Start `/goal <objective>` with a checkable exit predicate. Call `goal_complete` only after evidence proves the predicate. Call `goal_blocked` only for a repeated genuine impasse.
When progress depends on an external event, arrange its non-Goal wake message first. For an async child, register `subagent_wait` with `nonBlocking: true`. Then call `goal_wait` alone with `resume_after_ms` only as a bounded fallback. A time-only wake uses `goal_wait` with `resume_after_ms`. Do not use `goal_wait` for ordinary unfinished work.
`/loop` is a one-shot prompt template, not a timer. For an async child, register `subagent_wait` with `nonBlocking: true` when you need a completion wake, then return control.

Never background bash. Use tmux if you need a long-running process that is not a subagent.

Expand Down
64 changes: 22 additions & 42 deletions scripts/check-overlay.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ test -x bin/pstackctl.py || fail "missing executable bin/pstackctl.py"
test -x scripts/check-update-pstack.sh || fail "missing executable scripts/check-update-pstack.sh"
test -f skills/update-pstack/SKILL.md || fail "missing update-pstack skill"
test -f prompts/update-pstack.md || fail "missing update-pstack prompt"
test ! -e prompts/goal.md || fail "prompts/goal.md must be replaced by the pi-goal extension"
test ! -e prompts/goal.md || fail "unexpected bundled goal prompt"

grep -q '"grep"' overlay/settings.json || fail "overlay/settings.json defaultTools lacks grep"
grep -q '"find"' overlay/settings.json || fail "overlay/settings.json defaultTools lacks find"
Expand All @@ -25,7 +25,6 @@ grep -q '"read"' overlay/settings.json || fail "overlay/settings.json defaultToo
grep -q 'npm:pi-web-access' install.sh || fail "install.sh must install npm:pi-web-access"
grep -q 'npm:pi-hashline-edit' install.sh || fail "install.sh must install npm:pi-hashline-edit"
grep -q 'npm:pi-subagents' install.sh || fail "install.sh must install npm:pi-subagents"
grep -q 'npm:@narumitw/pi-goal' install.sh || fail "install.sh must install npm:@narumitw/pi-goal"
grep -q 'PI_STACK_SKIP_PACKAGES' install.sh || fail "install.sh must honor PI_STACK_SKIP_PACKAGES"
grep -q 'backups/subagents' install.sh || fail "install.sh must name backups/subagents"
grep -q 'agents/' install.sh || fail "install.sh must name agents/"
Expand All @@ -52,7 +51,7 @@ grep -q 'web_search' overlay/AGENTS.md || fail "AGENTS.md must name web_search"
grep -q 'fetch_content' overlay/AGENTS.md || fail "AGENTS.md must name fetch_content"
grep -q 'LINE#HASH' overlay/AGENTS.md || fail "AGENTS.md must name LINE#HASH"
grep -q 'poteto-mode' prompts/poteto.md || fail "prompts/poteto.md must tell the model to read poteto-mode"
for token in '/goal' goal_complete goal_blocked goal_wait subagent_wait nonBlocking; do
for token in subagent_wait nonBlocking; do
grep -q "$token" overlay/AGENTS.md || fail "overlay/AGENTS.md must name $token"
done

Expand Down Expand Up @@ -124,26 +123,13 @@ printf '%s\n' "$missing_stub_out" | grep -q 'missing selected skill root: how' |
test ! -e "$tmp/missing-home/.pi/agent" || fail "missing selected skill was detected after installation began"
home="$tmp/home"
mkdir -p "$home/.pi/agent/prompts"
cat >"$home/.pi/agent/prompts/goal.md" <<'EOF'
---
description: Pin an exit predicate and drive to it
argument-hint: "<predicate>"
---
Treat the rest of this message as the exit predicate.

${@:-Drive the current task to a checkable done state.}

Write the predicate at the top of `PLAN.md`. Work until it is true. Do not relax it. Log decisions in `decisions.tsv` when the work is long enough that a reviewer will need the trail.
EOF
mkdir -p "$home/.pi/agent/npm/node_modules/@narumitw/pi-goal"
printf '%s\n' '{"name":"@narumitw/pi-goal","version":"0.54.3"}' >"$home/.pi/agent/npm/node_modules/@narumitw/pi-goal/package.json"

# curl|bash: no checkout beside the process. PI_STACK already has overlay → skip clone.
mkdir -p "$home/.pi/agent"
cat >"$home/.pi/agent/settings.json" <<'EOF'
{
"theme": "keep-theme",
"packages": ["npm:keep-me"],
"packages": ["npm:keep-me", "npm:@narumitw/pi-goal"],
"defaultModel": "cursor/auto",
"enabledModels": ["cursor/auto", "cursor/composer-2.5"],
"subagents": {
Expand All @@ -164,17 +150,9 @@ EOF
test -f "$home/.pi/agent/APPEND_SYSTEM.md" || fail "piped install did not write overlay"
test ! -e "$home/.pi/agent/auth.json" || fail "piped install wrote auth.json"
test ! -d "$home/.pi/agent/npm/node_modules/pi-web-access" || fail "PI_STACK_SKIP_PACKAGES=1 still ran pi install"
test ! -e "$home/.pi/agent/prompts/goal.md" || fail "install did not remove the obsolete goal prompt"
python3 - "$home/.pi/agent/pi-goal.json" <<'PY' || fail "fresh install wrote the wrong pi-goal settings"
import json
import sys
from pathlib import Path

data = json.loads(Path(sys.argv[1]).read_text())
expected = {"continuationLimits": {"automaticTurns": None, "noProgressTurns": 3}}
if data != expected:
raise SystemExit(f"unexpected pi-goal settings: {data!r}")
PY
test ! -e "$home/.pi/agent/prompts/goal.md" || fail "install created a goal prompt"
test ! -e "$home/.pi/agent/pi-goal.json" || fail "install created pi-goal.json"
test ! -e "$home/.pi/agent/npm/node_modules/@narumitw/pi-goal" || fail "install fetched the removed goal package"
python3 - "$home/.pi/agent/settings.json" <<'PY' || fail "piped install dropped packages or skipped required ones"
import json
import sys
Expand Down Expand Up @@ -202,8 +180,7 @@ if not any("pi-hashline-edit" in source(p) for p in packages):
raise SystemExit("pi-hashline-edit missing")
if not any("pi-subagents" in source(p) for p in packages):
raise SystemExit("pi-subagents missing")
if joined.count("npm:@narumitw/pi-goal") != 1:
raise SystemExit("pi-goal missing or duplicated")
assert not any(s.startswith("npm:@narumitw/pi-goal") for s in joined), joined
skills = data.get("skills") or []
if not any("skills-pstack/poteto-mode" in s for s in skills):
raise SystemExit("skills do not point at skills-pstack/poteto-mode")
Expand Down Expand Up @@ -289,7 +266,6 @@ if ! second_out="$(
fail "second piped install with fake upstream failed"
fi
grep -q '^custom goal prompt$' "$home/.pi/agent/prompts/goal.md" || fail "install removed a custom goal prompt"
printf '%s\n' "$second_out" | grep -q 'keeping it' || fail "install did not warn about the custom goal prompt"
cmp -s "$home/.pi/agent/pi-goal.json" "$tmp/pi-goal.after-custom" || fail "install overwrote existing pi-goal settings"
for name in scout researcher reviewer worker delegate oracle; do
test ! -e "$home/.pi/agent/agents/$name.md" || fail "retired installed agent $name remains"
Expand Down Expand Up @@ -318,10 +294,8 @@ cmp -s "$home/.pi/agent/settings.json" "$tmp/settings.after-replace" || fail "th

home_ask="$tmp/home-ask"
mkdir -p "$home_ask/.pi/agent"
printf '%s\n' '{"defaultProjectTrust":"ask","packages":["npm:@narumitw/pi-goal@0.54.3"]}' >"$home_ask/.pi/agent/settings.json"
printf '%s\n' '{"defaultProjectTrust":"ask","packages":["npm:keep-me@1.0.0", "npm:@narumitw/pi-goal@0.54.3", {"source":"npm:@narumitw/pi-goal@0.54.4","extensions":[]}]}' >"$home_ask/.pi/agent/settings.json"
mkdir -p "$home_ask/.pi/agent/prompts"
mkdir -p "$home_ask/.pi/agent/npm/node_modules/@narumitw/pi-goal"
printf '%s\n' '{"name":"@narumitw/pi-goal","version":"0.54.3"}' >"$home_ask/.pi/agent/npm/node_modules/@narumitw/pi-goal/package.json"
printf 'symlink target\n' >"$tmp/custom-goal-target"
ln -s "$tmp/custom-goal-target" "$home_ask/.pi/agent/prompts/goal.md"
printf 'not json\n' >"$home_ask/.pi/agent/pi-goal.json"
Expand All @@ -341,11 +315,8 @@ if data.get("defaultProjectTrust") != "ask":
raise SystemExit("ask was overwritten")
packages = data.get("packages") or []
sources = [entry if isinstance(entry, str) else entry.get("source", "") for entry in packages]
pinned = "npm:@narumitw/pi-goal@0.54.3"
if sources.count(pinned) != 1:
raise SystemExit("pinned pi-goal package was replaced")
if "npm:@narumitw/pi-goal" in sources:
raise SystemExit("pinned pi-goal package was duplicated")
assert sources.count("npm:keep-me@1.0.0") == 1
assert not any(s.startswith("npm:@narumitw/pi-goal") for s in sources), sources
PY

home_nopi="$tmp/home-nopi"
Expand Down Expand Up @@ -411,7 +382,7 @@ from pathlib import Path

path = Path(sys.argv[1])
text = path.read_text()
needle = "\t@narumitw/pi-goal\n"
needle = "\tpi-subagents\n"
if text.count(needle) != 1:
raise SystemExit("required package insertion point changed")
path.write_text(text.replace(needle, needle + "\tpi-update-fixture\n"))
Expand Down Expand Up @@ -515,8 +486,6 @@ npm_root="$home2/.pi/agent/npm/node_modules"
for package in pi-web-access pi-hashline-edit pi-subagents; do
mkdir -p "$npm_root/$package"
done
mkdir -p "$npm_root/@narumitw/pi-goal"
printf '%s\n' '{"name":"@narumitw/pi-goal","version":"fixture"}' >"$npm_root/@narumitw/pi-goal/package.json"
fake_bin="$tmp/fake-bin"
mkdir -p "$fake_bin"
cat >"$fake_bin/pi" <<'EOF'
Expand All @@ -528,6 +497,17 @@ printf '%s\n' "$2" >>"$PI_INSTALL_LOG"
mkdir -p "$PI_CODING_AGENT_DIR/npm/node_modules/$package"
EOF
chmod +x "$fake_bin/pi"
cold_log="$tmp/cold-install.log"
for pass in 1 2; do
PATH="$fake_bin:$PATH" HOME="$tmp/cold-home" PI_STACK="$root" PSTACK="$stub" PI_STACK_SKIP_PACKAGES=0 PI_INSTALL_LOG="$cold_log" bash "$root/install.sh" >"$tmp/cold-install-$pass.log" 2>&1 || fail "cold package install pass $pass failed"
done
python3 - "$cold_log" <<'PY'
from pathlib import Path
import sys
assert Path(sys.argv[1]).read_text().splitlines() == ["npm:pi-web-access", "npm:pi-hashline-edit", "npm:pi-subagents"]
PY
test ! -e "$tmp/cold-home/.pi/agent/pi-goal.json" || fail "cold install created pi-goal.json"
test ! -e "$tmp/cold-home/.pi/agent/npm/node_modules/@narumitw/pi-goal" || fail "cold install fetched the removed goal package"
install_log="$tmp/pi-install.log"
if ! accept_out="$(PATH="$fake_bin:$PATH" HOME="$home2" PI_STACK_GIT="$seed_url" PSTACK_GIT="$pstack_url" PI_INSTALL_LOG="$install_log" python3 "$tty_runner" "$root/install.sh" y 2>&1)"; then
printf '%s\n' "$accept_out" >&2
Expand Down