git-steer had a problem: 720 lines of inline CVE fabric code — src/fabric/cve.ts (558 lines) and src/fabric/adapter.ts (163 lines) — duplicating logic that now lived in the standalone @git-fabric/cve package. Every fix had to land in two places. Every feature had to be wired twice.

The goal was simple on paper: replace all of that with a 65-line gateway module that routes fabric_cve_* tool calls through @git-fabric/gateway into @git-fabric/cve. One integration point. One source of truth.

The Upstream Work

Before git-steer could consume the gateway, the @git-fabric/cve package was missing a key piece: a createApp() factory function that the gateway’s registry system requires. We pushed a commit to git-fabric/cve adding:

  • src/app.ts — Factory exposing 8 tools (cve_scan, cve_enrich, cve_batch, cve_triage, cve_queue_list, cve_queue_stats, cve_queue_update, cve_compact) as a FabricApp
  • src/layers/state.ts — The compact() function for queue compaction (resolving entries older than a retention period)
  • src/adapters/env.ts — Hardened Octokit with @octokit/plugin-throttling and @octokit/plugin-retry (critical given a prior GitHub account suspension from automated patterns)

The git-steer Rewrite

The MCP server got a structural overhaul:

  1. Split the monolithic TOOLS array into CORE_TOOLS (32 tools) and FABRIC_CVE_TOOLS (6 tools), listed conditionally based on whether the gateway loaded
  2. Created src/fabric/gateway.ts — 65 lines that initialize the gateway, register the CVE app, and return a GatewayHandle with graceful degradation
  3. Unified routing — Replaced 6 individual case blocks and a getFabricAdapters() helper with a single handler using a TOOL_MAP that routes through gateway.router.route()
  4. Deleted the inline code — 720 lines gone, replaced by 65

The Testing Layer

After the integration was wired, we built a three-level test suite:

Unit tests (gateway.test.ts, 16 tests):

  • Gateway init success path with mocked @git-fabric/gateway and @git-fabric/cve
  • Degraded fallback when createApp() throws — verifying no exception escapes
  • Tool mapping completeness: all 6 fabric_cve_* names map to valid cve_* targets, no duplicates
  • Conditional tool listing: fabric tools separate from core, no cross-contamination
  • JSON result parsing: valid strings parsed, objects pass through, malformed JSON throws

Integration tests (gateway-integration.test.ts, 3 tests):

  • Live gateway initialization with real token
  • Routing cve_queue_stats (read-only) and verifying response shape
  • Routing cve_queue_list with filters
  • All guarded by describe.skipIf(!token) for CI safety

Manual smoke tests:

  • Start git-steer with stdio transport
  • Verify ListTools shows all 38 tools
  • Call fabric_cve_stats end-to-end through the full chain

The Roadblocks

The dist/ Problem

The first npm run build after wiring everything up failed hard:

error TS2307: Cannot find module '@git-fabric/gateway' or its corresponding type declarations.
error TS2307: Cannot find module '@git-fabric/cve' or its corresponding type declarations.

Root cause: both @git-fabric/gateway and @git-fabric/cve had "files": ["bin/", "dist/"] in their package.json, but no dist/ directory existed in the repos. When npm installs from a GitHub URL, it pulls the repo and filters to the declared files field. With dist/ missing, there was nothing to import — no JavaScript, no type declarations.

We considered several fixes:

  • TypeScript path mapping (too invasive)
  • "prepare": "tsc" script (npm doesn’t install devDependencies from GitHub sources, so tsc wouldn’t be available)
  • Maintaining local type stubs (duplicate maintenance burden)

The pragmatic fix: clone both repos, compile them, remove dist/ from .gitignore, add proper "main" and "types" fields, and push. Two commits later, npm install pulled the compiled packages and tsc was happy.

The Aiana Blind Spot

Here’s the one that stings a little. We have Aiana — our AI memory assistant — wired into Claude Desktop with MCP tools for recalling past session context, searching conversation history, and saving decisions. The CLAUDE.md files (both global and project-level) explicitly instruct Claude to call memory_recall at session start.

It never happened.

The entire gateway integration session — 130 assistant messages, 4 sub-agent tasks, commits pushed to 3 repos — ran without a single Aiana tool call. No memory_recall for project context. No memory_search for past architectural decisions. No memory_add to save what we learned.

Why? Aiana’s MCP server was connected, but Claude Code sessions don’t always trigger the proactive recall pattern the way Claude Desktop does. The instructions are in the CLAUDE.md, but in the heat of a complex multi-repo implementation, they got skipped. It’s a reminder that “put it in the instructions” isn’t the same as “it will happen.” We need hooks — automated context injection at session start — not just written policy.

The irony: had Aiana been consulted, she would have surfaced the GitHub account suspension history and the rate-limiting patterns we’d established. The @octokit/plugin-throttling hardening we added to the CVE adapter was the right call, but we arrived at it by re-reading the code rather than recalling the context.

Final Results

 Test Files  4 passed | 1 skipped (5)
       Tests  32 passed | 3 skipped (35)
    Duration  452ms

Build compiles cleanly. All 16 pre-existing tests pass (no regressions). All 16 new gateway tests pass. The 3 integration tests correctly skip without a token.

MetricBeforeAfter
Inline fabric code720 lines0 lines
Gateway module65 lines
Test coverage (gateway)0 tests19 tests
Fabric apps supported1 (hardcoded)N (registry-based)
Code duplicationcve.ts + @git-fabric/cve@git-fabric/cve only

The architecture is now ready for the next fabric app. Register it with the gateway, add tool definitions to FABRIC_*_TOOLS, extend the TOOL_MAP, and the routing just works.

Lessons

  1. GitHub-sourced npm packages need compiled artifacts. If your package.json says "files": ["dist/"] but you never push dist/, consumers get an empty package. This is different from npm registry publishing where a prepublishOnly script handles compilation.

  2. AI memory systems need mechanical triggers, not just written instructions. Aiana’s CLAUDE.md directives are necessary but not sufficient. The next step is wiring Claude Code hooks to automatically inject memory_recall at session start — making context retrieval a system behavior, not a suggestion.

  3. Gateway patterns pay off at N=2. The gateway is arguably over-engineered for a single CVE app. But the moment we add a second fabric app (compliance? drift detection?), the entire registration + routing + conditional listing infrastructure is already there. The bet is that N=2 is coming soon.


Built with Claude Code (Opus 4.6) in ~30 minutes across two sessions.