-
Notifications
You must be signed in to change notification settings - Fork 333
feat(agent): add pi coding agent integration in-tree #1170
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
61c0f61
feat(agent): add pi coding agent integration in-tree
195ec5d
fix(agent/pi): address review feedback
deb60dc
Resolve Pi sessions from native store
Soph 3b621b9
Drop SessionEnd from pi session_shutdown hook
Soph a1d59dc
Add Pi to the E2E agent registry
Soph 14d5040
Document Pi in CLAUDE.md
Soph 51d55dc
Handle both path separators in encodeRepoPathForPi
Soph 64e9441
Isolate PI_CODING_AGENT_DIR in the E2E adapter
Soph 9d4ce4b
Merge pull request #1173 from entireio/soph/pi-cold-attach-and-e2e
Soph f27f923
Add 'entire trail watch' to tail trail code-review SSE stream
dipree b37565c
Treat 404/410 as terminal in 'trail watch', propagate auth errors
dipree c7174ae
Address trail watch review feedback
dipree c473e29
small refactor
Soph a6164cf
Merge pull request #1177 from entireio/feat/trail-watch-sse
dipree File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| // Entire CLI extension for Pi | ||
| // Auto-generated by `entire enable --agent pi` | ||
| // Do not edit manually — changes will be overwritten on next install. | ||
| // | ||
| // Forwards Pi lifecycle events to `entire hooks pi <event>` so Entire can | ||
| // create checkpoints, capture transcripts, and offer rewind/resume. | ||
| // | ||
| // ENTIRE_CMD is replaced at install time by Entire's installer. | ||
|
|
||
| import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; | ||
| import { execFile } from "node:child_process"; | ||
|
|
||
| export default function (pi: ExtensionAPI) { | ||
| const ENTIRE_CMD = "__ENTIRE_CMD__"; | ||
|
|
||
| function fireHook(hookName: string, data: Record<string, unknown>): Promise<void> { | ||
| return new Promise((resolve) => { | ||
| try { | ||
| const child = execFile( | ||
| "sh", | ||
| ["-c", `${ENTIRE_CMD} hooks pi ${hookName}`], | ||
| { timeout: 10000, windowsHide: true }, | ||
| () => resolve(), | ||
| ); | ||
| child.stdin?.end(JSON.stringify(data)); | ||
| } catch { | ||
| // best effort — never block the agent on a hook failure | ||
| resolve(); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| // Agent-driven bash subprocesses inherit a real TTY but cannot answer | ||
| // hook prompts. Disable git/Entire terminal prompts for bash calls so | ||
| // Entire treats agent-driven commits as non-interactive. | ||
| pi.on("tool_call", async (event) => { | ||
| if (event.toolName !== "bash") return; | ||
| const input = event.input as { command?: string }; | ||
| if (typeof input.command !== "string" || input.command.includes("GIT_TERMINAL_PROMPT=")) { | ||
| return; | ||
| } | ||
| input.command = "export GIT_TERMINAL_PROMPT=0\n" + input.command; | ||
| }); | ||
|
|
||
| pi.on("session_start", async (_event, ctx) => { | ||
| await fireHook("session_start", { | ||
| type: "session_start", | ||
| cwd: ctx.cwd, | ||
| session_file: ctx.sessionManager.getSessionFile(), | ||
| }); | ||
| }); | ||
|
|
||
| pi.on("before_agent_start", async (event, ctx) => { | ||
| await fireHook("before_agent_start", { | ||
| type: "before_agent_start", | ||
| cwd: ctx.cwd, | ||
| session_file: ctx.sessionManager.getSessionFile(), | ||
| prompt: event.prompt, | ||
| }); | ||
| }); | ||
|
|
||
| pi.on("agent_end", async (_event, ctx) => { | ||
| await fireHook("agent_end", { | ||
| type: "agent_end", | ||
| cwd: ctx.cwd, | ||
| session_file: ctx.sessionManager.getSessionFile(), | ||
| }); | ||
| }); | ||
|
|
||
| pi.on("session_shutdown", async () => { | ||
| await fireHook("session_shutdown", { type: "session_shutdown" }); | ||
| }); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| package pi | ||
|
|
||
| import ( | ||
| "context" | ||
| _ "embed" | ||
| "fmt" | ||
| "os" | ||
| "path/filepath" | ||
| "strings" | ||
|
|
||
| "github.com/entireio/cli/cmd/entire/cli/agent" | ||
| "github.com/entireio/cli/cmd/entire/cli/paths" | ||
| ) | ||
|
|
||
| // Compile-time interface assertion | ||
| var _ agent.HookSupport = (*PiAgent)(nil) | ||
|
|
||
| //go:embed entire_extension.ts | ||
| var extensionTemplate string | ||
|
|
||
| const ( | ||
| // extensionDirName is the directory pi auto-discovers project-local | ||
| // extensions from. | ||
| extensionDirName = ".pi/extensions/entire" | ||
|
|
||
| // extensionFileName is the file pi loads from extensionDirName. | ||
| extensionFileName = "index.ts" | ||
|
|
||
| // entireMarker identifies the file as Entire-owned. Substring of the | ||
| // auto-generated header so AreHooksInstalled can verify ownership by | ||
| // content (and so it survives the ENTIRE_CMD placeholder substitution). | ||
| entireMarker = "Auto-generated by `entire enable --agent pi`" | ||
|
|
||
| // entireCmdPlaceholder is replaced at install time with either `entire` | ||
| // (production) or a `go run …` path (local-dev). | ||
| entireCmdPlaceholder = "__ENTIRE_CMD__" | ||
| ) | ||
|
|
||
| func extensionPath(ctx context.Context) (string, error) { | ||
| root, err := paths.WorktreeRoot(ctx) | ||
| if err != nil { | ||
| // Fall back to CWD for tests run outside a git repo. | ||
| //nolint:forbidigo // explicit fallback when WorktreeRoot fails | ||
| root, err = os.Getwd() | ||
| if err != nil { | ||
| return "", fmt.Errorf("resolve repo root: %w", err) | ||
| } | ||
| } | ||
| return filepath.Join(root, extensionDirName, extensionFileName), nil | ||
| } | ||
|
|
||
| func renderExtension(localDev bool) string { | ||
| var cmd string | ||
| if localDev { | ||
| cmd = `go run "$(git rev-parse --show-toplevel)"/cmd/entire/main.go` | ||
| } else { | ||
| cmd = "entire" | ||
| } | ||
| return strings.ReplaceAll(extensionTemplate, entireCmdPlaceholder, cmd) | ||
| } | ||
|
|
||
| // InstallHooks writes the Entire pi extension to .pi/extensions/entire/index.ts. | ||
| // Returns 1 if the extension was written, 0 if already up-to-date (idempotent). | ||
| // If the file exists but content differs (e.g., localDev vs production), it is | ||
| // rewritten as long as it is recognisable as Entire-owned (contains the | ||
| // marker). A foreign file at the same path is left untouched unless force is | ||
| // true — this protects user-authored extensions that happen to live at | ||
| // .pi/extensions/entire/index.ts. | ||
| func (a *PiAgent) InstallHooks(ctx context.Context, localDev bool, force bool) (int, error) { | ||
| path, err := extensionPath(ctx) | ||
| if err != nil { | ||
| return 0, err | ||
| } | ||
| content := renderExtension(localDev) | ||
|
|
||
| if !force { | ||
| //nolint:gosec // path constructed from validated repo root | ||
| existing, readErr := os.ReadFile(path) | ||
| switch { | ||
| case readErr == nil && string(existing) == content: | ||
| return 0, nil // already up-to-date | ||
| case readErr == nil && !strings.Contains(string(existing), entireMarker): | ||
| return 0, fmt.Errorf("refusing to overwrite foreign file at %s; remove it or pass --force", path) | ||
| } | ||
| } | ||
|
|
||
| //nolint:gosec // G301: pi reads the directory; standard 0755 permissions | ||
| if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { | ||
| return 0, fmt.Errorf("create extension dir: %w", err) | ||
| } | ||
| //nolint:gosec // G306: pi reads the file; standard 0644 permissions | ||
| if err := os.WriteFile(path, []byte(content), 0o644); err != nil { | ||
| return 0, fmt.Errorf("write extension: %w", err) | ||
| } | ||
| return 1, nil | ||
| } | ||
|
|
||
| // UninstallHooks removes the entire pi extension directory (if present). | ||
| func (a *PiAgent) UninstallHooks(ctx context.Context) error { | ||
| path, err := extensionPath(ctx) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| dir := filepath.Dir(path) | ||
| if err := os.RemoveAll(dir); err != nil { | ||
| return fmt.Errorf("remove pi extension dir: %w", err) | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| // AreHooksInstalled returns true when the extension file exists and is | ||
| // recognisable as Entire-owned (contains the marker string). | ||
| func (a *PiAgent) AreHooksInstalled(ctx context.Context) bool { | ||
| path, err := extensionPath(ctx) | ||
| if err != nil { | ||
| return false | ||
| } | ||
| //nolint:gosec // path from validated repo root | ||
| data, err := os.ReadFile(path) | ||
| if err != nil { | ||
| return false | ||
| } | ||
| return strings.Contains(string(data), entireMarker) | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.