| name | pi-inngest |
| displayName | Pi + Inngest |
| description | Use Pi from Inngest safely in joelclaw. Covers event-driven detached Pi CLI runners, direct pi-ai calls inside step.run, service-account OAuth auth, claim-check files, and when to choose each pattern. |
| version | 0.1.0 |
| author | joel |
| tags | ["pi","inngest","workflows","service-account","inference"] |
Pi + Inngest
Use this when implementing, reviewing, or debugging joelclaw flows where Inngest orchestrates work and Pi provides model execution, tool execution, or harness-native agent behavior.
This skill is joelclaw-specific. It assumes Inngest and Pi are first-class local infrastructure, not incidental dependencies.
When to Use
Load this skill when the task mentions:
- Pi inside Inngest
pi-ai, @earendil-works/pi-ai, or legacy @mariozechner/pi-ai
- launching
pi from an Inngest function
step.waitForEvent around agent work
- detached Pi CLI children
- link ingest
pi/workflow.* events
- service-account Pi auth
- OpenAI Codex OAuth/subscription auth from Pi
- choosing between Pi CLI and direct
pi-ai
Also use it when an Inngest function needs LLM context work and the current path is drifting into ad hoc API keys, polling loops, or mystery subprocesses.
Core Rule
Inngest owns durability. Pi owns agent/model execution.
Do not blur that boundary:
- Inngest functions should emit and wait on events, persist claim-check files, and record terminal outcomes.
- Pi CLI should be treated as an external actor, not an in-process library.
- Direct
pi-ai is allowed only for bounded model calls where Pi session/harness behavior is not needed.
- Never smuggle raw model API keys into a Pi subprocess when machine OAuth auth is available.
Decision Guide
| Use this | When | Avoid when |
|---|
Direct pi-ai inside step.run | Short deterministic source-context calls, classification, extraction repair, summary/context generation, no Pi tools/session needed | Need Pi tools, skills, extension behavior, session artifacts, long-running agent work, or isolation from the worker process |
| Detached Pi CLI + event result | Need real Pi harness behavior, tool policy, session logs, read/write tools, long-running agent work, or process isolation | Tiny model-only calls where pi-ai would be simpler and faster |
Synchronous pi -p inside one step.run | Tiny smoke/probe only | Production long-running work; this loses the event-driven shape and can wedge a step |
Polling result files with step.sleep | Temporary proof shim only | Production orchestration; prefer step.waitForEvent plus a timeout watchdog |
Bench receipt from 2026-06-20:
- Direct
pi-ai in step.run: 2/2 ok, ~5.5-6.5s wall, usage surfaced around 368 tokens / ~$0.0077 per call.
- Detached Pi CLI + result event: 2/2 ok, ~8.0-9.1s wall, wrote
pi-session/, pi-output.txt, and pi-result.json.
- Receipt root:
/Users/joel/.local/state/joelclaw-central/tmp/pi-runner-bench/e36f592d-786a-4c15-ba1d-affb5820bbdf.
Takeaway: use direct pi-ai for bounded source-context summarization. Use detached Pi CLI when harness parity, tools, session artifacts, or isolation matter.
Service Account Auth Contract
joelclaw Central services should use the macOS joelclaw service account for durable machine-owned auth where practical.
Current local facts:
- Central Inngest runs as
com.joelclaw.central.inngest under user joelclaw.
- Joel-user clients/canaries may still run as Joel when they write Joel-side Brain files.
- Pi auth is file-backed by
auth.json in Pi's agent dir.
- Pi agent dir is resolved by
PI_CODING_AGENT_DIR; otherwise Pi uses ~/.pi/agent for the current OS user.
- The verified model lane for this work is
openai-codex/gpt-5.6-sol.
For durable service access:
- Prefer a real service-owned Pi agent dir, usually:
/Users/joelclaw/.pi/agent
- Set this explicitly in launchd/env for any service that calls Pi or
pi-ai:
PI_CODING_AGENT_DIR=/Users/joelclaw/.pi/agent
-
Keep auth.json owned by joelclaw and mode 0600 or stricter. Never print it.
-
Verify auth without printing tokens:
sudo -u joelclaw \
PI_CODING_AGENT_DIR=/Users/joelclaw/.pi/agent \
node --input-type=module <<'NODE'
import { AuthStorage, ModelRegistry } from '@earendil-works/pi-coding-agent';
import { getModel } from '@earendil-works/pi-ai';
const provider = 'openai-codex';
const model = getModel(provider, 'gpt-5.6-sol');
const registry = ModelRegistry.create(AuthStorage.create());
const auth = await registry.getApiKeyAndHeaders(model);
console.log(JSON.stringify({
provider,
status: registry.getProviderAuthStatus(provider),
ok: auth.ok,
hasApiKey: Boolean(auth.apiKey),
hasHeaders: Boolean(auth.headers && Object.keys(auth.headers).length),
error: auth.error,
}, null, 2));
NODE
Expected safe shape:
{
"provider": "openai-codex",
"status": { "configured": true, "source": "stored" },
"ok": true,
"hasApiKey": true
}
Do not rely on Joel's personal ~/.pi/agent/auth.json from a Central service unless the service intentionally runs as Joel. Do not copy auth across accounts casually. If a one-time migration is unavoidable, record it as an explicit operator action, preserve 0600 permissions, and verify via the safe status check above.
Bridging Joel auth to the service account
Best options, in order:
- Independent service login: run Pi OAuth login under
joelclaw with PI_CODING_AGENT_DIR=/Users/joelclaw/.pi/agent. This is cleanest but requires an interactive browser/device-code flow.
- One-time provider bridge: copy only the
openai-codex OAuth credential from Joel's Pi auth store into the joelclaw Pi auth store. This grants the service account Joel's subscription-backed Codex access, so treat it as a privileged operator action.
- Do not symlink auth files across accounts. Shared lock files, refresh races, ownership, and
/Users/joel permissions make this fucky and fragile.
Repo helper:
cd /Users/joel/Code/joelhooks/joelclaw-central
scripts/setup-pi-service-auth.sh --dry-run
sudo scripts/setup-pi-service-auth.sh --apply
sudo scripts/setup-pi-service-auth.sh --test
The helper:
- copies only the
openai-codex OAuth credential;
- installs a service-readable Pi package wrapper at
/Users/Shared/joelclaw/bin/pi because Joel's personal global Pi install lives under /Users/joel/.local/share, which the service account should not depend on;
- sets and tests
PI_CODING_AGENT_DIR=/Users/joelclaw/.pi/agent;
- verifies both
AuthStorage/ModelRegistry and the Pi CLI as joelclaw without printing tokens.
Utah Sidecar Lesson
Utah's sidecar-management skill has the right shape: small dynamic Inngest functions live in a watched workspace, reconnect automatically, and notify the main agent by event. Steal the shape, not the names.
For joelclaw, the equivalent doctrine is:
- keep core runtime stable;
- let small event handlers/workflows be loaded as sidecar-style functions where practical;
- one function per file;
- no top-level side effects;
- watcher/reconnect beats manual restart for operator iteration;
- functions communicate by events and claim-check paths, not direct session poking;
- use structured logs, because sidecar bugs without logs are archaeology.
This pairs naturally with Pi-Inngest: sidecar functions can choose direct pi-ai for small model steps or detached Pi CLI for harness-native work.
Pattern A: Direct pi-ai in step.run
Use this for bounded model calls where a Pi session is unnecessary.
Implementation Skeleton
Declare package dependencies in the package doing the work. Do not import from a global absolute install path in production code; scratch tests may do that only as a receipt-gathering hack.
import { Inngest } from 'inngest';
import { complete, getModel } from '@earendil-works/pi-ai';
import { AuthStorage, ModelRegistry } from '@earendil-works/pi-coding-agent';
const inngest = new Inngest({ id: 'joelclaw-example' });
async function runPiAiSummary(prompt: string) {
const model = getModel('openai-codex', 'gpt-5.6-sol');
const registry = ModelRegistry.create(AuthStorage.create());
const auth = await registry.getApiKeyAndHeaders(model);
if (!auth.ok || !auth.apiKey) {
throw new Error(auth.error || 'missing openai-codex auth');
}
const result = await complete(
model,
{
systemPrompt: 'Return concise, grounded JSON only.',
messages: [{ role: , : prompt, : .() }],
: [],
},
{ : auth., : auth. },
);
text = result.
.( part. === )
.( part.)
.();
{
text,
: result.,
: result.,
};
}
summarize = inngest.(
{ : , : , : [{ : }] },
({ event, step }) => {
step.(, () =>
(event..),
);
},
);
Rules
- Keep prompts bounded and deterministic.
- Keep calls inside
step.run idempotent or make output writes idempotent.
- Surface
usage and stopReason in receipts when useful.
- Do not use this path if the work needs Pi tools, filesystem edits via Pi, skills, extensions, or a Pi session log.
- Do not manually read or print
auth.json; use AuthStorage / ModelRegistry and print only booleans/status.
Pattern B: Detached Pi CLI + Event Result
Use this for real Pi harness work.
The shape:
- Inngest receives
pi/workflow.requested.
- A short
step.run writes pi-request.json and spawns a detached child/wrapper.
- The wrapper invokes
pi -p @pi-prompt.md with a filtered environment.
- The child writes
pi-result.json and artifacts under a claim-check directory.
- The wrapper emits
pi/workflow.result.ready with { workflowId, sourceId, resultPath }.
- The parent function uses
step.waitForEvent to wait for that result.
- A result step reads
pi-result.json and emits one of:
pi/workflow.context.completed
pi/workflow.needs_human
pi/workflow.failed
Event Names
Use explicit event names. For link ingest, prefer:
pi/workflow.requested
pi/workflow.result.ready
pi/workflow.context.completed
pi/workflow.needs_human
pi/workflow.failed
The wait event must match a stable unique ID, usually data.workflowId.
Do not match only sourceId when multiple workflows can run for the same source.
Inngest Skeleton
export const piWorkflowCli = inngest.createFunction(
{
id: 'pi-workflow-cli',
retries: 0,
triggers: [{ event: 'pi/workflow.requested' }],
},
async ({ event, step }) => {
const { workflowId, sourceId } = event.data;
await step.run('start-detached-pi-cli', async () => {
const requestPath = await writePiRequestClaimCheck(event.data);
spawnDetachedPiWrapper({ workflowId, sourceId, requestPath });
return { workflowId, sourceId, requestPath };
});
const resultEvent = await step.waitForEvent('wait-for-pi-workflow-result', {
event: 'pi/workflow.result.ready',
timeout: '4m',
match: 'data.workflowId',
});
if (!resultEvent) {
const failure = await step.run('write-timeout-failure', async () =>
writePiWorkflowFailureContext({ workflowId, sourceId, reason: 'timeout' }),
);
await step.sendEvent('emit-pi-workflow-failed', {
name: ,
: failure,
});
failure;
}
result = step.(, () =>
(resultEvent..),
);
step.(, (result));
result;
},
);
Pi Wrapper Rules
- Use
spawn, not execFile, for long Pi sessions. execFile(pi) has hung under Node pipe capture on this machine.
- Detach the child from the Inngest function process when the work may be long.
- Give the Pi subprocess only the env it needs:
HOME
USER / LOGNAME
PATH
SHELL
PI_CODING_AGENT_DIR
- harmless display/color/session env if needed
- Do not pass
INNGEST_EVENT_KEY, webhook secrets, Discord tokens, or broad service env into the pi subprocess.
- The wrapper may hold Inngest dispatch auth to emit
pi/workflow.result.ready; the Pi CLI child should not need it.
- Write all large or detailed outputs as files, then send event claim-check paths.
CLI Invocation Defaults
For link-ingest style context work:
pi \
--model openai-codex/gpt-5.6-sol \
--thinking low \
--no-extensions \
--no-context-files \
--no-skills \
--no-prompt-templates \
--no-themes \
--session-dir "$WORKFLOW_DIR/pi-session" \
--name "Link Ingest Context" \
--tools read \
-p "@$WORKFLOW_DIR/pi-prompt.md"
Tighten or expand tools intentionally. Default to read-only for source-context work.
Failure Handling
Every Pi-Inngest path needs a terminal event.
- Direct
pi-ai errors should fail the step or emit a typed failure event with a claim-check receipt.
- Detached Pi CLI start failures must write failure context and emit
pi/workflow.failed.
- Timeout failures must write a timeout receipt before emitting failure.
- Global Inngest failure handlers can add observability, but they are not a replacement for workflow-level terminal events.
For Discord-facing flows, keep progress quiet:
- registry/manifest progress only during work.
- Discord gets terminal indexed/failed or focused HITL only.
Testing Checklist
Before calling a Pi-Inngest implementation done:
- Verify service auth without printing secrets.
- Run a direct
pi-ai smoke if that path is used.
- Run a detached Pi CLI child smoke if that path is used.
- Prove
step.waitForEvent resumes from pi/workflow.result.ready.
- Prove timeout writes a failure claim-check and emits terminal failure.
- Verify the Pi child environment does not include unnecessary service secrets.
- Check logs/receipts and keep reusable findings in Brain.
Useful local receipts from the link-ingest runner work live in:
/Users/joel/.brain/projects/joelclaw-link-ingest-2026-06-17.svx
/Users/joel/Code/joelhooks/joelclaw-central/.brain/projects/link-ingest-router-2026-06-17.svx
Documentation Rule
Use this skill for reusable implementation doctrine.
Use Brain for live project memory and receipts.
Use repo scripts/tests for executable proofs.
Do not bury operational truth only in chat transcripts. Chat is compost, not storage. 🐀