Skip to content

hosts: add pi (pi.dev) and oh-my-pi (omp) support - #403

Open
soydachi wants to merge 2 commits into
trailhq:mainfrom
soydachi:support-pi-omp
Open

soydachi wants to merge 2 commits into
trailhq:mainfrom
soydachi:support-pi-omp

Conversation

@soydachi

Copy link
Copy Markdown

What

Adds pi (pi.dev, @earendil-works/pi-coding-agent) and oh-my-pi / omp as instruction hosts, closing #402.

pi and omp already get two of graft's four channels for free:

Channel Status on pi/omp before this PR
Orientation (AGENTS.md) already works — both load AGENTS.md natively (candidate order verified in pi-coding-agent 0.85.1)
MCP tools (.mcp.json) already works — omp reads it natively; pi via pi-mcp-adapter or host-config discovery
Per-prompt retrieval missing
Statusline missing

How

pi and omp share one extension API (omp is a pi fork; omp's hook runner loads .omp/hooks/pre/*.ts factories, pi auto-discovers .pi/extensions/*.ts). One extension file serves both hosts:

src/pi/graft-extension.ts (new, single source of truth — graft init copies it to both repo-local paths):

  • before_agent_start (≈ Claude's UserPromptSubmit): runs graft ask --json --source on the prompt (8s budget, never blocks the turn) and injects a pointers-only pack behind the same gates as graft's own relevantRetrieval()STRONG_FLOOR 0.1 / HIGH_FLOOR 0.5 on lexical coverage, weak matches get at most 2 nudges per session, and an injected pointer is never injected twice (novelty memory capped at 40).
  • tool_result on edit/write: flips a stale flag; the [graft] graph is stale — run graft build note ships on the next turn's injection rather than clobbering the tool result. Same one-line posture as the Claude PostToolUse hook.
  • turn_endctx.ui.setStatus("graft", …): the statusline bar — graft · N nodes / M edges · ✓ synced · ~X tok saved — preferring graft/.cache/stats.json and falling back to graft/.graph/wiring.json, the same posture as resolveStats() in the Claude statusline.
  • Honesty rules carried over: the pack is pointers-only (~100 tok) so the saved-tokens credit is baselineChars/4 − pack, never the full baseline; a no-op outside graft/ repos.

src/hosts/pi-omp.ts (new): targets, detection (~/.pi, ~/.omp, mirroring the other CLIs' config-dir probes), and the install, which reuses writeOwned so a re-init reports unchanged instead of overwriting. Two copies, no symlinks: omp's scanner reads only .omp/hooks/pre/, pi only .pi/extensions/, and symlinks break repo packaging.

Registry: pi and omp entries render AGENTS.md via the existing instructionBody — no new instruction format.

Telemetry: 'pi' | 'omp' added to the AgentHost enum. The allowlist stays closed — without this, host attribution for the new surfaces would have to ship a free-form string, which the contract exists to prevent.

Init: the extension writes land as ConfigWrite in the hooks result, so --dry-run, the picker, and the actual run can't drift apart (the plan-vs-run consistency test enforces it).

Verification

  • Full suite: 1221 pass, 0 fail (npm test), tsc --noEmit clean.
  • End-to-end on a clean repo with graft init --no-build --agents pi --agents omp: AGENTS.md section + both extension paths written; pi -p (0.85.1) and omp -p --no-session (18.2.0) both inject correct packs, savedTokens accrues in the session file, and the statusline renders from the wiring.json fallback.
  • Extension file contains no network calls, no env reads, no machine paths — a spawnSync("graft", …) plus local reads, nothing else.

Closes #402

@trailhq-graft

trailhq-graft Bot commented Sep 15, 2026

Copy link
Copy Markdown

🌱 graft blast radius

3 areas changed → 6 areas can be affected. 18 dependent symbols, depth 2.
Tests: no test reaches Extension Grafting; 1 area updated its tests.
Tag: @anirudhkumar-nanonets — 8 of 9 areas · @shhdwi — 5 of 9 areas

flowchart TB
  A0(("Host Integration<br/>6 symbols"))
  A1(("CLI Wiring<br/>6 symbols"))
  A2(("Claude Initialization<br/>2 symbols"))
  A3(("Brain Connection<br/>2 symbols"))
  A4(("MCP Server<br/>1 symbol"))
  AX(("1 smaller area<br/>1 symbol"))
  classDef reached fill:#D9EDF3,stroke:#3AA7C9,stroke-width:1.5px,color:#0E313C;
  class A0,A1,A2,A3,A4 reached;
  classDef tail fill:#EEF2F3,stroke:#9AA4A9,stroke-width:1px,color:#3A4247;
  class AX tail;
Loading
Can be affected Symbols Nearest hop Reached from
Host Integration 6 src/hosts/retract.ts:L1-L500 retract.ts — imports, depth 1 Host Setup
CLI Wiring 6 src/cli.ts:L1088-L1173 wireTarget — calls, depth 1 Host Setup
Claude Initialization 2 src/claude/hooks.ts:L1-L463 hooks.ts — imports, depth 2 Host Setup
Brain Connection 2 src/brain/wire.ts:L1-L148 wire.ts — imports, depth 1 Host Setup
MCP Server 1 src/mcp/server.ts:L1-L152 server.ts — imports, depth 2 Host Setup
Event Tracking 1 src/telemetry/track.ts:L1-L156 track.ts — imports, depth 2 Host Setup
Who knows this code — 2 people across 9 areas
Area Who knows it
Host Setup · changed @shhdwi — 6 commits, last 1mo ago · @anirudhkumar-nanonets — 2 commits, last 14d ago
Telemetry Contract · changed @anirudhkumar-nanonets — 3 commits, last 15d ago
Host Integration · affected @shhdwi — 7 commits, last 1mo ago · @anirudhkumar-nanonets — 6 commits, last 5d ago
CLI Wiring · affected @anirudhkumar-nanonets — 40 commits, last 5d ago · @shhdwi — 23 commits, last 1mo ago
Claude Initialization · affected @anirudhkumar-nanonets — 17 commits, last 13d ago · @shhdwi — 11 commits, last 1mo ago
Brain Connection · affected @anirudhkumar-nanonets — 1 commit, last 5d ago
MCP Server · affected @anirudhkumar-nanonets — 5 commits, last 14d ago · @shhdwi — 2 commits, last 2mo ago
Event Tracking · affected @anirudhkumar-nanonets — 2 commits, last 15d ago
…1 further area

Ownership is git history over each area's own files, weighted towards recent work (120-day half-life). Merge commits and bots are dropped, and you are dropped from your own PR. A name with no @ has no GitHub handle in its commit email — tag them by hand, or add a .mailmap entry. A suggestion from history, not a CODEOWNERS rule.

All 18 dependent symbols, grouped by area

Host Integration — 6 symbols in 6 files

  • src/hosts/retract.ts:L1-L500 — retract.ts (imports, depth 1)
    4: * `init` is additive — it writes the files the *currently selected* hosts need
  • src/hosts/antigravity.ts:L1-L47 — antigravity.ts (imports, depth 2)
    5: * `~/.gemini/skills/`. graft's skill card is the same one Claude Code and AdaL get;
  • src/hosts/claude-global.ts:L1-L126 — claude-global.ts (imports, depth 2)
    5: * Why this exists. Everything `graft init` writes for Claude Code lands *in* the
  • src/hosts/codex-hooks.ts:L1-L91 — codex-hooks.ts (imports, depth 2)
    10: import type { PlannedWrite } from './plan.js';
  • src/hosts/cursor-hooks.ts:L1-L110 — cursor-hooks.ts (imports, depth 2)
    ```8: * .cursor/hooks/. That matches graft's Cursor-only posture — a `--no-global````
  • src/hosts/mcp-config.ts:L1-L235 — mcp-config.ts (imports, depth 2)
    14: import type { PlannedWrite } from './plan.js';

CLI Wiring — 6 symbols in 4 files

  • src/cli.ts:L1088-L1173 — wireTarget (calls, depth 1)
    1094: plan: ReturnType<typeof planInit>;
  • src/cli.ts:L1-L1407 — cli.ts (calls, depth 1)
    18: import { runHostsInit } from "./hosts/init.js";
  • src/upkeep-run.ts:L42-L51 — rewriteWiring (calls, depth 1)
    50: runHostsInit(repo, { agents: others, global: opts.global, mcp: opts.mcp, hooks: opts.hooks });
  • src/upkeep.ts:L1-L333 — upkeep.ts (imports, depth 1)
    11: * 2. **Wiring staleness** — `graft init` copies hooks, shims, skill text and
  • src/cli-picker.ts:L1-L385 — cli-picker.ts (imports, depth 2)
    2: * `graft init`'s agent picker and `--dry-run` plan printer.
  • src/upkeep-run.ts:L1-L82 — upkeep-run.ts (imports, depth 2)
    9: import { runHostsInit } from './hosts/init.js';

Claude Initialization — 2 symbols in 2 files

  • src/claude/hooks.ts:L1-L463 — hooks.ts (imports, depth 2)
    6: import { formatBlastRadius, relevantRetrieval, formatOrientation } from './format.js';
  • src/claude/init.ts:L1-L105 — init.ts (imports, depth 2)
    12: import type { PlannedWrite } from '../hosts/plan.js';

Brain Connection — 2 symbols in 2 files

  • src/brain/wire.ts:L1-L148 — wire.ts (imports, depth 1)
    7: * one: the repo-wide conventions, sitting in `AGENTS.md` / `CLAUDE.md` /
  • src/brain/connect.ts:L1-L111 — connect.ts (imports, depth 2)

MCP Server — 1 symbol in 1 file

  • src/mcp/server.ts:L1-L152 — server.ts (imports, depth 2)
    74: // refreshes rule files an older `graft init` wrote, and kicks off the cached

Event Tracking — 1 symbol in 1 file

  • src/telemetry/track.ts:L1-L156 — track.ts (imports, depth 2)
    21: import type { AgentHost } from './contract.js';
Test signal per changed area — 1 ✓ · 1 ✗ · 1 –

Reached = a node under a test path has a resolved edge into the changed symbol. It undercounts anything called indirectly — through a CLI, a spawned process or a dynamic import — so read a low ratio as “look here”, never as a coverage gate.

  • Host Setup — 2 of 10 reached · 2 test files changed here: test/hosts-init.test.ts, test/hosts-registry.test.ts
    • not reached: detectPiOmp, wants, installPiOmpGlobal, ompAgentDir, piAgentDir, piExtensionSourcePath, piOmpGlobalPaths, piOmpTargets
  • Telemetry Contract — no function, method or class changed here
  • Extension Grafting — 0 of 7 reached · no test file reaches it
    • not reached: askGraft, formatPack, readIndex, readState, relevantRetrieval, renderStatus, writeState
9 test suites also reference this code

11 symbols, kept out of the diagram and the table so they cannot crowd out the areas a reviewer has to look at.

  • test/claude-init.test.ts
  • test/cli-picker.test.ts
  • test/hosts-antigravity.test.ts
  • test/hosts-claude-global.test.ts
  • test/hosts-cursor-hooks.test.ts
  • test/hosts-plan.test.ts
  • test/hosts-retract.test.ts
  • test/upkeep-hooks.test.ts
  • test/upkeep.test.ts

⚠️ 3 changed files not in the graph (.omp/hooks/pre/graft.ts, .pi/extensions/graft.ts, AGENTS.md) — no parser claims the extension, or the index predates the file.

graft blast · origin/main...HEAD · depth 2 · 11 changed files

Open the interactive graph → — click an area to see its dependent symbols at file:line.

github-actions Bot added a commit that referenced this pull request Sep 15, 2026
pi and omp already get two of graft's four channels for free: both load
AGENTS.md natively (the `agents` host covers orientation), and both read
project .mcp.json (omp natively, pi via pi-mcp-adapter). What was missing
is per-prompt retrieval and the statusline - both fit the extension API
the two hosts share (omp's hook runner loads .omp/hooks/pre/*.ts
factories, pi auto-discovers .pi/extensions/*.ts).

- src/pi/graft-extension.ts: the extension. before_agent_start runs
  "graft ask --json --source" on the prompt and injects a pointers-only
  pack gated on the same floors as relevantRetrieval() (STRONG 0.1 /
  HIGH 0.5, nudges capped at 2, injected pointers never repeat);
  tool_result on edit/write flips a stale flag whose note ships on the
  next turn; turn_end paints the statusline bar (stats.json preferred,
  wiring.json fallback - same posture as resolveStats()) via
  ctx.ui.setStatus. No-ops outside graft/ repos; never blocks a turn on
  a slow or missing graft.
- src/hosts/pi-omp.ts: wiring + targets + detection (~/.pi, ~/.omp),
  reusing writeOwned so re-init reports 'unchanged' instead of
  overwriting. The extension installs BOTH ways, mirroring how the other
  hosts already split it: repo-local (.pi/extensions + .omp/hooks/pre,
  the Cursor posture - --no-global keeps it) and user-level in the agent
  dir ($PI_CODING_AGENT_DIR, else ~/.pi/agent / ~/.omp/agent - the
  Codex/Claude posture, --no-global suppresses it). The user-level copy
  governs every project safely because the extension itself no-ops in a
  project without graft/INDEX.md; per-project state lives under that
  project's graft/, so sessions never cross.
- registry: pi and omp entries render AGENTS.md via the existing
  instructionBody.
- telemetry: 'pi' | 'omp' added to the AgentHost enum - the allowlist
  stays closed.
- init reports every write as ConfigWrite so --dry-run, the picker, and
  the actual run can't drift apart.

Verified end-to-end: pi 0.85.1 and omp 18.2.0 in a clean repo - packs
inject, savedTokens accrues per project, statusline renders; full suite
green (1223 pass, 0 fail).

Fixes trailhq#402
@soydachi

Copy link
Copy Markdown
Author

Pushed an update: the extension now installs both repo-locally and user-level (mirroring how Codex/Claude Code already split it):

  • Repo-local: .pi/extensions/graft.ts + .omp/hooks/pre/graft.ts (Cursor posture — --no-global keeps it)
  • User-level: `` (else ~/.pi/agent/extensions/ and `~/.omp/agent/hooks/pre/`) (Codex/Claude posture — `--no-global` suppresses it)

The user-level copy governs every project safely because the extension gates on graft/INDEX.md in the project cwd — no graph, no-op. Per-project session state lives under that project's graft/, so sessions never cross. --dry-run reports the global writes as scope 'global' the same way the Claude/Codex ones already are.

github-actions Bot added a commit that referenced this pull request Sep 15, 2026
Self-hosting, same as the .claude/ wiring the README documents: the
graft:init extension bytes copied into the paths pi (0.85.1) and omp
(18.2.0) auto-discover. AGENTS.md carries the instruction section.
github-actions Bot added a commit that referenced this pull request Sep 15, 2026
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.

Support request: pi (pi.dev) and oh-my-pi (omp) hosts

1 participant