Skip to content

chore(agents): track .pi config, add AGENTS.md symlink and pi format-on-edit - #433

Merged
pepe-anchor merged 5 commits into
mainfrom
worktree-50c24c64
Aug 5, 2026
Merged

chore(agents): track .pi config, add AGENTS.md symlink and pi format-on-edit#433
pepe-anchor merged 5 commits into
mainfrom
worktree-50c24c64

Conversation

@pepe-anchor

Copy link
Copy Markdown
Contributor

Summary

Brings pi to parity with the existing Claude Code agent setup so both agents share the same project instructions and post-edit formatting, with one source of truth.

Changes

  • Track .pi/extensions/protect-build-artifacts.ts was already present but untracked; now committed so the team shares it. New format-on-edit.ts mirrors .claude/hooks/format-on-edit.sh, running rustfmt --edition 2024 after a write/edit on a .rs file (best-effort: never blocks the agent on a formatting failure; skips when the edit itself errored).
  • AGENTS.md — symlink to CLAUDE.md so tools that look for AGENTS.md (pi, Cursor, others) find the same instructions Claude Code already loads. Verified in pi's source (resource-loader.js): existsSync/readFileSync follow symlinks; candidate order is AGENTS.md first and the per-dir loop returns on first match, so CLAUDE.md is not double-loaded.
  • CONTRIBUTING.md — new "Agent setup" section documenting the shared, tracked config vs. the per-user scratch that stays gitignored.

Why

Both agents already worked here, but pi's project-local config was untracked and pi lacked the format-on-edit hook Claude had. This makes the setup shared and consistent across agents, with instructions living in a single file.

Verification

  • `node --check` passes on both extensions.
  • Load test: both extensions import under Node 22.23, export a `default` function, and subscribe to the correct events (`tool_result` for format-on-edit, `tool_call` for protect-build-artifacts).
  • Handler logic: simulated events confirm correct filtering (.rs filter, `isError` guard, `write`/`edit` only).
  • End-to-end: invoked the real handler on a misformatted `.rs` file; it spawned `rustfmt --edition 2024` and the file came out properly reformatted.

