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 aFabricAppsrc/layers/state.ts— Thecompact()function for queue compaction (resolving entries older than a retention period)src/adapters/env.ts— Hardened Octokit with@octokit/plugin-throttlingand@octokit/plugin-retry(critical given a prior GitHub account suspension from automated patterns)
The git-steer Rewrite
The MCP server got a structural overhaul:
- Split the monolithic
TOOLSarray intoCORE_TOOLS(32 tools) andFABRIC_CVE_TOOLS(6 tools), listed conditionally based on whether the gateway loaded - Created
src/fabric/gateway.ts— 65 lines that initialize the gateway, register the CVE app, and return aGatewayHandlewith graceful degradation - Unified routing — Replaced 6 individual
caseblocks and agetFabricAdapters()helper with a single handler using aTOOL_MAPthat routes throughgateway.router.route() - 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/gatewayand@git-fabric/cve - Degraded fallback when
createApp()throws — verifying no exception escapes - Tool mapping completeness: all 6
fabric_cve_*names map to validcve_*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_listwith filters - All guarded by
describe.skipIf(!token)for CI safety
Manual smoke tests:
- Start git-steer with stdio transport
- Verify
ListToolsshows all 38 tools - Call
fabric_cve_statsend-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.
| Metric | Before | After |
|---|---|---|
| Inline fabric code | 720 lines | 0 lines |
| Gateway module | — | 65 lines |
| Test coverage (gateway) | 0 tests | 19 tests |
| Fabric apps supported | 1 (hardcoded) | N (registry-based) |
| Code duplication | cve.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
-
GitHub-sourced npm packages need compiled artifacts. If your
package.jsonsays"files": ["dist/"]but you never pushdist/, consumers get an empty package. This is different from npm registry publishing where aprepublishOnlyscript handles compilation. -
AI memory systems need mechanical triggers, not just written instructions. Aiana’s
CLAUDE.mddirectives are necessary but not sufficient. The next step is wiring Claude Code hooks to automatically injectmemory_recallat session start — making context retrieval a system behavior, not a suggestion. -
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.