diff --git a/CHANGELOG.md b/CHANGELOG.md
index c9b68b091..b54f04fa0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -25,6 +25,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
### New Features
+- **Oh My Pi can load CodeGraph context automatically before a new user run** through a native extension that reuses your existing index without installing tools or indexing code.
+
- **Codex and Astra read project guidance from `AGENTS.md`.** The canonical agent guide now lives in `AGENTS.md` (with a nested `docs/AGENTS.md` for long validation notes); `CLAUDE.md` is a thin `@AGENTS.md` wrapper for Claude Code. Codex/Astra no longer miss the old CLAUDE-only instructions.
- **A big screen's picture stops wrapping into a column.** How wide a screen's lines run before they wrap was worked out with a formula, and the formula was wrong for the way these pictures are actually drawn: a part of a screen spends lines on its own structure — a step that fires things gets a line to itself, and what it fires starts another — so estimating the lines from the boxes alone badly undercounted them, and one screen's 98 boxes wrapped into a 4,356px column. Laying a picture out is cheap and exact, so the widths are now simply tried and the one that comes out closest to the shape of a window is kept. Across one app's 51 screens the tallest picture went from 4,356px to 3,796px, total height fell 8%, and — because a shorter picture is also a picture whose lines have less far to go — lines running over other boxes fell by a third and lines crossing each other went from 13 to 5.
diff --git a/README.md b/README.md
index 10a325ae2..ba79fe894 100644
--- a/README.md
+++ b/README.md
@@ -50,6 +50,7 @@ Follow [@getcodegraph](https://x.com/getcodegraph) on X for updates.
## Contents
- [Get Started](#get-started)
+- [Oh My Pi (native extension)](#oh-my-pi-native-extension)
- [Language Support](#language-support)
- [Why CodeGraph?](#why-codegraph)
- [Key Features](#key-features)
@@ -108,6 +109,9 @@ codegraph install
Detects and auto-configures Claude Code, Cursor, Codex CLI, opencode, Hermes Agent, Gemini CLI, Antigravity IDE, Kiro, and GitHub Copilot (VS Code, Copilot CLI, JetBrains IDEs) — wiring the CodeGraph MCP server into each. **This is the step that connects CodeGraph to your agent;** installing the CLI in step 1 does not do it on its own. It only wires up your agent — it does **not** index any code; building each project's graph is the separate `codegraph init` in step 3. (Shortcut: `npx @colbymchenry/codegraph` downloads and runs this in one go.)
+Oh My Pi uses a [native extension](#oh-my-pi-native-extension) installed through
+OMP, not the interactive MCP installer.
+
### 3. Initialize each project
```bash
@@ -149,6 +153,63 @@ Pass `--keep-cli` to remove only the agent configurations and keep the CLI insta
Reverses the installer — strips CodeGraph's MCP server config, instructions, and permissions from each configured agent. Your project indexes (`.codegraph/`) are left untouched; remove those per-project with `codegraph uninit`. Use `--target` to remove from specific agents, or `--yes` to run non-interactively.
+## Oh My Pi (native extension)
+
+Install the package with OMP, then restart the session:
+
+```bash
+omp plugin install @colbymchenry/codegraph
+```
+
+The package declares `omp.extensions` and ships the native extension alongside
+the CLI. It uses the already-installed platform bundle, or an installed
+`codegraph` executable on PATH when loading from a source checkout. It never
+runs `npx`, installs a missing runtime, creates an index, or runs `init`/`sync`.
+On Windows, install the npm package with its matching platform dependency;
+shell-only `.cmd`/`.bat` launchers are not executed by the extension.
+
+In a project you have already indexed, ask a structural question such as
+“Explain download and its callers.” Before each new user run, the extension
+passes the prompt and working directory to the existing `codegraph prompt-hook`.
+The hook's raw `` output becomes a hidden custom message,
+not a replacement system prompt. Existing prompt selection, project discovery,
+query logic, and Claude Code hooks are unchanged. This does not configure an
+MCP server; an existing CodeGraph MCP connection can be used alongside it.
+
+Use an OMP version that fires `before_agent_start` for every new user run,
+including a fresh steer and a follow-up promoted from the queue. The extension
+does not replay input or regenerate context for each provider request. Session
+switches, branches, tree navigation, shutdown, and newer runs cancel pending
+work and discard its context.
+The target host is OMP integration commit
+`6aef0e8ad51b3bc5ea7a5f2a255c3d48e4c5af72`, containing the queue/input lifecycle
+fix `ad3fb437d3`; the `18.1.17` version string alone does not guarantee those
+fixes are present.
+
+The subprocess has a two-second deadline, a 256 KiB JSON input limit, and a
+64 KiB limit for each output stream. Downloads are disabled with
+`CODEGRAPH_NO_DOWNLOAD=1`. Missing executables/indexes, disabled hooks, failed
+queries, malformed output, and limit violations simply add no context.
+The existing `CODEGRAPH_NO_PROMPT_HOOK=1` or `CODEGRAPH_PROMPT_HOOK=0` switches
+also disable this extension's automatic work; normal CodeGraph telemetry
+preferences still apply.
+
+**Trust boundary:** the extension checks `ctx.isProjectTrusted()` before
+execution and before accepting its result. Current OMP returns `true`
+unconditionally: this API is a compatibility signal, **not a sandbox or an
+enforced per-project approval gate**. Enable this extension only in workspaces
+you trust, and use only a trusted installed CLI/PATH. It does not run commands
+from project configuration.
+
+For a source checkout, install the CLI first and load the package directory:
+
+```bash
+omp --extension /absolute/path/to/codegraph
+```
+
+Remove the extension with `omp plugin uninstall @colbymchenry/codegraph`.
+This is separate from `codegraph uninstall`; existing indexes are preserved.
+
---
## Language Support
@@ -864,6 +925,9 @@ is written):
- **Kiro**
- **GitHub Copilot** — Copilot Chat in VS Code (`copilot-vscode`), the Copilot CLI (`copilot-cli`), and the Copilot plugin in JetBrains IDEs (`copilot-jetbrains`)
+**Oh My Pi (OMP)** has a separate [native prompt-context extension](#oh-my-pi-native-extension)
+installed through `omp plugin install`, not through the interactive installer.
+
## Supported Languages
| Language | Extension | Status |
diff --git a/__tests__/omp-extension.test.ts b/__tests__/omp-extension.test.ts
new file mode 100644
index 000000000..b08681a94
--- /dev/null
+++ b/__tests__/omp-extension.test.ts
@@ -0,0 +1,144 @@
+import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from 'vitest';
+import * as fs from 'node:fs';
+import * as os from 'node:os';
+import * as path from 'node:path';
+import type { ExtensionAPI, ExtensionContext } from '@oh-my-pi/pi-coding-agent';
+import codegraph from '../omp/index';
+
+const context = '\nfunction download() {}\n';
+type HookContext = Pick;
+type Handler = (event: unknown, ctx: HookContext) => unknown;
+
+describe('native OMP prompt hook', () => {
+ let cwd: string;
+ let trusted: boolean;
+ let handlers: Map;
+ let which: Mock<() => string | null>;
+ const ctx = () => ({ cwd, isProjectTrusted: () => trusted });
+ const run = (prompt: string) => handlers.get('before_agent_start')!({
+ type: 'before_agent_start', prompt, systemPrompt: ['base policy'],
+ }, ctx());
+ const load = () => {
+ // Only registration is needed; these handlers use the narrow context above.
+ const api = { on: (event: string, handler: Handler) => handlers.set(event, handler) } as unknown as ExtensionAPI;
+ codegraph(api);
+ };
+
+ beforeEach(() => {
+ cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-omp-'));
+ trusted = true;
+ handlers = new Map();
+ which = vi.fn(() => process.execPath);
+ vi.stubGlobal('Bun', { which });
+ vi.stubEnv('CODEGRAPH_NO_PROMPT_HOOK', '');
+ vi.stubEnv('CODEGRAPH_PROMPT_HOOK', '');
+ // Real subprocess, no CodeGraph index or host dependency. Node treats the
+ // fixed `prompt-hook` argument as this fixture's entry point on every OS.
+ fs.writeFileSync(path.join(cwd, 'prompt-hook'), `
+ const fs = require('node:fs');
+ fs.appendFileSync('invocations', 'started\\n');
+ let raw = '';
+ process.stdin.setEncoding('utf8');
+ process.stdin.on('data', chunk => raw += chunk);
+ process.stdin.on('end', () => {
+ const input = JSON.parse(raw);
+ if (process.env.CODEGRAPH_NO_DOWNLOAD !== '1') process.exit(2);
+ const context = ${JSON.stringify(context)};
+ switch (input.prompt) {
+ // Real child deadline integration: parent fake timers cannot control
+ // the subprocess or prove that its open handles are terminated.
+ case 'hang': setInterval(() => {}, 100); break;
+ case 'overflow': process.stdout.write('x'.repeat(128 * 1024)); break;
+ case 'invalid': process.stdout.write('codegraph: missing bundle'); break;
+ case 'partial': process.stdout.write('partial'); break;
+ case 'error': process.stdout.write(context); process.exitCode = 3; break;
+ case 'empty': break;
+ default: process.stdout.write(context);
+ }
+ });
+ `);
+ });
+
+ afterEach(() => {
+ handlers.get('session_shutdown')?.({}, ctx());
+ vi.unstubAllGlobals();
+ vi.unstubAllEnvs();
+ fs.rmSync(cwd, { recursive: true, force: true });
+ });
+
+ it('delivers raw context as a hidden custom message', async () => {
+ load();
+ expect(await run('Explain download and its callers')).toEqual({
+ message: { customType: 'codegraph-context', content: context, display: false },
+ });
+ expect(fs.readFileSync(path.join(cwd, 'invocations'), 'utf8')).toBe('started\n');
+ });
+
+ it('does not execute when the host reports an untrusted project', async () => {
+ trusted = false;
+ load();
+ expect(await run('Explain download')).toBeUndefined();
+ expect(fs.existsSync(path.join(cwd, 'invocations'))).toBe(false);
+ });
+
+ it('does not execute without an installed executable', async () => {
+ which.mockReturnValue(null);
+ load();
+ expect(await run('Explain download')).toBeUndefined();
+ expect(fs.existsSync(path.join(cwd, 'invocations'))).toBe(false);
+ });
+
+ it.each(['CODEGRAPH_NO_PROMPT_HOOK', 'CODEGRAPH_PROMPT_HOOK'])('honors %s before launching', async (name) => {
+ vi.stubEnv(name, name === 'CODEGRAPH_NO_PROMPT_HOOK' ? '1' : '0');
+ load();
+ expect(await run('Explain download')).toBeUndefined();
+ expect(fs.existsSync(path.join(cwd, 'invocations'))).toBe(false);
+ });
+
+ it('rejects oversized serialized input, including escaping and multibyte text', async () => {
+ load();
+ for (const prompt of ['x'.repeat(256 * 1024 + 1), '\u0000'.repeat(50 * 1024), '界'.repeat(90 * 1024)]) {
+ expect(await run(prompt)).toBeUndefined();
+ }
+ expect(fs.existsSync(path.join(cwd, 'invocations'))).toBe(false);
+ });
+
+ it.each(['overflow', 'invalid', 'partial', 'error', 'empty'])('drops %s output without breaking the prompt', async (prompt) => {
+ load();
+ expect(await run(prompt)).toBeUndefined();
+ expect(fs.readFileSync(path.join(cwd, 'invocations'), 'utf8')).toBe('started\n');
+ });
+
+ it('bounds execution time even when the hook does not finish', async () => {
+ load();
+ expect(await run('hang')).toBeUndefined();
+ expect(fs.readFileSync(path.join(cwd, 'invocations'), 'utf8')).toBe('started\n');
+ }, 4000);
+
+ it.each(['session_start', 'session_switch', 'session_branch', 'session_tree', 'session_shutdown'])('drops pending context on %s', async (event) => {
+ load();
+ const pending = run('hang');
+ handlers.get(event)!({}, ctx());
+ expect(await pending).toBeUndefined();
+ expect(await run('Explain download')).toEqual({
+ message: { customType: 'codegraph-context', content: context, display: false },
+ });
+ });
+
+ it('does not attach an older run result to a newer run', async () => {
+ load();
+ const old = run('hang');
+ const current = run('Explain download');
+ expect(await old).toBeUndefined();
+ expect(await current).toEqual({
+ message: { customType: 'codegraph-context', content: context, display: false },
+ });
+ });
+
+ it('drops context if trust is revoked while the hook is running', async () => {
+ load();
+ const pending = run('Explain download');
+ trusted = false;
+ expect(await pending).toBeUndefined();
+ });
+});
diff --git a/omp/index.ts b/omp/index.ts
new file mode 100644
index 000000000..04d6a98b6
--- /dev/null
+++ b/omp/index.ts
@@ -0,0 +1,90 @@
+import { execFile, type ChildProcess } from 'node:child_process';
+import { createRequire } from 'node:module';
+import type { ExtensionAPI } from '@oh-my-pi/pi-coding-agent';
+
+const require = createRequire(import.meta.url);
+const MAX_INPUT_BYTES = 256 * 1024;
+const MAX_OUTPUT_BYTES = 64 * 1024;
+const TIMEOUT_MS = 2000;
+
+// Prefer this package's installed runtime: no PATH setup, shell, or download.
+// The standalone CLI remains usable when loading the extension from source.
+function installedCommand(): { command: string; args: string[] } | undefined {
+ const pkg = `@colbymchenry/codegraph-${process.platform}-${process.arch}`;
+ try {
+ return {
+ command: require.resolve(`${pkg}/${process.platform === 'win32' ? 'node.exe' : 'node'}`),
+ args: ['--liftoff-only', '--disable-warning=ExperimentalWarning',
+ require.resolve(`${pkg}/lib/dist/bin/codegraph.js`), 'prompt-hook'],
+ };
+ } catch {
+ const command = Bun.which('codegraph');
+ // Windows npm/standalone .cmd launchers require a shell. Use the installed
+ // platform package above instead; never execute a project-supplied command.
+ if (!command || /\.(cmd|bat)$/i.test(command)) return undefined;
+ return { command, args: ['prompt-hook'] };
+ }
+}
+
+/** Native OMP integration; the existing CLI owns project discovery and queries. */
+export default function codegraph(omp: ExtensionAPI): void {
+ const installed = installedCommand();
+ let generation = 0;
+ let cancel: (() => void) | undefined;
+ const reset = () => {
+ generation++;
+ cancel?.();
+ cancel = undefined;
+ };
+ for (const event of ['session_start', 'session_switch', 'session_branch', 'session_tree', 'session_shutdown'] as const) {
+ omp.on(event, reset);
+ }
+
+ omp.on('before_agent_start', async (event, ctx) => {
+ reset();
+ if (!ctx.isProjectTrusted() || !installed || process.env.CODEGRAPH_NO_PROMPT_HOOK === '1' || process.env.CODEGRAPH_PROMPT_HOOK === '0') return;
+ // Reject before serializing as well as after: JSON escaping can grow input.
+ if (event.prompt.length > MAX_INPUT_BYTES || ctx.cwd.length > MAX_INPUT_BYTES) return;
+ const input = JSON.stringify({ prompt: event.prompt, cwd: ctx.cwd });
+ if (Buffer.byteLength(input) > MAX_INPUT_BYTES) return;
+ const current = generation;
+ const cwd = ctx.cwd;
+ const content = await new Promise((resolve) => {
+ let child: ChildProcess | undefined;
+ let timer: NodeJS.Timeout | undefined;
+ let settled = false;
+ const finish = (output?: string) => {
+ if (settled) return;
+ settled = true;
+ clearTimeout(timer);
+ // A PATH launcher can spawn the real CLI. Kill its process group too,
+ // including when a descendant keeps stdout open after its parent exits.
+ try {
+ if (process.platform !== 'win32' && child?.pid) process.kill(-child.pid, 'SIGKILL');
+ else child?.kill('SIGKILL');
+ } catch { /* Already exited. */ }
+ resolve(output);
+ };
+ cancel = () => finish();
+ try {
+ child = execFile(installed.command, installed.args, {
+ cwd, encoding: 'utf8', maxBuffer: MAX_OUTPUT_BYTES, killSignal: 'SIGKILL',
+ detached: process.platform !== 'win32', windowsHide: true,
+ env: { ...process.env, CLAUDE_PROJECT_DIR: cwd, CODEGRAPH_NO_DOWNLOAD: '1' },
+ }, (error, stdout) => finish(error ? undefined : stdout.trim() || undefined));
+ timer = setTimeout(() => finish(), TIMEOUT_MS);
+ child.stdin?.on('error', () => finish());
+ child.stdin?.end(input);
+ } catch {
+ finish();
+ }
+ });
+ if (current !== generation) return;
+ cancel = undefined;
+ if (!ctx.isProjectTrusted() || ctx.cwd !== cwd || !content) return;
+ // prompt-hook emits raw tagged context, not Claude hook JSON. Never inject
+ // launcher diagnostics or a partial result as agent instructions.
+ if (!/^]*)?>[\s\S]*<\/codegraph_context>$/.test(content)) return;
+ return { message: { customType: 'codegraph-context', content, display: false } };
+ });
+}
diff --git a/package.json b/package.json
index d5a742420..1918b97a9 100644
--- a/package.json
+++ b/package.json
@@ -11,9 +11,13 @@
"bin": {
"codegraph": "./dist/bin/codegraph.js"
},
+ "omp": {
+ "extensions": ["./omp/index.ts"]
+ },
"files": [
"dist",
"scripts",
+ "omp",
"README.md"
],
"workspaces": [
diff --git a/scripts/pack-npm.sh b/scripts/pack-npm.sh
index d2e1768dd..79d3022e1 100755
--- a/scripts/pack-npm.sh
+++ b/scripts/pack-npm.sh
@@ -86,6 +86,7 @@ done
# per-platform bundle so its deps aren't duplicated here.
cp "$ROOT/scripts/npm-shim.js" "$NPM/main/npm-shim.js"
cp "$ROOT/scripts/npm-sdk.js" "$NPM/main/npm-sdk.js"
+cp -R "$ROOT/omp" "$NPM/main/omp"
[ -f "$ROOT/README.md" ] && cp "$ROOT/README.md" "$NPM/main/README.md"
# Ship the type declarations so `types`/`exports.types` resolve. Built from this
@@ -112,12 +113,13 @@ VERSION="$VERSION" SCOPE="$SCOPE" TARGETS="${targets[*]}" \
bin: { codegraph: "npm-shim.js" },
main: "npm-sdk.js",
types: "dist/index.d.ts",
+ omp: { extensions: ["./omp/index.ts"] },
exports: {
".": { types: "./dist/index.d.ts", default: "./npm-sdk.js" },
"./package.json": "./package.json"
},
optionalDependencies: opt,
- files: ["npm-shim.js","npm-sdk.js","dist","README.md"],
+ files: ["npm-shim.js","npm-sdk.js","dist","omp","README.md"],
license: "MIT",
repository: { type: "git", url: "git+https://github.com/colbymchenry/codegraph.git" }
}, null, 2) + "\n");