README.md

pi-tracks #

A pi package for working in parallel git worktrees and for the spec-driven coordinator, plus chook tool-call policy enforcement. Plain pi, any provider, Linux or macOS.

What it provides #

  • Prompts: /track, /tracks, /handoff, /pickup, /land, /spec, /spec-watch, /track-ops, and /waiting, which provides a read-only list of what needs a person's action across every spec-driven repository under $TRACKS_REPO_ROOTS (default $HOME/workspace and $HOME/misc), including repositories no session has open. For example:

    /waiting
    
  • Extensions:

    • path.ts puts this package's bin/ on PATH, so prompts and the bash tool call the scripts by bare name.
    • chook/ runs every tool call past chook and adds Pi-specific guards (path containment, the track repo root guard).
    • track-context/ reports at session start whether the session is in a track worktree or the primary checkout.
    • subagent/ is pi's example subagent tool, vendored; it also reads this package's agents/, applies SPEC_WATCH_REVIEWER_MODEL to spec-reviewer, and accepts an optional per-call model.
    • track-monitor/ posts read-only track/CI change notifications into an interactive session, and only in repositories that opt in (see "Track monitor (opt-in)" below).
    • track-loop/ is the coordinator's default-on wake loop: in an interactive/RPC session standing in the primary checkout of a repo declaring tracks.json, it polls spec/worktree state every 30 s and wakes the agent like the operator-run watcher does (read-only framing, batched, rate-limited). Each wake names the change and a suggested next move — e.g. awaiting-approval → prep /land; a parked track → tell the person. Advisory only: no new authority, lease-held exactly one session per checkout, /track-loop off silences it for the session (on restores).
    • track-reporter/ injects finished background-work reports from the shared report directory into the session (see "Track reporter" below).
  • Agents: spec-reviewer, the read-only reviewer /spec-watch runs before marking work awaiting-approval; track-worker, which /track uses to spawn a track's first writer and /pickup uses to resume one, both through the active harness's agent facility.

  • Scripts in bin/: track placement and status, spec status, the coordinator's runtime and writer launcher, the scheduled tick, and the scheduler. out-of-boundary.sh is the agent-run audited channel for writes outside a session's tree: the session writes an idempotent, secret-free script with a # side effects: header and runs it through the wrapper, which keeps a private run log and appends start/end lines (with the exit code) to .operator-run/journal.jsonl — the audit ledger the person can tail. Interactive or credential-bearing steps still belong to the person via the operator-run skill. This bin/ is shared: the Claude adapter links Claude Code directly to these files rather than maintaining copies.

Where work happens #

In every repository that declares tracks.json — this package included — feature and fix work goes through the track system: /track <name> <task> opens a track with its own worktree, branch, writer, review and landing; /spec <name> writes a spec the schedule or the loop then drives. The primary checkout's committed surface is coordination files only: tracks.json, specs/, .gitignore (handoff.md is never committed). No drive-by commits to the base branch, and no subagent produces repo work outside a track — a subagent dispatch is reserved for the sanctioned spec-reviewer review leg and for a track's own writer.

The one exception: the person explicitly says "commit direct", and the handoff records that instruction. Work the person asks for in conversation otherwise gets a track like any other work.

Watched sessions — print mode, a subagent, a background writer — were already blocked from editing the primary checkout and stay blocked.

Install #

Point pi at a checkout, so edits take effect without reinstalling:

pi install "$HOME/workspace/pi-tracks"

A git URL works too (pi install git:<host>/<owner>/pi-tracks), at the cost of pi update --extensions to pick up changes.

After cloning, enable the repo's commit checks:

git config core.hooksPath .githooks

Claude Code adapter #

Install the adapter once on each machine that runs these workflows in Claude Code. It is machine-wide, not per repository:

bin/install-claude-adapter.sh install
bin/install-claude-adapter.sh doctor

It creates the stable machine-local link $HOME/.local/share/pi-tracks -> <this checkout>, backs up conflicting old Claude files, and links this package's scripts, prompts, and agents into $HOME/.claude/. Changes in the checkout then affect both Pi and Claude without copying or regeneration. remove deletes only adapter-owned links and never deletes its backups.

The links are derived from the package's bin/, prompts/ and claude/agents/ directories, so a file added there needs no edit to the installer; only the few helpers in bin/ that are not entry points are listed by name in install-claude-adapter.sh. Nor does landing such a file need a manual install: each Claude tick (bin/spec-watch-tick.sh) runs doctor and, when the only problems are adapter links, runs install, logging what it did to $HOME/.local/state/spec-watch/spec-watch.log. A wrong stable link is only reported there. Run install by hand once per machine, the first time.

Set TRACKS_TOOL=claude in $HOME/.config/pi-tracks/env for schedules that should launch Claude; use pi or omit it for Pi.

The adapter is installed only once per machine, but schedules are configured per watched repository. After installing the adapter for the first time, reinstall each repository's existing spec-watch schedule once so its command uses the stable canonical path:

bin/spec-watch-schedule.sh install /path/to/repository

Repositories without a spec-watch schedule need no per-repository migration.

chook #

The chook extension runs the first of $CHOOK_PATH, /usr/local/bin/chook and $HOME/chook/chook.mjs that exists, and is disabled with a warning if none does. Rules come from $HOME/.config/chook-pi.toml if present, else the package's chook-pi.toml.

Track monitor (opt-in) #

extensions/track-monitor/ watches a repository's tracks and one configured CI workflow from inside an interactive/RPC Pi session and posts a short read-only notification when something changes. It is strictly opt-in: a repository declares it with .pi/track-monitor.json or with a monitor object in tracks.json (.pi/track-monitor.json wins when both exist). A repository that declares neither gets no monitor, no lease, no polling and no warning; /track-monitor on there warns that nothing is configured instead of inventing defaults. It starts only in an interactive/RPC session in the primary checkout, never in print/JSON writer processes or linked worktrees.

Configuration fields, revalidated on every poll:

  • targets: up to 16 explicit {name, kind} watches; kind: "spec" (name must start with spec-, state comes from canonical spec status) or kind: "manual" (state comes from Git and writer runtime metadata).
  • githubRepo + workflow: the one CI workflow whose latest run is watched (via gh run list).
  • discover: boolean, default true; also watch direct registered worktrees under the tracks.json worktree directory. false selects explicit targets only.
  • exclude: exact track names to drop. Names on tracks.json's parked list are excluded even when explicitly targeted.

One coordinator session per checkout owns .tmp/pi-track-monitor.lock (gitignored scratch); existing or malformed locks are never stolen. The monitor samples every 30 s, batches notifications at least 60 s apart, and sends at most 30 automatic updates per hour; pending changes survive throttling and runtime-reload checkpoints.

The monitor is read-only: bounded local git reads, spec-status.mjs --tool pi --read-only resolved from this package's bin/, and gh run list for the configured repository/workflow. No fetch/pull/push, no token-file reads, no raw test/CI/session log reads, no shell interpolation. Commands:

  • /track-monitor status — activation, last sample, pending changes, quotas.
  • /track-monitor off / on — stop/resume for this session branch.
  • /track-monitor test — one clearly labelled synthetic wake-up.

Track reporter #

extensions/track-reporter/ watches the shared directory /tmp/track-reports-global for *.report files written by a finished background worker's launcher, and injects each matching report into the session as a message that starts a turn, so a coordinator reports finished work without being pinged. Reports are repo-scoped by their first line: a report beginning repo=<name> is delivered only in a session whose working directory ends in <name>, and a report with no tag goes to every session. A report for another repository stays on disk for that repository's session; leftover reports are purged on session start.

The extension reads only the report file itself. It never captures credentials, token files, environment values, session transcripts or raw worker/CI logs, runs no commands, and keeps no UI ledger — delivery is a message only.

The poller is unref'd and both the poller and the watcher are closed on session_shutdown; otherwise every pi --print process stays alive after its turn. extensions/track-monitor/ follows the same rule, and test/extension-lifetime.mjs asserts both with a real bounded print run.

Remove the older global copy at ~/.pi/agent/extensions/track-reporter.ts (and its .bak-* files) once this package is installed: two loaded copies deliver every finished report twice.

Starting a writer #

A repository that uses these tracks starts a writer in a worktree through one harness-neutral command and never names a harness binary, a harness config directory or a harness guard package itself:

"$PI_TRACKS_HOME/bin/writer-exec.sh" --tool <name> --model <model> --brief-file <path>

PI_TRACKS_HOME is the package location, default $HOME/.local/share/pi-tracks.

  • Run it with the working directory set to the worktree the writer should work in. --model "" leaves the harness on its default model.
  • <name> is claude or pi. An unknown name exits 2 with one line on stderr naming the known adapters.
  • It first brings the harness's own configuration up to date on this host, then starts one background session with the brief read from the file, unmodified, and returns as soon as the session has started. For claude the configuration is the config repository ($CLAUDE_HOME, default $HOME/.claude), the guard repository ($CHOOK_HOME, default $HOME/chook) and the package itself, each fast-forwarded from its upstream (a repository with no upstream is left alone; a missing one or a failed update refuses). For pi it is the package.
  • Exit 0 means started, and stdout carries only what the harness prints. Any refusal (untrusted folder, missing binary, failed update) exits nonzero with one line on stderr saying why; a folder the harness does not trust exits 3.
  • Unattended-run settings live in the adapter: claude runs claude --bg with CHOOK_UNATTENDED=1 set through --settings (a --bg session runs under the background daemon's environment, never the launcher's) and the accept-edits permission mode; pi runs pi -p --no-session in its own session with CHOOK_UNATTENDED=1.
  • Optional, for a caller that runs on the host it is already running from: PI_TRACKS_WRITER_PREPARE=skip skips the configuration update. The pi adapter appends the session's output to PI_TRACKS_WRITER_LOG and writes its process id to PI_TRACKS_WRITER_PID_FILE when those are set. The binaries are found at the SPEC_WATCH_CLAUDE_BIN / SPEC_WATCH_PI_BIN override, then $HOME/.local/bin, then PATH.

To support another harness, add adapters/<name>.sh; bin/writer-exec.sh needs no edit. It is sourced with the working directory, model and brief set, and defines two functions: prepare (bring the harness's configuration up to date) and start (start the session and return). Adapters call refuse <exit-code> <one-line reason> to refuse, and may use `update_repo