AGENTS.md · git:20261104.ccd4163 · 2026-11-04 · sha256 db65de33684b9ee4

AGENTS.md git:20261104.ccd4163B

Immutable. This exact content never changes and is served at /api/v1/blob/db65de33684b9ee4.

<!-- project-manual:start -->
## Toolchain

| Layer | Version | Notes |
|-------|---------|-------|
| Node | **24** (`.node-version`) | floor is `>=22.12.0` (`engines`); CI tests 22.x + 24.x, Docker uses `node:24-slim` |
| Package manager | **npm or pnpm** | both supported, both lockfiles committed and kept in sync; deliberately NOT pinned in `package.json` so nobody is forced onto one. CI runs the full build under **both** — that matrix is what catches a dep that only resolves under one of them (e.g. a package imported directly but only present as a hoisted transitive under npm) |
| TypeScript | **7.0.2** | `tsc -p tsconfig.build.json` emits; `postbuild` adds `.js` extensions |
| Vite | **8.3.4** | dev-only, pulled in as vitest's peer; pinned explicitly so the resolution is not a lottery |
| Vitest | **5.x** | test runner (not Jest) |
| Biome | **2.5.x** | lint + format; `pnpm run lint` must exit 0 |

Stable/LTS only: no beta, alpha, rc or `next` tags anywhere in the dependency
tree. The next release of a dependency is adopted only after the full gate
(`build` → `typecheck` → `lint` → `test`) passes on it.

## Commands

Every script below is package-manager agnostic — `npm run <script>` works
exactly like `pnpm run <script>`, and `npm test` like `pnpm test`.

| Task | Command |
|------|---------|
| Build | `pnpm run build` (tsc → `fix-imports.js` → chmod) |
| Typecheck | `pnpm run typecheck` (src + examples, then src + tests) |
| Dev (watch) | `pnpm run dev` (tsx — `ts-node` cannot load under TS 7) |
| Unit tests | `pnpm test` · core only `pnpm run test:core` · AI `pnpm run test:ai` |
| E2E (mock) | `pnpm run test:e2e` · tools `pnpm run test:e2e:tools` |
| E2E (live API) | `E2E_REAL_API=true pnpm run test:e2e:tools:real` · agent orchestration tiers: `pnpm run test:e2e:smoke` (~242 GraphQL pts) · `pnpm run test:e2e:full` (~4,500 of 5,000/h) |
| E2E runner | `pnpm run e2e` — `--suite <name>`, `--real-api`, `--json <path>` (never `--testPathPattern`: Vitest 5 rejects it) |
| Action sweep | `pnpm run test:e2e:sweep` · gate `pnpm run test:e2e:coverage` — every one of the 169 actions, no credentials needed |
| Coverage | `pnpm run test:coverage` |
| Lint / format | `pnpm run lint` (Biome) · `pnpm run format` |
| Inspect MCP server | `pnpm run inspect` |

## Architecture (DDD, MCP stdio server)

- `src/index.ts` — entry; boots an `McpServer` (`@modelcontextprotocol/server`
  v2, 2026-07-28 spec) via `serveStdio`, which negotiates the protocol revision
  per connection. Each tool is registered with `registerTool`; all of them share
  one `dispatchTool` carrying the shutdown gate, in-flight tracking and result
  formatting. Wires `ToolRegistry` + `ProjectManagementService` through
  `src/container.ts` (tsyringe DI).
- Tool errors: an execution failure returns `{content, isError: true}` so the
  model can see and recover from it. A JSON-RPC error is reserved for protocol
  faults (unknown tool). Note v2 validates arguments against the declared
  `inputSchema` *before* the handler runs, so `ToolValidator` never sees a
  missing required field.
- `src/domain/` — types, zod schemas, errors. No logic.
- `src/services/` — business logic. `ProjectManagementService` orchestrates
  per-feature services; `services/ai/` holds AI SDK providers.
- `src/infrastructure/` — `github/` (Octokit GraphQL/REST), `tools/` (MCP tool
  defs + registry + validators), plus cache, persistence, resilience, events.