Not verified: a live interactive `pi` session confirming hot-load + `/trust` prompt + the build-artifact guard blocking a real edit in a TTY (can't be driven headless). Code is proven correct and loadable.

Notes

  • .claude/settings.local.json remains a dangling symlink in this worktree (points to a repo-root file that doesn't exist) — that's per-user, gitignored state, not part of this shared config.
  • The existing protect-build-artifacts.ts uses direct event.input.path access (no ?.); confirmed non-optional in pi's ToolResultEventBase (Record<string, unknown>), and the new extension matches that style.

…on-edit

Prepare the repo for Claude Code and pi agents at parity:

- Track .pi/extensions/: protect-build-artifacts.ts (was untracked) plus a
  new format-on-edit.ts that mirrors .claude/hooks/format-on-edit.sh,
  running  after write/edit on .rs files (best-effort,
  never blocks the agent on a formatting failure).
- Add AGENTS.md as a symlink to CLAUDE.md so tools that look for AGENTS.md
  (pi, Cursor, etc.) find the same instructions Claude Code already loads.
- Document the shared agent setup in CONTRIBUTING.md.
- format-on-edit.ts: use event.input.path (not event.input?.path) to match
  protect-build-artifacts.ts. Confirmed non-optional in pi's
  ToolResultEventBase (Record<string, unknown>).
- CONTRIBUTING.md: clarify that only shared .claude/ config is tracked;
  per-user scratch (settings.local.json, worktrees/) is gitignored.
- Verified at runtime: both extensions import + subscribe to the correct
  events; handler filters .rs/isError/write|edit correctly; end-to-end
  run on a misformatted .rs file invoked rustfmt --edition 2024 and
  reformatted it.
Copilot AI review requested due to automatic review settings August 3, 2026 13:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Aligns the pi coding agent’s project-local configuration with the existing Claude Code setup so both agents share the same instructions source and post-edit Rust formatting behavior.

Changes:

  • Adds/commits pi agent extensions for build-artifact protection and Rust format-on-edit.
  • Documents shared vs. per-user agent configuration in CONTRIBUTING.md.
  • Introduces pi-side rustfmt formatting hook triggered after write/edit results.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
CONTRIBUTING.md Adds an “Agent setup” section describing shared tracked agent config and per-user scratch.
.pi/extensions/protect-build-artifacts.ts New pi extension to block agent edits to certain protected paths.
.pi/extensions/format-on-edit.ts New pi extension to run rustfmt --edition 2024 after successful Rust edits.
Suppressed comments (1)

.pi/extensions/protect-build-artifacts.ts:31

  • The block reason always suggests make -C src generated, but this guard also blocks target/ and Cargo.lock, where that guidance is inaccurate. Consider tailoring the message based on which pattern matched (or making it generic).
		if (PROTECTED.some((p) => path.includes(p))) {
			ctx.ui.notify?.(`Blocked write to build artifact: ${path}`, "warning");
			return { block: true, reason: `"${path}" is a generated/build artifact; regenerate via \`make -C src generated\` instead of editing` };
		}

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread .pi/extensions/protect-build-artifacts.ts Outdated
Comment thread .pi/extensions/format-on-edit.ts Outdated
protect-build-artifacts.ts:
- Remove codegen/ from PROTECTED. It is the hand-written generator crate
  (src/codegen/src/main.rs), not generated output; CLAUDE.md lists it as the
  tonic_build script. Only its output crate src/generated is protected.
  The old comment ('regenerate, don't hand-edit') did not apply to it.
- Resolve tool paths to absolute before matching so relative inputs like
  'target/debug/...' are caught by '/target/' (previously only matched
  absolute paths).
- Tailor the block reason per pattern instead of always suggesting
  'make -C src generated' (which was inaccurate for target/ and Cargo.lock).

format-on-edit.ts:
- Surface rustfmt failures as a notification, matching the header comment
  (previously spawn errors and non-zero exits were swallowed silently).
  Suppresses the notification when ctx.signal aborted (turn cancelled,
  not a real formatting failure).

Verified: load + subscribe OK; protect now blocks relative target/, allows
codegen/, returns tailored reasons; format-on-edit notifies on non-zero
exit and ENOENT, stays silent on success and on abort.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

Suppressed comments (2)

CONTRIBUTING.md:21

  • AGENTS.md is referenced here as an existing symlink to CLAUDE.md, but there is no AGENTS.md in this PR checkout. Either add the symlink as part of the PR (to match the description) or adjust this line so CONTRIBUTING doesn’t claim a file that isn’t present.
- `CLAUDE.md` (also symlinked as `AGENTS.md`) — project instructions both agents load at startup.

.pi/extensions/protect-build-artifacts.ts:33

  • resolve(path) returns platform-native separators (e.g., \ on Windows), but the patterns include forward slashes (e.g., "/target/"). On Windows this will fail to match and the guard won’t block writes into target/. Normalize the resolved path before includes matching.
		const resolved = resolve(path);
		const match = PROTECTED.find((p) => resolved.includes(p.pattern));

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

Suppressed comments (1)

.pi/extensions/protect-build-artifacts.ts:33

  • resolve(path) returns platform-specific separators (e.g., C:\...\src\generated\... on Windows). Since the protected patterns use forward slashes ("src/generated", "/target/"), the guard won’t match on Windows and edits to generated/build output paths would slip through.
		const resolved = resolve(path);
		const match = PROTECTED.find((p) => resolved.includes(p.pattern));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice parity work — the two pi hooks are a faithful, not-cargo-culted port of the existing Claude Code equivalents (format-on-edit.ts mirrors .claude/hooks/format-on-edit.sh's semantics exactly: .rs-only, best-effort, never blocks the agent), and the AGENTS.md symlink is the right call over a copy — single source of truth, and .claude-loaded instructions and pi-loaded ones can't drift apart. Also good to see both rounds of Copilot's feedback genuinely landed (verified against the current head blobs, not just the reply threads) — the codegen/ false-positive, the /target/ relative-path miss, and the swallowed-failure notification are all fixed.

I ran the two extension files through pi's own loader (jiti) with synthetic tool_call/tool_result events, and separately installed the real pi CLI against a sample project layout to confirm both files load cleanly under real project-trust + discovery (with a negative control: intentionally broken syntax does surface a loud Failed to load extension error, so the clean load is a meaningful signal, not a silent no-op). Core behavior — POSIX path blocking, the rustfmt reformat, the isError short-circuit, graceful handling of a file rustfmt can't parse — all checked out.

A handful of non-blocking notes for a fast-follow, roughly in order of "worth doing soon" to "whenever":

  • protect-build-artifacts.ts:32-33resolved.includes(pattern) on a fully-resolved absolute path can match an ancestor directory, not just a repo-relative one (e.g. a checkout under ~/target/visualsign-parser would get every write blocked repo-wide). Relativizing to repo root before matching would tighten this. Same root cause makes the guard a silent no-op on Windows, since resolve() yields \ separators against forward-slash patterns.
  • No .gitignore entries were added for .pi/'s own per-user scratch (e.g. whatever /trust persists) — worth mirroring the .claude/ treatment so contributors running pi don't pick up untracked local state in git status.
  • "Cargo.lock" as a bare substring also matches nested lockfiles like fuzz/Cargo.lock, with a block reason ("workspace lockfile") that wouldn't be accurate for that one. A basename check would be more precise.
  • The guard only intercepts write/editbash (e.g. sed -i) bypasses it entirely. The doc comment reads a bit stronger than what's implemented; a one-line caveat would keep the next reader from over-trusting it.
  • No timeout on the spawned rustfmt process in format-on-edit.ts — a wedged process would stall the turn indefinitely.
  • stdio: "ignore" in the same file discards rustfmt's actual error output, so the warning toast can't say more than "exited 1."
  • No try/catch around the await — the file's contract is "never blocks the agent," and a bare try/catch would make that airtight against an unexpected synchronous throw.
  • Minor: the per-user-scratch sentence in the new CONTRIBUTING.md section is stated twice; and AGENTS.md inherits a CLAUDE.md opening line that's Claude-Code-specific phrasing, harmless but slightly odd for a pi/Cursor reader.

Scanned other open and recently-merged PRs — nothing touches .claude/, .pi/, AGENTS.md, or CONTRIBUTING.md, so no conflicts or rebases needed here.

Addresses the 8 non-blocking notes from shahan's review on PR #433:

protect-build-artifacts.ts:
- Match paths relative to the project root (ctx.cwd), normalized to
  forward slashes. Fixes the ancestor-dir false positive (a checkout under
  ~/target/visualsign-parser no longer trips /target/) and the Windows
  separator no-op in one shot.
- Cargo.lock now blocks the workspace root lockfile only, so a nested
  fuzz/Cargo.lock is left alone and the 'workspace lockfile' reason stays
  accurate. target/ matched as a real path segment, not a substring.
- Header note that the guard only intercepts write/edit; bash (sed -i)
  bypasses it, so it is a guardrail, not an airtight sandbox.

format-on-edit.ts:
- Add a 10s timeout that SIGKILLs a wedged rustfmt instead of stalling
  the agent turn.
- Capture stderr (stdio pipe) and include rustfmt's last stderr line in
  the warning so failures say more than 'exited 1'.
- Wrap the spawn in try/catch so a synchronous throw can never block the
  agent, matching the file's 'never blocks' contract.

.gitignore:
- Ignore .pi/mega-compact/ (per-user scratch: sqlite db, events log,
  dashboard), mirroring the .claude/ treatment. Trust decisions live in
  the global ~/.pi/agent store, not in the repo.

CONTRIBUTING.md:
- Dedup the per-user-scratch sentence (it was stated twice); document
  .pi/mega-compact/ alongside the .claude scratch entries.

CLAUDE.md:
- Soften the opening line to agent-neutral phrasing now that AGENTS.md
  symlinks here and both Claude Code and pi load it.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (2)

.pi/extensions/protect-build-artifacts.ts:57

  • toRel() claims that paths outside the repo "can't match" because relative() yields a .. prefix, but the current matchers (notably rel.includes("/target/")) will still match strings like ../target/... and incorrectly block edits outside the repo. Either update the comment or explicitly treat any ..-prefixed relative path as non-matchable.
// Repo-relative, forward-slash path. resolve() handles both absolute and
// relative tool inputs; relative() re-roots them at the project dir so a path
// outside the repo can't match (it yields a `..` prefix).
function toRel(cwd: string, raw: string): string {
	const rel = relative(cwd, resolve(cwd, raw));

CLAUDE.md:4

  • This file now states it is symlinked as AGENTS.md, but AGENTS.md does not exist in the PR workspace. Either add the AGENTS.md -> CLAUDE.md symlink/file as described, or remove the symlink claim here (and in CONTRIBUTING.md) to avoid misleading setup instructions.
# CLAUDE.md

This file provides guidance to coding agents (Claude Code and pi) when working with code in this repository. It is symlinked as `AGENTS.md` so both agents load the same instructions.

Copilot's re-review of 6208606 (suppressed, low-priority) noted that
toRel()'s claim 'a path outside the repo can't match' was wrong: relative()
yields a '..' prefix, but the '/target/' matcher uses rel.includes('/target/')
which still matches '../target/...'. That re-introduced the exact ancestor-dir
false positive the repo-relative rewrite was meant to fix (e.g. an edit under
~/target/visualsign-parser, resolved to ../target/..., would be blocked).

Now explicitly skip any rel path that starts with '..' before matching, so
outside-repo edits are never treated as repo build artifacts.

@shahan-khatchadourian-anchorage shahan-khatchadourian-anchorage left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at c032f6d8. All eight of the fast-follow notes landed, and I checked them against the head blobs rather than the reply threads: the repo-relative rewrite kills the ancestor-dir false positive and the Windows separator no-op in one move, target is now a real path segment (my-target/ is correctly left alone), the bash/sed -i caveat is in the header, and format-on-edit.ts picked up the timeout, stderr capture, and the outer try/catch. The .. short-circuit in patch 5 is the right shape too — I confirmed it doesn't over-fire: ../visualsign-parser/src/generated/y.rs still normalizes back inside the repo and blocks, so only genuinely-outside paths get skipped. Nice iteration density for a chore PR, and the PROTECTED table growing a per-entry match predicate reads much better than the substring list it replaced.

All checks green on head (ubuntu, both CodeQL analyses, CLA, labeler).

On the Cargo.lock rule

.pi/extensions/protect-build-artifacts.ts:47 — the Cargo.lock rule no longer matches anything. There is no root Cargo.lock in this repo, so rel === "Cargo.lock" is dead code:

$ git ls-tree -r --name-only HEAD | grep 'Cargo.lock$'
src/Cargo.lock                                          <- the workspace lockfile
src/visualsign/Cargo.lock
src/chain_parsers/visualsign-solana/fuzz/Cargo.lock
tools/tvc-deploy/Cargo.lock

The pre-patch-4 path.includes("Cargo.lock") did block all four (over-broadly); the tightening overshot and now blocks none. Simulating the current matcher against these paths: src/Cargo.lock → allow. My earlier note was aiming at a basename check rather than root-only — sorry if that read as "repo root," the intent was "the reason string should match the file it fires on."

Either of these restores the guard:

// Option A - protect the actual workspace lockfile.
{ reason: "workspace lockfile", match: (rel) => rel === "src/Cargo.lock" },

// Option B - all lockfiles, with a reason that stays true for the nested ones.
{ reason: "a cargo lockfile (regenerate via cargo, don't hand-edit)",
  match: (rel) => rel === "Cargo.lock" || rel.endsWith("/Cargo.lock") },

I'd lean B, since fuzz/Cargo.lock and src/visualsign/Cargo.lock are equally not-hand-editable, and it keeps the rule from silently rotting if the workspace root ever moves again.

Other notes

  • .gitignore misses pi's own project-scoped dirs. pi writes project-scope package installs to .pi/npm/ and .pi/git/ (package-manager.js, getNpmInstallRoot/getGitInstallRoot: scope === "project"join(cwd, ".pi", "npm" | "git")). Those are machine-specific trees that'd land in git status for anyone who installs a project-scoped pi package. Conversely, .pi/mega-compact/ has no reference anywhere in pi core (checked 0.83.0) — it looks like it comes from a globally-installed extension, so as written the shared ignore file encodes one contributor's extension while leaving pi's own dirs uncovered. Adding .pi/npm/ and .pi/git/ alongside it would make the "mirrors the .claude/ treatment" comment true.

  • The parity is one-directional on the guard. .claude/settings.json has only the PostToolUse fmt hook — no permissions block, so Claude Code has no equivalent of the build-artifact guard. CONTRIBUTING's "applies to both Claude Code and pi" is accurate for the instructions and formatting, slightly generous for the guard. A permissions.deny entry (Write(./src/generated/**), Edit(./src/generated/**)) would close it, or the bullet could say the guard is pi-side today.

  • ctx.cwd isn't quite "the project root." It's pi's launch cwd (main.js process.cwd()AgentSession._cwdExtensionRunner). It happens to equal the repo root whenever this extension is live, because discoverAndLoadExtensions only looks at join(cwd, ".pi", "extensions") with no parent walk — launch from src/ and the extension isn't loaded at all, so there's no wrong-root window. An explicitly-configured extension path would bypass that and silently stop matching src/generated. Worth a clause in the header noting the invariant comes from cwd-local discovery, so a future reader doesn't have to re-derive why the assumption holds.

  • format-on-edit.ts:73 — last stderr line is usually the least useful one. stderr.trim().split("\n").pop() on a rustfmt parse error lands on the caret/| continuation line; the first line carries the actual message. split("\n")[0] (or first + the --> location) would make the toast say more.

  • format-on-edit.ts:31 — no cwd on the spawn, while the guard now carefully uses ctx.cwd. Relative tool paths only work because the process cwd happens to match; passing cwd: ctx.cwd makes the two files symmetric for free.

  • format-on-edit.ts:63 — the timer kills but doesn't settle. In practice SIGKILL guarantees close, so the 10s bound holds; calling finish() from the timer as well would make it unconditional rather than dependent on the child behaving.

Related in-flight work

  • #421 (also yours) touches CLAUDE.md in the same Workspace Lint Policy neighbourhood — it inserts a "Deploy-Time ABI Trust Posture" section right after the Exceptions: line, a few lines below the blank-line-before-list change here. Close enough that whichever merges second may want a quick rebase check.
  • #436 adds src/rustfmt.toml with edition = "2024". No conflict, but once it lands the hardcoded --edition 2024 in both format hooks is redundant for anything under src/. Keeping the flag is still defensible since it also covers .rs files outside src/ (e.g. tools/) — just flagging so the two don't drift if the edition ever bumps.

Nothing else open or recently merged touches .claude/, .pi/, AGENTS.md, or CONTRIBUTING.md.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. The Cargo.lock matcher note above is a follow-up rather than something to hold this on — the rest of the guard and the format hook both check out, and the shared-config setup is a clear improvement over the untracked state.

@pepe-anchor
pepe-anchor merged commit 9afd611 into main Aug 5, 2026
10 checks passed
@pepe-anchor
pepe-anchor deleted the worktree-50c24c64 branch August 5, 2026 08:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants