@pome-sh/twin-github
v0.2.0
Published
Deterministic GitHub-shaped twin for agent testing — REST + MCP, SQLite-backed state.
Maintainers
Readme
Pome Twin: GitHub
One of three twins in this repository (GitHub, Stripe x402, Slack).
@pome-sh/twin-github is a local, stateful GitHub twin for agent testing. It exposes GitHub-shaped REST routes plus a 65-tool MCP-style API backed by the same SQLite domain services.
Quickstart
npm install
npm run seed
export TWIN_AUTH_SECRET=$(openssl rand -hex 32)
npm run devGitHub-shaped REST + MCP routes live under /s/:sid/* and require a
bearer token whose sid claim matches the URL :sid. /healthz and
/admin/* stay at the root (admin is localhost-only).
# Mint a token (32-char minimum secret recommended; use the SAME secret as the server)
TOKEN=$(node -e "import('hono/jwt').then(m => m.sign({ sid: 'demo', team_id: 'tm_1', exp: Math.floor(Date.now()/1000)+3600 }, process.env.TWIN_AUTH_SECRET).then(t => console.log(t)))")
# Public health probe — no auth
curl http://127.0.0.1:3333/healthz
# Session-scoped routes — auth required, sid in path must equal sid in claim
curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:3333/s/demo/repos/acme/api/issues/1
curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:3333/s/demo/mcp/tools
curl -s -X POST http://127.0.0.1:3333/s/demo/mcp/call \
-H "Authorization: Bearer $TOKEN" \
-H 'content-type: application/json' \
-d '{"tool":"search_repositories","arguments":{"query":"acme"}}'The default seed creates:
acme/api- default branch
main - files
README.mdandsrc/index.ts - labels
bug,feature, andquestion - issue
#1, titled500 error on POST /orders after deploy - users/orgs
acme,alice,bob, andpome-agent
APIs
- REST base URL:
http://127.0.0.1:3333 - Real MCP (JSON-RPC, Streamable HTTP, stateless):
POST /s/:sid/mcp— speaks the protocol the@modelcontextprotocol/sdkClient+StreamableHTTPClientTransportexpect (initialize,tools/list,tools/call,ping,notifications/*). 65 tools exposed viatools/listwith camelCaseinputSchema. - Legacy custom MCP routes (compat surface for already-deployed agents):
GET /s/:sid/mcp/tools— returns{ tools: [{ name, description, input_schema }, ...] }POST /s/:sid/mcp/tools/:name— body is the tool's arguments objectPOST /s/:sid/mcp/call— body{ tool, arguments }
All session-scoped REST and MCP routes require a bearer token whose sid
claim matches the path. Mutating operations write to SQLite inside transactions
and append to audit_log.
Connecting from @modelcontextprotocol/sdk or the Anthropic Agent SDK
// Anthropic claude-agent-sdk mcpServers config
mcpServers: {
github: {
type: "http",
url: `${TWIN_BASE_URL}/s/${sid}/mcp`,
headers: { Authorization: `Bearer ${token}` }
}
}The endpoint is stateless: each POST is independent; no Mcp-Session-Id
round-trip, no SSE. GET and DELETE on /s/:sid/mcp return 405. The
bearer-auth contract is unchanged — the JWT sid claim (or
ghp_pome_<sid>_<hmac> PAT) still has to match the path's :sid.
Tracing parity
Every tools/call reaching /s/:sid/mcp produces one recorder event whose
request_body is { tool, arguments } and whose response_body is the raw
domain return — identical to what POST /s/:sid/mcp/call records. The
only intentional difference is path. Run npm run validate:mcp to
regenerate the side-by-side diff in scripts/validate-mcp.output.txt.
Runtime contract (for snapshot consumers)
pome-cloud builds a Vercel Sandbox snapshot from this package's signed source
artifact. The following constraints must hold for that build to succeed and for
the resulting snapshot to boot. Changing any of these is a breaking change for
hosted; land the producer change here first, then open the cloud consumer PR
that pins and verifies the new signed digest.
Build
- Package is
npm install-able frompackage.jsonalone (noworkspace:*protocols, no package-manager-specific deps; no committed lockfile is required, the snapshot build regenerates one on each rebuild) npm run buildexits 0 and emitsdist/src/server.js- Built output is loadable under Node 24 — the snapshot runs
runtime: "node24". SQLite is the built-innode:sqlite(via the sdk'sopenTwinDatabase(), F-703) — no native modules, no compiler toolchain.
Runtime
- Server entry:
node dist/src/server.js(cwd = package root) - Listens on
:3333 - Honors
GITHUB_CLONE_HOST=0.0.0.0env (default127.0.0.1is unreachable via Vercel Sandbox port forwarding) GET /healthzreturns 200 within ~3s of process start (the snapshot build sleeps 3s afternode dist/src/server.jsbefore probing)- All admin routes are localhost-only (
/admin/*) - Bearer auth at
Authorization: Bearer <jwt>— engine mechanism (@pome-sh/sdk), shape pinned insrc/twin.ts(F-712)
Cloud consumer coordination
- Bumping any of the above = publish a signed twin digest and open the matching
pome-cloudconsumer PR. - The cloud-side snapshot build script lives at
pome-cloud/notes/build-twin-github-template.ts - The snapshot manifest at
pome-cloud/infra/twin-github-snapshot.jsonrecords the OSS git sha and signed OCI digest each snapshot was built from.
Review harness
The side-by-side review uses three tests:
functional-pr-flow: create repo, create branch, write file, open PR, approve PR, merge PR, read merged file.negative-fidelity: missing file must fail, creating a file must succeed, stale update with the wrongshamust fail.concurrency-stress: eight concurrent writes to the same file path; exactly one create should win and the final file should be readable.
Run against local:
PORT=3333 GITHUB_CLONE_DB=.github_clone/review-harness.db npm run dev
REVIEW_TARGET=local npm run review:harnessKeep agent assertions behavior-based: compare invariants (PR merged, stale sha rejected), not hard-coded IDs or SHAs.
Use In A New Project
- Install and start the twin:
cd ~/pome-twins/packages/twin-github
npm install
GITHUB_CLONE_DB=.github_clone/my-project.db npm run dev- Reset or seed state before each run:
curl -X POST http://127.0.0.1:3333/admin/resetFor a project-specific seed, post JSON to /admin/seed:
curl -s -X POST http://127.0.0.1:3333/admin/seed \
-H 'content-type: application/json' \
-d '{
"users": [
{ "login": "my-org", "type": "Organization", "name": "My Org" },
{ "login": "agent-user", "type": "User", "name": "Agent User" }
],
"repositories": [
{
"owner": "my-org",
"name": "my-app",
"description": "Repository under test",
"default_branch": "main",
"collaborators": ["agent-user"],
"labels": [
{ "name": "bug", "color": "d73a4a", "description": "Something is broken" }
],
"files": [
{ "path": "README.md", "content": "# My App\n" },
{ "path": "src/index.ts", "content": "export const ok = true;\n" }
],
"issues": [
{
"number": 1,
"title": "Fix checkout error",
"body": "Users see a 500 after submitting checkout.",
"labels": ["bug"],
"assignees": []
}
]
}
]
}'- Give your agent these inputs:
POME_GITHUB_REST_URL=http://127.0.0.1:3333POME_GITHUB_MCP_URL=http://127.0.0.1:3333/s/demo/mcpGITHUB_MCP_TOKEN=<JWT whose sid claim is demo>- task text that names the exact repo, branch, issue, file, or PR it should touch
- the expected actor, usually
pome-agent - whether it should use REST or MCP
- Keep agent assertions behavior-based:
- good: "PR was merged and
claude-agent.txtexists onmain" - good: "wrong
shaupdate fails" - bad: "PR number is exactly
1" - bad: "commit SHA equals this hard-coded value"
Claude Agent Example
examples/claude-github-agent.ts is a tiny Claude-powered GitHub agent. It reads ANTHROPIC_API_KEY and optional ANTHROPIC_MODEL from the process environment.
Run against local:
GITHUB_MCP_URL=http://127.0.0.1:3333/s/demo/mcp npm run agent:claude -- \
"Create a branch, push claude-agent.txt, open a pull request, approve it, and merge it in acme/api."The example captures the PR number returned by create_pull_request and reuses it for review and merge calls.
Local Commands
npm run typecheck
npm test
npm run smoke
npm run fidelity:parity
npm run validate:mcp # rewrites scripts/validate-mcp.output.txt
npm run review:harness
npm run agent:claudenpm run capture:fixtures uses gh api to refresh sanitized response-shape fixtures when GitHub CLI auth is available.
Pome CLI Entry Point
The user-facing pome CLI lives at cli/ in this repo. From cli/:
npm run dev -- twin start github --port 3333The command prints:
POME_GITHUB_REST_URL=http://127.0.0.1:3333
POME_GITHUB_MCP_URL=http://127.0.0.1:3333/s/<sid>/mcp