- `src/env.ts` + `src/cli.ts` — config. CLI flags override env vars.

## Environment (`.env` or CLI flags; CLI wins)

- Required: `GITHUB_OWNER`, `GITHUB_REPO`. `GITHUB_TOKEN` is NOT required.
- GitHub credential chain, first hit wins, re-resolved per tool call:
  1. GitHub App installation (`GITHUB_APP_ID` + `GITHUB_APP_PRIVATE_KEY` +
     `GITHUB_APP_INSTALLATION_ID`; a partial config falls through)
  2. `--token` CLI flag
  3. `$SECRETS_DIR/GITHUB_TOKEN` file
  4. `GITHUB_TOKEN` env
  5. stored OAuth token from `mcp-github-project-manager auth login`
  6. `gh auth token` (disable with `GH_CLI_TOKEN_FALLBACK=false`; off under
     `NODE_ENV=test`, so tests never shell out)
  None → boot fails listing all methods. Static tokens are rotation-aware.
- OAuth device flow (`build/bin.js auth login|status|logout`):
  `GITHUB_OAUTH_CLIENT_ID` (required), `GITHUB_OAUTH_CLIENT_SECRET` (only for
  revoke on logout), `GITHUB_OAUTH_SCOPES` (default `repo project read:org`),
  `GITHUB_OAUTH_TOKEN_FILE` (default
  `$XDG_CONFIG_HOME/mcp-github-project-manager/github-oauth.json`, 0600).
  Entry is `build/bin.js`; `build/index.js` has no `auth` subcommands.
- `GitHubAuth` / `resolveGitHubAuth()` (`src/infrastructure/github/auth/`) is
  the ONLY credential seam. Never pass raw token strings around and never read
  `process.env.GITHUB_TOKEN` directly; build factories from a `GitHubAuth`
  (`GitHubRepositoryFactory(auth, owner, repo)`, `configureContainer(auth, ...)`).
  `requireToken` and the module-level `GITHUB_TOKEN` export are gone. Other
  config/secrets still go through `getSecret()`.
- AI (optional, unlocks AI tools): `ANTHROPIC_API_KEY`, `GOOGLE_API_KEY`,
  `OPENAI_API_KEY`, `PERPLEXITY_API_KEY`; model overrides `AI_MAIN_MODEL`,
  `AI_PRD_MODEL`, `AI_RESEARCH_MODEL`, `AI_FALLBACK_MODEL`.
- Optional: `SYNC_ENABLED`, `SYNC_TIMEOUT_MS`, `CACHE_DIRECTORY`,
  `WEBHOOK_SECRET`, `WEBHOOK_PORT`, `SSE_ENABLED`.
- Inbound `/events/*` auth (bearer header only): `EVENTS_AUTH_TOKEN` (shared
  secret) and/or OAuth 2.1 resource server `OAUTH_ISSUER` + `OAUTH_AUDIENCE`
  (required together), `OAUTH_JWKS_URI`, `OAUTH_REQUIRED_SCOPES`,
  `OAUTH_RESOURCE_URL`. Nothing configured → 401; opt out with
  `EVENTS_ALLOW_UNAUTHENTICATED=true`. Anonymous `/health` returns status only.
- Vertex AI Gemini (provider `google-vertex`, Google ADC, no key in our
  config): `GOOGLE_VERTEX_PROJECT` (enables it), `GOOGLE_VERTEX_LOCATION`
  (default `us-central1`); creds via `GOOGLE_APPLICATION_CREDENTIALS` / gcloud
  ADC / workload identity.
- Webhook security: signature validation fails closed. With no `WEBHOOK_SECRET`,
  webhooks are rejected unless `WEBHOOK_ALLOW_UNSIGNED=true` (trusted dev only).
- Secrets: set `SECRETS_DIR` (e.g. `/run/secrets`) to load any config/secret from
  a file named after it (Docker/k8s secret convention), checked before env vars.
  `getSecret(name)` in `env.ts` reads fresh (rotation-aware). Vault/AWS SM are an
  extension point via `SecretProvider` (`src/infrastructure/secrets/`).
- Startup validation runs against the *resolved* config, not raw `process.env` —
  validating the environment directly meant a token supplied by file or CLI flag
  failed the required-field check and the process exited before the resolver ran.
- Logs are redacted: `redactSecrets()` in `infrastructure/logger` strips
  token/secret/password/apiKey/authorization-shaped keys from anything the logger
  stringifies, and `GitHubConfig.token` is non-enumerable so `JSON.stringify`
  cannot emit the PAT.
  `redactSecretPatterns()` additionally scrubs credential-shaped *values* (GitHub
  `ghp_`/`github_pat_`, JWT, Bearer/Basic, `sk-`/`sk-ant-`, `AIza`, PEM keys) from
  messages, string args and Error text in every log sink and startup error write.

## Gotchas

- ESM extensions: source omits `.js` in imports; `postbuild` runs
  `scripts/fix-imports.js` to add them. Never ship raw `tsc` output — always
  `pnpm run build`.
- `zod` is on v4 (`^4.6.7`), paired with `ai`@^7.0.146 / `@ai-sdk/*`@^4
  (peer-accept `^4.1.8`) and `@modelcontextprotocol/server`@^2. Tool JSON Schema
  is generated by **zod 4's native `z.toJSONSchema({io:'input'})`** in `ToolRegistry`.
  Do NOT reintroduce `zod-to-json-schema`: it does not support zod 4 and returns
  `{type:'object'}` with no `properties` — silently, with no error — which left
  every tool advertising a parameterless schema. Guarded by
  `src/__tests__/unit/infrastructure/tools/schema-generation.test.ts`.
  zod-4 gotchas:
  `z.record` needs an explicit key schema (`z.record(z.string(), v)`);
  `ZodError.errors` → `.issues`; `.nonstrict()` removed (objects strip by default).
- `@ai-sdk/provider` is a **direct** dependency (pinned exact, `4.0.31`) even
  though only types are imported from it: `ai`@7 does not re-export
  `LanguageModelV4*`, and pnpm does not hoist transitive packages the way npm
  did. An undeclared import of it compiles under npm and fails under pnpm.
- Tests run on **Vitest 5** (not Jest). Vitest rejects Jest-only CLI flags
  outright — `--testPathPattern` / `--testPathIgnorePatterns` exit with
  `CACError: Unknown option`, which silently broke 17 npm scripts. Filter by
  positional pattern (`vitest run ai-services`) and exclude with
  `--exclude='**/tests/x/**'`.
- Test mocks must use `function`, not an arrow, wherever the code under test
  calls `new` — `mockImplementation(() => ({...}))` fails with
  "is not a constructor". Likewise a `beforeEach(() => x.mockReset())` with an
  *implicit return* hands the mock back to Vitest, which treats a returned
  function as a teardown callback and calls it; always use a block body.
- E2E tool tests mock GitHub by default; `E2E_REAL_API=true` hits the live API
  and consumes rate limit.
- `isolatedModules` is ON. Every file must transpile independently, so a type
  re-exported as a value is an error. `tsc` elides those; `tsx`/esbuild/bundlers
  emit a real runtime import that then fails to resolve. Use `export type` /
  `import type` — including for **decorated constructor params**, whose types
  `emitDecoratorMetadata` references at runtime.
- Unit tests that start a loopback HTTP server (OAuth/inbound-auth/webhook
  tests) must call `nock.enableNetConnect(<loopback matcher>)` — the global
  setup `src/__tests__/e2e/setup.ts` disables net connect for every file, so
  real `127.0.0.1` requests otherwise fail.
- Agent token budgets are metered server-side: `AIServiceFactory.getModel`
  wraps every model with middleware that accumulates provider-reported usage
  into `CorrelationContext`'s AsyncLocalStorage, and `dispatchTool` debits the
  agent's budget **once per tool call** (never per AI call — `AgentStore` is
  GitHub-backed, so each debit is a compare-and-swap commit via `updateAgent`).
  A failed debit is retried once, then parked in the in-memory `UnpaidDebitLedger`,
  logged at error level and flagged on the result as `_meta.budgetDebitFailed`; the
  next `checkout_task` settles it first and refuses if the registry is still
  unreachable. The ledger is not persisted. This covers only what the
  *server* spends; an agent's own runtime spend never reaches this process, so
  `record_usage` remains the channel for that.
- `ai`@7 reports usage as `inputTokens: {total,...}` / `outputTokens: {total,...}`
  — **objects, not numbers**. Reading them as numbers yields `NaN`.
- The agent registry is **not an issue body**: it is `registry.json` on the orphan
  branch `agent-registry`, written through the GitHub Contents API with sha
  compare-and-swap (`AgentStore`). The token needs **`contents: write`** and no
  branch ruleset may block that branch; a repo with no commits cannot host it.
  Never reintroduce read-modify-write on an issue body — it is last-write-wins and
  silently drops registrations. A legacy "Agent Registry" issue is imported once
  and rewritten to a pointer (`LegacyAgentRegistry`).
- Registry writes are **field-level**: `AgentStore` exposes `createAgent` and
  `updateAgent(id, mutator)` only — the mutator runs on the *fresh* record on every
  compare-and-swap attempt, so concurrent writers to the same agent never overwrite
  each other. There is no whole-record upsert; never do `getAgent → modify copy →
  write back` (it lost budget debits and heartbeats). Budget writes are deltas.
- Heartbeat/progress/branch are **hot state on the claimed project item**
  (`agent_heartbeat_at`, `agent_progress`, `agent_work_branch`; `ItemHeartbeatStore`),
  not registry commits. The registry is touched only on cold changes (status, task) or
  an idle agent's beat older than the 30-min stale window. Staleness is the newer of
  item beat and registry beat (`AgentHeartbeatResolver`). Projects missing any `agent_*`
  field are healed additively on first checkout/heartbeat per process
  (`AgentFieldProvisioner` → `ProjectFieldSetup.ensureFields`; concurrent first calls
  share one promise; a type conflict is a hard error; a permission failure returns the
  `setup_fields` / project-admin message and is retried on the next call).

> `AGENTS.md` and `CLAUDE.md` are kept identical. Edit both together.
<!-- project-manual:end -->

<!-- gitnexus:start -->
# GitNexus — Code Intelligence

This project is indexed by GitNexus as **mcp-github-project-manager** (13683 symbols, 31596 relationships, 671 execution flows).

> Index stale? Run `node .gitnexus/run.cjs analyze --index-only` from the project root — it auto-selects an available runner. No `.gitnexus/run.cjs` yet? Bootstrap with `npx`, `bunx`, or `pnpm dlx` — e.g. `bunx gitnexus@latest analyze` (npm 11 npx crash; #1939).

## Always Do

- **MUST run impact before editing.** Use `impact({target: "symbolName", direction: "upstream"})` or `node .gitnexus/run.cjs impact "symbolName" --direction upstream --repo .`; report callers, processes, and risk. Never substitute grep for graph analysis.
- **MUST analyze graph changes before committing.** Use `detect_changes({scope: "all"})` (MCP) or `node .gitnexus/run.cjs detect-changes --scope all --repo .` (CLI fallback). `partial: true` or `truncated: true` is not a clean check — a zero means unseen, not unaffected; re-run it. For regression review: `detect_changes({scope: "compare", base_ref: "main"})` or `node .gitnexus/run.cjs detect-changes --scope compare --base-ref "main" --repo .`.
- MUST warn on HIGH/CRITICAL `risk` pre-edit; never use `riskSharedAxes` to waive a HIGH/CRITICAL `risk` warning. Compare File/symbol: MCP File omits axes; Graph-RAG expands File.
- **MUST treat `risk: UNKNOWN` as unresolved, not as low.** An empty caller set is not evidence the symbol is unused — it can also mean the callers are not resolvable by the index (plain-object property access, dynamic dispatch, cross-language calls). `impact` pairs `UNKNOWN` with a `riskNote` saying so. Confirm with a text search before treating the symbol as safe to change or delete; do not proceed on the strength of a zero.
- **MUST use `query({search_query: "concept"})` for concepts/flows, `context({name: "symbolName"})` for a named symbol, or `impact` for blast radius, on read-only callers, dependencies, imports, or execution flow.** Graph first; text search only for empty/`UNKNOWN`/literals.
- For security review, `explain({target: "fileOrSymbol"})` lists taint findings (source→sink flows; needs `analyze --pdg`).

## Never Do

- NEVER edit a function, class, or method before MCP/CLI impact analysis.
- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis, and never read `UNKNOWN` as an all-clear — it means the walk could not answer, which is the one verdict that requires confirming by other means.
- NEVER rename symbols with find-and-replace — use `rename` which understands the call graph.
- NEVER commit before MCP/CLI graph change analysis.

## Resources

| Resource | Use for |
| --- | --- |
| `gitnexus://repo/mcp-github-project-manager/context` | Codebase overview, check index freshness |
| `gitnexus://repo/mcp-github-project-manager/clusters` | All functional areas |
| `gitnexus://repo/mcp-github-project-manager/processes` | All execution flows |
| `gitnexus://repo/mcp-github-project-manager/process/{name}` | Step-by-step execution trace |

## CLI

| Task | Read this skill file |
| --- | --- |
| Understand architecture / "How does X work?" | `.claude/skills/gitnexus-exploring/SKILL.md` |
| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus-impact-analysis/SKILL.md` |
| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus-debugging/SKILL.md` |
| Rename / extract / split / refactor | `.claude/skills/gitnexus-refactoring/SKILL.md` |
| Tools, resources, schema reference | `.claude/skills/gitnexus-guide/SKILL.md` |
| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus-cli/SKILL.md` |

<!-- gitnexus:end -->


<!-- BEGIN BEADS INTEGRATION v:1 profile:minimal hash:6cd5cc61 -->
## Beads Issue Tracker

This project uses **bd (beads)** for issue tracking. Run `bd prime` to see full workflow context and commands.

### Quick Reference

```bash
bd ready              # Find available work
bd show <id>          # View issue details
bd update <id> --claim  # Claim work
bd close <id>         # Complete work
```

### Rules

- Use `bd` for ALL task tracking — do NOT use TodoWrite, TaskCreate, or markdown TODO lists
- Run `bd prime` for detailed command reference and session close protocol
- Use `bd remember` for persistent knowledge — do NOT use MEMORY.md files

**Architecture in one line:** issues live in a local Dolt DB; sync uses `refs/dolt/data` on your git remote; `.beads/issues.jsonl` is a passive export. See https://github.com/gastownhall/beads/blob/main/docs/SYNC_CONCEPTS.md for details and anti-patterns.

## Agent Context Profiles

The managed Beads block is task-tracking guidance, not permission to override repository, user, or orchestrator instructions.

- **Conservative (default)**: Use `bd` for task tracking. Do not run git commits, git pushes, or Dolt remote sync unless explicitly asked. At handoff, report changed files, validation, and suggested next commands.
- **Minimal**: Keep tool instruction files as pointers to `bd prime`; use the same conservative git policy unless active instructions say otherwise.
- **Team-maintainer**: Only when the repository explicitly opts in, agents may close beads, run quality gates, commit, and push as part of session close. A current "do not commit" or "do not push" instruction still wins.

## Session Completion

This protocol applies when ending a Beads implementation workflow. It is subordinate to explicit user, repository, and orchestrator instructions.

1. **File issues for remaining work** - Create beads for anything that needs follow-up
2. **Run quality gates** (if code changed) - Tests, linters, builds
3. **Update issue status** - Close finished work, update in-progress items
4. **Handle git/sync by active profile**:
   ```bash
   # Conservative/minimal/default: report status and proposed commands; wait for approval.
   git status

   # Team-maintainer opt-in only, unless current instructions forbid it:
   git pull --rebase
   git push
   git status
   ```
5. **Hand off** - Summarize changes, validation, issue status, and any blocked sync/commit/push step

**Critical rules:**
- Explicit user or orchestrator instructions override this Beads block.
- Do not commit or push without clear authority from the active profile or the current user request.
- If a required sync or push is blocked, stop and report the exact command and error.
<!-- END BEADS INTEGRATION -->